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.FileSystem;
28  import java.nio.file.FileSystems;
29  import java.nio.file.Files;
30  import java.nio.file.LinkOption;
31  import java.nio.file.Path;
32  import java.nio.file.PathMatcher;
33  import java.nio.file.Paths;
34  import java.nio.file.StandardCopyOption;
35  import java.time.Instant;
36  import java.time.format.DateTimeFormatter;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.stream.Collectors;
40  
41  import org.apache.maven.archiver.MavenArchiver;
42  import org.apache.maven.execution.MavenSession;
43  import org.apache.maven.plugin.AbstractMojo;
44  import org.apache.maven.plugin.MojoExecutionException;
45  import org.apache.maven.plugin.logging.Log;
46  import org.apache.maven.plugins.annotations.Component;
47  import org.apache.maven.plugins.annotations.Parameter;
48  import org.apache.maven.project.MavenProject;
49  import org.apache.maven.rtinfo.RuntimeInformation;
50  import org.apache.maven.toolchain.Toolchain;
51  import org.apache.maven.toolchain.ToolchainManager;
52  import org.eclipse.aether.artifact.Artifact;
53  
54  /**
55   * Base buildinfo-generating class, for goals related to Reproducible Builds {@code .buildinfo} files.
56   *
57   * @since 3.2.0
58   */
59  public abstract class AbstractBuildinfoMojo extends AbstractMojo {
60      /**
61       * The Maven project.
62       */
63      @Component
64      protected MavenProject project;
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(property = "buildinfo.skipModules")
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     protected 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     protected String outputTimestamp;
127 
128     /**
129      * Diagnose {@code outputTimestamp} effective value based on execution context.
130      *
131      * @since 3.5.2
132      */
133     @Parameter(property = "diagnose", defaultValue = "false")
134     private boolean diagnose;
135 
136     /**
137      * To obtain a toolchain if possible.
138      */
139     @Component
140     private ToolchainManager toolchainManager;
141 
142     @Component
143     protected RuntimeInformation rtInformation;
144 
145     @Override
146     public void execute() throws MojoExecutionException {
147         boolean mono = session.getProjects().size() == 1;
148 
149         hasBadOutputTimestamp(outputTimestamp, getLog(), project, session.getProjects(), diagnose);
150 
151         if (!mono) {
152             // if module skips install and/or deploy
153             if (isSkip(project)) {
154                 getLog().info("Skipping goal because module skips install and/or deploy");
155                 return;
156             }
157             // if multi-module build, generate (aggregate) buildinfo only in last module
158             MavenProject last = getLastProject();
159             if (project != last) {
160                 skip(last);
161                 return;
162             }
163         }
164 
165         // generate buildinfo
166         Map<Artifact, String> artifacts = generateBuildinfo(mono);
167         getLog().info("Saved " + (mono ? "" : "aggregate ") + "info on build to " + buildinfoFile);
168 
169         copyAggregateToRoot(buildinfoFile);
170 
171         execute(artifacts);
172     }
173 
174     static boolean hasBadOutputTimestamp(
175             String outputTimestamp,
176             Log log,
177             MavenProject project,
178             List<MavenProject> reactorProjects,
179             boolean diagnose) {
180         Instant timestamp =
181                 MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).orElse(null);
182         String effective = ((timestamp == null) ? "disabled" : DateTimeFormatter.ISO_INSTANT.format(timestamp));
183 
184         if (diagnose) {
185             diagnose(outputTimestamp, log, project, reactorProjects, effective);
186         }
187 
188         if (timestamp == null) {
189             log.error("Reproducible Build not activated by project.build.outputTimestamp property: "
190                     + "see https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
191 
192             String projectProperty = project.getProperties().getProperty("project.build.outputTimestamp");
193             if (projectProperty != null && projectProperty.startsWith("${git.")) {
194                 log.error("project.build.outputTimestamp = \"" + projectProperty + "\": isn't Git value set?");
195                 log.error("Did validate phase run and Git plugin set the value?");
196                 if (project.getPackaging().equals("pom")) {
197                     log.error("if using git-commit-id-plugin, <skipPoms>false</skipPoms> may solve the issue.");
198                 }
199             }
200             return true;
201         }
202 
203         if (log.isDebugEnabled()) {
204             log.debug("project.build.outputTimestamp = \"" + outputTimestamp + "\" => "
205                     + (effective.equals(outputTimestamp) ? "" : (" => " + effective)));
206         }
207 
208         // check if timestamp defined in a project from reactor: info if it is not the case
209         boolean parentInReactor = false;
210         MavenProject reactorParent = project;
211         while (reactorProjects.contains(reactorParent.getParent())) {
212             parentInReactor = true;
213             reactorParent = reactorParent.getParent();
214         }
215         String prop = reactorParent.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
216         if (prop == null) {
217             log.info("<project.build.outputTimestamp> property (= " + outputTimestamp + ") is inherited"
218                     + (parentInReactor ? " from outside the reactor" : "") + ", you can override in "
219                     + (parentInReactor ? "parent POM from reactor " + reactorParent.getFile() : "pom.xml"));
220             return false;
221         }
222 
223         return false;
224     }
225 
226     static void diagnose(
227             String outputTimestamp,
228             Log log,
229             MavenProject project,
230             List<MavenProject> reactorProjects,
231             String effective) {
232         log.info("outputTimestamp = " + outputTimestamp
233                 + (effective.equals(outputTimestamp) ? "" : (" => " + effective)));
234 
235         String projectProperty = project.getProperties().getProperty("project.build.outputTimestamp");
236         String modelProperty = project.getModel().getProperties().getProperty("project.build.outputTimestamp");
237         String originalModelProperty =
238                 project.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
239 
240         log.info("plugin outputTimestamp parameter diagnostics:" + System.lineSeparator()
241                 + "        - plugin outputTimestamp parameter (defaultValue=\"${project.build.outputTimestamp}\") = "
242                 + outputTimestamp + System.lineSeparator()
243                 + "        - project.build.outputTimestamp property from project = " + projectProperty
244                 + System.lineSeparator()
245                 + "        - project.build.outputTimestamp property from project model = " + modelProperty
246                 + System.lineSeparator()
247                 + "        - project.build.outputTimestamp property from project original model = "
248                 + originalModelProperty);
249 
250         MavenProject parent = project.getParent();
251         if (parent != null) {
252             StringBuilder sb = new StringBuilder("Inheritance analysis property:" + System.lineSeparator()
253                     + "        - current " + project.getId() + " property = " + projectProperty);
254             while (parent != null) {
255                 String parentProperty = parent.getProperties().getProperty("project.build.outputTimestamp");
256                 sb.append(System.lineSeparator());
257                 sb.append("        - " + (reactorProjects.contains(parent) ? "reactor" : "external") + " parent "
258                         + parent.getId() + " property = " + parentProperty);
259                 if (!projectProperty.equals(parentProperty)) {
260                     break;
261                 }
262                 parent = parent.getParent();
263             }
264             log.info(sb.toString());
265         }
266     }
267 
268     /**
269      * Execute after buildinfo has been generated for current build (eventually aggregated).
270      *
271      * @param artifacts a Map of artifacts added to the build info with their associated property key prefix
272      *         (<code>outputs.[#module.].#artifact</code>)
273      */
274     abstract void execute(Map<Artifact, String> artifacts) throws MojoExecutionException;
275 
276     protected void skip(MavenProject last) throws MojoExecutionException {
277         getLog().info("Skipping intermediate goal run, aggregate will be " + last.getArtifactId());
278     }
279 
280     protected void copyAggregateToRoot(File aggregate) throws MojoExecutionException {
281         if (session.getProjects().size() == 1) {
282             // mono-module, no aggregate file to deal with
283             return;
284         }
285 
286         // copy aggregate file to root target directory
287         MavenProject root = getExecutionRoot();
288         String extension = aggregate.getName().substring(aggregate.getName().lastIndexOf('.'));
289         File rootCopy =
290                 new File(root.getBuild().getDirectory(), root.getArtifactId() + '-' + root.getVersion() + extension);
291         try {
292             rootCopy.getParentFile().mkdirs();
293             Files.copy(
294                     aggregate.toPath(),
295                     rootCopy.toPath(),
296                     LinkOption.NOFOLLOW_LINKS,
297                     StandardCopyOption.REPLACE_EXISTING);
298             getLog().info("Aggregate " + extension.substring(1) + " copied to " + rootCopy);
299         } catch (IOException ioe) {
300             throw new MojoExecutionException("Could not copy " + aggregate + " to " + rootCopy, ioe);
301         }
302     }
303 
304     protected BuildInfoWriter newBuildInfoWriter(PrintWriter p, boolean mono) {
305         BuildInfoWriter bi = new BuildInfoWriter(getLog(), p, mono, rtInformation);
306         bi.setIgnoreJavadoc(ignoreJavadoc);
307         bi.setIgnore(ignore);
308         bi.setToolchain(getToolchain());
309 
310         return bi;
311     }
312     /**
313      * Generate buildinfo file.
314      *
315      * @param mono is it a mono-module build?
316      * @return a Map of artifacts added to the build info with their associated property key prefix
317      *         (<code>outputs.[#module.].#artifact</code>)
318      * @throws MojoExecutionException if anything goes wrong
319      */
320     protected Map<Artifact, String> generateBuildinfo(boolean mono) throws MojoExecutionException {
321         MavenProject root = mono ? project : getExecutionRoot();
322 
323         buildinfoFile.getParentFile().mkdirs();
324 
325         try (PrintWriter p = new PrintWriter(new BufferedWriter(
326                 new OutputStreamWriter(Files.newOutputStream(buildinfoFile.toPath()), StandardCharsets.UTF_8)))) {
327             BuildInfoWriter bi = newBuildInfoWriter(p, mono);
328             bi.printHeader(root, mono ? null : project, reproducible);
329 
330             // artifact(s) fingerprints
331             if (mono) {
332                 bi.printArtifacts(project);
333             } else {
334                 for (MavenProject project : session.getProjects()) {
335                     if (!isSkip(project)) {
336                         bi.printArtifacts(project);
337                     }
338                 }
339             }
340 
341             if (p.checkError()) {
342                 throw new MojoExecutionException("Write error to " + buildinfoFile);
343             }
344 
345             return bi.getArtifacts();
346         } catch (IOException e) {
347             throw new MojoExecutionException("Error creating file " + buildinfoFile, e);
348         }
349     }
350 
351     protected MavenProject getExecutionRoot() {
352         for (MavenProject p : session.getProjects()) {
353             if (p.isExecutionRoot()) {
354                 return p;
355             }
356         }
357         return null;
358     }
359 
360     private MavenProject getLastProject() {
361         int i = session.getProjects().size();
362         while (i > 0) {
363             MavenProject project = session.getProjects().get(--i);
364             if (!isSkip(project)) {
365                 return project;
366             }
367         }
368         return null;
369     }
370 
371     protected boolean isSkip(MavenProject project) {
372         // manual/configured module skip
373         boolean skipModule = false;
374         if (skipModules != null && !skipModules.isEmpty()) {
375             if (skipModulesMatcher == null) {
376                 FileSystem fs = FileSystems.getDefault();
377                 skipModulesMatcher = skipModules.stream()
378                         .map(i -> fs.getPathMatcher("glob:" + i))
379                         .collect(Collectors.toList());
380             }
381             Path path = Paths.get(project.getGroupId() + '/' + project.getArtifactId());
382             skipModule = skipModulesMatcher.stream().anyMatch(m -> m.matches(path));
383         }
384         // detected skip
385         return skipModule || (detectSkip && PluginUtil.isSkip(project));
386     }
387 
388     private Toolchain getToolchain() {
389         Toolchain tc = null;
390         if (toolchainManager != null) {
391             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
392         }
393 
394         return tc;
395     }
396 }