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