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