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