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