1 package org.apache.maven.plugins.artifact.buildinfo;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import org.apache.maven.artifact.Artifact;
23 import org.apache.maven.artifact.factory.ArtifactFactory;
24 import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
25 import org.apache.maven.execution.MavenSession;
26 import org.apache.maven.plugin.AbstractMojo;
27 import org.apache.maven.plugin.MojoExecutionException;
28
29 import org.apache.maven.plugins.annotations.Component;
30 import org.apache.maven.plugins.annotations.LifecyclePhase;
31 import org.apache.maven.plugins.annotations.Mojo;
32 import org.apache.maven.plugins.annotations.Parameter;
33 import org.apache.maven.project.MavenProject;
34 import org.apache.maven.project.MavenProjectHelper;
35 import org.apache.maven.shared.utils.io.FileUtils;
36 import org.apache.maven.shared.utils.logging.MessageUtils;
37 import org.apache.maven.toolchain.Toolchain;
38 import org.apache.maven.toolchain.ToolchainManager;
39 import org.apache.maven.shared.utils.PropertyUtils;
40 import org.apache.maven.shared.utils.StringUtils;
41 import org.eclipse.aether.RepositorySystem;
42 import org.eclipse.aether.RepositorySystemSession;
43 import org.eclipse.aether.repository.RemoteRepository;
44
45 import java.io.BufferedWriter;
46 import java.io.File;
47 import java.io.FileOutputStream;
48 import java.io.IOException;
49 import java.io.OutputStreamWriter;
50 import java.io.PrintWriter;
51 import java.nio.charset.StandardCharsets;
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Properties;
56 import java.util.Set;
57
58
59
60
61
62
63
64 @Mojo( name = "buildinfo", defaultPhase = LifecyclePhase.VERIFY )
65 public class BuildinfoMojo
66 extends AbstractMojo
67 {
68
69
70
71 @Parameter( defaultValue = "${project}", readonly = true )
72 private MavenProject project;
73
74
75
76
77 @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true )
78 private List<MavenProject> reactorProjects;
79
80
81
82
83 @Parameter( defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo",
84 required = true, readonly = true )
85 private File buildinfoFile;
86
87
88
89
90 @Parameter( property = "buildinfo.ignoreJavadoc", defaultValue = "true" )
91 private boolean ignoreJavadoc;
92
93
94
95
96 @Parameter( property = "buildinfo.ignore", defaultValue = "" )
97 private Set<String> ignore;
98
99
100
101
102 @Parameter( property = "buildinfo.attach", defaultValue = "true" )
103 private boolean attach;
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122 @Parameter( property = "reference.repo" )
123 private String referenceRepo;
124
125
126
127
128
129
130
131 @Parameter( property = "reference.compare.save", defaultValue = "false" )
132 private boolean referenceCompareSave;
133
134
135
136
137 @Parameter( property = "buildinfo.detect.skip", defaultValue = "true" )
138 private boolean detectSkip;
139
140
141
142
143
144
145
146 @Parameter( property = "buildinfo.reproducible", defaultValue = "false" )
147 private boolean reproducible;
148
149
150
151
152 @Component
153 private MavenProjectHelper projectHelper;
154
155 @Component
156 private ArtifactFactory artifactFactory;
157
158
159
160
161 @Component
162 private RepositorySystem repoSystem;
163
164
165
166
167 @Parameter( defaultValue = "${repositorySystemSession}", readonly = true )
168 private RepositorySystemSession repoSession;
169
170
171
172
173 @Parameter( defaultValue = "${project.remoteProjectRepositories}", readonly = true )
174 private List<RemoteRepository> remoteRepos;
175
176 @Component
177 private ArtifactRepositoryLayout artifactRepositoryLayout;
178
179
180
181
182 @Parameter( defaultValue = "${session}", readonly = true, required = true )
183 private MavenSession session;
184
185
186
187
188 @Component
189 private ToolchainManager toolchainManager;
190
191 @Override
192 public void execute()
193 throws MojoExecutionException
194 {
195 boolean mono = reactorProjects.size() == 1;
196
197 if ( !mono )
198 {
199
200 if ( isSkip( project ) )
201 {
202 getLog().info( "Skipping buildinfo for module that skips install and/or deploy" );
203 return;
204 }
205
206 MavenProject last = getLastProject();
207 if ( project != last )
208 {
209 getLog().info( "Skipping intermediate buildinfo, aggregate will be " + last.getArtifactId() );
210 return;
211 }
212 }
213
214
215 Map<Artifact, String> artifacts = generateBuildinfo( mono );
216 getLog().info( "Saved " + ( mono ? "" : "aggregate " ) + "info on build to " + buildinfoFile );
217
218
219 if ( attach )
220 {
221 getLog().info( "Attaching buildinfo" );
222 projectHelper.attachArtifact( project, "buildinfo", buildinfoFile );
223 }
224 else
225 {
226 getLog().info( "NOT adding buildinfo to the list of attached artifacts." );
227 }
228
229 copyAggregateToRoot( buildinfoFile );
230
231
232 if ( referenceRepo != null )
233 {
234 getLog().info( "Checking against reference build from " + referenceRepo + "..." );
235 checkAgainstReference( mono, artifacts );
236 }
237 }
238
239 private void copyAggregateToRoot( File aggregate )
240 throws MojoExecutionException
241 {
242 if ( reactorProjects.size() == 1 )
243 {
244
245 return;
246 }
247
248
249 MavenProject root = getExecutionRoot();
250 String compare = aggregate.getName().endsWith( ".compare" ) ? ".compare" : "";
251 File rootCopy = new File( root.getBuild().getDirectory(),
252 root.getArtifactId() + '-' + root.getVersion() + ".buildinfo" + compare );
253 try
254 {
255 FileUtils.copyFile( aggregate, rootCopy );
256 getLog().info( "Aggregate buildinfo" + compare + " copied to " + rootCopy );
257 }
258 catch ( IOException ioe )
259 {
260 throw new MojoExecutionException( "Could not copy " + aggregate + "to " + rootCopy );
261 }
262 }
263
264
265
266
267
268
269
270
271
272 private Map<Artifact, String> generateBuildinfo( boolean mono )
273 throws MojoExecutionException
274 {
275 MavenProject root = mono ? project : getExecutionRoot();
276
277 buildinfoFile.getParentFile().mkdirs();
278
279 try ( PrintWriter p = new PrintWriter( new BufferedWriter(
280 new OutputStreamWriter( new FileOutputStream( buildinfoFile ), StandardCharsets.UTF_8 ) ) ) )
281 {
282 BuildInfoWriter bi = new BuildInfoWriter( getLog(), p, mono );
283 bi.setIgnoreJavadoc( ignoreJavadoc );
284 bi.setIgnore( ignore );
285 bi.setToolchain( getToolchain() );
286
287 bi.printHeader( root, mono ? null : project, reproducible );
288
289
290 if ( mono )
291 {
292 bi.printArtifacts( project );
293 }
294 else
295 {
296 for ( MavenProject project : reactorProjects )
297 {
298 if ( !isSkip( project ) )
299 {
300 bi.printArtifacts( project );
301 }
302 }
303 }
304
305 if ( p.checkError() )
306 {
307 throw new MojoExecutionException( "Write error to " + buildinfoFile );
308 }
309
310 return bi.getArtifacts();
311 }
312 catch ( IOException e )
313 {
314 throw new MojoExecutionException( "Error creating file " + buildinfoFile, e );
315 }
316 }
317
318
319
320
321
322
323
324
325
326 private void checkAgainstReference( boolean mono, Map<Artifact, String> artifacts )
327 throws MojoExecutionException
328 {
329 MavenProject root = mono ? project : getExecutionRoot();
330 File referenceDir = new File( root.getBuild().getDirectory(), "reference" );
331 referenceDir.mkdirs();
332
333
334 File referenceBuildinfo = downloadOrCreateReferenceBuildinfo( mono, artifacts, referenceDir );
335
336
337 compareWithReference( artifacts, referenceBuildinfo );
338 }
339
340 private File downloadOrCreateReferenceBuildinfo( boolean mono, Map<Artifact, String> artifacts, File referenceDir )
341 throws MojoExecutionException
342 {
343 RemoteRepository repo = createReferenceRepo();
344
345 ReferenceBuildinfoUtil rmb = new ReferenceBuildinfoUtil( getLog(), referenceDir, artifacts,
346 artifactFactory, repoSystem, repoSession );
347
348 return rmb.downloadOrCreateReferenceBuildinfo( repo, project, buildinfoFile, mono );
349 }
350
351 private void compareWithReference( Map<Artifact, String> artifacts, File referenceBuildinfo )
352 throws MojoExecutionException
353 {
354 Properties actual = BuildInfoWriter.loadOutputProperties( buildinfoFile );
355 Properties reference = BuildInfoWriter.loadOutputProperties( referenceBuildinfo );
356
357 int ok = 0;
358 List<String> okFilenames = new ArrayList<>();
359 List<String> koFilenames = new ArrayList<>();
360 List<String> diffoscopes = new ArrayList<>();
361 File referenceDir = referenceBuildinfo.getParentFile();
362 for ( Map.Entry<Artifact, String> entry : artifacts.entrySet() )
363 {
364 Artifact artifact = entry.getKey();
365 String prefix = entry.getValue();
366
367 String diffoscope = checkArtifact( artifact, prefix, reference, actual, referenceDir );
368 if ( diffoscope == null )
369 {
370 ok++;
371 okFilenames.add( artifact.getFile().getName() );
372 }
373 else
374 {
375 koFilenames.add( artifact.getFile().getName() );
376 diffoscopes.add( diffoscope );
377 }
378 }
379
380 int ko = artifacts.size() - ok;
381 int missing = reference.size() / 3 ;
382
383 if ( ko + missing > 0 )
384 {
385 getLog().warn( "Reproducible Build output summary: " + MessageUtils.buffer().success( ok + " files ok" )
386 + ", " + MessageUtils.buffer().failure( ko + " different" )
387 + ( ( missing == 0 ) ? "" : ( ", " + MessageUtils.buffer().warning( missing + " missing" ) ) ) );
388 getLog().warn( "see " + MessageUtils.buffer().project( "diff " + relative( referenceBuildinfo ) + " "
389 + relative( buildinfoFile ) ).toString() );
390 getLog().warn( "see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html" );
391 }
392 else
393 {
394 getLog().info( "Reproducible Build output summary: " + MessageUtils.buffer().success( ok + " files ok" ) );
395 }
396
397 if ( referenceCompareSave )
398 {
399 File compare = new File( buildinfoFile.getParentFile(), buildinfoFile.getName() + ".compare" );
400 try ( PrintWriter p =
401 new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream( compare ),
402 StandardCharsets.UTF_8 ) ) ) )
403 {
404 p.println( "version=" + project.getVersion() );
405 p.println( "ok=" + ok );
406 p.println( "ko=" + ko );
407 p.println( "okFiles=\"" + StringUtils.join( okFilenames.iterator(), " " ) + '"' );
408 p.println( "koFiles=\"" + StringUtils.join( koFilenames.iterator(), " " ) + '"' );
409 Properties ref = PropertyUtils.loadOptionalProperties( referenceBuildinfo );
410 String v = ref.getProperty( "java.version" );
411 if ( v != null )
412 {
413 p.println( "reference_java_version=" + v );
414 }
415 v = ref.getProperty( "os.name" );
416 if ( v != null )
417 {
418 p.println( "reference_os_name=" + v );
419 }
420 for ( String diffoscope : diffoscopes )
421 {
422 p.print( "# " );
423 p.println( diffoscope );
424 }
425 getLog().info( "Reproducible Build comparison saved to " + compare );
426 }
427 catch ( IOException e )
428 {
429 throw new MojoExecutionException( "Error creating file " + compare, e );
430 }
431
432 copyAggregateToRoot( compare );
433 }
434 }
435
436 private String checkArtifact( Artifact artifact, String prefix, Properties reference, Properties actual,
437 File referenceDir )
438 {
439 String actualFilename = (String) actual.remove( prefix + ".filename" );
440 String actualLength = (String) actual.remove( prefix + ".length" );
441 String actualSha512 = (String) actual.remove( prefix + ".checksums.sha512" );
442
443 String referencePrefix = findPrefix( reference, actualFilename );
444 String referenceLength = (String) reference.remove( referencePrefix + ".length" );
445 String referenceSha512 = (String) reference.remove( referencePrefix + ".checksums.sha512" );
446
447 String issue = null;
448 if ( !actualLength.equals( referenceLength ) )
449 {
450 issue = "size";
451 }
452 else if ( !actualSha512.equals( referenceSha512 ) )
453 {
454 issue = "sha512";
455 }
456
457 if ( issue != null )
458 {
459 String diffoscope = diffoscope( artifact, referenceDir );
460 getLog().warn( issue + " mismatch " + MessageUtils.buffer().strong( actualFilename ) + ": investigate with "
461 + MessageUtils.buffer().project( diffoscope ) );
462 return diffoscope;
463 }
464 return null;
465 }
466
467 private String diffoscope( Artifact a, File referenceDir )
468 {
469 File actual = a.getFile();
470
471
472 File reference = new File( referenceDir, getRepositoryFilename( a ) );
473 return "diffoscope " + relative( reference ) + " " + relative( actual );
474 }
475
476 private String getRepositoryFilename( Artifact a )
477 {
478 String path = artifactRepositoryLayout.pathOf( a );
479 return path.substring( path.lastIndexOf( '/' ) );
480 }
481
482 private String relative( File file )
483 {
484 return file.getPath().substring( getExecutionRoot().getBasedir().getPath().length() + 1 );
485 }
486
487 private static String findPrefix( Properties reference, String actualFilename )
488 {
489 for ( String name : reference.stringPropertyNames() )
490 {
491 if ( name.endsWith( ".filename" ) && actualFilename.equals( reference.getProperty( name ) ) )
492 {
493 reference.remove( name );
494 return name.substring( 0, name.length() - ".filename".length() );
495 }
496 }
497 return null;
498 }
499
500 private RemoteRepository createReferenceRepo()
501 throws MojoExecutionException
502 {
503 if ( referenceRepo.contains( "::" ) )
504 {
505
506 int index = referenceRepo.indexOf( "::" );
507 String id = referenceRepo.substring( 0, index );
508 String url = referenceRepo.substring( index + 2 );
509 return createDeploymentArtifactRepository( id, url );
510 }
511 else if ( referenceRepo.contains( ":" ) )
512 {
513
514 return createDeploymentArtifactRepository( "reference", referenceRepo );
515 }
516
517
518 for ( RemoteRepository repo : remoteRepos )
519 {
520 if ( referenceRepo.equals( repo.getId() ) )
521 {
522 return repo;
523 }
524 }
525 throw new MojoExecutionException( "Could not find repository with id = " + referenceRepo );
526 }
527
528 private static RemoteRepository createDeploymentArtifactRepository( String id, String url )
529 {
530 return new RemoteRepository.Builder( id, "default", url ).build();
531 }
532
533 private MavenProject getExecutionRoot()
534 {
535 for ( MavenProject p : reactorProjects )
536 {
537 if ( p.isExecutionRoot() )
538 {
539 return p;
540 }
541 }
542 return null;
543 }
544
545 private MavenProject getLastProject()
546 {
547 int i = reactorProjects.size();
548 while ( i > 0 )
549 {
550 MavenProject project = reactorProjects.get( --i );
551 if ( !isSkip( project ) )
552 {
553 return project;
554 }
555 }
556 return null;
557 }
558
559 private boolean isSkip( MavenProject project )
560 {
561 return detectSkip && PluginUtil.isSkip( project );
562 }
563
564 private Toolchain getToolchain()
565 {
566 Toolchain tc = null;
567 if ( toolchainManager != null )
568 {
569 tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
570 }
571
572 return tc;
573 }
574 }