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