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.plugin.MojoExecutionException;
26
27 import org.apache.maven.plugins.annotations.Component;
28 import org.apache.maven.plugins.annotations.Mojo;
29 import org.apache.maven.plugins.annotations.Parameter;
30 import org.apache.maven.project.MavenProject;
31 import org.apache.maven.shared.utils.logging.MessageUtils;
32 import org.apache.maven.shared.utils.PropertyUtils;
33 import org.apache.maven.shared.utils.StringUtils;
34 import org.eclipse.aether.RepositorySystem;
35 import org.eclipse.aether.RepositorySystemSession;
36 import org.eclipse.aether.repository.RemoteRepository;
37
38 import java.io.BufferedWriter;
39 import java.io.File;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.OutputStreamWriter;
43 import java.io.PrintWriter;
44 import java.nio.charset.StandardCharsets;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Properties;
49
50
51
52
53
54
55
56 @Mojo( name = "compare" )
57 public class CompareMojo
58 extends AbstractBuildinfoMojo
59 {
60
61
62
63
64
65
66
67
68
69
70
71 @Parameter( property = "reference.repo", defaultValue = "central" )
72 private String referenceRepo;
73
74
75
76
77
78 @Parameter( property = "compare.aggregate.only", defaultValue = "false" )
79 private boolean aggregateOnly;
80
81 @Component
82 private ArtifactFactory artifactFactory;
83
84
85
86
87 @Component
88 private RepositorySystem repoSystem;
89
90
91
92
93 @Parameter( defaultValue = "${repositorySystemSession}", readonly = true )
94 private RepositorySystemSession repoSession;
95
96
97
98
99 @Parameter( defaultValue = "${project.remoteProjectRepositories}", readonly = true )
100 private List<RemoteRepository> remoteRepos;
101
102 @Component
103 private ArtifactRepositoryLayout artifactRepositoryLayout;
104
105 @Override
106 public void execute( Map<Artifact, String> artifacts )
107 throws MojoExecutionException
108 {
109 getLog().info( "Checking against reference build from " + referenceRepo + "..." );
110 checkAgainstReference( artifacts, reactorProjects.size() == 1 );
111 }
112
113 @Override
114 protected void skip( MavenProject last )
115 throws MojoExecutionException
116 {
117 if ( aggregateOnly )
118 {
119 return;
120 }
121
122
123 checkAgainstReference( generateBuildinfo( true ), true );
124 }
125
126
127
128
129
130
131
132
133 private void checkAgainstReference( Map<Artifact, String> artifacts, boolean mono )
134 throws MojoExecutionException
135 {
136 MavenProject root = mono ? project : getExecutionRoot();
137 File referenceDir = new File( root.getBuild().getDirectory(), "reference" );
138 referenceDir.mkdirs();
139
140
141 File referenceBuildinfo = downloadOrCreateReferenceBuildinfo( mono, artifacts, referenceDir );
142
143
144 compareWithReference( artifacts, referenceBuildinfo );
145 }
146
147 private File downloadOrCreateReferenceBuildinfo( boolean mono, Map<Artifact, String> artifacts, File referenceDir )
148 throws MojoExecutionException
149 {
150 RemoteRepository repo = createReferenceRepo();
151
152 ReferenceBuildinfoUtil rmb = new ReferenceBuildinfoUtil( getLog(), referenceDir, artifacts, artifactFactory,
153 repoSystem, repoSession, artifactHandlerManager );
154
155 return rmb.downloadOrCreateReferenceBuildinfo( repo, project, buildinfoFile, mono );
156 }
157
158 private void compareWithReference( Map<Artifact, String> artifacts, File referenceBuildinfo )
159 throws MojoExecutionException
160 {
161 Properties actual = BuildInfoWriter.loadOutputProperties( buildinfoFile );
162 Properties reference = BuildInfoWriter.loadOutputProperties( referenceBuildinfo );
163
164 int ok = 0;
165 List<String> okFilenames = new ArrayList<>();
166 List<String> koFilenames = new ArrayList<>();
167 List<String> diffoscopes = new ArrayList<>();
168 File referenceDir = referenceBuildinfo.getParentFile();
169 for ( Map.Entry<Artifact, String> entry : artifacts.entrySet() )
170 {
171 Artifact artifact = entry.getKey();
172 String prefix = entry.getValue();
173
174 String[] checkResult = checkArtifact( artifact, prefix, reference, actual, referenceDir );
175 String filename = checkResult[0];
176 String diffoscope = checkResult[1];
177
178 if ( diffoscope == null )
179 {
180 ok++;
181 okFilenames.add( filename );
182 }
183 else
184 {
185 koFilenames.add( filename );
186 diffoscopes.add( diffoscope );
187 }
188 }
189
190 int ko = artifacts.size() - ok;
191 int missing = reference.size() / 3 ;
192
193 if ( ko + missing > 0 )
194 {
195 getLog().warn( "Reproducible Build output summary: " + MessageUtils.buffer().success( ok + " files ok" )
196 + ", " + MessageUtils.buffer().failure( ko + " different" )
197 + ( ( missing == 0 ) ? "" : ( ", " + MessageUtils.buffer().warning( missing + " missing" ) ) ) );
198 getLog().warn( "see " + MessageUtils.buffer().project( "diff " + relative( referenceBuildinfo ) + " "
199 + relative( buildinfoFile ) ).toString() );
200 getLog().warn( "see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html" );
201 }
202 else
203 {
204 getLog().info( "Reproducible Build output summary: " + MessageUtils.buffer().success( ok + " files ok" ) );
205 }
206
207
208
209 File buildcompare = new File( buildinfoFile.getParentFile(),
210 buildinfoFile.getName().replaceFirst( ".buildinfo$", ".buildcompare" ) );
211 try ( PrintWriter p =
212 new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream( buildcompare ),
213 StandardCharsets.UTF_8 ) ) ) )
214 {
215 p.println( "version=" + project.getVersion() );
216 p.println( "ok=" + ok );
217 p.println( "ko=" + ko );
218 p.println( "okFiles=\"" + StringUtils.join( okFilenames.iterator(), " " ) + '"' );
219 p.println( "koFiles=\"" + StringUtils.join( koFilenames.iterator(), " " ) + '"' );
220 Properties ref = PropertyUtils.loadOptionalProperties( referenceBuildinfo );
221 String v = ref.getProperty( "java.version" );
222 if ( v != null )
223 {
224 p.println( "reference_java_version=\"" + v + '"' );
225 }
226 v = ref.getProperty( "os.name" );
227 if ( v != null )
228 {
229 p.println( "reference_os_name=\"" + v + '"' );
230 }
231 for ( String diffoscope : diffoscopes )
232 {
233 p.print( "# " );
234 p.println( diffoscope );
235 }
236 getLog().info( "Reproducible Build output comparison saved to " + buildcompare );
237 }
238 catch ( IOException e )
239 {
240 throw new MojoExecutionException( "Error creating file " + buildcompare, e );
241 }
242
243 copyAggregateToRoot( buildcompare );
244 }
245
246
247 private String[] checkArtifact( Artifact artifact, String prefix, Properties reference, Properties actual,
248 File referenceDir )
249 {
250 String actualFilename = (String) actual.remove( prefix + ".filename" );
251 String actualLength = (String) actual.remove( prefix + ".length" );
252 String actualSha512 = (String) actual.remove( prefix + ".checksums.sha512" );
253
254 String referencePrefix = findPrefix( reference, actualFilename );
255 String referenceLength = (String) reference.remove( referencePrefix + ".length" );
256 String referenceSha512 = (String) reference.remove( referencePrefix + ".checksums.sha512" );
257
258 String issue = null;
259 if ( !actualLength.equals( referenceLength ) )
260 {
261 issue = "size";
262 }
263 else if ( !actualSha512.equals( referenceSha512 ) )
264 {
265 issue = "sha512";
266 }
267
268 if ( issue != null )
269 {
270 String diffoscope = diffoscope( artifact, referenceDir );
271 getLog().warn( issue + " mismatch " + MessageUtils.buffer().strong( actualFilename ) + ": investigate with "
272 + MessageUtils.buffer().project( diffoscope ) );
273 return new String[] { actualFilename, diffoscope };
274 }
275 return new String[] { actualFilename, null };
276 }
277
278 private String diffoscope( Artifact a, File referenceDir )
279 {
280 File actual = a.getFile();
281
282
283 File reference = new File( referenceDir, getRepositoryFilename( a ) );
284 if ( ( actual == null ) || ( reference == null ) )
285 {
286 return "missing file for " + a.getId() + " reference = "
287 + ( reference == null ? "null" : relative( reference ) ) + " actual = "
288 + ( actual == null ? "null" : relative( actual ) );
289 }
290 return "diffoscope " + relative( reference ) + " " + relative( actual );
291 }
292
293 private String getRepositoryFilename( Artifact a )
294 {
295 String path = artifactRepositoryLayout.pathOf( a );
296 return path.substring( path.lastIndexOf( '/' ) );
297 }
298
299 private String relative( File file )
300 {
301 File basedir = getExecutionRoot().getBasedir();
302 int length = basedir.getPath().length();
303 String path = file.getPath();
304 return path.substring( length + 1 );
305 }
306
307 private static String findPrefix( Properties reference, String actualFilename )
308 {
309 for ( String name : reference.stringPropertyNames() )
310 {
311 if ( name.endsWith( ".filename" ) && actualFilename.equals( reference.getProperty( name ) ) )
312 {
313 reference.remove( name );
314 return name.substring( 0, name.length() - ".filename".length() );
315 }
316 }
317 return null;
318 }
319
320 private RemoteRepository createReferenceRepo()
321 throws MojoExecutionException
322 {
323 if ( referenceRepo.contains( "::" ) )
324 {
325
326 int index = referenceRepo.indexOf( "::" );
327 String id = referenceRepo.substring( 0, index );
328 String url = referenceRepo.substring( index + 2 );
329 return createDeploymentArtifactRepository( id, url );
330 }
331 else if ( referenceRepo.contains( ":" ) )
332 {
333
334 return createDeploymentArtifactRepository( "reference", referenceRepo );
335 }
336
337
338 for ( RemoteRepository repo : remoteRepos )
339 {
340 if ( referenceRepo.equals( repo.getId() ) )
341 {
342 return repo;
343 }
344 }
345 throw new MojoExecutionException( "Could not find repository with id = " + referenceRepo );
346 }
347
348 private static RemoteRepository createDeploymentArtifactRepository( String id, String url )
349 {
350 return new RemoteRepository.Builder( id, "default", url ).build();
351 }
352 }