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 javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.StandardCopyOption;
30  import java.time.Instant;
31  import java.time.format.DateTimeFormatter;
32  import java.util.Comparator;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.stream.Collectors;
36  
37  import org.apache.commons.codec.digest.DigestUtils;
38  import org.apache.maven.RepositoryUtils;
39  import org.apache.maven.archiver.MavenArchiver;
40  import org.apache.maven.execution.MavenSession;
41  import org.apache.maven.plugin.MojoExecutionException;
42  import org.apache.maven.plugins.annotations.Mojo;
43  import org.apache.maven.project.MavenProject;
44  import org.apache.maven.rtinfo.RuntimeInformation;
45  import org.apache.maven.shared.utils.logging.MessageUtils;
46  import org.apache.maven.toolchain.ToolchainManager;
47  import org.eclipse.aether.artifact.Artifact;
48  import org.eclipse.aether.artifact.DefaultArtifact;
49  
50  /**
51   * Describe build output (experimental).
52   * It is expected to be used aggregator used from CLI; that is, run at root after everything has run, but not bound to any build
53   * phase, where it would be run at root before modules.
54   * @since 3.5.2
55   */
56  @Mojo(name = "describe-build-output", aggregator = true, threadSafe = true)
57  public class DescribeBuildOutputMojo extends AbstractBuildinfoMojo {
58  
59      @Inject
60      public DescribeBuildOutputMojo(
61              ToolchainManager toolchainManager,
62              RuntimeInformation rtInformation,
63              MavenProject project,
64              MavenSession session) {
65          super(toolchainManager, rtInformation, project, session);
66      }
67  
68      @Override
69      public void execute() throws MojoExecutionException {
70          // super.execute(); // do not generate buildinfo, just reuse logic from abstract class
71          Instant timestamp =
72                  MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).orElse(null);
73          String effective = ((timestamp == null) ? "disabled" : DateTimeFormatter.ISO_INSTANT.format(timestamp));
74  
75          diagnose(outputTimestamp, getLog(), project, session, effective);
76          getLog().info("");
77          describeBuildOutput();
78      }
79  
80      private Path rootPath;
81      private BuildInfoWriter bi;
82  
83      private void describeBuildOutput() throws MojoExecutionException {
84          rootPath = session.getTopLevelProject().getBasedir().toPath();
85          bi = newBuildInfoWriter(null, false);
86  
87          Map<MavenProject, Long> reactorParents = session.getProjects().stream()
88                  .collect(Collectors.groupingBy(
89                          p -> DescribeBuildOutputMojo.getReactorParent(session, p), Collectors.counting()));
90          reactorParents.entrySet().stream()
91                  .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
92                  .forEach(e -> getLog().info("parent in reactor: " + e.getKey().getGroupId() + ":"
93                          + e.getKey().getArtifactId() + " @ "
94                          + rootPath.relativize(e.getKey().getFile().toPath()) + " (" + e.getValue() + " module"
95                          + ((e.getValue() > 1) ? "s" : "") + "), property = "
96                          + e.getKey().getProperties().get("project.build.outputTimestamp")));
97  
98          getLog().info("");
99  
100         Map<String, Long> groupIds = session.getProjects().stream()
101                 .collect(Collectors.groupingBy(MavenProject::getGroupId, Collectors.counting()));
102         groupIds.entrySet().stream()
103                 .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
104                 .forEach(e -> getLog().info("groupId: " + e.getKey() + " (" + e.getValue() + " artifactId"
105                         + ((e.getValue() > 1) ? "s" : "") + ")"));
106 
107         Map<String, Set<String>> artifactIds = session.getProjects().stream()
108                 .collect(Collectors.groupingBy(
109                         MavenProject::getArtifactId, Collectors.mapping(MavenProject::getGroupId, Collectors.toSet())));
110         artifactIds.entrySet().stream()
111                 .sorted(Map.Entry.comparingByKey())
112                 .filter(e -> e.getValue().size() > 1)
113                 .forEach(e ->
114                         getLog().info("artifactId: " + e.getKey() + " defined for multiple groupIds: " + e.getValue()));
115 
116         getLog().info("");
117         getLog().info(MessageUtils.buffer()
118                 .a("skip/ignore? artifactId")
119                 .strong("[:classifier][:extension]")
120                 .a(" = build-path repository-filename size [sha256]")
121                 .build());
122 
123         for (MavenProject p : session.getProjects()) {
124             boolean skipped = isSkip(p);
125             String s = skipped ? "not-deployed " : "             ";
126 
127             // project = pom
128             // detect Maven 4 consumer POM transient attachment
129             Artifact consumerPom = RepositoryUtils.toArtifacts(p.getAttachedArtifacts()).stream()
130                     .filter(a -> "pom".equals(a.getExtension()) && "consumer".equals(a.getClassifier()))
131                     .findAny()
132                     .orElse(null);
133 
134             Artifact pomArtifact = new DefaultArtifact(p.getGroupId(), p.getArtifactId(), null, "pom", p.getVersion());
135             if (consumerPom != null) {
136                 // Maven 4 transient consumer POM attachment is published as the POM, overrides build POM, see
137                 // https://github.com/apache/maven/blob/c79a7a02721f0f9fd7e202e99f60b593461ba8cc/maven-core/src/main/java/org/apache/maven/internal/transformation/ConsumerPomArtifactTransformer.java#L130-L155
138                 try {
139                     Path pomFile = Files.createTempFile(Paths.get(p.getBuild().getDirectory()), "consumer-", ".pom");
140                     Files.copy(consumerPom.getFile().toPath(), pomFile, StandardCopyOption.REPLACE_EXISTING);
141                     pomArtifact = pomArtifact.setFile(pomFile.toFile());
142                     getLog().info(s + describeArtifact(pomArtifact)); // consumer pom
143                     // build pom
144                     pomArtifact =
145                             new DefaultArtifact(p.getGroupId(), p.getArtifactId(), "build", "pom", p.getVersion());
146                 } catch (IOException e) {
147                     throw new MojoExecutionException("Error processing consumer POM", e);
148                 }
149             }
150             pomArtifact = pomArtifact.setFile(p.getFile());
151             getLog().info(s + describeArtifact(pomArtifact, skipped));
152 
153             // main artifact (when available: pom packaging does not provide main artifact)
154             if (p.getArtifact().getFile() != null) {
155                 getLog().info(s + describeArtifact(RepositoryUtils.toArtifact(p.getArtifact()), skipped));
156             }
157 
158             // attached artifacts (when available)
159             for (Artifact a : RepositoryUtils.toArtifacts(p.getAttachedArtifacts())) {
160                 if ("pom".equals(a.getExtension()) && "consumer".equals(a.getClassifier())) {
161                     // ignore transient consumer POM attachment
162                     continue;
163                 }
164                 boolean ignored = skipped ? false : isIgnore(a);
165                 String i = skipped ? s : (ignored ? "RB-ignored   " : "             ");
166                 getLog().info(i + describeArtifact(a, skipped || ignored));
167             }
168         }
169     }
170 
171     private boolean isIgnore(Artifact a) {
172         if (a.getExtension().endsWith(".asc")) {
173             return true;
174         }
175         if (bi.getIgnoreJavadoc() && "javadoc".equals(a.getClassifier())) {
176             return true;
177         }
178         return bi.isIgnore(a);
179     }
180 
181     private String describeArtifact(Artifact a) throws MojoExecutionException {
182         return describeArtifact(a, false);
183     }
184 
185     private String describeArtifact(Artifact a, boolean skipped) throws MojoExecutionException {
186         String sha256 = skipped ? "" : (" " + sha256(a.getFile()));
187         String ce = ("".equals(a.getClassifier()) ? "" : (':' + a.getClassifier()))
188                 + ("jar".equals(a.getExtension()) ? "" : (":" + a.getExtension()));
189         String path = rootPath.relativize(a.getFile().toPath()).toString();
190         int i = path.indexOf("target/");
191         if (i >= 0) {
192             path = MessageUtils.buffer().mojo(path.substring(0, i + 7)).build() + path.substring(i + 7);
193         }
194         String remoteFilename = BuildInfoWriter.getArtifactFilename(a);
195         return /*a.getGroupId() + ':' +*/ a.getArtifactId() /*+ ':' + a.getVersion()*/
196                 + MessageUtils.buffer().strong(ce) + " = "
197                 + path + " "
198                 + (path.endsWith(remoteFilename)
199                         ? "-"
200                         : MessageUtils.buffer().strong(remoteFilename).build())
201                 + " " + a.getFile().length() + sha256;
202     }
203 
204     private String sha256(File file) throws MojoExecutionException {
205         try (InputStream is = Files.newInputStream(file.toPath())) {
206             return DigestUtils.sha256Hex(is);
207         } catch (IOException ioe) {
208             throw new MojoExecutionException("cannot read " + file, ioe);
209         }
210     }
211 
212     static MavenProject getReactorParent(MavenSession session, MavenProject project) {
213         MavenProject reactorParent = project;
214         while (session.getProjects().contains(reactorParent.getParent())) {
215             reactorParent = reactorParent.getParent();
216         }
217         return reactorParent;
218     }
219 
220     @Override
221     public void execute(Map<org.eclipse.aether.artifact.Artifact, String> artifacts) throws MojoExecutionException {
222         // buildinfo generation skipped, method not called
223     }
224 }