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.BufferedWriter;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.OutputStreamWriter;
25 import java.io.PrintWriter;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.FileSystem;
28 import java.nio.file.FileSystems;
29 import java.nio.file.Files;
30 import java.nio.file.LinkOption;
31 import java.nio.file.Path;
32 import java.nio.file.PathMatcher;
33 import java.nio.file.Paths;
34 import java.nio.file.StandardCopyOption;
35 import java.time.Instant;
36 import java.time.format.DateTimeFormatter;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.stream.Collectors;
40
41 import org.apache.maven.archiver.MavenArchiver;
42 import org.apache.maven.execution.MavenSession;
43 import org.apache.maven.plugin.AbstractMojo;
44 import org.apache.maven.plugin.MojoExecutionException;
45 import org.apache.maven.plugin.logging.Log;
46 import org.apache.maven.plugins.annotations.Parameter;
47 import org.apache.maven.project.MavenProject;
48 import org.apache.maven.rtinfo.RuntimeInformation;
49 import org.apache.maven.toolchain.Toolchain;
50 import org.apache.maven.toolchain.ToolchainManager;
51 import org.eclipse.aether.artifact.Artifact;
52
53
54
55
56
57
58 public abstract class AbstractBuildinfoMojo extends AbstractMojo {
59
60
61
62
63 @Parameter(
64 defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo",
65 required = true,
66 readonly = true)
67 protected File buildinfoFile;
68
69
70
71
72 @Parameter(property = "buildinfo.ignoreJavadoc", defaultValue = "true")
73 private boolean ignoreJavadoc;
74
75
76
77
78
79 @Parameter(property = "buildinfo.ignore", defaultValue = "")
80 private List<String> ignore;
81
82
83
84
85
86 @Parameter(property = "buildinfo.detect.skip", defaultValue = "true")
87 private boolean detectSkip;
88
89
90
91
92
93
94
95 @Parameter(property = "buildinfo.skipModules")
96 private List<String> skipModules;
97
98 private List<PathMatcher> skipModulesMatcher = null;
99
100
101
102
103
104
105
106 @Parameter(property = "buildinfo.reproducible", defaultValue = "false")
107 private boolean reproducible;
108
109
110
111
112
113
114
115
116 @Parameter(defaultValue = "${project.build.outputTimestamp}")
117 protected String outputTimestamp;
118
119
120
121
122
123
124 @Parameter(property = "diagnose", defaultValue = "false")
125 private boolean diagnose;
126
127
128
129
130 private final ToolchainManager toolchainManager;
131
132 protected final RuntimeInformation rtInformation;
133
134
135
136
137 protected final MavenProject project;
138
139
140
141
142 protected final MavenSession session;
143
144 protected AbstractBuildinfoMojo(
145 ToolchainManager toolchainManager,
146 RuntimeInformation rtInformation,
147 MavenProject project,
148 MavenSession session) {
149 this.toolchainManager = toolchainManager;
150 this.rtInformation = rtInformation;
151 this.project = project;
152 this.session = session;
153 }
154
155 @Override
156 public void execute() throws MojoExecutionException {
157 boolean mono = session.getProjects().size() == 1;
158
159 hasBadOutputTimestamp(outputTimestamp, getLog(), project, session, diagnose);
160
161 if (!mono) {
162
163 if (detectSkip && PluginUtil.isSkip(project)) {
164 getLog().info("Auto-skipping goal because module skips install and/or deploy");
165 return;
166 }
167 if (isSkipModule(project)) {
168 getLog().info("Skipping goal for module");
169 return;
170 }
171
172 MavenProject last = getLastProject();
173 if (project != last) {
174 skip(last);
175 return;
176 }
177 }
178
179
180 Map<Artifact, String> artifacts = generateBuildinfo(mono);
181 getLog().info("Saved " + (mono ? "" : "aggregate ") + "info on build to " + buildinfoFile);
182
183 copyAggregateToRoot(buildinfoFile);
184
185 execute(artifacts);
186 }
187
188 static boolean hasBadOutputTimestamp(
189 String outputTimestamp, Log log, MavenProject project, MavenSession session, boolean diagnose) {
190 Instant timestamp =
191 MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).orElse(null);
192 String effective = ((timestamp == null) ? "disabled" : DateTimeFormatter.ISO_INSTANT.format(timestamp));
193
194 if (diagnose) {
195 diagnose(outputTimestamp, log, project, session, effective);
196 }
197
198 if (timestamp == null) {
199 log.error("Reproducible Build not activated by project.build.outputTimestamp property: "
200 + "see https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
201
202 String projectProperty = project.getProperties().getProperty("project.build.outputTimestamp");
203 if (projectProperty != null && projectProperty.startsWith("${git.")) {
204 log.error("project.build.outputTimestamp = \"" + projectProperty + "\": isn't Git value set?");
205 log.error("Did validate phase run and Git plugin set the value?");
206 if (project.getPackaging().equals("pom")) {
207 log.error("if using git-commit-id-plugin, <skipPoms>false</skipPoms> may solve the issue.");
208 }
209 }
210 return true;
211 }
212
213 if (log.isDebugEnabled()) {
214 log.debug("project.build.outputTimestamp = \"" + outputTimestamp + "\" => "
215 + (effective.equals(outputTimestamp) ? "" : (" => " + effective)));
216 }
217
218
219 boolean parentInReactor = false;
220 MavenProject reactorParent = project;
221 while (session.getProjects().contains(reactorParent.getParent())) {
222 parentInReactor = true;
223 reactorParent = reactorParent.getParent();
224 }
225 String prop = reactorParent.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
226 if (prop == null) {
227 log.info("<project.build.outputTimestamp> property (= " + outputTimestamp + ") is inherited"
228 + (parentInReactor ? " from outside the reactor" : "") + ", you can override in "
229 + (parentInReactor ? "parent POM from reactor " + reactorParent.getFile() : "pom.xml"));
230 return false;
231 }
232
233 return false;
234 }
235
236 static void diagnose(
237 String outputTimestamp, Log log, MavenProject project, MavenSession session, String effective) {
238 if (session.getProjects().size() > 1) {
239 MavenProject root = session.getTopLevelProject();
240 MavenProject first = session.getProjects().get(0);
241 Path firstRelative =
242 root.getBasedir().toPath().relativize(first.getFile().toPath());
243 MavenProject firstParent = first.getParent();
244 log.info("reactor executionRoot = " + root.getId() + (root.equals(project) ? " (current)" : "")
245 + System.lineSeparator()
246 + " reactor first = "
247 + (first.equals(root) ? "executionRoot" : (first.getId() + " @ " + firstRelative))
248 + System.lineSeparator()
249 + " first.parent = " + ((firstParent == null) ? firstParent : firstParent.getId()));
250 if (firstParent != null && session.getProjects().contains(firstParent)) {
251
252 log.warn("reactor first parent = " + firstParent.getId() + " is in reactor");
253 }
254 }
255
256 log.info("outputTimestamp = " + outputTimestamp
257 + (effective.equals(outputTimestamp) ? "" : (" => " + effective)));
258
259 String projectProperty = project.getProperties().getProperty("project.build.outputTimestamp");
260 String modelProperty = project.getModel().getProperties().getProperty("project.build.outputTimestamp");
261 String originalModelProperty =
262 project.getOriginalModel().getProperties().getProperty("project.build.outputTimestamp");
263
264 log.info("plugin outputTimestamp parameter diagnostics:" + System.lineSeparator()
265 + " - plugin outputTimestamp parameter (defaultValue=\"${project.build.outputTimestamp}\") = "
266 + outputTimestamp + System.lineSeparator()
267 + " - project.build.outputTimestamp property from project = " + projectProperty
268 + System.lineSeparator()
269 + " - project.build.outputTimestamp property from project model = " + modelProperty
270 + System.lineSeparator()
271 + " - project.build.outputTimestamp property from project original model = "
272 + originalModelProperty);
273
274 if (outputTimestamp == null) {
275 return;
276 }
277
278 MavenProject parent = project.getParent();
279 if (parent != null) {
280 StringBuilder sb = new StringBuilder("Inheritance analysis for property:" + System.lineSeparator()
281 + " - current " + project.getId() + " property = " + projectProperty);
282 while (parent != null) {
283 String parentProperty = parent.getProperties().getProperty("project.build.outputTimestamp");
284 sb.append(System.lineSeparator());
285 sb.append(" - " + (session.getProjects().contains(parent) ? "reactor" : "external") + " parent "
286 + parent.getId() + " property = " + parentProperty);
287 if (!projectProperty.equals(parentProperty)) {
288 break;
289 }
290 parent = parent.getParent();
291 }
292 log.info(sb.toString());
293 }
294 }
295
296
297
298
299
300
301
302 abstract void execute(Map<Artifact, String> artifacts) throws MojoExecutionException;
303
304 protected void skip(MavenProject last) throws MojoExecutionException {
305 getLog().info("Skipping intermediate goal run, aggregate will be " + last.getArtifactId());
306 }
307
308 protected File copyAggregateToRoot(File aggregate) throws MojoExecutionException {
309 if (session.getProjects().size() == 1) {
310
311 return aggregate;
312 }
313
314
315 MavenProject root = session.getTopLevelProject();
316 String extension = aggregate.getName().substring(aggregate.getName().lastIndexOf('.'));
317 File rootCopy =
318 new File(root.getBuild().getDirectory(), root.getArtifactId() + '-' + root.getVersion() + extension);
319 try {
320 rootCopy.getParentFile().mkdirs();
321 Files.copy(
322 aggregate.toPath(),
323 rootCopy.toPath(),
324 LinkOption.NOFOLLOW_LINKS,
325 StandardCopyOption.REPLACE_EXISTING);
326 getLog().info("Aggregate " + extension.substring(1) + " copied to " + relative(rootCopy));
327 } catch (IOException ioe) {
328 throw new MojoExecutionException("Could not copy " + aggregate + " to " + rootCopy, ioe);
329 }
330 return rootCopy;
331 }
332
333 protected BuildInfoWriter newBuildInfoWriter(PrintWriter p, boolean mono) {
334 BuildInfoWriter bi = new BuildInfoWriter(getLog(), p, mono, rtInformation);
335 bi.setIgnoreJavadoc(ignoreJavadoc);
336 bi.setIgnore(ignore);
337 bi.setToolchain(getToolchain());
338
339 return bi;
340 }
341
342
343
344
345
346
347
348
349 protected Map<Artifact, String> generateBuildinfo(boolean mono) throws MojoExecutionException {
350 MavenProject root = mono ? project : session.getTopLevelProject();
351
352 buildinfoFile.getParentFile().mkdirs();
353
354 try (PrintWriter p = new PrintWriter(new BufferedWriter(
355 new OutputStreamWriter(Files.newOutputStream(buildinfoFile.toPath()), StandardCharsets.UTF_8)))) {
356 BuildInfoWriter bi = newBuildInfoWriter(p, mono);
357 bi.printHeader(root, mono ? null : project, reproducible);
358
359
360 if (mono) {
361 bi.printArtifacts(project);
362 } else {
363 for (MavenProject project : session.getProjects()) {
364 if (!isSkip(project)) {
365 bi.printArtifacts(project);
366 }
367 }
368 }
369
370 if (p.checkError()) {
371 throw new MojoExecutionException("Write error to " + buildinfoFile);
372 }
373
374 return bi.getArtifacts();
375 } catch (IOException e) {
376 throw new MojoExecutionException("Error creating file " + buildinfoFile, e);
377 }
378 }
379
380 protected MavenProject getLastProject() {
381 int i = session.getProjects().size();
382 while (i > 0) {
383 MavenProject project = session.getProjects().get(--i);
384 if (!isSkip(project)) {
385 return project;
386 }
387 }
388 return null;
389 }
390
391 protected boolean isSkip(MavenProject project) {
392 return isSkipModule(project) || (detectSkip && PluginUtil.isSkip(project));
393 }
394
395 protected boolean isSkipModule(MavenProject project) {
396 if (skipModules == null || skipModules.isEmpty()) {
397 return false;
398 }
399 if (skipModulesMatcher == null) {
400 FileSystem fs = FileSystems.getDefault();
401 skipModulesMatcher = skipModules.stream()
402 .map(i -> fs.getPathMatcher("glob:" + i))
403 .collect(Collectors.toList());
404 }
405 Path path = Paths.get(project.getGroupId() + '/' + project.getArtifactId());
406 return skipModulesMatcher.stream().anyMatch(m -> m.matches(path));
407 }
408
409 private Toolchain getToolchain() {
410 Toolchain tc = null;
411 if (toolchainManager != null) {
412 tc = toolchainManager.getToolchainFromBuildContext("jdk", session);
413 }
414
415 return tc;
416 }
417
418 protected String relative(File file) {
419 File basedir = session.getTopLevelProject().getBasedir();
420 int length = basedir.getPath().length();
421 String path = file.getPath();
422 return path.substring(length + 1);
423 }
424 }