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.InputStream;
25  import java.io.OutputStreamWriter;
26  import java.io.PrintWriter;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.jar.Attributes;
35  import java.util.jar.JarFile;
36  import java.util.jar.Manifest;
37  import java.util.zip.ZipEntry;
38  
39  import org.apache.maven.artifact.Artifact;
40  import org.apache.maven.artifact.factory.ArtifactFactory;
41  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
42  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
43  import org.apache.maven.plugin.MojoExecutionException;
44  import org.apache.maven.plugin.logging.Log;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.maven.rtinfo.RuntimeInformation;
47  import org.apache.maven.shared.utils.io.FileUtils;
48  import org.apache.maven.shared.utils.io.IOUtil;
49  import org.eclipse.aether.AbstractForwardingRepositorySystemSession;
50  import org.eclipse.aether.RepositorySystem;
51  import org.eclipse.aether.RepositorySystemSession;
52  import org.eclipse.aether.artifact.DefaultArtifact;
53  import org.eclipse.aether.repository.RemoteRepository;
54  import org.eclipse.aether.repository.WorkspaceReader;
55  import org.eclipse.aether.resolution.ArtifactRequest;
56  import org.eclipse.aether.resolution.ArtifactResult;
57  
58  /**
59   * Utility to download reference artifacts and download or generate reference buildinfo.
60   */
61  class ReferenceBuildinfoUtil {
62      private static final Set<String> JAR_TYPES;
63  
64      static {
65          Set<String> types = new HashSet<>();
66          types.add("jar");
67          types.add("test-jar");
68          types.add("war");
69          types.add("ear");
70          types.add("rar");
71          types.add("maven-plugin");
72          JAR_TYPES = Collections.unmodifiableSet(types);
73      }
74  
75      private final Log log;
76  
77      /**
78       * Directory of the downloaded reference files.
79       */
80      private final File referenceDir;
81  
82      private final Map<Artifact, String> artifacts;
83  
84      private final ArtifactFactory artifactFactory;
85  
86      private final RepositorySystem repoSystem;
87  
88      private final RepositorySystemSession repoSession;
89  
90      private final ArtifactHandlerManager artifactHandlerManager;
91  
92      private final RuntimeInformation rtInformation;
93  
94      ReferenceBuildinfoUtil(
95              Log log,
96              File referenceDir,
97              Map<Artifact, String> artifacts,
98              ArtifactFactory artifactFactory,
99              RepositorySystem repoSystem,
100             RepositorySystemSession repoSession,
101             ArtifactHandlerManager artifactHandlerManager,
102             RuntimeInformation rtInformation) {
103         this.log = log;
104         this.referenceDir = referenceDir;
105         this.artifacts = artifacts;
106         this.artifactFactory = artifactFactory;
107         this.repoSystem = repoSystem;
108         this.repoSession = repoSession;
109         this.artifactHandlerManager = artifactHandlerManager;
110         this.rtInformation = rtInformation;
111     }
112 
113     File downloadOrCreateReferenceBuildinfo(
114             RemoteRepository repo, MavenProject project, File buildinfoFile, boolean mono)
115             throws MojoExecutionException {
116         File referenceBuildinfo = downloadReferenceBuildinfo(repo, project);
117 
118         if (referenceBuildinfo != null) {
119             log.warn("dropping downloaded reference buildinfo because it may be generated"
120                     + " from different maven-artifact-plugin release...");
121             // TODO keep a save?
122             referenceBuildinfo = null;
123         }
124 
125         // download reference artifacts and guess Java version and OS
126         String javaVersion = null;
127         String osName = null;
128         String currentJavaVersion = null;
129         String currentOsName = null;
130         Map<Artifact, File> referenceArtifacts = new HashMap<>();
131         for (Artifact artifact : artifacts.keySet()) {
132             try {
133                 // download
134                 File file = downloadReference(repo, artifact);
135                 referenceArtifacts.put(artifact, file);
136 
137                 // guess Java version and OS
138                 if ((javaVersion == null) && JAR_TYPES.contains(artifact.getType())) {
139                     ReproducibleEnv env = extractEnv(file, artifact);
140                     if ((env != null) && (env.javaVersion != null)) {
141                         javaVersion = env.javaVersion;
142                         osName = env.osName;
143 
144                         ReproducibleEnv currentEnv = extractEnv(artifact.getFile(), artifact);
145                         currentJavaVersion = currentEnv.javaVersion;
146                         currentOsName = currentEnv.osName;
147                     }
148                 }
149             } catch (ArtifactNotFoundException e) {
150                 log.warn("Reference artifact not found " + artifact);
151             }
152         }
153 
154         // generate buildinfo from reference artifacts
155         referenceBuildinfo = getReference(null, buildinfoFile);
156         try (PrintWriter p = new PrintWriter(new BufferedWriter(
157                 new OutputStreamWriter(Files.newOutputStream(referenceBuildinfo.toPath()), StandardCharsets.UTF_8)))) {
158             BuildInfoWriter bi = new BuildInfoWriter(log, p, mono, artifactHandlerManager, rtInformation);
159 
160             if (javaVersion != null || osName != null) {
161                 p.println("# effective build environment information");
162                 if (javaVersion != null) {
163                     p.println("java.version=" + javaVersion);
164                     log.info("Reference build java.version: " + javaVersion);
165                     if (!javaVersion.equals(currentJavaVersion)) {
166                         log.error("Current build java.version: " + currentJavaVersion);
167                     }
168                 }
169                 if (osName != null) {
170                     p.println("os.name=" + osName);
171                     log.info("Reference build os.name: " + osName);
172 
173                     // check against current line separator
174                     if (!osName.equals(currentOsName)) {
175                         log.error("Current build os.name: " + currentOsName);
176                     }
177                     String expectedLs = osName.startsWith("Windows") ? "\r\n" : "\n";
178                     if (!expectedLs.equals(System.lineSeparator())) {
179                         log.warn("Current System.lineSeparator() does not match reference build OS");
180 
181                         String ls = System.getProperty("line.separator");
182                         if (!ls.equals(System.lineSeparator())) {
183                             log.warn("System.lineSeparator() != System.getProperty( \"line.separator\" ): "
184                                     + "too late standard system property update...");
185                         }
186                     }
187                 }
188             }
189 
190             for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) {
191                 Artifact artifact = entry.getKey();
192                 String prefix = entry.getValue();
193                 File referenceFile = referenceArtifacts.get(artifact);
194                 if (referenceFile != null) {
195                     bi.printFile(prefix, artifact.getGroupId(), referenceFile);
196                 }
197             }
198 
199             if (p.checkError()) {
200                 throw new MojoExecutionException("Write error to " + referenceBuildinfo);
201             }
202 
203             log.info("Minimal buildinfo generated from downloaded artifacts: " + referenceBuildinfo);
204         } catch (IOException e) {
205             throw new MojoExecutionException("Error creating file " + referenceBuildinfo, e);
206         }
207 
208         return referenceBuildinfo;
209     }
210 
211     private ReproducibleEnv extractEnv(File file, Artifact artifact) {
212         log.debug("Guessing java.version and os.name from jar " + file);
213         try (JarFile jar = new JarFile(file)) {
214             Manifest manifest = jar.getManifest();
215             if (manifest != null) {
216                 String javaVersion = extractJavaVersion(manifest);
217                 String osName = extractOsName(artifact, jar);
218                 return new ReproducibleEnv(javaVersion, osName);
219             } else {
220                 log.warn("no MANIFEST.MF found in jar " + file);
221             }
222         } catch (IOException e) {
223             log.warn("unable to open jar file " + file, e);
224         }
225         return null;
226     }
227 
228     private String extractJavaVersion(Manifest manifest) {
229         Attributes attr = manifest.getMainAttributes();
230 
231         String value = attr.getValue("Build-Jdk-Spec"); // MSHARED-797
232         if (value != null) {
233             return value + " (from MANIFEST.MF Build-Jdk-Spec)";
234         }
235 
236         value = attr.getValue("Build-Jdk");
237         if (value != null) {
238             return String.valueOf(value) + " (from MANIFEST.MF Build-Jdk)";
239         }
240 
241         return null;
242     }
243 
244     private String extractOsName(Artifact a, JarFile jar) {
245         String entryName = "META-INF/maven/" + a.getGroupId() + '/' + a.getArtifactId() + "/pom.properties";
246         ZipEntry zipEntry = jar.getEntry(entryName);
247         if (zipEntry == null) {
248             return null;
249         }
250         try (InputStream in = jar.getInputStream(zipEntry)) {
251             String content = IOUtil.toString(in, StandardCharsets.UTF_8.name());
252             log.debug("Manifest content: " + content);
253             if (content.contains("\r\n")) {
254                 return "Windows (from pom.properties newline)";
255             } else if (content.contains("\n")) {
256                 return "Unix (from pom.properties newline)";
257             }
258         } catch (IOException e) {
259             log.warn("Unable to read " + entryName + " from " + jar, e);
260         }
261         return null;
262     }
263 
264     private File downloadReferenceBuildinfo(RemoteRepository repo, MavenProject project) throws MojoExecutionException {
265         Artifact buildinfo = artifactFactory.createArtifactWithClassifier(
266                 project.getGroupId(), project.getArtifactId(), project.getVersion(), "buildinfo", "");
267         try {
268             File file = downloadReference(repo, buildinfo);
269 
270             log.info("Reference buildinfo file found, copied to " + file);
271 
272             return file;
273         } catch (ArtifactNotFoundException e) {
274             log.info("Reference buildinfo file not found: "
275                     + "it will be generated from downloaded reference artifacts");
276         }
277 
278         return null;
279     }
280 
281     private File downloadReference(RemoteRepository repo, Artifact artifact)
282             throws MojoExecutionException, ArtifactNotFoundException {
283         try {
284             ArtifactRequest request = new ArtifactRequest();
285             request.setArtifact(new DefaultArtifact(
286                     artifact.getGroupId(),
287                     artifact.getArtifactId(),
288                     artifact.getClassifier(),
289                     (artifact.getArtifactHandler() != null)
290                             ? artifact.getArtifactHandler().getExtension()
291                             : artifact.getType(),
292                     artifact.getVersion()));
293             request.setRepositories(Collections.singletonList(repo));
294 
295             ArtifactResult result =
296                     repoSystem.resolveArtifact(new NoWorkspaceRepositorySystemSession(repoSession), request);
297             File resultFile = result.getArtifact().getFile();
298             File destFile = getReference(artifact.getGroupId(), resultFile);
299 
300             FileUtils.copyFile(resultFile, destFile);
301 
302             return destFile;
303         } catch (org.eclipse.aether.resolution.ArtifactResolutionException are) {
304             if (are.getResult().isMissing()) {
305                 throw new ArtifactNotFoundException("Artifact not found " + artifact, artifact);
306             }
307             throw new MojoExecutionException("Error resolving reference artifact " + artifact, are);
308         } catch (IOException ioe) {
309             throw new MojoExecutionException("Error copying reference artifact " + artifact, ioe);
310         }
311     }
312 
313     private File getReference(String groupId, File file) {
314         File dir;
315         if (groupId == null) {
316             dir = referenceDir;
317         } else {
318             dir = new File(referenceDir, groupId);
319             if (!dir.isDirectory()) {
320                 dir.mkdir();
321             }
322         }
323         return new File(dir, file.getName());
324     }
325 
326     private static class NoWorkspaceRepositorySystemSession extends AbstractForwardingRepositorySystemSession {
327         private final RepositorySystemSession rss;
328 
329         NoWorkspaceRepositorySystemSession(RepositorySystemSession rss) {
330             this.rss = rss;
331         }
332 
333         @Override
334         protected RepositorySystemSession getSession() {
335             return rss;
336         }
337 
338         @Override
339         public WorkspaceReader getWorkspaceReader() {
340             return null;
341         }
342     }
343 
344     private static class ReproducibleEnv {
345         @SuppressWarnings("checkstyle:visibilitymodifier")
346         public final String javaVersion;
347 
348         @SuppressWarnings("checkstyle:visibilitymodifier")
349         public final String osName;
350 
351         ReproducibleEnv(String javaVersion, String osName) {
352             this.javaVersion = javaVersion;
353             this.osName = osName;
354         }
355     }
356 }