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.text.SimpleDateFormat;
29  import java.util.Date;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  
34  import org.apache.maven.archiver.MavenArchiver;
35  import org.apache.maven.artifact.Artifact;
36  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
37  import org.apache.maven.execution.MavenSession;
38  import org.apache.maven.plugin.AbstractMojo;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugins.annotations.Component;
41  import org.apache.maven.plugins.annotations.Parameter;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.rtinfo.RuntimeInformation;
44  import org.apache.maven.shared.utils.io.FileUtils;
45  import org.apache.maven.toolchain.Toolchain;
46  import org.apache.maven.toolchain.ToolchainManager;
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 extends AbstractMojo {
54      /**
55       * The Maven project.
56       */
57      @Parameter(defaultValue = "${project}", readonly = true)
58      protected MavenProject project;
59  
60      /**
61       * The reactor projects.
62       */
63      @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true)
64      protected List<MavenProject> reactorProjects;
65  
66      /**
67       * Location of the generated buildinfo file.
68       */
69      @Parameter(
70              defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo",
71              required = true,
72              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     @Component
128     protected RuntimeInformation rtInformation;
129 
130     @Override
131     public void execute() throws MojoExecutionException {
132         boolean mono = reactorProjects.size() == 1;
133 
134         MavenArchiver archiver = new MavenArchiver();
135         Date timestamp = archiver.parseOutputTimestamp(outputTimestamp);
136         if (timestamp == null) {
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         } else {
140             if (getLog().isDebugEnabled()) {
141                 getLog().debug("project.build.outputTimestamp = \"" + outputTimestamp + "\" => "
142                         + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(timestamp));
143             }
144 
145             // check if timestamp well defined in a project from reactor
146             boolean parentInReactor = false;
147             MavenProject reactorParent = project;
148             while (reactorProjects.contains(reactorParent.getParent())) {
149                 parentInReactor = true;
150                 reactorParent = reactorParent.getParent();
151             }
152             String prop = reactorParent.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
153             if (prop == null) {
154                 getLog().error("project.build.outputTimestamp property should not be inherited but defined in "
155                         + (parentInReactor ? "parent POM from reactor " : "POM ") + reactorParent.getFile());
156             }
157         }
158 
159         if (!mono) {
160             // if module skips install and/or deploy
161             if (isSkip(project)) {
162                 getLog().info("Skipping goal because module skips install and/or deploy");
163                 return;
164             }
165             // if multi-module build, generate (aggregate) buildinfo only in last module
166             MavenProject last = getLastProject();
167             if (project != last) {
168                 skip(last);
169                 return;
170             }
171         }
172 
173         // generate buildinfo
174         Map<Artifact, String> artifacts = generateBuildinfo(mono);
175         getLog().info("Saved " + (mono ? "" : "aggregate ") + "info on build to " + buildinfoFile);
176 
177         copyAggregateToRoot(buildinfoFile);
178 
179         execute(artifacts);
180     }
181 
182     /**
183      * Execute after buildinfo has been generated for current build (eventually aggregated).
184      *
185      * @param artifacts a Map of artifacts added to the build info with their associated property key prefix
186      *         (<code>outputs.[#module.].#artifact</code>)
187      */
188     abstract void execute(Map<Artifact, String> artifacts) throws MojoExecutionException;
189 
190     protected void skip(MavenProject last) throws MojoExecutionException {
191         getLog().info("Skipping intermediate goal run, aggregate will be " + last.getArtifactId());
192     }
193 
194     protected void copyAggregateToRoot(File aggregate) throws MojoExecutionException {
195         if (reactorProjects.size() == 1) {
196             // mono-module, no aggregate file to deal with
197             return;
198         }
199 
200         // copy aggregate file to root target directory
201         MavenProject root = getExecutionRoot();
202         String extension = aggregate.getName().substring(aggregate.getName().lastIndexOf('.'));
203         File rootCopy =
204                 new File(root.getBuild().getDirectory(), root.getArtifactId() + '-' + root.getVersion() + extension);
205         try {
206             FileUtils.copyFile(aggregate, rootCopy);
207             getLog().info("Aggregate " + extension.substring(1) + " copied to " + rootCopy);
208         } catch (IOException ioe) {
209             throw new MojoExecutionException("Could not copy " + aggregate + "to " + rootCopy);
210         }
211     }
212 
213     /**
214      * Generate buildinfo file.
215      *
216      * @param mono is it a mono-module build?
217      * @return a Map of artifacts added to the build info with their associated property key prefix
218      *         (<code>outputs.[#module.].#artifact</code>)
219      * @throws MojoExecutionException
220      */
221     protected Map<Artifact, String> generateBuildinfo(boolean mono) throws MojoExecutionException {
222         MavenProject root = mono ? project : getExecutionRoot();
223 
224         buildinfoFile.getParentFile().mkdirs();
225 
226         try (PrintWriter p = new PrintWriter(new BufferedWriter(
227                 new OutputStreamWriter(new FileOutputStream(buildinfoFile), StandardCharsets.UTF_8)))) {
228             BuildInfoWriter bi = new BuildInfoWriter(getLog(), p, mono, artifactHandlerManager, rtInformation);
229             bi.setIgnoreJavadoc(ignoreJavadoc);
230             bi.setIgnore(ignore);
231             bi.setToolchain(getToolchain());
232 
233             bi.printHeader(root, mono ? null : project, reproducible);
234 
235             // artifact(s) fingerprints
236             if (mono) {
237                 bi.printArtifacts(project);
238             } else {
239                 for (MavenProject project : reactorProjects) {
240                     if (!isSkip(project)) {
241                         bi.printArtifacts(project);
242                     }
243                 }
244             }
245 
246             if (p.checkError()) {
247                 throw new MojoExecutionException("Write error to " + buildinfoFile);
248             }
249 
250             return bi.getArtifacts();
251         } catch (IOException e) {
252             throw new MojoExecutionException("Error creating file " + buildinfoFile, e);
253         }
254     }
255 
256     protected MavenProject getExecutionRoot() {
257         for (MavenProject p : reactorProjects) {
258             if (p.isExecutionRoot()) {
259                 return p;
260             }
261         }
262         return null;
263     }
264 
265     private MavenProject getLastProject() {
266         int i = reactorProjects.size();
267         while (i > 0) {
268             MavenProject project = reactorProjects.get(--i);
269             if (!isSkip(project)) {
270                 return project;
271             }
272         }
273         return null;
274     }
275 
276     private boolean isSkip(MavenProject project) {
277         return detectSkip && PluginUtil.isSkip(project);
278     }
279 
280     private Toolchain getToolchain() {
281         Toolchain tc = null;
282         if (toolchainManager != null) {
283             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
284         }
285 
286         return tc;
287     }
288 }