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.IOException;
24  import java.io.OutputStreamWriter;
25  import java.io.PrintWriter;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.*;
28  import java.text.SimpleDateFormat;
29  import java.util.Date;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.stream.Collectors;
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      @Component
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 a glob matching against <code>${groupId}/${filename}</code>, for example
83       * <code>*</>/*.xml</code>.
84       */
85      @Parameter(property = "buildinfo.ignore", defaultValue = "")
86      private List<String> ignore;
87  
88      /**
89       * Detect projects/modules with install or deploy skipped: avoid taking fingerprints.
90       */
91      @Parameter(property = "buildinfo.detect.skip", defaultValue = "true")
92      private boolean detectSkip;
93  
94      /**
95       * Avoid taking fingerprints for modules specified as glob matching against <code>${groupId}/${artifactId}</code>.
96       * @since 3.5.0
97       */
98      @Parameter
99      private List<String> skipModules;
100 
101     private List<PathMatcher> skipModulesMatcher = null;
102 
103     /**
104      * Makes the generated {@code .buildinfo} file reproducible, by dropping detailed environment recording: OS will be
105      * recorded as "Windows" or "Unix", JVM version as major version only.
106      *
107      * @since 3.1.0
108      */
109     @Parameter(property = "buildinfo.reproducible", defaultValue = "false")
110     private boolean reproducible;
111 
112     /**
113      * The current build session instance. This is used for toolchain manager API calls.
114      */
115     @Component
116     private MavenSession session;
117 
118     /**
119      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
120      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
121      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
122      *
123      * @since 3.2.0
124      */
125     @Parameter(defaultValue = "${project.build.outputTimestamp}")
126     private String outputTimestamp;
127 
128     /**
129      * To obtain a toolchain if possible.
130      */
131     @Component
132     private ToolchainManager toolchainManager;
133 
134     @Component
135     protected ArtifactHandlerManager artifactHandlerManager;
136 
137     @Component
138     protected RuntimeInformation rtInformation;
139 
140     @Override
141     public void execute() throws MojoExecutionException {
142         boolean mono = reactorProjects.size() == 1;
143 
144         MavenArchiver archiver = new MavenArchiver();
145         Date timestamp = archiver.parseOutputTimestamp(outputTimestamp);
146         if (timestamp == null) {
147             getLog().warn("Reproducible Build not activated by project.build.outputTimestamp property: "
148                     + "see https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
149         } else {
150             if (getLog().isDebugEnabled()) {
151                 getLog().debug("project.build.outputTimestamp = \"" + outputTimestamp + "\" => "
152                         + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(timestamp));
153             }
154 
155             // check if timestamp well defined in a project from reactor
156             boolean parentInReactor = false;
157             MavenProject reactorParent = project;
158             while (reactorProjects.contains(reactorParent.getParent())) {
159                 parentInReactor = true;
160                 reactorParent = reactorParent.getParent();
161             }
162             String prop = reactorParent.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
163             if (prop == null) {
164                 getLog().error("project.build.outputTimestamp property should not be inherited but defined in "
165                         + (parentInReactor ? "parent POM from reactor " : "POM ") + reactorParent.getFile());
166             }
167         }
168 
169         if (!mono) {
170             // if module skips install and/or deploy
171             if (isSkip(project)) {
172                 getLog().info("Skipping goal because module skips install and/or deploy");
173                 return;
174             }
175             // if multi-module build, generate (aggregate) buildinfo only in last module
176             MavenProject last = getLastProject();
177             if (project != last) {
178                 skip(last);
179                 return;
180             }
181         }
182 
183         // generate buildinfo
184         Map<Artifact, String> artifacts = generateBuildinfo(mono);
185         getLog().info("Saved " + (mono ? "" : "aggregate ") + "info on build to " + buildinfoFile);
186 
187         copyAggregateToRoot(buildinfoFile);
188 
189         execute(artifacts);
190     }
191 
192     /**
193      * Execute after buildinfo has been generated for current build (eventually aggregated).
194      *
195      * @param artifacts a Map of artifacts added to the build info with their associated property key prefix
196      *         (<code>outputs.[#module.].#artifact</code>)
197      */
198     abstract void execute(Map<Artifact, String> artifacts) throws MojoExecutionException;
199 
200     protected void skip(MavenProject last) throws MojoExecutionException {
201         getLog().info("Skipping intermediate goal run, aggregate will be " + last.getArtifactId());
202     }
203 
204     protected void copyAggregateToRoot(File aggregate) throws MojoExecutionException {
205         if (reactorProjects.size() == 1) {
206             // mono-module, no aggregate file to deal with
207             return;
208         }
209 
210         // copy aggregate file to root target directory
211         MavenProject root = getExecutionRoot();
212         String extension = aggregate.getName().substring(aggregate.getName().lastIndexOf('.'));
213         File rootCopy =
214                 new File(root.getBuild().getDirectory(), root.getArtifactId() + '-' + root.getVersion() + extension);
215         try {
216             FileUtils.copyFile(aggregate, rootCopy);
217             getLog().info("Aggregate " + extension.substring(1) + " copied to " + rootCopy);
218         } catch (IOException ioe) {
219             throw new MojoExecutionException("Could not copy " + aggregate + "to " + rootCopy);
220         }
221     }
222 
223     /**
224      * Generate buildinfo file.
225      *
226      * @param mono is it a mono-module build?
227      * @return a Map of artifacts added to the build info with their associated property key prefix
228      *         (<code>outputs.[#module.].#artifact</code>)
229      * @throws MojoExecutionException if anything goes wrong
230      */
231     protected Map<Artifact, String> generateBuildinfo(boolean mono) throws MojoExecutionException {
232         MavenProject root = mono ? project : getExecutionRoot();
233 
234         buildinfoFile.getParentFile().mkdirs();
235 
236         try (PrintWriter p = new PrintWriter(new BufferedWriter(
237                 new OutputStreamWriter(Files.newOutputStream(buildinfoFile.toPath()), StandardCharsets.UTF_8)))) {
238             BuildInfoWriter bi = new BuildInfoWriter(getLog(), p, mono, artifactHandlerManager, rtInformation);
239             bi.setIgnoreJavadoc(ignoreJavadoc);
240             bi.setIgnore(ignore);
241             bi.setToolchain(getToolchain());
242 
243             bi.printHeader(root, mono ? null : project, reproducible);
244 
245             // artifact(s) fingerprints
246             if (mono) {
247                 bi.printArtifacts(project);
248             } else {
249                 for (MavenProject project : reactorProjects) {
250                     if (!isSkip(project)) {
251                         bi.printArtifacts(project);
252                     }
253                 }
254             }
255 
256             if (p.checkError()) {
257                 throw new MojoExecutionException("Write error to " + buildinfoFile);
258             }
259 
260             return bi.getArtifacts();
261         } catch (IOException e) {
262             throw new MojoExecutionException("Error creating file " + buildinfoFile, e);
263         }
264     }
265 
266     protected MavenProject getExecutionRoot() {
267         for (MavenProject p : reactorProjects) {
268             if (p.isExecutionRoot()) {
269                 return p;
270             }
271         }
272         return null;
273     }
274 
275     private MavenProject getLastProject() {
276         int i = reactorProjects.size();
277         while (i > 0) {
278             MavenProject project = reactorProjects.get(--i);
279             if (!isSkip(project)) {
280                 return project;
281             }
282         }
283         return null;
284     }
285 
286     private boolean isSkip(MavenProject project) {
287         // manual/configured module skip
288         boolean skipModule = false;
289         if (skipModules != null && !skipModules.isEmpty()) {
290             if (skipModulesMatcher == null) {
291                 FileSystem fs = FileSystems.getDefault();
292                 skipModulesMatcher = skipModules.stream()
293                         .map(i -> fs.getPathMatcher("glob:" + i))
294                         .collect(Collectors.toList());
295             }
296             Path path = Paths.get(project.getGroupId() + '/' + project.getArtifactId());
297             skipModule = skipModulesMatcher.stream().anyMatch(m -> m.matches(path));
298         }
299         // detected skip
300         return skipModule || (detectSkip && PluginUtil.isSkip(project));
301     }
302 
303     private Toolchain getToolchain() {
304         Toolchain tc = null;
305         if (toolchainManager != null) {
306             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
307         }
308 
309         return tc;
310     }
311 }