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