blob: 156e2d04cf1d9485b1028e0efdf1d87e6f69fa23 [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.plugins.artifact.buildinfo;
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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.PropertyUtils;
import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;
/**
* Compare current build output with reference either previously installed or downloaded from a remote repository:
* results go to {@code .buildcompare} file.
*
* @since 3.2.0
*/
@Mojo(name = "compare", threadSafe = true)
public class CompareMojo extends AbstractBuildinfoMojo {
/**
* 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 repository id</dd>
* <dt>url</dt>
* <dd>The url of the repository</dd>
* </dl>
* @see <a href="https://maven.apache.org/ref/current/maven-model/maven.html#repository">repository definition</a>
*/
@Parameter(property = "reference.repo", defaultValue = "central")
private String referenceRepo;
/**
* Compare aggregate only (ie wait for the last module) or do buildcompare on each module.
* @since 3.2.0
*/
@Parameter(property = "compare.aggregate.only", defaultValue = "false")
private boolean aggregateOnly;
@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;
@Component
private ArtifactRepositoryLayout artifactRepositoryLayout;
@Override
public void execute(Map<Artifact, String> artifacts) throws MojoExecutionException {
getLog().info("Checking against reference build from " + referenceRepo + "...");
checkAgainstReference(artifacts, reactorProjects.size() == 1);
}
@Override
protected void skip(MavenProject last) throws MojoExecutionException {
if (aggregateOnly) {
return;
}
// try to download reference artifacts for current project and check if there are issues to give early feedback
checkAgainstReference(generateBuildinfo(true), true);
}
/**
* Check current build result with reference.
*
* @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(Map<Artifact, String> artifacts, boolean mono) 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,
artifactHandlerManager,
rtInformation);
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<>();
List<String> diffoscopes = new ArrayList<>();
File referenceDir = referenceBuildinfo.getParentFile();
for (Map.Entry<Artifact, String> entry : artifacts.entrySet()) {
Artifact artifact = entry.getKey();
String prefix = entry.getValue();
String[] checkResult = checkArtifact(artifact, prefix, reference, actual, referenceDir);
String filename = checkResult[0];
String diffoscope = checkResult[1];
if (diffoscope == null) {
ok++;
okFilenames.add(filename);
} else {
koFilenames.add(filename);
diffoscopes.add(diffoscope);
}
}
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().error("Reproducible Build output summary: "
+ MessageUtils.buffer().success(ok + " files ok")
+ ", " + MessageUtils.buffer().failure(ko + " different")
+ ((missing == 0) ? "" : (", " + MessageUtils.buffer().failure(missing + " missing"))));
getLog().error("see "
+ MessageUtils.buffer()
.project("diff " + relative(referenceBuildinfo) + " " + relative(buildinfoFile))
.toString());
getLog().error("see also https://maven.apache.org/guides/mini/guide-reproducible-builds.html");
} else {
getLog().info("Reproducible Build output summary: "
+ MessageUtils.buffer().success(ok + " files ok"));
}
// save .compare file
File buildcompare = new File(
buildinfoFile.getParentFile(), buildinfoFile.getName().replaceFirst(".buildinfo$", ".buildcompare"));
try (PrintWriter p = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(buildcompare), StandardCharsets.UTF_8)))) {
p.println("version=" + project.getVersion());
p.println("ok=" + ok);
p.println("ko=" + ko);
p.println("okFiles=\"" + StringUtils.join(okFilenames.iterator(), " ") + '"');
p.println("koFiles=\"" + StringUtils.join(koFilenames.iterator(), " ") + '"');
Properties ref = PropertyUtils.loadOptionalProperties(referenceBuildinfo);
String v = ref.getProperty("java.version");
if (v != null) {
p.println("reference_java_version=\"" + v + '"');
}
v = ref.getProperty("os.name");
if (v != null) {
p.println("reference_os_name=\"" + v + '"');
}
for (String diffoscope : diffoscopes) {
p.print("# ");
p.println(diffoscope);
}
getLog().info("Reproducible Build output comparison saved to " + buildcompare);
} catch (IOException e) {
throw new MojoExecutionException("Error creating file " + buildcompare, e);
}
copyAggregateToRoot(buildcompare);
if (ko + missing > 0) {
throw new MojoExecutionException("Build artifacts are different from reference");
}
}
// { filename, diffoscope }
private String[] 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, artifact.getGroupId(), actualFilename);
String referenceLength = (String) reference.remove(referencePrefix + ".length");
String referenceSha512 = (String) reference.remove(referencePrefix + ".checksums.sha512");
reference.remove(referencePrefix + ".groupId");
String issue = null;
if (!actualLength.equals(referenceLength)) {
issue = "size";
} else if (!actualSha512.equals(referenceSha512)) {
issue = "sha512";
}
if (issue != null) {
String diffoscope = diffoscope(artifact, referenceDir);
getLog().error(issue + " mismatch " + MessageUtils.buffer().strong(actualFilename) + ": investigate with "
+ MessageUtils.buffer().project(diffoscope));
return new String[] {actualFilename, diffoscope};
}
return new String[] {actualFilename, null};
}
private String diffoscope(Artifact a, File referenceDir) {
File actual = a.getFile();
// notice: actual file name may have been defined in pom
// reference file name is taken from repository format
File reference = new File(new File(referenceDir, a.getGroupId()), getRepositoryFilename(a));
if ((actual == null) || (reference == null)) {
return "missing file for " + a.getId() + " reference = "
+ (reference == null ? "null" : relative(reference)) + " actual = "
+ (actual == null ? "null" : relative(actual));
}
return "diffoscope " + relative(reference) + " " + relative(actual);
}
private String getRepositoryFilename(Artifact a) {
String path = artifactRepositoryLayout.pathOf(a);
return path.substring(path.lastIndexOf('/'));
}
private String relative(File file) {
File basedir = getExecutionRoot().getBasedir();
int length = basedir.getPath().length();
String path = file.getPath();
return path.substring(length + 1);
}
private static String findPrefix(Properties reference, String actualGroupId, String actualFilename) {
for (String name : reference.stringPropertyNames()) {
if (name.endsWith(".filename") && actualFilename.equals(reference.getProperty(name))) {
String prefix = name.substring(0, name.length() - ".filename".length());
if (actualGroupId.equals(reference.getProperty(prefix + ".groupId"))) {
reference.remove(name);
return prefix;
}
}
}
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 static RemoteRepository createDeploymentArtifactRepository(String id, String url) {
return new RemoteRepository.Builder(id, "default", url).build();
}
}