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