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