View Javadoc
1   package org.apache.maven.plugins.artifact.buildinfo;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.artifact.Artifact;
23  import org.apache.maven.artifact.factory.ArtifactFactory;
24  import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
25  import org.apache.maven.execution.MavenSession;
26  import org.apache.maven.plugin.AbstractMojo;
27  import org.apache.maven.plugin.MojoExecutionException;
28  
29  import org.apache.maven.plugins.annotations.Component;
30  import org.apache.maven.plugins.annotations.LifecyclePhase;
31  import org.apache.maven.plugins.annotations.Mojo;
32  import org.apache.maven.plugins.annotations.Parameter;
33  import org.apache.maven.project.MavenProject;
34  import org.apache.maven.project.MavenProjectHelper;
35  import org.apache.maven.shared.utils.io.FileUtils;
36  import org.apache.maven.shared.utils.logging.MessageUtils;
37  import org.apache.maven.toolchain.Toolchain;
38  import org.apache.maven.toolchain.ToolchainManager;
39  import org.apache.maven.shared.utils.PropertyUtils;
40  import org.apache.maven.shared.utils.StringUtils;
41  import org.eclipse.aether.RepositorySystem;
42  import org.eclipse.aether.RepositorySystemSession;
43  import org.eclipse.aether.repository.RemoteRepository;
44  
45  import java.io.BufferedWriter;
46  import java.io.File;
47  import java.io.FileOutputStream;
48  import java.io.IOException;
49  import java.io.OutputStreamWriter;
50  import java.io.PrintWriter;
51  import java.nio.charset.StandardCharsets;
52  import java.util.ArrayList;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.Properties;
56  import java.util.Set;
57  
58  /**
59   * Creates a buildinfo file recording build environment and output, as specified in
60   * <a href="https://reproducible-builds.org/docs/jvm/">Reproducible Builds for the JVM</a>
61   * for mono-module build, and extended for multi-module build.
62   * Then if a remote repository is configured, check against reference content in it.
63   */
64  @Mojo( name = "buildinfo", defaultPhase = LifecyclePhase.VERIFY )
65  public class BuildinfoMojo
66      extends AbstractMojo
67  {
68      /**
69       * The Maven project.
70       */
71      @Parameter( defaultValue = "${project}", readonly = true )
72      private MavenProject project;
73  
74      /**
75       * The reactor projects.
76       */
77      @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true )
78      private List<MavenProject> reactorProjects;
79  
80      /**
81       * Location of the generated buildinfo file.
82       */
83      @Parameter( defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo",
84                      required = true, readonly = true )
85      private File buildinfoFile;
86  
87      /**
88       * Ignore javadoc attached artifacts from buildinfo generation.
89       */
90      @Parameter( property = "buildinfo.ignoreJavadoc", defaultValue = "true" )
91      private boolean ignoreJavadoc;
92  
93      /**
94       * Artifacts to ignore, specified as <code>extension</code> or <code>classifier.extension</code>.
95       */
96      @Parameter( property = "buildinfo.ignore", defaultValue = "" )
97      private Set<String> ignore;
98  
99      /**
100      * Specifies whether to attach the generated buildinfo file to the project.
101      */
102     @Parameter( property = "buildinfo.attach", defaultValue = "true" )
103     private boolean attach;
104 
105     /**
106      * Rebuild arguments.
107      */
108     //@Parameter( property = "buildinfo.rebuild-args", defaultValue = "-DskipTests verify" )
109     //private String rebuildArgs;
110 
111     /**
112      * Repository for reference build, containing either reference buildinfo file or reference artifacts.<br/>
113      * Format: <code>id</code> or <code>url</code> or <code>id::url</code>
114      * <dl>
115      * <dt>id</dt>
116      * <dd>The repository id</dd>
117      * <dt>url</dt>
118      * <dd>The url of the repository</dd>
119      * </dl>
120      * @see <a href="https://maven.apache.org/ref/current/maven-model/maven.html#repository">repository definition</a>
121      */
122     @Parameter( property = "reference.repo" )
123     private String referenceRepo;
124 
125     /**
126      * Specifies if reference comparison output file should be saved.
127      * This is expected to be a temporary feature to ease
128      * <a href="https://github.com/jvm-repo-rebuild/reproducible-central">Central Repository rebuild</a>
129      * results display.
130      */
131     @Parameter( property = "reference.compare.save", defaultValue = "false" )
132     private boolean referenceCompareSave;
133 
134     /**
135      * Detect projects/modules with install or deploy skipped: avoid taking fingerprinting.
136      */
137     @Parameter( property = "buildinfo.detect.skip", defaultValue = "true" )
138     private boolean detectSkip;
139 
140     /**
141      * Makes the generated {@code .buildinfo} file reproducible, by dropping detailed environment recording: OS will be
142      * recorded as "Windows" or "Unix", JVM version only as major version.
143      *
144      * @since 3.1.0
145      */
146     @Parameter( property = "buildinfo.reproducible", defaultValue = "false" )
147     private boolean reproducible;
148 
149     /**
150      * Used for attaching the buildinfo file in the project.
151      */
152     @Component
153     private MavenProjectHelper projectHelper;
154 
155     @Component
156     private ArtifactFactory artifactFactory;
157 
158     /**
159      * The entry point to Maven Artifact Resolver, i.e. the component doing all the work.
160      */
161     @Component
162     private RepositorySystem repoSystem;
163 
164     /**
165      * The current repository/network configuration of Maven.
166      */
167     @Parameter( defaultValue = "${repositorySystemSession}", readonly = true )
168     private RepositorySystemSession repoSession;
169 
170     /**
171      * The project's remote repositories to use for the resolution.
172      */
173     @Parameter( defaultValue = "${project.remoteProjectRepositories}", readonly = true )
174     private List<RemoteRepository> remoteRepos;
175 
176     @Component
177     private ArtifactRepositoryLayout artifactRepositoryLayout;
178 
179     /**
180      * The current build session instance. This is used for toolchain manager API calls.
181      */
182     @Parameter( defaultValue = "${session}", readonly = true, required = true )
183     private MavenSession session;
184 
185     /**
186      * To obtain a toolchain if possible.
187      */
188     @Component
189     private ToolchainManager toolchainManager;
190 
191     @Override
192     public void execute()
193         throws MojoExecutionException
194     {
195         boolean mono = reactorProjects.size() == 1;
196 
197         if ( !mono )
198         {
199             // if module skips install and/or deploy
200             if ( isSkip( project ) )
201             {
202                 getLog().info( "Skipping buildinfo for module that skips install and/or deploy" );
203                 return;
204             }
205             // if multi-module build, generate (aggregate) buildinfo only in last module
206             MavenProject last = getLastProject();
207             if  ( project != last )
208             {
209                 getLog().info( "Skipping intermediate buildinfo, aggregate will be " + last.getArtifactId() );
210                 return;
211             }
212         }
213 
214         // generate buildinfo
215         Map<Artifact, String> artifacts = generateBuildinfo( mono );
216         getLog().info( "Saved " + ( mono ? "" : "aggregate " ) + "info on build to " + buildinfoFile );
217 
218         // eventually attach
219         if ( attach )
220         {
221             getLog().info( "Attaching buildinfo" );
222             projectHelper.attachArtifact( project, "buildinfo", buildinfoFile );
223         }
224         else
225         {
226             getLog().info( "NOT adding buildinfo to the list of attached artifacts." );
227         }
228 
229         copyAggregateToRoot( buildinfoFile );
230 
231         // eventually check against reference
232         if ( referenceRepo != null )
233         {
234             getLog().info( "Checking against reference build from " + referenceRepo + "..." );
235             checkAgainstReference( mono, artifacts );
236         }
237     }
238 
239     private void copyAggregateToRoot( File aggregate )
240         throws MojoExecutionException
241     {
242         if ( reactorProjects.size() == 1 )
243         {
244             // mono-module, no aggregate buildinfo to deal with
245             return;
246         }
247 
248         // copy aggregate buildinfo to root target directory
249         MavenProject root = getExecutionRoot();
250         String compare = aggregate.getName().endsWith( ".compare" ) ? ".compare" : "";
251         File rootCopy = new File( root.getBuild().getDirectory(),
252                                   root.getArtifactId() + '-' + root.getVersion() + ".buildinfo" + compare );
253         try
254         {
255             FileUtils.copyFile( aggregate, rootCopy );
256             getLog().info( "Aggregate buildinfo" + compare + " copied to " + rootCopy );
257         }
258         catch ( IOException ioe )
259         {
260             throw new MojoExecutionException( "Could not copy " + aggregate + "to " + rootCopy );
261         }
262     }
263 
264     /**
265      * Generate buildinfo file.
266      *
267      * @param mono is it a mono-module build?
268      * @return a Map of artifacts added to the build info with their associated property key prefix
269      *         (<code>outputs.[#module.].#artifact</code>)
270      * @throws MojoExecutionException
271      */
272     private Map<Artifact, String> generateBuildinfo( boolean mono )
273             throws MojoExecutionException
274     {
275         MavenProject root = mono ? project : getExecutionRoot();
276 
277         buildinfoFile.getParentFile().mkdirs();
278 
279         try ( PrintWriter p = new PrintWriter( new BufferedWriter(
280                 new OutputStreamWriter( new FileOutputStream( buildinfoFile ), StandardCharsets.UTF_8 ) ) ) )
281         {
282             BuildInfoWriter bi = new BuildInfoWriter( getLog(), p, mono );
283             bi.setIgnoreJavadoc( ignoreJavadoc );
284             bi.setIgnore( ignore );
285             bi.setToolchain( getToolchain() );
286 
287             bi.printHeader( root, mono ? null : project, reproducible );
288 
289             // artifact(s) fingerprints
290             if ( mono )
291             {
292                 bi.printArtifacts( project );
293             }
294             else
295             {
296                 for ( MavenProject project : reactorProjects )
297                 {
298                     if ( !isSkip( project ) )
299                     {
300                         bi.printArtifacts( project );
301                     }
302                 }
303             }
304 
305             if ( p.checkError() )
306             {
307                 throw new MojoExecutionException( "Write error to " + buildinfoFile );
308             }
309 
310             return bi.getArtifacts();
311         }
312         catch ( IOException e )
313         {
314             throw new MojoExecutionException( "Error creating file " + buildinfoFile, e );
315         }
316     }
317 
318     /**
319      * Check current build result with reference.
320      *
321      * @param mono is it a mono-module build?
322      * @artifacts a Map of artifacts added to the build info with their associated property key prefix
323      *            (<code>outputs.[#module.].#artifact</code>)
324      * @throws MojoExecutionException
325      */
326     private void checkAgainstReference( boolean mono, Map<Artifact, String> artifacts )
327         throws MojoExecutionException
328     {
329         MavenProject root = mono ? project : getExecutionRoot();
330         File referenceDir = new File( root.getBuild().getDirectory(), "reference" );
331         referenceDir.mkdirs();
332 
333         // download or create reference buildinfo
334         File referenceBuildinfo = downloadOrCreateReferenceBuildinfo( mono, artifacts, referenceDir );
335 
336         // compare outputs from reference buildinfo vs actual
337         compareWithReference( artifacts, referenceBuildinfo );
338     }
339 
340     private File downloadOrCreateReferenceBuildinfo( boolean mono, Map<Artifact, String> artifacts, File referenceDir )
341         throws MojoExecutionException
342     {
343         RemoteRepository repo = createReferenceRepo();
344 
345         ReferenceBuildinfoUtil rmb = new ReferenceBuildinfoUtil( getLog(), referenceDir, artifacts,
346                                                                        artifactFactory, repoSystem, repoSession );
347 
348         return rmb.downloadOrCreateReferenceBuildinfo( repo, project, buildinfoFile, mono );
349     }
350 
351     private void compareWithReference( Map<Artifact, String> artifacts, File referenceBuildinfo )
352         throws MojoExecutionException
353     {
354         Properties actual = BuildInfoWriter.loadOutputProperties( buildinfoFile );
355         Properties reference = BuildInfoWriter.loadOutputProperties( referenceBuildinfo );
356 
357         int ok = 0;
358         List<String> okFilenames = new ArrayList<>();
359         List<String> koFilenames = new ArrayList<>();
360         List<String> diffoscopes = new ArrayList<>();
361         File referenceDir = referenceBuildinfo.getParentFile();
362         for ( Map.Entry<Artifact, String> entry : artifacts.entrySet() )
363         {
364             Artifact artifact = entry.getKey();
365             String prefix = entry.getValue();
366 
367             String diffoscope = checkArtifact( artifact, prefix, reference, actual, referenceDir ); 
368             if ( diffoscope == null )
369             {
370                 ok++;
371                 okFilenames.add( artifact.getFile().getName() );
372             }
373             else
374             {
375                 koFilenames.add( artifact.getFile().getName() );
376                 diffoscopes.add( diffoscope );
377             }
378         }
379 
380         int ko = artifacts.size() - ok;
381         int missing = reference.size() / 3 /* 3 property keys par file: filename, length and checksums.sha512 */;
382 
383         if ( ko + missing > 0 )
384         {
385             getLog().warn( "Reproducible Build output summary: " + MessageUtils.buffer().success( ok + " files ok" )
386                 + ", " + MessageUtils.buffer().failure( ko + " different" )
387                 + ( ( missing == 0 ) ? "" : ( ", " + MessageUtils.buffer().warning( missing + " missing" ) ) ) );
388             getLog().warn( "see " + MessageUtils.buffer().project( "diff " + relative( referenceBuildinfo ) + " "
389                 + relative( buildinfoFile ) ).toString() );
390             getLog().warn( "see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html" );
391           }
392         else
393         {
394             getLog().info( "Reproducible Build output summary: " + MessageUtils.buffer().success( ok + " files ok" ) );
395         }
396 
397         if ( referenceCompareSave )
398         {
399             File compare = new File( buildinfoFile.getParentFile(), buildinfoFile.getName() + ".compare" );
400             try ( PrintWriter p =
401                 new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream( compare ),
402                                                                              StandardCharsets.UTF_8 ) ) ) )
403             {
404                 p.println( "version=" + project.getVersion() );
405                 p.println( "ok=" + ok );
406                 p.println( "ko=" + ko );
407                 p.println( "okFiles=\"" + StringUtils.join( okFilenames.iterator(), " " ) + '"' );
408                 p.println( "koFiles=\"" + StringUtils.join( koFilenames.iterator(), " " ) + '"' );
409                 Properties ref = PropertyUtils.loadOptionalProperties( referenceBuildinfo );
410                 String v = ref.getProperty( "java.version" );
411                 if ( v != null )
412                 {
413                     p.println( "reference_java_version=" + v );
414                 }
415                 v = ref.getProperty( "os.name" );
416                 if ( v != null )
417                 {
418                     p.println( "reference_os_name=" + v );
419                 }
420                 for ( String diffoscope : diffoscopes )
421                 {
422                     p.print( "# " );
423                     p.println( diffoscope );
424                 }
425                 getLog().info( "Reproducible Build comparison saved to " + compare );
426             }
427             catch ( IOException e )
428             {
429                 throw new MojoExecutionException( "Error creating file " + compare, e );
430             }
431 
432             copyAggregateToRoot( compare );
433         }
434     }
435 
436     private String checkArtifact( Artifact artifact, String prefix, Properties reference, Properties actual,
437                                   File referenceDir )
438     {
439         String actualFilename = (String) actual.remove( prefix + ".filename" );
440         String actualLength = (String) actual.remove( prefix + ".length" );
441         String actualSha512 = (String) actual.remove( prefix + ".checksums.sha512" );
442 
443         String referencePrefix = findPrefix( reference, actualFilename );
444         String referenceLength = (String) reference.remove( referencePrefix + ".length" );
445         String referenceSha512 = (String) reference.remove( referencePrefix + ".checksums.sha512" );
446 
447         String issue = null;
448         if ( !actualLength.equals( referenceLength ) )
449         {
450             issue = "size";
451         }
452         else if ( !actualSha512.equals( referenceSha512 ) )
453         {
454             issue = "sha512";
455         }
456 
457         if ( issue != null )
458         {
459             String diffoscope = diffoscope( artifact, referenceDir );
460             getLog().warn( issue + " mismatch " + MessageUtils.buffer().strong( actualFilename ) + ": investigate with "
461                 + MessageUtils.buffer().project( diffoscope ) );
462             return diffoscope;
463         }
464         return null;
465     }
466 
467     private String diffoscope( Artifact a, File referenceDir )
468     {
469         File actual = a.getFile();
470         // notice: actual file name may have been defined in pom
471         // reference file name is taken from repository format
472         File reference = new File( referenceDir, getRepositoryFilename( a ) );
473         return "diffoscope " + relative( reference ) + " " + relative( actual );
474     }
475 
476     private String getRepositoryFilename( Artifact a )
477     {
478         String path = artifactRepositoryLayout.pathOf( a );
479         return path.substring( path.lastIndexOf( '/' ) );
480     }
481 
482     private String relative( File file )
483     {
484         return file.getPath().substring( getExecutionRoot().getBasedir().getPath().length() + 1 );
485     }
486 
487     private static String findPrefix( Properties reference, String actualFilename )
488     {
489         for ( String name : reference.stringPropertyNames() )
490         {
491             if ( name.endsWith( ".filename" ) && actualFilename.equals( reference.getProperty( name ) ) )
492             {
493                 reference.remove( name );
494                 return name.substring( 0, name.length() - ".filename".length() );
495             }
496         }
497         return null;
498     }
499 
500     private RemoteRepository createReferenceRepo()
501         throws MojoExecutionException
502     {
503         if ( referenceRepo.contains( "::" ) )
504         {
505             // id::url
506             int index = referenceRepo.indexOf( "::" );
507             String id = referenceRepo.substring( 0, index );
508             String url = referenceRepo.substring( index + 2 );
509             return createDeploymentArtifactRepository( id, url );
510         }
511         else if ( referenceRepo.contains( ":" ) )
512         {
513             // url, will use default "reference" id
514             return createDeploymentArtifactRepository( "reference", referenceRepo );
515         }
516 
517         // id
518         for ( RemoteRepository repo : remoteRepos )
519         {
520             if ( referenceRepo.equals( repo.getId() ) )
521             {
522                 return repo;
523             }
524         }
525         throw new MojoExecutionException( "Could not find repository with id = " + referenceRepo );
526     }
527 
528     private static RemoteRepository createDeploymentArtifactRepository( String id, String url )
529     {
530         return new RemoteRepository.Builder( id, "default", url ).build();
531     }
532 
533     private MavenProject getExecutionRoot()
534     {
535         for ( MavenProject p : reactorProjects )
536         {
537             if ( p.isExecutionRoot() )
538             {
539                 return p;
540             }
541         }
542         return null;
543     }
544 
545     private MavenProject getLastProject()
546     {
547         int i = reactorProjects.size();
548         while ( i > 0 )
549         {
550             MavenProject project = reactorProjects.get( --i );
551             if ( !isSkip( project ) )
552             {
553                 return project;
554             }
555         }
556         return null;
557     }
558 
559     private boolean isSkip( MavenProject project )
560     {
561         return detectSkip && PluginUtil.isSkip( project );
562     }
563 
564     private Toolchain getToolchain()
565     {
566         Toolchain tc = null;
567         if ( toolchainManager != null )
568         {
569             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
570         }
571 
572         return tc;
573     }
574 }