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