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