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.nio.file.Files;
25  import java.util.HashSet;
26  import java.util.Map;
27  import java.util.Properties;
28  import java.util.Set;
29  
30  import org.apache.maven.execution.MavenSession;
31  import org.apache.maven.lifecycle.LifecycleExecutor;
32  import org.apache.maven.lifecycle.MavenExecutionPlan;
33  import org.apache.maven.model.Plugin;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecution;
36  import org.apache.maven.plugin.MojoExecutionException;
37  import org.apache.maven.plugins.annotations.Component;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.project.MavenProject;
41  import org.eclipse.aether.util.version.GenericVersionScheme;
42  import org.eclipse.aether.version.InvalidVersionSpecificationException;
43  import org.eclipse.aether.version.Version;
44  import org.eclipse.aether.version.VersionScheme;
45  
46  /**
47   * Check from buildplan that plugins used don't have known Reproducible Builds issues.
48   *
49   * @since 3.3.0
50   */
51  @Mojo(name = "check-buildplan", threadSafe = true, requiresProject = true)
52  public class CheckBuildPlanMojo extends AbstractMojo {
53      @Component
54      private MavenProject project;
55  
56      @Component
57      private MavenSession session;
58  
59      @Component
60      private LifecycleExecutor lifecycleExecutor;
61  
62      /** Allow to specify which goals/phases will be used to calculate execution plan. */
63      @Parameter(property = "check.buildplan.tasks", defaultValue = "deploy")
64      private String[] tasks;
65  
66      /**
67       * Timestamp for reproducible output archive entries, either formatted as ISO 8601
68       * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
69       * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
70       */
71      @Parameter(defaultValue = "${project.build.outputTimestamp}")
72      private String outputTimestamp;
73  
74      /**
75       * Diagnose {@code outputTimestamp} effective value based on execution context.
76       *
77       * @since 3.5.2
78       */
79      @Parameter(property = "diagnose", defaultValue = "false")
80      private boolean diagnose;
81  
82      /**
83       * Provide a plugin issues property file to override plugin's <code>not-reproducible-plugins.properties</code>.
84       */
85      @Parameter(property = "check.plugin-issues")
86      private File pluginIssues;
87  
88      /**
89       * Make build fail if execution plan contains non-reproducible plugins.
90       */
91      @Parameter(property = "check.failOnNonReproducible", defaultValue = "true")
92      private boolean failOnNonReproducible;
93  
94      private final VersionScheme versionScheme = new GenericVersionScheme();
95  
96      protected MavenExecutionPlan calculateExecutionPlan() throws MojoExecutionException {
97          try {
98              return lifecycleExecutor.calculateExecutionPlan(session, tasks);
99          } catch (Exception e) {
100             throw new MojoExecutionException("Cannot calculate Maven execution plan" + e.getMessage(), e);
101         }
102     }
103 
104     @Override
105     public void execute() throws MojoExecutionException {
106         boolean fail = AbstractBuildinfoMojo.hasBadOutputTimestamp(
107                 outputTimestamp, getLog(), project, session.getProjects(), diagnose);
108 
109         // TODO check maven-jar-plugin module-info.class?
110 
111         Properties issues = loadIssues();
112 
113         MavenExecutionPlan plan = calculateExecutionPlan();
114 
115         Set<String> plugins = new HashSet<>();
116         int okCount = 0;
117         for (MojoExecution exec : plan.getMojoExecutions()) {
118             Plugin plugin = exec.getPlugin();
119             String id = plugin.getId();
120 
121             if (plugins.add(id)) {
122                 // check reproducibility status
123                 String issue = issues.getProperty(plugin.getKey());
124                 if (issue == null) {
125                     okCount++;
126                     getLog().debug("No known issue with " + id);
127                 } else if (issue.startsWith("fail:")) {
128                     String logMessage = "plugin without solution " + id + ", see " + issue.substring(5);
129                     if (failOnNonReproducible) {
130                         getLog().error(logMessage);
131                     } else {
132                         getLog().warn(logMessage);
133                     }
134                     fail = true;
135                 } else {
136                     try {
137                         Version minimum = versionScheme.parseVersion(issue);
138                         Version version = versionScheme.parseVersion(plugin.getVersion());
139                         if (version.compareTo(minimum) < 0) {
140                             String logMessage =
141                                     "plugin with non-reproducible output: " + id + ", require minimum " + issue;
142                             if (failOnNonReproducible) {
143                                 getLog().error(logMessage);
144                             } else {
145                                 getLog().warn(logMessage);
146                             }
147                             fail = true;
148                         } else {
149                             okCount++;
150                             getLog().debug("No known issue with " + id + " (>= " + issue + ")");
151                         }
152                     } catch (InvalidVersionSpecificationException e) {
153                         throw new MojoExecutionException(e);
154                     }
155                 }
156             }
157         }
158         if (okCount > 0) {
159             getLog().info("No known issue in " + okCount + " plugins");
160         }
161 
162         if (fail) {
163             getLog().info("current module pom.xml is " + project.getBasedir() + "/pom.xml");
164             MavenProject parent = project;
165             while (true) {
166                 parent = parent.getParent();
167                 if ((parent == null) || !session.getProjects().contains(parent)) {
168                     break;
169                 }
170                 getLog().info("        parent pom.xml is " + parent.getBasedir() + "/pom.xml");
171             }
172             String message = "non-reproducible plugin or configuration found with fix available";
173             if (failOnNonReproducible) {
174                 throw new MojoExecutionException(message);
175             } else {
176                 getLog().warn(message);
177             }
178         }
179     }
180 
181     private Properties loadIssues() throws MojoExecutionException {
182         try (InputStream in = (pluginIssues == null)
183                 ? getClass().getResourceAsStream("not-reproducible-plugins.properties")
184                 : Files.newInputStream(pluginIssues.toPath())) {
185             Properties prop = new Properties();
186             prop.load(in);
187 
188             Properties result = new Properties();
189             for (Map.Entry<Object, Object> entry : prop.entrySet()) {
190                 String plugin = entry.getKey().toString().replace('+', ':');
191                 if (!plugin.contains(":")) {
192                     plugin = "org.apache.maven.plugins:" + plugin;
193                 }
194                 result.put(plugin, entry.getValue());
195             }
196             return result;
197         } catch (IOException ioe) {
198             throw new MojoExecutionException("Cannot load issues file", ioe);
199         }
200     }
201 }