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