View Javadoc
1   package org.apache.maven.plugins.artifact.buildinfo;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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   * Compare current build output with reference either previously installed or downloaded from a remote repository:
52   * results go to {@code .buildcompare} file.
53   *
54   * @since 3.2.0
55   */
56  @Mojo( name = "compare" )
57  public class CompareMojo
58      extends AbstractBuildinfoMojo
59  {
60      /**
61       * Repository for reference build, containing either reference buildinfo file or reference artifacts.<br/>
62       * Format: <code>id</code> or <code>url</code> or <code>id::url</code>
63       * <dl>
64       * <dt>id</dt>
65       * <dd>The repository id</dd>
66       * <dt>url</dt>
67       * <dd>The url of the repository</dd>
68       * </dl>
69       * @see <a href="https://maven.apache.org/ref/current/maven-model/maven.html#repository">repository definition</a>
70       */
71      @Parameter( property = "reference.repo", defaultValue = "central" )
72      private String referenceRepo;
73  
74      /**
75       * Compare aggregate only (ie wait for the last module) or do buildcompare on each module.
76       * @since 3.2.0
77       */
78      @Parameter( property = "compare.aggregate.only", defaultValue = "false" )
79      private boolean aggregateOnly;
80  
81      @Component
82      private ArtifactFactory artifactFactory;
83  
84      /**
85       * The entry point to Maven Artifact Resolver, i.e. the component doing all the work.
86       */
87      @Component
88      private RepositorySystem repoSystem;
89  
90      /**
91       * The current repository/network configuration of Maven.
92       */
93      @Parameter( defaultValue = "${repositorySystemSession}", readonly = true )
94      private RepositorySystemSession repoSession;
95  
96      /**
97       * The project's remote repositories to use for the resolution.
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         // try to download reference artifacts for current project and check if there are issues to give early feedback
123         checkAgainstReference( generateBuildinfo( true ), true );
124     }
125 
126     /**
127      * Check current build result with reference.
128      *
129      * @artifacts a Map of artifacts added to the build info with their associated property key prefix
130      *            (<code>outputs.[#module.].#artifact</code>)
131      * @throws MojoExecutionException
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         // download or create reference buildinfo
141         File referenceBuildinfo = downloadOrCreateReferenceBuildinfo( mono, artifacts, referenceDir );
142 
143         // compare outputs from reference buildinfo vs actual
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 /* 3 property keys par file: filename, length and checksums.sha512 */;
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         // save .compare file
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     // { filename, diffoscope }
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         // notice: actual file name may have been defined in pom
282         // reference file name is taken from repository format
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             // id::url
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             // url, will use default "reference" id
334             return createDeploymentArtifactRepository( "reference", referenceRepo );
335         }
336 
337         // id
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 }