| package org.apache.maven.plugins.buildinfo; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import org.apache.commons.codec.Charsets; |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.artifact.factory.ArtifactFactory; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| |
| import org.apache.maven.plugins.annotations.Component; |
| import org.apache.maven.plugins.annotations.LifecyclePhase; |
| import org.apache.maven.plugins.annotations.Mojo; |
| import org.apache.maven.plugins.annotations.Parameter; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.project.MavenProjectHelper; |
| import org.apache.maven.shared.utils.logging.MessageUtils; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.eclipse.aether.RepositorySystem; |
| import org.eclipse.aether.RepositorySystemSession; |
| import org.eclipse.aether.repository.RemoteRepository; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| |
| /** |
| * Creates a buildinfo file recording build environment and output, as specified in |
| * <a href="https://reproducible-builds.org/docs/jvm/">Reproducible Builds for the JVM</a> |
| * for mono-module build, and extended for multi-module build. |
| * Then if a remote repository is configured, check against reference content in it. |
| */ |
| @Mojo( name = "buildinfo", defaultPhase = LifecyclePhase.VERIFY ) |
| public class BuildinfoMojo |
| extends AbstractMojo |
| { |
| /** |
| * The Maven project. |
| */ |
| @Parameter( defaultValue = "${project}", readonly = true ) |
| protected MavenProject project; |
| |
| /** |
| * The reactor projects. |
| */ |
| @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true ) |
| protected List<MavenProject> reactorProjects; |
| |
| /** |
| * Location of the generated buildinfo file. |
| */ |
| @Parameter( defaultValue = "${project.build.directory}/${project.artifactId}-${project.version}.buildinfo", |
| required = true, readonly = true ) |
| private File buildinfoFile; |
| |
| /** |
| * Specifies whether to attach the generated buildinfo file to the project. |
| */ |
| @Parameter( property = "buildinfo.attach", defaultValue = "true" ) |
| private boolean attach; |
| |
| /** |
| * Rebuild arguments. |
| */ |
| //@Parameter( property = "buildinfo.rebuild-args", defaultValue = "-DskipTests verify" ) |
| //private String rebuildArgs; |
| |
| /** |
| * Repository for reference build, containing either reference buildinfo file or reference artifacts.<br/> |
| * Format: <code>id</code> or <code>url</code> or <code>id::url</code> |
| * <dl> |
| * <dt>id</dt> |
| * <dd>The id can be used to pick up the correct credentials from the settings.xml</dd> |
| * <dt>url</dt> |
| * <dd>The location of the repository</dd> |
| * </dl> |
| */ |
| @Parameter( property = "reference.repo" ) |
| private String referenceRepo; |
| |
| /** |
| * Specifies if reference comparison output file should be saved. |
| * This is expected to be a temporary feature to ease |
| * <a href="https://github.com/jvm-repo-rebuild/reproducible-central">Central Repository rebuild</a> |
| * results display. |
| */ |
| @Parameter( property = "reference.compare.save", defaultValue = "false" ) |
| private boolean referenceCompareSave; |
| |
| /** |
| * Used for attaching the buildinfo file in the project. |
| */ |
| @Component |
| private MavenProjectHelper projectHelper; |
| |
| @Component |
| private ArtifactFactory artifactFactory; |
| |
| /** |
| * The entry point to Maven Artifact Resolver, i.e. the component doing all the work. |
| */ |
| @Component |
| private RepositorySystem repoSystem; |
| |
| /** |
| * The current repository/network configuration of Maven. |
| */ |
| @Parameter( defaultValue = "${repositorySystemSession}", readonly = true ) |
| private RepositorySystemSession repoSession; |
| |
| /** |
| * The project's remote repositories to use for the resolution. |
| */ |
| @Parameter( defaultValue = "${project.remoteProjectRepositories}", readonly = true ) |
| private List<RemoteRepository> remoteRepos; |
| |
| public void execute() |
| throws MojoExecutionException |
| { |
| boolean mono = reactorProjects.size() == 1; |
| |
| if ( !mono ) |
| { |
| // if multi-module build, generate (aggregate) buildinfo only in last module |
| MavenProject last = reactorProjects.get( reactorProjects.size() - 1 ); |
| if ( project != last ) |
| { |
| getLog().info( "Skipping intermediate buildinfo, aggregate will be " + last.getArtifactId() ); |
| return; |
| } |
| } |
| |
| // generate buildinfo |
| Map<Artifact, String> artifacts = generateBuildinfo( mono ); |
| getLog().info( "Saved " + ( mono ? "" : "aggregate " ) + "info on build to " + buildinfoFile ); |
| |
| // eventually attach |
| if ( attach ) |
| { |
| projectHelper.attachArtifact( project, "buildinfo", buildinfoFile ); |
| } |
| else |
| { |
| getLog().info( "NOT adding buildinfo to the list of attached artifacts." ); |
| } |
| |
| // eventually check against reference |
| if ( referenceRepo != null ) |
| { |
| getLog().info( "Checking against reference build from " + referenceRepo + "..." ); |
| checkAgainstReference( mono, artifacts ); |
| } |
| } |
| |
| /** |
| * Generate buildinfo file. |
| * |
| * @param mono is it a mono-module build? |
| * @return a Map of artifacts added to the build info with their associated property key prefix |
| * (<code>outputs.[#module.].#artifact</code>) |
| * @throws MojoExecutionException |
| */ |
| private Map<Artifact, String> generateBuildinfo( boolean mono ) |
| throws MojoExecutionException |
| { |
| MavenProject root = mono ? project : getExecutionRoot(); |
| |
| buildinfoFile.getParentFile().mkdirs(); |
| |
| try ( PrintWriter p = new PrintWriter( new BufferedWriter( |
| new OutputStreamWriter( new FileOutputStream( buildinfoFile ), Charsets.ISO_8859_1 ) ) ) ) |
| { |
| BuildInfoWriter bi = new BuildInfoWriter( getLog(), p, mono ); |
| |
| bi.printHeader( root ); |
| |
| // artifact(s) fingerprints |
| if ( mono ) |
| { |
| bi.printArtifacts( project ); |
| } |
| else |
| { |
| for ( MavenProject project : reactorProjects ) |
| { |
| bi.printArtifacts( project ); |
| } |
| } |
| |
| return bi.getArtifacts(); |
| } |
| catch ( IOException e ) |
| { |
| throw new MojoExecutionException( "Error creating file " + buildinfoFile, e ); |
| } |
| } |
| |
| /** |
| * Check current build result with reference. |
| * |
| * @param mono is it a mono-module build? |
| * @artifacts a Map of artifacts added to the build info with their associated property key prefix |
| * (<code>outputs.[#module.].#artifact</code>) |
| * @throws MojoExecutionException |
| */ |
| private void checkAgainstReference( boolean mono, Map<Artifact, String> artifacts ) |
| throws MojoExecutionException |
| { |
| MavenProject root = mono ? project : getExecutionRoot(); |
| File referenceDir = new File( root.getBuild().getDirectory(), "reference" ); |
| referenceDir.mkdirs(); |
| |
| // download or create reference buildinfo |
| File referenceBuildinfo = downloadOrCreateReferenceBuildinfo( mono, artifacts, referenceDir ); |
| |
| // compare outputs from reference buildinfo vs actual |
| compareWithReference( artifacts, referenceBuildinfo ); |
| } |
| |
| private File downloadOrCreateReferenceBuildinfo( boolean mono, Map<Artifact, String> artifacts, File referenceDir ) |
| throws MojoExecutionException |
| { |
| RemoteRepository repo = createReferenceRepo(); |
| |
| ReferenceBuildinfoUtil rmb = new ReferenceBuildinfoUtil( getLog(), referenceDir, artifacts, |
| artifactFactory, repoSystem, repoSession ); |
| |
| return rmb.downloadOrCreateReferenceBuildinfo( repo, project, buildinfoFile, mono ); |
| } |
| |
| private void compareWithReference( Map<Artifact, String> artifacts, File referenceBuildinfo ) |
| throws MojoExecutionException |
| { |
| Properties actual = BuildInfoWriter.loadOutputProperties( buildinfoFile ); |
| Properties reference = BuildInfoWriter.loadOutputProperties( referenceBuildinfo ); |
| |
| int ok = 0; |
| List<String> okFilenames = new ArrayList<>(); |
| List<String> koFilenames = new ArrayList<>(); |
| File referenceDir = referenceBuildinfo.getParentFile(); |
| for ( Map.Entry<Artifact, String> entry : artifacts.entrySet() ) |
| { |
| Artifact artifact = entry.getKey(); |
| String prefix = entry.getValue(); |
| |
| if ( checkArtifact( artifact, prefix, reference, actual, referenceDir ) ) |
| { |
| ok++; |
| okFilenames.add( artifact.getFile().getName() ); |
| } |
| else |
| { |
| koFilenames.add( artifact.getFile().getName() ); |
| } |
| } |
| |
| int ko = artifacts.size() - ok; |
| int missing = reference.size() / 3 /* 3 property keys par file: filename, length and checksums.sha512 */; |
| |
| if ( ko + missing > 0 ) |
| { |
| getLog().warn( "Reproducible Build output summary: " + ok + " files ok, " + ko + " different, " + missing |
| + " missing" ); |
| getLog().warn( "diff " + relative( referenceBuildinfo ) + " " + relative( buildinfoFile ) ); |
| } |
| else |
| { |
| getLog().info( "Reproducible Build output summary: " + ok + " files ok" ); |
| } |
| |
| if ( referenceCompareSave ) |
| { |
| File compare = new File( buildinfoFile.getParentFile(), buildinfoFile.getName() + ".compare" ); |
| try ( PrintWriter p = |
| new PrintWriter( new BufferedWriter( new OutputStreamWriter( new FileOutputStream( compare ), |
| Charsets.ISO_8859_1 ) ) ) ) |
| { |
| p.println( "ok=" + ok ); |
| p.println( "ko=" + ko ); |
| p.println( "okFiles=\"" + StringUtils.join( okFilenames.iterator(), " " ) + '"' ); |
| p.println( "koFiles=\"" + StringUtils.join( koFilenames.iterator(), " " ) + '"' ); |
| getLog().info( "Reproducible Build comparison saved to " + compare ); |
| } |
| catch ( IOException e ) |
| { |
| throw new MojoExecutionException( "Error creating file " + compare, e ); |
| } |
| } |
| } |
| |
| private boolean checkArtifact( Artifact artifact, String prefix, Properties reference, Properties actual, |
| File referenceDir ) |
| { |
| String actualFilename = (String) actual.remove( prefix + ".filename" ); |
| String actualLength = (String) actual.remove( prefix + ".length" ); |
| String actualSha512 = (String) actual.remove( prefix + ".checksums.sha512" ); |
| |
| String referencePrefix = findPrefix( reference, actualFilename ); |
| String referenceLength = (String) reference.remove( referencePrefix + ".length" ); |
| String referenceSha512 = (String) reference.remove( referencePrefix + ".checksums.sha512" ); |
| |
| if ( !actualLength.equals( referenceLength ) ) |
| { |
| getLog().warn( "size mismatch " + MessageUtils.buffer().strong( actualFilename ) |
| + diffoscope( artifact, referenceDir ) ); |
| return false; |
| } |
| else if ( !actualSha512.equals( referenceSha512 ) ) |
| { |
| getLog().warn( "sha512 mismatch " + MessageUtils.buffer().strong( actualFilename ) |
| + diffoscope( artifact, referenceDir ) ); |
| return false; |
| } |
| return true; |
| } |
| |
| private String diffoscope( Artifact a, File referenceDir ) |
| { |
| File actual = a.getFile(); |
| File reference = new File( referenceDir, actual.getName() ); |
| return ": diffoscope " + relative( reference ) + " " + relative( actual ); |
| } |
| |
| private String relative( File file ) |
| { |
| return file.getPath().substring( getExecutionRoot().getBasedir().getPath().length() + 1 ); |
| } |
| |
| private String findPrefix( Properties reference, String actualFilename ) |
| { |
| for ( String name : reference.stringPropertyNames() ) |
| { |
| if ( name.endsWith( ".filename" ) && actualFilename.equals( reference.getProperty( name ) ) ) |
| { |
| reference.remove( name ); |
| return name.substring( 0, name.length() - ".filename".length() ); |
| } |
| } |
| return null; |
| } |
| |
| private RemoteRepository createReferenceRepo() |
| throws MojoExecutionException |
| { |
| if ( referenceRepo.contains( "::" ) ) |
| { |
| // id::url |
| int index = referenceRepo.indexOf( "::" ); |
| String id = referenceRepo.substring( 0, index ); |
| String url = referenceRepo.substring( index + 2 ); |
| return createDeploymentArtifactRepository( id, url ); |
| } |
| else if ( referenceRepo.contains( ":" ) ) |
| { |
| // url, will use default "reference" id |
| return createDeploymentArtifactRepository( "reference", referenceRepo ); |
| } |
| |
| // id |
| for ( RemoteRepository repo : remoteRepos ) |
| { |
| if ( referenceRepo.equals( repo.getId() ) ) |
| { |
| return repo; |
| } |
| } |
| throw new MojoExecutionException( "Could not find repository with id = " + referenceRepo ); |
| } |
| |
| private RemoteRepository createDeploymentArtifactRepository( String id, String url ) |
| { |
| return new RemoteRepository.Builder( id, "default", url ).build(); |
| } |
| |
| private MavenProject getExecutionRoot() |
| { |
| for ( MavenProject p : reactorProjects ) |
| { |
| if ( p.isExecutionRoot() ) |
| { |
| return p; |
| } |
| } |
| return null; |
| } |
| } |