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