View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * Compare current build output (from {@code package}) against reference either previously {@code install}-ed or downloaded from a remote
54   * repository: comparison results go to {@code .buildcompare} file.
55   *
56   * @since 3.2.0
57   */
58  @Mojo(name = "compare", threadSafe = false)
59  public class CompareMojo extends AbstractBuildinfoMojo {
60      /**
61       * Repository for reference build, containing either reference buildinfo file or reference artifacts.<br/>
62       * Format: <code>id</code> or <code>url</code> or <code>id::url</code>
63       * <dl>
64       * <dt>id</dt>
65       * <dd>The repository id</dd>
66       * <dt>url</dt>
67       * <dd>The url of the repository</dd>
68       * </dl>
69       * @see <a href="https://maven.apache.org/ref/current/maven-model/maven.html#repository">repository definition</a>
70       */
71      @Parameter(property = "reference.repo", defaultValue = "central")
72      private String referenceRepo;
73  
74      /**
75       * Compare aggregate only (ie wait for the last module) or also compare on each module.
76       * @since 3.2.0
77       */
78      @Parameter(property = "compare.aggregate.only", defaultValue = "false")
79      private boolean aggregateOnly;
80  
81      /**
82       * The current repository/network configuration of Maven.
83       */
84      @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
85      private RepositorySystemSession repoSession;
86  
87      /**
88       * The project's remote repositories to use for the resolution.
89       */
90      @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
91      private List<RemoteRepository> remoteRepos;
92  
93      /**
94       * Fail the build if differences are found against reference build.
95       * @since 3.5.0
96       */
97      @Parameter(property = "compare.fail", defaultValue = "true")
98      private boolean fail;
99  
100     /**
101      * The entry point to Maven Artifact Resolver, i.e. the component doing all the work.
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         // try to download reference artifacts for current project and check if there are issues to give early feedback
129         checkAgainstReference(generateBuildinfo(true), true);
130     }
131 
132     /**
133      * Check current build result with reference.
134      *
135      * @param artifacts a Map of artifacts added to the build info with their associated property key prefix
136      *            (<code>outputs.[#module.].#artifact</code>)
137      * @throws MojoExecutionException if anything goes wrong
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         // download or create reference buildinfo
145         File referenceBuildinfo = downloadOrCreateReferenceBuildinfo(mono, artifacts, referenceDir);
146 
147         // compare outputs from reference buildinfo vs actual
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                 // ignored file
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]; // diffoscope or wget
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         // save .compare file
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                     // nothing
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     // { filename, diffoscope or wget }
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         // notice: actual file name may have been defined in pom
309         // reference file name is taken from repository format
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             // id::url
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             // url, will use default "reference" id
353             return createDeploymentArtifactRepository("reference", referenceRepo);
354         }
355 
356         // id
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 }