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.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 = RepositoryUtils.toArtifacts(project.getAttachedArtifacts()).stream()
166                 .filter(a -> "pom".equals(a.getExtension()) && "consumer".equals(a.getClassifier()))
167                 .findAny()
168                 .orElse(null);
169 
170         int n = 0;
171         Artifact pomArtifact =
172                 new DefaultArtifact(project.getGroupId(), project.getArtifactId(), null, "pom", project.getVersion());
173         if (consumerPom != null) {
174             // Maven 4 transient consumer POM attachment is published as the POM, overrides build POM, see
175             // https://github.com/apache/maven/blob/c79a7a02721f0f9fd7e202e99f60b593461ba8cc/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java#L130-L155
176             try {
177                 Path pomFile = Files.createTempFile(Paths.get(project.getBuild().getDirectory()), "consumer-", ".pom");
178                 Files.copy(consumerPom.getFile().toPath(), pomFile, StandardCopyOption.REPLACE_EXISTING);
179                 pomArtifact = pomArtifact.setFile(pomFile.toFile());
180             } catch (IOException e) {
181                 p.println("Error processing consumer POM: " + e);
182             }
183         } else {
184             pomArtifact = pomArtifact.setFile(project.getFile());
185         }
186 
187         artifacts.put(pomArtifact, prefix + n);
188         printFile(
189                 prefix + n++,
190                 pomArtifact.getGroupId(),
191                 pomArtifact.getFile(),
192                 project.getArtifactId() + '-' + project.getVersion() + ".pom");
193 
194         if (consumerPom != null) {
195             // build pom
196             Artifact buildPomArtifact = new DefaultArtifact(
197                     project.getGroupId(), project.getArtifactId(), "build", "pom", project.getVersion());
198             buildPomArtifact = buildPomArtifact.setFile(project.getFile());
199 
200             artifacts.put(buildPomArtifact, prefix + n);
201             printFile(
202                     prefix + n++,
203                     buildPomArtifact.getGroupId(),
204                     buildPomArtifact.getFile(),
205                     project.getArtifactId() + '-' + project.getVersion() + "-build.pom");
206         }
207 
208         if (project.getArtifact() == null) {
209             return;
210         }
211 
212         if (project.getArtifact().getFile() != null) {
213             printArtifact(prefix, n++, RepositoryUtils.toArtifact(project.getArtifact()));
214         }
215 
216         for (Artifact attached : RepositoryUtils.toArtifacts(project.getAttachedArtifacts())) {
217             if ("pom".equals(attached.getExtension()) && "consumer".equals(attached.getClassifier())) {
218                 // ignore consumer pom
219                 continue;
220             }
221             if (attached.getExtension().endsWith(".asc")) {
222                 // ignore pgp signatures
223                 continue;
224             }
225             if (ignoreJavadoc && "javadoc".equals(attached.getClassifier())) {
226                 // TEMPORARY ignore javadoc, waiting for MJAVADOC-627 in m-javadoc-p 3.2.0
227                 continue;
228             }
229             if (isIgnore(attached)) {
230                 p.println("# ignored " + getArtifactFilename(attached));
231                 artifacts.put(attached, null);
232                 continue;
233             }
234             printArtifact(prefix, n++, attached);
235         }
236     }
237 
238     private void printArtifact(String prefix, int i, Artifact artifact) throws MojoExecutionException {
239         prefix = prefix + i;
240         File artifactFile = artifact.getFile();
241         if (artifactFile.isDirectory()) {
242             if ("pom".equals(artifact.getExtension())) {
243                 // ignore .pom files: they should not be treated as Artifacts
244                 return;
245             }
246             // edge case found in a distribution module with default packaging and skip set for
247             // m-jar-p: should use pom packaging instead
248             throw new MojoExecutionException("Artifact " + ArtifactIdUtils.toId(artifact) + " points to a directory: "
249                     + artifactFile + ". Packaging should be 'pom'?");
250         }
251         if (!artifactFile.isFile()) {
252             log.warn("Ignoring artifact " + ArtifactIdUtils.toId(artifact) + " because it points to inexistent "
253                     + artifactFile);
254             return;
255         }
256 
257         printFile(prefix, artifact.getGroupId(), artifact.getFile(), getArtifactFilename(artifact));
258         artifacts.put(artifact, prefix);
259     }
260 
261     static String getArtifactFilename(Artifact artifact) {
262         StringBuilder path = new StringBuilder(128);
263 
264         path.append(artifact.getArtifactId()).append('-').append(artifact.getBaseVersion());
265 
266         if (!artifact.getClassifier().isEmpty()) {
267             path.append('-').append(artifact.getClassifier());
268         }
269 
270         if (!artifact.getExtension().isEmpty()) {
271             path.append('.').append(artifact.getExtension());
272         }
273 
274         return path.toString();
275     }
276 
277     void printFile(String prefix, String groupId, File file) throws MojoExecutionException {
278         printFile(prefix, groupId, file, file.getName());
279     }
280 
281     private void printFile(String prefix, String groupId, File file, String filename) throws MojoExecutionException {
282         p.println();
283         p.println(prefix + ".groupId=" + groupId);
284         p.println(prefix + ".filename=" + filename);
285         p.println(prefix + ".length=" + file.length());
286         try (InputStream is = Files.newInputStream(file.toPath())) {
287             p.println(prefix + ".checksums.sha512=" + DigestUtils.sha512Hex(is));
288         } catch (IOException ioe) {
289             throw new MojoExecutionException("Error processing file " + file, ioe);
290         } catch (IllegalArgumentException iae) {
291             throw new MojoExecutionException("Could not get hash algorithm", iae.getCause());
292         }
293     }
294 
295     Map<Artifact, String> getArtifacts() {
296         return artifacts;
297     }
298 
299     /**
300      * Load buildinfo file and extracts properties on output files.
301      *
302      * @param buildinfo the build info file
303      * @return output properties
304      * @throws MojoExecutionException
305      */
306     static Properties loadOutputProperties(File buildinfo) throws MojoExecutionException {
307         Properties prop = new Properties();
308         if (buildinfo != null) {
309             try (InputStream is = Files.newInputStream(buildinfo.toPath())) {
310                 prop.load(is);
311             } catch (IOException e) {
312                 // silent
313             }
314         }
315         for (String name : prop.stringPropertyNames()) {
316             if (!name.startsWith("outputs.") || name.endsWith(".coordinates")) {
317                 prop.remove(name);
318             }
319         }
320         return prop;
321     }
322 
323     boolean getIgnoreJavadoc() {
324         return ignoreJavadoc;
325     }
326 
327     void setIgnoreJavadoc(boolean ignoreJavadoc) {
328         this.ignoreJavadoc = ignoreJavadoc;
329     }
330 
331     void setIgnore(List<String> ignore) {
332         FileSystem fs = FileSystems.getDefault();
333         this.ignore = ignore.stream().map(i -> fs.getPathMatcher("glob:" + i)).collect(Collectors.toList());
334     }
335 
336     boolean isIgnore(Artifact attached) {
337         Path path = Paths.get(attached.getGroupId() + '/' + getArtifactFilename(attached));
338         return ignore.stream().anyMatch(m -> m.matches(path));
339     }
340 
341     public void setToolchain(Toolchain toolchain) {
342         this.toolchain = toolchain;
343     }
344 }