1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.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
50
51
52
53 @Mojo(name = "check-buildplan", threadSafe = true, requiresProject = true)
54 public class CheckBuildPlanMojo extends AbstractMojo {
55 @Component
56 private List<MavenProject> reactorProjects;
57
58 @Component
59 private MavenProject project;
60
61 @Component
62 private MavenSession session;
63
64 @Component
65 private LifecycleExecutor lifecycleExecutor;
66
67
68 @Parameter(property = "check.buildplan.tasks", defaultValue = "deploy")
69 private String[] tasks;
70
71
72
73
74
75
76 @Parameter(defaultValue = "${project.build.outputTimestamp}")
77 private String outputTimestamp;
78
79
80
81
82 @Parameter(property = "check.plugin-issues")
83 private File pluginIssues;
84
85
86
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
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
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
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 : Files.newInputStream(pluginIssues.toPath())) {
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 }