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.Parameter;
47  import org.apache.maven.project.MavenProject;
48  import org.apache.maven.rtinfo.RuntimeInformation;
49  import org.apache.maven.toolchain.Toolchain;
50  import org.apache.maven.toolchain.ToolchainManager;
51  import org.eclipse.aether.artifact.Artifact;
52  
53  /**
54   * Base buildinfo-generating class, for goals related to Reproducible Builds {@code .buildinfo} files.
55   *
56   * @since 3.2.0
57   */
58  public abstract class AbstractBuildinfoMojo extends AbstractMojo {
59  
60      /**
61       * Location of the generated buildinfo file.
62       */
63      @Parameter(
64              defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo",
65              required = true,
66              readonly = true)
67      protected File buildinfoFile;
68  
69      /**
70       * Ignore javadoc attached artifacts from buildinfo generation.
71       */
72      @Parameter(property = "buildinfo.ignoreJavadoc", defaultValue = "true")
73      private boolean ignoreJavadoc;
74  
75      /**
76       * Artifacts to ignore, specified as a glob matching against <code>${groupId}/${filename}</code>, for example
77       * <code>*</>/*.xml</code>. If all artifacts from a module are to be ignored, prefer {@code skipModules}.
78       */
79      @Parameter(property = "buildinfo.ignore", defaultValue = "")
80      private List<String> ignore;
81  
82      /**
83       * Auto-detect projects/modules with install or deploy skipped: avoid taking fingerprints.
84       * If auto-detection gives unexpected results, prefer handwritten {@code skipModules}.
85       */
86      @Parameter(property = "buildinfo.detect.skip", defaultValue = "true")
87      private boolean detectSkip;
88  
89      /**
90       * Avoid taking fingerprints for full modules, specified as glob matching against
91       * <code>${groupId}/${artifactId}</code>.
92       * For ignoring only a few files instead of full modules, see {@code ignore}.
93       * @since 3.5.0
94       */
95      @Parameter(property = "buildinfo.skipModules")
96      private List<String> skipModules;
97  
98      private List<PathMatcher> skipModulesMatcher = null;
99  
100     /**
101      * Makes the generated {@code .buildinfo} file reproducible, by dropping detailed environment recording: OS will be
102      * recorded as "Windows" or "Unix", JVM version as major version only.
103      *
104      * @since 3.1.0
105      */
106     @Parameter(property = "buildinfo.reproducible", defaultValue = "false")
107     private boolean reproducible;
108 
109     /**
110      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
111      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
112      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
113      *
114      * @since 3.2.0
115      */
116     @Parameter(defaultValue = "${project.build.outputTimestamp}")
117     protected String outputTimestamp;
118 
119     /**
120      * Diagnose {@code outputTimestamp} effective value based on execution context.
121      *
122      * @since 3.5.2
123      */
124     @Parameter(property = "diagnose", defaultValue = "false")
125     private boolean diagnose;
126 
127     /**
128      * To obtain a toolchain if possible.
129      */
130     private final ToolchainManager toolchainManager;
131 
132     protected final RuntimeInformation rtInformation;
133 
134     /**
135      * The Maven project.
136      */
137     protected final MavenProject project;
138 
139     /**
140      * The current build session instance. This is used for toolchain manager API calls.
141      */
142     protected final MavenSession session;
143 
144     protected AbstractBuildinfoMojo(
145             ToolchainManager toolchainManager,
146             RuntimeInformation rtInformation,
147             MavenProject project,
148             MavenSession session) {
149         this.toolchainManager = toolchainManager;
150         this.rtInformation = rtInformation;
151         this.project = project;
152         this.session = session;
153     }
154 
155     @Override
156     public void execute() throws MojoExecutionException {
157         boolean mono = session.getProjects().size() == 1;
158 
159         hasBadOutputTimestamp(outputTimestamp, getLog(), project, session, diagnose);
160 
161         if (!mono) {
162             // if module skips install and/or deploy
163             if (detectSkip && PluginUtil.isSkip(project)) {
164                 getLog().info("Auto-skipping goal because module skips install and/or deploy");
165                 return;
166             }
167             if (isSkipModule(project)) {
168                 getLog().info("Skipping goal for module");
169                 return;
170             }
171             // if multi-module build, generate (aggregate) buildinfo only in last module
172             MavenProject last = getLastProject();
173             if (project != last) {
174                 skip(last);
175                 return;
176             }
177         }
178 
179         // generate buildinfo
180         Map<Artifact, String> artifacts = generateBuildinfo(mono);
181         getLog().info("Saved " + (mono ? "" : "aggregate ") + "info on build to " + buildinfoFile);
182 
183         copyAggregateToRoot(buildinfoFile);
184 
185         execute(artifacts);
186     }
187 
188     static boolean hasBadOutputTimestamp(
189             String outputTimestamp, Log log, MavenProject project, MavenSession session, boolean diagnose) {
190         Instant timestamp =
191                 MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).orElse(null);
192         String effective = ((timestamp == null) ? "disabled" : DateTimeFormatter.ISO_INSTANT.format(timestamp));
193 
194         if (diagnose) {
195             diagnose(outputTimestamp, log, project, session, effective);
196         }
197 
198         if (timestamp == null) {
199             log.error("Reproducible Build not activated by project.build.outputTimestamp property: "
200                     + "see https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
201 
202             String projectProperty = project.getProperties().getProperty("project.build.outputTimestamp");
203             if (projectProperty != null && projectProperty.startsWith("${git.")) {
204                 log.error("project.build.outputTimestamp = \"" + projectProperty + "\": isn't Git value set?");
205                 log.error("Did validate phase run and Git plugin set the value?");
206                 if (project.getPackaging().equals("pom")) {
207                     log.error("if using git-commit-id-plugin, <skipPoms>false</skipPoms> may solve the issue.");
208                 }
209             }
210             return true;
211         }
212 
213         if (log.isDebugEnabled()) {
214             log.debug("project.build.outputTimestamp = \"" + outputTimestamp + "\" => "
215                     + (effective.equals(outputTimestamp) ? "" : (" => " + effective)));
216         }
217 
218         // check if timestamp defined in a project from reactor: info if it is not the case
219         boolean parentInReactor = false;
220         MavenProject reactorParent = project;
221         while (session.getProjects().contains(reactorParent.getParent())) {
222             parentInReactor = true;
223             reactorParent = reactorParent.getParent();
224         }
225         String prop = reactorParent.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
226         if (prop == null) {
227             log.info("<project.build.outputTimestamp> property (= " + outputTimestamp + ") is inherited"
228                     + (parentInReactor ? " from outside the reactor" : "") + ", you can override in "
229                     + (parentInReactor ? "parent POM from reactor " + reactorParent.getFile() : "pom.xml"));
230             return false;
231         }
232 
233         return false;
234     }
235 
236     static void diagnose(
237             String outputTimestamp, Log log, MavenProject project, MavenSession session, String effective) {
238         if (session.getProjects().size() > 1) {
239             MavenProject root = session.getTopLevelProject();
240             MavenProject first = session.getProjects().get(0);
241             Path firstRelative =
242                     root.getBasedir().toPath().relativize(first.getFile().toPath());
243             MavenProject firstParent = first.getParent();
244             log.info("reactor executionRoot = " + root.getId() + (root.equals(project) ? " (current)" : "")
245                     + System.lineSeparator()
246                     + "       reactor first = "
247                     + (first.equals(root) ? "executionRoot" : (first.getId() + " @ " + firstRelative))
248                     + System.lineSeparator()
249                     + "               first.parent = " + ((firstParent == null) ? firstParent : firstParent.getId()));
250             if (firstParent != null && session.getProjects().contains(firstParent)) {
251                 // should not happen...
252                 log.warn("reactor first parent = " + firstParent.getId() + " is in reactor");
253             }
254         }
255 
256         log.info("outputTimestamp = " + outputTimestamp
257                 + (effective.equals(outputTimestamp) ? "" : (" => " + effective)));
258 
259         String projectProperty = project.getProperties().getProperty("project.build.outputTimestamp");
260         String modelProperty = project.getModel().getProperties().getProperty("project.build.outputTimestamp");
261         String originalModelProperty =
262                 project.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
263 
264         log.info("plugin outputTimestamp parameter diagnostics:" + System.lineSeparator()
265                 + "        - plugin outputTimestamp parameter (defaultValue=\"${project.build.outputTimestamp}\") = "
266                 + outputTimestamp + System.lineSeparator()
267                 + "        - project.build.outputTimestamp property from project = " + projectProperty
268                 + System.lineSeparator()
269                 + "        - project.build.outputTimestamp property from project model = " + modelProperty
270                 + System.lineSeparator()
271                 + "        - project.build.outputTimestamp property from project original model = "
272                 + originalModelProperty);
273 
274         if (outputTimestamp == null) {
275             return;
276         }
277 
278         MavenProject parent = project.getParent();
279         if (parent != null) {
280             StringBuilder sb = new StringBuilder("Inheritance analysis for property:" + System.lineSeparator()
281                     + "        - current " + project.getId() + " property = " + projectProperty);
282             while (parent != null) {
283                 String parentProperty = parent.getProperties().getProperty("project.build.outputTimestamp");
284                 sb.append(System.lineSeparator());
285                 sb.append("        - " + (session.getProjects().contains(parent) ? "reactor" : "external") + " parent "
286                         + parent.getId() + " property = " + parentProperty);
287                 if (!projectProperty.equals(parentProperty)) {
288                     break;
289                 }
290                 parent = parent.getParent();
291             }
292             log.info(sb.toString());
293         }
294     }
295 
296     /**
297      * Execute after buildinfo has been generated for current build (eventually aggregated).
298      *
299      * @param artifacts a Map of artifacts added to the build info with their associated property key prefix
300      *         (<code>outputs.[#module.].#artifact</code>)
301      */
302     abstract void execute(Map<Artifact, String> artifacts) throws MojoExecutionException;
303 
304     protected void skip(MavenProject last) throws MojoExecutionException {
305         getLog().info("Skipping intermediate goal run, aggregate will be " + last.getArtifactId());
306     }
307 
308     protected File copyAggregateToRoot(File aggregate) throws MojoExecutionException {
309         if (session.getProjects().size() == 1) {
310             // mono-module, no aggregate file to deal with
311             return aggregate;
312         }
313 
314         // copy aggregate file to root target directory
315         MavenProject root = session.getTopLevelProject();
316         String extension = aggregate.getName().substring(aggregate.getName().lastIndexOf('.'));
317         File rootCopy =
318                 new File(root.getBuild().getDirectory(), root.getArtifactId() + '-' + root.getVersion() + extension);
319         try {
320             rootCopy.getParentFile().mkdirs();
321             Files.copy(
322                     aggregate.toPath(),
323                     rootCopy.toPath(),
324                     LinkOption.NOFOLLOW_LINKS,
325                     StandardCopyOption.REPLACE_EXISTING);
326             getLog().info("Aggregate " + extension.substring(1) + " copied to " + relative(rootCopy));
327         } catch (IOException ioe) {
328             throw new MojoExecutionException("Could not copy " + aggregate + " to " + rootCopy, ioe);
329         }
330         return rootCopy;
331     }
332 
333     protected BuildInfoWriter newBuildInfoWriter(PrintWriter p, boolean mono) {
334         BuildInfoWriter bi = new BuildInfoWriter(getLog(), p, mono, rtInformation);
335         bi.setIgnoreJavadoc(ignoreJavadoc);
336         bi.setIgnore(ignore);
337         bi.setToolchain(getToolchain());
338 
339         return bi;
340     }
341     /**
342      * Generate buildinfo file.
343      *
344      * @param mono is it a mono-module build?
345      * @return a Map of artifacts added to the build info with their associated property key prefix
346      *         (<code>outputs.[#module.].#artifact</code>)
347      * @throws MojoExecutionException if anything goes wrong
348      */
349     protected Map<Artifact, String> generateBuildinfo(boolean mono) throws MojoExecutionException {
350         MavenProject root = mono ? project : session.getTopLevelProject();
351 
352         buildinfoFile.getParentFile().mkdirs();
353 
354         try (PrintWriter p = new PrintWriter(new BufferedWriter(
355                 new OutputStreamWriter(Files.newOutputStream(buildinfoFile.toPath()), StandardCharsets.UTF_8)))) {
356             BuildInfoWriter bi = newBuildInfoWriter(p, mono);
357             bi.printHeader(root, mono ? null : project, reproducible);
358 
359             // artifact(s) fingerprints
360             if (mono) {
361                 bi.printArtifacts(project);
362             } else {
363                 for (MavenProject project : session.getProjects()) {
364                     if (!isSkip(project)) {
365                         bi.printArtifacts(project);
366                     }
367                 }
368             }
369 
370             if (p.checkError()) {
371                 throw new MojoExecutionException("Write error to " + buildinfoFile);
372             }
373 
374             return bi.getArtifacts();
375         } catch (IOException e) {
376             throw new MojoExecutionException("Error creating file " + buildinfoFile, e);
377         }
378     }
379 
380     protected MavenProject getLastProject() {
381         int i = session.getProjects().size();
382         while (i > 0) {
383             MavenProject project = session.getProjects().get(--i);
384             if (!isSkip(project)) {
385                 return project;
386             }
387         }
388         return null;
389     }
390 
391     protected boolean isSkip(MavenProject project) {
392         return isSkipModule(project) || (detectSkip && PluginUtil.isSkip(project));
393     }
394 
395     protected boolean isSkipModule(MavenProject project) {
396         if (skipModules == null || skipModules.isEmpty()) {
397             return false;
398         }
399         if (skipModulesMatcher == null) {
400             FileSystem fs = FileSystems.getDefault();
401             skipModulesMatcher = skipModules.stream()
402                     .map(i -> fs.getPathMatcher("glob:" + i))
403                     .collect(Collectors.toList());
404         }
405         Path path = Paths.get(project.getGroupId() + '/' + project.getArtifactId());
406         return skipModulesMatcher.stream().anyMatch(m -> m.matches(path));
407     }
408 
409     private Toolchain getToolchain() {
410         Toolchain tc = null;
411         if (toolchainManager != null) {
412             tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
413         }
414 
415         return tc;
416     }
417 
418     protected String relative(File file) {
419         File basedir = session.getTopLevelProject().getBasedir();
420         int length = basedir.getPath().length();
421         String path = file.getPath();
422         return path.substring(length + 1);
423     }
424 }