1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.artifact.buildinfo;
20
21 import javax.inject.Inject;
22
23 import java.io.BufferedWriter;
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStreamWriter;
28 import java.io.PrintWriter;
29 import java.nio.charset.StandardCharsets;
30 import java.nio.file.Files;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Properties;
35
36 import org.apache.maven.execution.MavenSession;
37 import org.apache.maven.plugin.MojoExecutionException;
38 import org.apache.maven.plugins.annotations.Mojo;
39 import org.apache.maven.plugins.annotations.Parameter;
40 import org.apache.maven.project.MavenProject;
41 import org.apache.maven.rtinfo.RuntimeInformation;
42 import org.apache.maven.shared.utils.logging.MessageUtils;
43 import org.apache.maven.toolchain.ToolchainManager;
44 import org.eclipse.aether.RepositorySystem;
45 import org.eclipse.aether.RepositorySystemSession;
46 import org.eclipse.aether.artifact.Artifact;
47 import org.eclipse.aether.repository.RemoteRepository;
48 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
49
50 import static org.apache.maven.plugins.artifact.buildinfo.BuildInfoWriter.getArtifactFilename;
51
52
53
54
55
56
57
58 @Mojo(name = "compare", threadSafe = false)
59 public class CompareMojo extends AbstractBuildinfoMojo {
60
61
62
63
64
65
66
67
68
69
70
71 @Parameter(property = "reference.repo", defaultValue = "central")
72 private String referenceRepo;
73
74
75
76
77
78 @Parameter(property = "compare.aggregate.only", defaultValue = "false")
79 private boolean aggregateOnly;
80
81
82
83
84 @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
85 private RepositorySystemSession repoSession;
86
87
88
89
90 @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
91 private List<RemoteRepository> remoteRepos;
92
93
94
95
96
97 @Parameter(property = "compare.fail", defaultValue = "true")
98 private boolean fail;
99
100
101
102
103 private final RepositorySystem repoSystem;
104
105 @Inject
106 public CompareMojo(
107 ToolchainManager toolchainManager,
108 RuntimeInformation rtInformation,
109 MavenProject project,
110 MavenSession session,
111 RepositorySystem repoSystem) {
112 super(toolchainManager, rtInformation, project, session);
113 this.repoSystem = repoSystem;
114 }
115
116 @Override
117 public void execute(Map<Artifact, String> artifacts) throws MojoExecutionException {
118 getLog().info("Checking against reference build from " + referenceRepo + "...");
119 checkAgainstReference(artifacts, session.getProjects().size() == 1);
120 }
121
122 @Override
123 protected void skip(MavenProject last) throws MojoExecutionException {
124 if (aggregateOnly) {
125 return;
126 }
127
128
129 checkAgainstReference(generateBuildinfo(true), true);
130 }
131
132
133
134
135
136
137
138
139 private void checkAgainstReference(Map<Artifact, String> artifacts, boolean mono) throws MojoExecutionException {
140 MavenProject root = mono ? project : session.getTopLevelProject();
141 File referenceDir = new File(root.getBuild().getDirectory(), "reference");
142 referenceDir.mkdirs();
143
144
145 File referenceBuildinfo = downloadOrCreateReferenceBuildinfo(mono, artifacts, referenceDir);
146
147
148 compareWithReference(artifacts, referenceBuildinfo);
149 }
150
151 private File downloadOrCreateReferenceBuildinfo(boolean mono, Map<Artifact, String> artifacts, File referenceDir)
152 throws MojoExecutionException {
153 RemoteRepository repo = createReferenceRepo();
154
155 ReferenceBuildinfoUtil rmb =
156 new ReferenceBuildinfoUtil(getLog(), referenceDir, artifacts, repoSystem, repoSession, rtInformation);
157
158 return rmb.downloadOrCreateReferenceBuildinfo(repo, project, buildinfoFile, mono);
159 }
160
161 private void compareWithReference(Map<Artifact, String> artifacts, File referenceBuildinfo)
162 throws MojoExecutionException {
163 Properties actual = BuildInfoWriter.loadOutputProperties(buildinfoFile);
164 Properties reference = BuildInfoWriter.loadOutputProperties(referenceBuildinfo);
165
166 int ok = 0;
167 List<String> okFilenames = new ArrayList<>();
168 List<String> koFilenames = new ArrayList<>();
169 List<String> missingFilenames = new ArrayList<>();
170 List<String> diffoscopes = new ArrayList<>();
171 List<String> ignored = new ArrayList<>();
172 File referenceDir = referenceBuildinfo.getParentFile();
173 for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) {
174 Artifact artifact = entry.getKey();
175 String prefix = entry.getValue();
176 if (prefix == null) {
177
178 ignored.add(getArtifactFilename(artifact));
179 continue;
180 }
181
182 String[] checkResult = checkArtifact(artifact, prefix, reference, actual, referenceDir);
183 String filename = checkResult[0];
184 String diffoscope = checkResult[1];
185
186 if (diffoscope == null) {
187 ok++;
188 okFilenames.add(filename);
189 } else {
190 (diffoscope.startsWith("wget") ? missingFilenames : koFilenames).add(filename);
191 diffoscopes.add(diffoscope);
192 }
193 }
194
195 int ko = artifacts.size() - ok - ignored.size();
196 int missing = missingFilenames.size();
197
198 if (ko + missing > 0) {
199 getLog().error("[Reproducible Builds] rebuild comparison result: "
200 + MessageUtils.buffer().success(ok + " files match")
201 + ", " + MessageUtils.buffer().failure(ko + " differ")
202 + ((missing == 0) ? "" : (", " + MessageUtils.buffer().failure(missing + " missing")))
203 + ((ignored.isEmpty()) ? "" : (", " + MessageUtils.buffer().warning(ignored.size() + " ignored"))));
204 } else {
205 getLog().info("[Reproducible Builds] rebuild comparison result: "
206 + MessageUtils.buffer().success(ok + " files match")
207 + ((ignored.isEmpty()) ? "" : (", " + MessageUtils.buffer().warning(ignored.size() + " ignored"))));
208 }
209
210
211 File buildcompare = new File(
212 buildinfoFile.getParentFile(), buildinfoFile.getName().replaceFirst(".buildinfo$", ".buildcompare"));
213 try (PrintWriter p = new PrintWriter(new BufferedWriter(
214 new OutputStreamWriter(Files.newOutputStream(buildcompare.toPath()), StandardCharsets.UTF_8)))) {
215 p.println("version=" + project.getVersion());
216 p.println("ok=" + ok);
217 p.println("ko=" + ko);
218 p.println("ignored=" + ignored.size());
219 p.println("okFiles=\"" + String.join(" ", okFilenames) + '"');
220 p.println("koFiles=\"" + String.join(" ", koFilenames) + '"');
221 p.println("ignoredFiles=\"" + String.join(" ", ignored) + '"');
222 if (missing > 0) {
223 p.println("missing=" + missing);
224 p.println("missingFiles=\"" + String.join(" ", missingFilenames) + '"');
225 }
226 Properties ref = new Properties();
227 if (referenceBuildinfo != null) {
228 try (InputStream in = Files.newInputStream(referenceBuildinfo.toPath())) {
229 ref.load(in);
230 } catch (IOException e) {
231
232 }
233 }
234 String v = ref.getProperty("java.version");
235 if (v != null) {
236 p.println("reference_java_version=\"" + v + '"');
237 }
238 v = ref.getProperty("os.name");
239 if (v != null) {
240 p.println("reference_os_name=\"" + v + '"');
241 }
242 for (String diffoscope : diffoscopes) {
243 p.print("# ");
244 p.println(diffoscope);
245 }
246 } catch (IOException e) {
247 throw new MojoExecutionException("Error creating file " + buildcompare, e);
248 }
249
250 String saved = " saved to " + relative(buildcompare);
251 if (ko + missing > 0) {
252 getLog().error(saved);
253 } else {
254 getLog().info(saved);
255 }
256 if (session.getProjects().size() > 1) {
257 MavenProject last = getLastProject();
258 if (project == last) {
259 buildcompare = copyAggregateToRoot(buildcompare);
260 }
261 }
262
263 if (ko + missing > 0) {
264 getLog().error("[Reproducible Builds] to analyze the differences, see diffoscope instructions in "
265 + relative(buildcompare));
266 getLog().error(
267 " see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
268
269 if (fail) {
270 throw new MojoExecutionException("Rebuilt artifacts are different from reference");
271 }
272 }
273 }
274
275
276 private String[] checkArtifact(
277 Artifact artifact, String prefix, Properties reference, Properties actual, File referenceDir)
278 throws MojoExecutionException {
279 String actualFilename = (String) actual.remove(prefix + ".filename");
280 String actualLength = (String) actual.remove(prefix + ".length");
281 String actualSha512 = (String) actual.remove(prefix + ".checksums.sha512");
282
283 String referencePrefix = findPrefix(reference, artifact.getGroupId(), actualFilename);
284 String referenceLength = (String) reference.remove(referencePrefix + ".length");
285 String referenceSha512 = (String) reference.remove(referencePrefix + ".checksums.sha512");
286 reference.remove(referencePrefix + ".groupId");
287
288 String issue = null;
289 if (referenceLength == null) {
290 issue = "missing reference file";
291 } else if (!actualLength.equals(referenceLength)) {
292 issue = "size";
293 } else if (!actualSha512.equals(referenceSha512)) {
294 issue = "sha512";
295 }
296
297 if (issue != null) {
298 String diffoscope = diffoscope(artifact, referenceDir);
299 getLog().error(issue + " mismatch " + MessageUtils.buffer().strong(actualFilename) + ": investigate with "
300 + MessageUtils.buffer().project(diffoscope));
301 return new String[] {actualFilename, diffoscope};
302 }
303 return new String[] {actualFilename, null};
304 }
305
306 private String diffoscope(Artifact a, File referenceDir) throws MojoExecutionException {
307 File actual = a.getFile();
308
309
310 File reference = new File(new File(referenceDir, a.getGroupId()), getRepositoryFilename(a));
311 if (actual == null) {
312 return "missing file for " + ArtifactIdUtils.toId(a) + " reference = " + relative(reference)
313 + " actual = null";
314 }
315 if (!reference.exists()) {
316 RemoteRepository repo = createReferenceRepo();
317 String url = repo.getUrl() + "/"
318 + session.getRepositorySession()
319 .getLocalRepositoryManager()
320 .getPathForRemoteArtifact(a, repo, null);
321 return "wget " + url + "; ls -l " + relative(actual);
322 }
323 return "diffoscope " + relative(reference) + " " + relative(actual);
324 }
325
326 private String getRepositoryFilename(Artifact a) {
327 String path = session.getRepositorySession().getLocalRepositoryManager().getPathForLocalArtifact(a);
328 return path.substring(path.lastIndexOf('/'));
329 }
330
331 private static String findPrefix(Properties reference, String actualGroupId, String actualFilename) {
332 for (String name : reference.stringPropertyNames()) {
333 if (name.endsWith(".filename") && actualFilename.equals(reference.getProperty(name))) {
334 String prefix = name.substring(0, name.length() - ".filename".length());
335 if (actualGroupId.equals(reference.getProperty(prefix + ".groupId"))) {
336 reference.remove(name);
337 return prefix;
338 }
339 }
340 }
341 return null;
342 }
343
344 private RemoteRepository createReferenceRepo() throws MojoExecutionException {
345 if (referenceRepo.contains("::")) {
346
347 int index = referenceRepo.indexOf("::");
348 String id = referenceRepo.substring(0, index);
349 String url = referenceRepo.substring(index + 2);
350 return createDeploymentArtifactRepository(id, url);
351 } else if (referenceRepo.contains(":")) {
352
353 return createDeploymentArtifactRepository("reference", referenceRepo);
354 }
355
356
357 for (RemoteRepository repo : remoteRepos) {
358 if (referenceRepo.equals(repo.getId())) {
359 return repo;
360 }
361 }
362 throw new MojoExecutionException("Could not find repository with id = " + referenceRepo);
363 }
364
365 private static RemoteRepository createDeploymentArtifactRepository(String id, String url) {
366 return new RemoteRepository.Builder(id, "default", url).build();
367 }
368 }