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.archiver.MavenArchiver;
23  import org.apache.maven.artifact.Artifact;
24  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
25  import org.apache.maven.execution.MavenSession;
26  import org.apache.maven.plugin.AbstractMojo;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugins.annotations.Component;
29  import org.apache.maven.plugins.annotations.Parameter;
30  import org.apache.maven.project.MavenProject;
31  import org.apache.maven.shared.utils.io.FileUtils;
32  import org.apache.maven.toolchain.Toolchain;
33  import org.apache.maven.toolchain.ToolchainManager;
34  
35  import java.io.BufferedWriter;
36  import java.io.File;
37  import java.io.FileOutputStream;
38  import java.io.IOException;
39  import java.io.OutputStreamWriter;
40  import java.io.PrintWriter;
41  import java.nio.charset.StandardCharsets;
42  import java.text.SimpleDateFormat;
43  import java.util.Date;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Set;
47  
48  /**
49   * Base buildinfo-generating class, for goals related to Reproducible Builds {@code .buildinfo} files.
50   *
51   * @since 3.2.0
52   */
53  public abstract class AbstractBuildinfoMojo
54      extends AbstractMojo
55  {
56      /**
57       * The Maven project.
58       */
59      @Parameter( defaultValue = "${project}", readonly = true )
60      protected MavenProject project;
61  
62      /**
63       * The reactor projects.
64       */
65      @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true )
66      protected List<MavenProject> reactorProjects;
67  
68      /**
69       * Location of the generated buildinfo file.
70       */
71      @Parameter( defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo",
72                      required = true, readonly = true )
73      protected File buildinfoFile;
74  
75      /**
76       * Ignore javadoc attached artifacts from buildinfo generation.
77       */
78      @Parameter( property = "buildinfo.ignoreJavadoc", defaultValue = "true" )
79      private boolean ignoreJavadoc;
80  
81      /**
82       * Artifacts to ignore, specified as <code>extension</code> or <code>classifier.extension</code>.
83       */
84      @Parameter( property = "buildinfo.ignore", defaultValue = "" )
85      private Set<String> ignore;
86  
87      /**
88       * Detect projects/modules with install or deploy skipped: avoid taking fingerprints.
89       */
90      @Parameter( property = "buildinfo.detect.skip", defaultValue = "true" )
91      private boolean detectSkip;
92  
93      /**
94       * Makes the generated {@code .buildinfo} file reproducible, by dropping detailed environment recording: OS will be
95       * recorded as "Windows" or "Unix", JVM version as major version only.
96       *
97       * @since 3.1.0
98       */
99      @Parameter( property = "buildinfo.reproducible", defaultValue = "false" )
100     private boolean reproducible;
101 
102     /**
103      * The current build session instance. This is used for toolchain manager API calls.
104      */
105     @Parameter( defaultValue = "${session}", readonly = true, required = true )
106     private MavenSession session;
107 
108     /**
109      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
110      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
111      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
112      *
113      * @since 3.2.0
114      */
115     @Parameter( defaultValue = "${project.build.outputTimestamp}" )
116     private String outputTimestamp;
117 
118     /**
119      * To obtain a toolchain if possible.
120      */
121     @Component
122     private ToolchainManager toolchainManager;
123 
124     @Component
125     protected ArtifactHandlerManager artifactHandlerManager;
126 
127     @Override
128     public void execute()
129         throws MojoExecutionException
130     {
131         boolean mono = reactorProjects.size() == 1;
132 
133         MavenArchiver archiver = new MavenArchiver();
134         Date timestamp = archiver.parseOutputTimestamp( outputTimestamp );
135         if ( timestamp == null )
136         {
137             getLog().warn( "Reproducible Build not activated by project.build.outputTimestamp property: "
138                 + "see https://maven.apache.org/guides/mini/guide-reproducible-builds.html" );
139         }
140         else
141         {
142             if ( getLog().isDebugEnabled() )
143             {
144                 getLog().debug( "project.build.outputTimestamp = \"" + outputTimestamp + "\" => "
145                     + new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssXXX" ).format( timestamp ) );
146             }
147 
148             // check if timestamp well defined in a project from reactor
149             boolean parentInReactor = false;
150             MavenProject reactorParent = project;
151             while ( reactorProjects.contains( reactorParent.getParent() ) )
152             {
153                 parentInReactor = true;
154                 reactorParent = reactorParent.getParent();
155             }
156             String prop =
157                 reactorParent.getOriginalModel().getProperties().getProperty( "project.build.outputTimestamp" );
158             if ( prop == null )
159             {
160                 getLog().error( "project.build.outputTimestamp property should not be inherited but defined in "
161                     + ( parentInReactor ? "parent POM from reactor " : "POM " ) + reactorParent.getFile() );
162             }
163         }
164 
165         if ( !mono )
166         {
167             // if module skips install and/or deploy
168             if ( isSkip( project ) )
169             {
170                 getLog().info( "Skipping goal because module skips install and/or deploy" );
171                 return;
172             }
173             // if multi-module build, generate (aggregate) buildinfo only in last module
174             MavenProject last = getLastProject();
175             if  ( project != last )
176             {
177                 skip( last );
178                 return;
179             }
180         }
181 
182         // generate buildinfo
183         Map<Artifact, String> artifacts = generateBuildinfo( mono );
184         getLog().info( "Saved " + ( mono ? "" : "aggregate " ) + "info on build to " + buildinfoFile );
185 
186         copyAggregateToRoot( buildinfoFile );
187 
188         execute( artifacts );
189     }
190 
191     /**
192      * Execute after buildinfo has been generated for current build (eventually aggregated).
193      *
194      * @param artifacts a Map of artifacts added to the build info with their associated property key prefix
195      *         (<code>outputs.[#module.].#artifact</code>)
196      */
197     abstract void execute( Map<Artifact, String> artifacts )
198         throws MojoExecutionException;
199 
200     protected void skip( MavenProject last )
201         throws MojoExecutionException
202     {
203         getLog().info( "Skipping intermediate goal run, aggregate will be " + last.getArtifactId() );
204     }
205 
206     protected void copyAggregateToRoot( File aggregate )
207         throws MojoExecutionException
208     {
209         if ( reactorProjects.size() == 1 )
210         {
211             // mono-module, no aggregate file to deal with
212             return;
213         }
214 
215         // copy aggregate file to root target directory
216         MavenProject root = getExecutionRoot();
217         String extension = aggregate.getName().substring( aggregate.getName().lastIndexOf( '.' ) );
218         File rootCopy = new File( root.getBuild().getDirectory(),
219                                   root.getArtifactId() + '-' + root.getVersion() + extension );
220         try
221         {
222             FileUtils.copyFile( aggregate, rootCopy );
223             getLog().info( "Aggregate " + extension.substring( 1 ) + " copied to " + rootCopy );
224         }
225         catch ( IOException ioe )
226         {
227             throw new MojoExecutionException( "Could not copy " + aggregate + "to " + rootCopy );
228         }
229     }
230 
231     /**
232      * Generate buildinfo file.
233      *
234      * @param mono is it a mono-module build?
235      * @return a Map of artifacts added to the build info with their associated property key prefix
236      *         (<code>outputs.[#module.].#artifact</code>)
237      * @throws MojoExecutionException
238      */
239     protected Map<Artifact, String> generateBuildinfo( boolean mono )
240         throws MojoExecutionException
241     {
242         MavenProject root = mono ? project : getExecutionRoot();
243 
244         buildinfoFile.getParentFile().mkdirs();
245 
246         try ( PrintWriter p = new PrintWriter( new BufferedWriter(
247                 new OutputStreamWriter( new FileOutputStream( buildinfoFile ), StandardCharsets.UTF_8 ) ) ) )
248         {
249             BuildInfoWriter bi = new BuildInfoWriter( getLog(), p, mono, artifactHandlerManager );
250             bi.setIgnoreJavadoc( ignoreJavadoc );
251             bi.setIgnore( ignore );
252             bi.setToolchain( getToolchain() );
253 
254             bi.printHeader( root, mono ? null : project, reproducible );
255 
256             // artifact(s) fingerprints
257             if ( mono )
258             {
259                 bi.printArtifacts( project );
260             }
261             else
262             {
263                 for ( MavenProject project : reactorProjects )
264                 {
265                     if ( !isSkip( project ) )
266                     {
267                         bi.printArtifacts( project );
268                     }
269                 }
270             }
271 
272             if ( p.checkError() )
273             {
274                 throw new MojoExecutionException( "Write error to " + buildinfoFile );
275             }
276 
277             return bi.getArtifacts();
278         }
279         catch ( IOException e )
280         {
281             throw new MojoExecutionException( "Error creating file " + buildinfoFile, e );
282         }
283     }
284 
285     protected MavenProject getExecutionRoot()
286     {
287         for ( MavenProject p : reactorProjects )
288         {
289             if ( p.isExecutionRoot() )
290             {
291                 return p;
292             }
293         }
294         return null;
295     }
296 
297     private MavenProject getLastProject()
298     {
299         int i = reactorProjects.size();
300         while ( i > 0 )
301         {
302             MavenProject project = reactorProjects.get( --i );
303             if ( !isSkip( project ) )
304             {
305                 return project;
306             }
307         }
308         return null;
309     }
310 
311     private boolean isSkip( MavenProject project )
312     {
313         return detectSkip && PluginUtil.isSkip( project );
314     }
315 
316     private Toolchain getToolchain()
317     {
318         Toolchain tc = null;
319         if ( toolchainManager != null )
320         {
321             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
322         }
323 
324         return tc;
325     }
326 }