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.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.PrintWriter;
25  import java.nio.file.FileSystem;
26  import java.nio.file.FileSystems;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.PathMatcher;
30  import java.nio.file.Paths;
31  import java.nio.file.StandardCopyOption;
32  import java.util.LinkedHashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Properties;
36  import java.util.stream.Collectors;
37  
38  import org.apache.commons.codec.digest.DigestUtils;
39  import org.apache.maven.RepositoryUtils;
40  import org.apache.maven.plugin.MojoExecutionException;
41  import org.apache.maven.plugin.logging.Log;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.rtinfo.RuntimeInformation;
44  import org.apache.maven.toolchain.Toolchain;
45  import org.eclipse.aether.artifact.Artifact;
46  import org.eclipse.aether.artifact.DefaultArtifact;
47  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
48  
49  /**
50   * Buildinfo content writer.
51   */
52  class BuildInfoWriter {
53      private final Log log;
54      private final PrintWriter p;
55      private final boolean mono;
56      private final RuntimeInformation rtInformation;
57      private final Map<Artifact, String> artifacts = new LinkedHashMap<>();
58      private int projectCount = -1;
59      private boolean ignoreJavadoc = true;
60      private List<PathMatcher> ignore;
61      private Toolchain toolchain;
62  
63      BuildInfoWriter(Log log, PrintWriter p, boolean mono, RuntimeInformation rtInformation) {
64          this.log = log;
65          this.p = p;
66          this.mono = mono;
67          this.rtInformation = rtInformation;
68      }
69  
70      void printHeader(MavenProject project, MavenProject aggregate, boolean reproducible) {
71          p.println("# https://reproducible-builds.org/docs/jvm/");
72          p.println("buildinfo.version=1.0-SNAPSHOT");
73          p.println();
74          p.println("name=" + project.getName());
75          p.println("group-id=" + project.getGroupId());
76          p.println("artifact-id=" + project.getArtifactId());
77          p.println("version=" + project.getVersion());
78          p.println();
79          printSourceInformation(project);
80          p.println();
81          p.println("# build instructions");
82          p.println("build-tool=mvn");
83          // p.println( "# optional build setup url, as plugin parameter" );
84          p.println();
85          if (reproducible) {
86              p.println("# build environment information (simplified for reproducibility)");
87              p.println("java.version=" + extractJavaMajorVersion(System.getProperty("java.version")));
88              String ls = System.lineSeparator();
89              p.println("os.name=" + ("\n".equals(ls) ? "Unix" : "Windows"));
90          } else {
91              p.println("# effective build environment information");
92              p.println("java.version=" + System.getProperty("java.version"));
93              p.println("java.vendor=" + System.getProperty("java.vendor"));
94              p.println("os.name=" + System.getProperty("os.name"));
95              p.println("os.version=" + System.getProperty("os.version"));
96              p.println("os.arch=" + System.getProperty("os.arch"));
97              p.println("line.separator="
98                      + System.lineSeparator().replace("\r", "\\r").replace("\n", "\\n"));
99          }
100         p.println();
101         p.println("# Maven rebuild instructions and effective environment");
102         if (!reproducible) {
103             p.println("mvn.version=" + rtInformation.getMavenVersion());
104         }
105         if ((project.getPrerequisites() != null) && (project.getPrerequisites().getMaven() != null)) {
106             // TODO wrong algorithm, should reuse algorithm written in versions-maven-plugin
107             p.println("mvn.minimum.version=" + project.getPrerequisites().getMaven());
108         }
109         if (toolchain != null) {
110             String javaVersion = JdkToolchainUtil.getJavaVersion(toolchain);
111             if (reproducible) {
112                 javaVersion = extractJavaMajorVersion(javaVersion);
113             }
114             p.println("mvn.toolchain.jdk=" + javaVersion);
115         }
116 
117         if (!mono && (aggregate != null)) {
118             p.println("mvn.aggregate.artifact-id=" + aggregate.getArtifactId());
119         }
120 
121         p.println();
122         p.println("# " + (mono ? "" : "aggregated ") + "output");
123     }
124 
125     private static String extractJavaMajorVersion(String javaVersion) {
126         if (javaVersion.startsWith("1.")) {
127             javaVersion = javaVersion.substring(2);
128         }
129         int index = javaVersion.indexOf('.'); // for example 8.0_202
130         if (index < 0) {
131             index = javaVersion.indexOf('-'); // for example 17-ea
132         }
133         return (index < 0) ? javaVersion : javaVersion.substring(0, index);
134     }
135 
136     private void printSourceInformation(MavenProject project) {
137         boolean sourceAvailable = false;
138         p.println("# source information");
139         // p.println( "# TBD source.* artifact, url should be parameters" );
140         if (project.getScm() != null) {
141             sourceAvailable = true;
142             p.println("source.scm.uri=" + project.getScm().getConnection());
143             p.println("source.scm.tag=" + project.getScm().getTag());
144         } else {
145             p.println("# no scm configured in pom.xml");
146         }
147 
148         if (!sourceAvailable) {
149             log.warn("No source information available in buildinfo for rebuilders...");
150         }
151     }
152 
153     void printArtifacts(MavenProject project) throws MojoExecutionException {
154         String prefix = "outputs.";
155         if (!mono) {
156             // aggregated buildinfo output
157             projectCount++;
158             prefix += projectCount + ".";
159             p.println();
160             p.println(prefix + "coordinates=" + project.getGroupId() + ':' + project.getArtifactId());
161         }
162 
163         // detect Maven 4 consumer POM transient attachment
164         Artifact consumerPom = RepositoryUtils.toArtifacts(project.getAttachedArtifacts()).stream()
165                 .filter(a -> "pom".equals(a.getExtension()) && "consumer".equals(a.getClassifier()))
166                 .findAny()
167                 .orElse(null);
168 
169         int n = 0;
170         Artifact pomArtifact =
171                 new DefaultArtifact(project.getGroupId(), project.getArtifactId(), null, "pom", project.getVersion());
172         if (consumerPom != null) {
173             // Maven 4 transient consumer POM attachment is published as the POM, overrides build POM, see
174             // https://github.com/apache/maven/blob/c79a7a02721f0f9fd7e202e99f60b593461ba8cc/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java#L130-L155
175             try {
176                 Path pomFile = Files.createTempFile(Paths.get(project.getBuild().getDirectory()), "consumer-", ".pom");
177                 Files.copy(consumerPom.getFile().toPath(), pomFile, StandardCopyOption.REPLACE_EXISTING);
178                 pomArtifact = pomArtifact.setFile(pomFile.toFile());
179             } catch (IOException e) {
180                 p.println("Error processing consumer POM: " + e);
181             }
182         } else {
183             pomArtifact = pomArtifact.setFile(project.getFile());
184         }
185 
186         artifacts.put(pomArtifact, prefix + n);
187         if (isIgnore(pomArtifact)) {
188             p.println("# ignored " + getArtifactFilename(pomArtifact));
189         } else {
190             printFile(
191                     prefix + n++,
192                     pomArtifact.getGroupId(),
193                     pomArtifact.getFile(),
194                     project.getArtifactId() + '-' + project.getVersion() + ".pom");
195         }
196 
197         if (consumerPom != null) {
198             // build pom
199             Artifact buildPomArtifact = new DefaultArtifact(
200                     project.getGroupId(), project.getArtifactId(), "build", "pom", project.getVersion());
201             buildPomArtifact = buildPomArtifact.setFile(project.getFile());
202 
203             if (isIgnore(buildPomArtifact)) {
204                 p.println("# ignored " + getArtifactFilename(buildPomArtifact));
205             } else {
206                 artifacts.put(buildPomArtifact, prefix + n);
207                 printFile(
208                         prefix + n++,
209                         buildPomArtifact.getGroupId(),
210                         buildPomArtifact.getFile(),
211                         project.getArtifactId() + '-' + project.getVersion() + "-build.pom");
212             }
213         }
214 
215         if (project.getArtifact() == null) {
216             return;
217         }
218 
219         if (project.getArtifact().getFile() != null) {
220             Artifact main = RepositoryUtils.toArtifact(project.getArtifact());
221             if (isIgnore(main)) {
222                 p.println("# ignored " + getArtifactFilename(main));
223             } else {
224                 printArtifact(prefix, n++, RepositoryUtils.toArtifact(project.getArtifact()));
225             }
226         }
227 
228         for (Artifact attached : RepositoryUtils.toArtifacts(project.getAttachedArtifacts())) {
229             if ("pom".equals(attached.getExtension()) && "consumer".equals(attached.getClassifier())) {
230                 // ignore consumer pom
231                 continue;
232             }
233             if (attached.getExtension().endsWith(".asc")) {
234                 // ignore pgp signatures
235                 continue;
236             }
237             if (ignoreJavadoc && "javadoc".equals(attached.getClassifier())) {
238                 // TEMPORARY ignore javadoc, waiting for MJAVADOC-627 in m-javadoc-p 3.2.0
239                 continue;
240             }
241             if (isIgnore(attached)) {
242                 p.println("# ignored " + getArtifactFilename(attached));
243                 artifacts.put(attached, null);
244                 continue;
245             }
246             printArtifact(prefix, n++, attached);
247         }
248     }
249 
250     private void printArtifact(String prefix, int i, Artifact artifact) throws MojoExecutionException {
251         prefix = prefix + i;
252         File artifactFile = artifact.getFile();
253         if (artifactFile.isDirectory()) {
254             if ("pom".equals(artifact.getExtension())) {
255                 // ignore .pom files: they should not be treated as Artifacts
256                 return;
257             }
258             // edge case found in a distribution module with default packaging and skip set for
259             // m-jar-p: should use pom packaging instead
260             throw new MojoExecutionException("Artifact " + ArtifactIdUtils.toId(artifact) + " points to a directory: "
261                     + artifactFile + ". Packaging should be 'pom'?");
262         }
263         if (!artifactFile.isFile()) {
264             log.warn("Ignoring artifact " + ArtifactIdUtils.toId(artifact) + " because it points to inexistent "
265                     + artifactFile);
266             return;
267         }
268 
269         printFile(prefix, artifact.getGroupId(), artifact.getFile(), getArtifactFilename(artifact));
270         artifacts.put(artifact, prefix);
271     }
272 
273     static String getArtifactFilename(Artifact artifact) {
274         StringBuilder path = new StringBuilder(128);
275 
276         path.append(artifact.getArtifactId()).append('-').append(artifact.getBaseVersion());
277 
278         if (!artifact.getClassifier().isEmpty()) {
279             path.append('-').append(artifact.getClassifier());
280         }
281 
282         if (!artifact.getExtension().isEmpty()) {
283             path.append('.').append(artifact.getExtension());
284         }
285 
286         return path.toString();
287     }
288 
289     void printFile(String prefix, String groupId, File file) throws MojoExecutionException {
290         printFile(prefix, groupId, file, file.getName());
291     }
292 
293     private void printFile(String prefix, String groupId, File file, String filename) throws MojoExecutionException {
294         p.println();
295         p.println(prefix + ".groupId=" + groupId);
296         p.println(prefix + ".filename=" + filename);
297         p.println(prefix + ".length=" + file.length());
298         try (InputStream is = Files.newInputStream(file.toPath())) {
299             p.println(prefix + ".checksums.sha512=" + DigestUtils.sha512Hex(is));
300         } catch (IOException ioe) {
301             throw new MojoExecutionException("Error processing file " + file, ioe);
302         } catch (IllegalArgumentException iae) {
303             throw new MojoExecutionException("Could not get hash algorithm", iae.getCause());
304         }
305     }
306 
307     Map<Artifact, String> getArtifacts() {
308         return artifacts;
309     }
310 
311     /**
312      * Load buildinfo file and extracts properties on output files.
313      *
314      * @param buildinfo the build info file
315      * @return output properties
316      * @throws MojoExecutionException
317      */
318     static Properties loadOutputProperties(File buildinfo) throws MojoExecutionException {
319         Properties prop = new Properties();
320         if (buildinfo != null) {
321             try (InputStream is = Files.newInputStream(buildinfo.toPath())) {
322                 prop.load(is);
323             } catch (IOException e) {
324                 // silent
325             }
326         }
327         for (String name : prop.stringPropertyNames()) {
328             if (!name.startsWith("outputs.") || name.endsWith(".coordinates")) {
329                 prop.remove(name);
330             }
331         }
332         return prop;
333     }
334 
335     boolean getIgnoreJavadoc() {
336         return ignoreJavadoc;
337     }
338 
339     void setIgnoreJavadoc(boolean ignoreJavadoc) {
340         this.ignoreJavadoc = ignoreJavadoc;
341     }
342 
343     void setIgnore(List<String> ignore) {
344         FileSystem fs = FileSystems.getDefault();
345         this.ignore = ignore.stream().map(i -> fs.getPathMatcher("glob:" + i)).collect(Collectors.toList());
346     }
347 
348     boolean isIgnore(Artifact attached) {
349         Path path = Paths.get(attached.getGroupId() + '/' + getArtifactFilename(attached));
350         return ignore.stream().anyMatch(m -> m.matches(path));
351     }
352 
353     public void setToolchain(Toolchain toolchain) {
354         this.toolchain = toolchain;
355     }
356 }