diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/Cachi2Lockfile.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/Cachi2Lockfile.java new file mode 100644 index 00000000..8c42e61b --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/Cachi2Lockfile.java @@ -0,0 +1,58 @@ +package org.jboss.pnc.bacon.pig; + +import org.jboss.pnc.bacon.pig.impl.addons.cachi2.Cachi2LockfileGenerator; +import picocli.CommandLine; + +import java.io.File; +import java.util.List; +import java.util.concurrent.Callable; + +@CommandLine.Command( + name = "cachi2lockfile", + description = "Generates a Cachi2 lock file for a given Maven repository ZIP file.") +public class Cachi2Lockfile implements Callable { + + @CommandLine.Parameters(description = "Comma-separated paths to Maven repositories (ZIPs or directories)") + private List repositories = List.of(); + + @CommandLine.Option( + names = "--output", + description = "Target output file. If not provided, defaults to " + + Cachi2LockfileGenerator.DEFAULT_OUTPUT_FILENAME) + private File output; + + @CommandLine.Option( + names = "--maven-repository-url", + description = "Maven repository URL to record in the generated lock file. If not provided, org.jboss.pnc.bacon.pig.impl.utils.indy.Indy.getIndyUrl() will be used as the default one") + private String mavenRepoUrl; + + @CommandLine.Option( + names = "--preferred-checksum-alg", + description = "Preferred checksum algorithm to record in the generated lock file. If not provided, the strongest available SHA version will be used") + private String preferredChecksumAlg; + + @Override + public Integer call() { + if (repositories.isEmpty()) { + throw new IllegalArgumentException("Maven repository location was not provided"); + } + var generator = Cachi2LockfileGenerator.newInstance(); + if (output != null) { + generator.setOutputFile(output.toPath()); + } + if (mavenRepoUrl != null) { + generator.setDefaultMavenRepositoryUrl(mavenRepoUrl); + } + if (preferredChecksumAlg != null) { + generator.setPreferredChecksumAlg(preferredChecksumAlg); + } + for (var path : repositories) { + if (!path.exists()) { + throw new IllegalArgumentException(path + " does not exist"); + } + generator.addMavenRepository(path.toPath()); + } + generator.generate(); + return 0; + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/Pig.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/Pig.java index e5c23d1e..f6c38f8c 100644 --- a/pig/src/main/java/org/jboss/pnc/bacon/pig/Pig.java +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/Pig.java @@ -59,6 +59,7 @@ name = "pig", description = "PiG tool", subcommands = { + Cachi2Lockfile.class, Pig.Configure.class, Pig.Cancel.class, Pig.Build.class, diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/AddOnFactory.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/AddOnFactory.java index df769e66..5552fb45 100644 --- a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/AddOnFactory.java +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/AddOnFactory.java @@ -19,6 +19,7 @@ import org.jboss.pnc.bacon.pig.impl.addons.camel.CamelRuntimeDependenciesToAlignTree; import org.jboss.pnc.bacon.pig.impl.addons.camel.RuntimeDependenciesToAlignTree; +import org.jboss.pnc.bacon.pig.impl.addons.cachi2.Cachi2LockfileAddon; import org.jboss.pnc.bacon.pig.impl.addons.microprofile.MicroProfileSmallRyeCommunityDepAnalyzer; import org.jboss.pnc.bacon.pig.impl.addons.quarkus.QuarkusCommunityDepAnalyzer; import org.jboss.pnc.bacon.pig.impl.addons.quarkus.QuarkusPostBuildAnalyzer; @@ -73,6 +74,7 @@ public static List listAddOns( releasePath, extrasPath, deliverables)); + resultList.add(new Cachi2LockfileAddon(pigConfiguration, builds, releasePath, extrasPath)); return resultList; } diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2Lockfile.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2Lockfile.java new file mode 100644 index 00000000..1c0a75f2 --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2Lockfile.java @@ -0,0 +1,146 @@ +package org.jboss.pnc.bacon.pig.impl.addons.cachi2; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static org.jboss.pnc.bacon.pig.impl.addons.cachi2.YamlUtil.initYamlMapper; + +/** + * Cachi2 lockfile + */ +@JsonInclude(JsonInclude.Include.NON_DEFAULT) +public class Cachi2Lockfile { + + /** + * Reads a Cachi2 lockfile. + * + * @param lockfile Cachi2 lock file + * @return Java object model representation of a Cachi2 lock file + */ + public static Cachi2Lockfile readFrom(Path lockfile) { + try (BufferedReader reader = Files.newBufferedReader(lockfile)) { + return initYamlMapper().readValue(reader, Cachi2Lockfile.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Serializes an instance of {@link Cachi2Lockfile} to a YAML file. + * + * @param lockfile target YAML file + */ + public static void persistTo(Cachi2Lockfile lockfile, Path file) { + var parentDir = file.getParent(); + if (parentDir != null) { + try { + Files.createDirectories(parentDir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + initYamlMapper().writeValue(writer, lockfile); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + public static class Cachi2Artifact { + + private String type; + private String filename; + private Map attributes = new TreeMap<>(); + private String checksum; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public String getChecksum() { + return checksum; + } + + public void setChecksum(String checksum) { + this.checksum = checksum; + } + + public void setGroupId(String groupId) { + attributes.put("group_id", groupId); + } + + public void setArtifactId(String artifactId) { + attributes.put("artifact_id", artifactId); + } + + public void setArtifactType(String type) { + attributes.put("type", type); + } + + public void setClassifier(String classifier) { + attributes.put("classifier", classifier); + } + + public void setVersion(String version) { + attributes.put("version", version); + } + + public void setRepositoryUrl(String repositoryUrl) { + attributes.put("repository_url", repositoryUrl); + } + } + + private Map metadata = new TreeMap<>(); + private List artifacts = new ArrayList<>(); + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public List getArtifacts() { + return artifacts; + } + + public void addArtifact(Cachi2Artifact artifact) { + artifacts.add(artifact); + } + + public void setArtifacts(List content) { + this.artifacts = content; + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2LockfileAddon.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2LockfileAddon.java new file mode 100644 index 00000000..03ea9fcc --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2LockfileAddon.java @@ -0,0 +1,95 @@ +package org.jboss.pnc.bacon.pig.impl.addons.cachi2; + +import org.jboss.pnc.bacon.pig.impl.PigContext; +import org.jboss.pnc.bacon.pig.impl.addons.AddOn; +import org.jboss.pnc.bacon.pig.impl.config.PigConfiguration; +import org.jboss.pnc.bacon.pig.impl.pnc.PncBuild; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +/** + * An add-on that generates Cachi2 lock files. + */ +public class Cachi2LockfileAddon extends AddOn { + + /** + * Output file name + */ + private static final String PARAM_FILENAME = "filename"; + + /** + * Default repository URL for artifacts not recognized by PNC + */ + private static final String PARAM_DEFAULT_REPO_URL = "default-repository-url"; + + /** + * Preferred checksum algorithm to record in the generated lock file + */ + private static final String PARAM_PREFERRED_CHECKSUM_ALG = "preferred-checksum-alg"; + + public Cachi2LockfileAddon( + PigConfiguration pigConfiguration, + Map builds, + String releasePath, + String extrasPath) { + super(pigConfiguration, builds, releasePath, extrasPath); + } + + @Override + public String getName() { + return "cachi2LockFile"; + } + + @Override + public void trigger() { + var repoPath = PigContext.get().getRepositoryData().getRepositoryPath(); + if (!Files.exists(repoPath)) { + throw new IllegalArgumentException(repoPath + " does not exist"); + } + + Cachi2LockfileGenerator cachi2Lockfile = Cachi2LockfileGenerator.newInstance() + .setOutputDirectory(Path.of(extrasPath)) + .addMavenRepository(repoPath); + + setParams(cachi2Lockfile); + + cachi2Lockfile.generate(); + } + + /** + * Set configured parameters on the generator. + * + * @param cachi2Lockfile lock file generator + */ + private void setParams(Cachi2LockfileGenerator cachi2Lockfile) { + var params = getAddOnConfiguration(); + if (params != null) { + setFilename(cachi2Lockfile, params); + setDefaultRepositoryUrl(cachi2Lockfile, params); + setPreferredChecksumAlg(cachi2Lockfile, params); + } + } + + private void setFilename(Cachi2LockfileGenerator cachi2Lockfile, Map params) { + var value = params.get(PARAM_FILENAME); + if (value != null) { + cachi2Lockfile.setOutputFileName(value.toString()); + } + } + + private void setDefaultRepositoryUrl(Cachi2LockfileGenerator cachi2Lockfile, Map params) { + var value = params.get(PARAM_DEFAULT_REPO_URL); + if (value != null) { + cachi2Lockfile.setDefaultMavenRepositoryUrl(value.toString()); + } + } + + private void setPreferredChecksumAlg(Cachi2LockfileGenerator cachi2Lockfile, Map params) { + var value = params.get(PARAM_PREFERRED_CHECKSUM_ALG); + if (value != null) { + cachi2Lockfile.setPreferredChecksumAlg(value.toString()); + } + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2LockfileGenerator.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2LockfileGenerator.java new file mode 100644 index 00000000..1eaf6125 --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/Cachi2LockfileGenerator.java @@ -0,0 +1,360 @@ +package org.jboss.pnc.bacon.pig.impl.addons.cachi2; + +import io.quarkus.fs.util.ZipUtils; +import org.jboss.pnc.bacon.pig.impl.repo.visitor.ArtifactVisit; +import org.jboss.pnc.bacon.pig.impl.repo.visitor.VisitableArtifactRepository; +import org.jboss.pnc.bacon.pig.impl.utils.GAV; +import org.jboss.pnc.bacon.pig.impl.utils.indy.Indy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Formatter; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Phaser; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Cachi2 lock file generator + */ +public class Cachi2LockfileGenerator { + + private static final Logger log = LoggerFactory.getLogger(Cachi2LockfileGenerator.class); + + private static final String MAVEN_RESPOSITORY_DIR = "maven-repository/"; + private static final String FORMAT_BASE = "[%s/%s %.1f%%] "; + private static final String CACHI_2_LOCKFILE_ADDED = "Cachi2 lockfile added "; + private static final String CACHI_2_LOCKFILE_SKIPPED_DUPLICATE = "Cachi2 lockfile skipped duplicate "; + private static final String SHA = "sha"; + public static final String DEFAULT_OUTPUT_FILENAME = "artifacts.lock.yaml"; + public static final String DEFAULT_REPOSITORY_URL = Indy.getIndyUrl(); + + public static Cachi2LockfileGenerator newInstance() { + return new Cachi2LockfileGenerator(); + } + + private Path outputDir; + private String outputFileName; + private Path outputFile; + private VisitableArtifactRepository repository; + private List repositoryLocations = List.of(); + private String defaultRepositoryUrl = DEFAULT_REPOSITORY_URL; + private String preferredChecksumAlg; + + /** + * Set the output directory. If not set, the default value will be the current user directory. The output file will + * be created in the output directory with the name configured with {@link #setOutputFileName(String)} or its + * default value {@link #DEFAULT_OUTPUT_FILENAME} unless the target output file was configured with + * {@link #setOutputFile(Path)}, in which case the output directory value and the file name will be ignored. + * + * @param outputDir output directory + * @return this instance + */ + public Cachi2LockfileGenerator setOutputDirectory(Path outputDir) { + this.outputDir = outputDir; + return this; + } + + /** + * Set the output file name. The output file will be created in the output directory with the name configured with + * the configured file name or its default value {@link #DEFAULT_OUTPUT_FILENAME} unless the target output file was + * configured with {@link #setOutputFile(Path)}, in which case the output directory value and the file name will be + * ignored. + * + * @param outputFileName output file name + * @return this instance + */ + public Cachi2LockfileGenerator setOutputFileName(String outputFileName) { + this.outputFileName = outputFileName; + return this; + } + + /** + * Sets the output file. If an output file is configured then values set with {@link #setOutputDirectory(Path)} and + * {@link #setOutputFileName(String)} will be ignored. + * + * @param outputFile output file + * @return this instance + */ + public Cachi2LockfileGenerator setOutputFile(Path outputFile) { + this.outputFile = outputFile; + return this; + } + + /** + * Maven repository that implements the visitor pattern for its artifacts. If a Maven repository is configured with + * this method and {@link #addMavenRepository(Path)} the artifacts from all the Maven repositories will be collected + * in a single lock file. + * + * @param mavenRepository visitable Maven repository + * @return this instance + */ + public Cachi2LockfileGenerator setMavenRepository(VisitableArtifactRepository mavenRepository) { + this.repository = mavenRepository; + return this; + } + + /** + * Path to a local Maven repository to generate a lock file for. The path can point to a directory or a ZIP file. In + * case {@link #setMavenRepository(VisitableArtifactRepository)} is also called, the artifacts from all repositories + * will be collected in a single lock file. + * + * @param mavenRepo path to a local Maven repository + * @return this instance + */ + public Cachi2LockfileGenerator addMavenRepository(Path mavenRepo) { + if (this.repositoryLocations.isEmpty()) { + this.repositoryLocations = new ArrayList<>(1); + } + this.repositoryLocations.add(mavenRepo); + return this; + } + + /** + * Sets the default Maven repository URL, which will be used in case PNC information is not available for an + * artifact. + * + * @param defaultMavenRepositoryUrl default Maven repository URL for artifacts + * @return this instance + */ + public Cachi2LockfileGenerator setDefaultMavenRepositoryUrl(String defaultMavenRepositoryUrl) { + this.defaultRepositoryUrl = defaultMavenRepositoryUrl; + return this; + } + + /** + * Preferred checksum algorithm to include in the generated lock file. If not configured, the strongest available + * SHA version will be used. + * + * @param preferredChecksumAlg preferred checksum algorithm to include in the generated lock file + */ + public void setPreferredChecksumAlg(String preferredChecksumAlg) { + this.preferredChecksumAlg = preferredChecksumAlg; + } + + private Path getOutputFile() { + if (outputFile != null) { + return outputFile; + } + + return (outputDir == null ? Path.of("") : outputDir) + .resolve(outputFileName == null ? DEFAULT_OUTPUT_FILENAME : outputFileName); + } + + public void generate() { + log.info("Generating Cachi2 lockfile"); + var start = System.currentTimeMillis(); + final Path lockfileYaml = persistLockfile(generateLockfile()); + logDone(lockfileYaml, System.currentTimeMillis() - start); + } + + private Path persistLockfile(Cachi2Lockfile lockfile) { + final Path lockfileYaml = getOutputFile(); + log.debug("Persisting the lock file to {}", lockfileYaml); + Cachi2Lockfile.persistTo(lockfile, lockfileYaml); + return lockfileYaml; + } + + private Cachi2Lockfile generateLockfile() { + var arr = collectArtifacts(); + Arrays.sort(arr, Comparator.comparing(Cachi2Lockfile.Cachi2Artifact::getFilename)); + final Cachi2Lockfile lockfile = new Cachi2Lockfile(); + lockfile.setMetadata(Map.of("version", "1.0")); + lockfile.setArtifacts(List.of(arr)); + return lockfile; + } + + private Cachi2Lockfile.Cachi2Artifact[] collectArtifacts() { + final Map cachi2Artifacts = new ConcurrentHashMap<>(); + collectArtifacts(cachi2Artifacts); + return cachi2Artifacts.values().toArray(new Cachi2Lockfile.Cachi2Artifact[0]); + } + + private void collectArtifacts(Map cachi2Artifacts) { + if (repository != null) { + generateLockfile(repository, cachi2Artifacts); + } else if (repositoryLocations.isEmpty()) { + throw new IllegalArgumentException( + "Neither visitable Maven repository nor Maven repository paths were configured"); + } + for (var repositoryLocation : repositoryLocations) { + if (Files.isDirectory(repositoryLocation)) { + generateLockfile(VisitableArtifactRepository.of(repositoryLocation), cachi2Artifacts); + } else { + try (FileSystem fs = ZipUtils.newFileSystem(repositoryLocation)) { + generateLockfile(VisitableArtifactRepository.of(fs.getPath("")), cachi2Artifacts); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + } + + private void generateLockfile( + VisitableArtifactRepository mavenRepo, + Map cachi2Artifacts) { + final Phaser phaser = new Phaser(1); + final Collection errors = new ConcurrentLinkedDeque<>(); + final AtomicInteger artifactCounter = new AtomicInteger(); + mavenRepo.visit(visit -> { + if (cachi2Artifacts.containsKey(visit.getGav().toGapvc())) { + logProcessedArtifact( + CACHI_2_LOCKFILE_SKIPPED_DUPLICATE, + visit.getGav(), + artifactCounter, + mavenRepo.getArtifactsTotal()); + } else { + phaser.register(); + CompletableFuture.runAsync(() -> { + final Cachi2Lockfile.Cachi2Artifact ca = new Cachi2Lockfile.Cachi2Artifact(); + ca.setType("maven"); + try { + ca.setFilename(MAVEN_RESPOSITORY_DIR + visit.getGav().toUri()); + addNonPncArtifact(visit, ca); + cachi2Artifacts.put(visit.getGav().toGapvc(), ca); + } catch (Exception e) { + errors.add(e); + } finally { + phaser.arriveAndDeregister(); + } + logProcessedArtifact( + CACHI_2_LOCKFILE_ADDED, + visit.getGav(), + artifactCounter, + mavenRepo.getArtifactsTotal()); + }); + } + }); + phaser.arriveAndAwaitAdvance(); + assertNoErrors(errors); + } + + private static void logDone(Path lockfileYaml, long totalMs) { + var secTotal = totalMs / 1000; + var minTotal = secTotal / 60; + var hoursTotal = minTotal / 60; + var sb = new StringBuilder().append("Generated Cachi2 lock file ").append(lockfileYaml).append(" in "); + boolean appendUnit = hoursTotal > 0; + if (appendUnit) { + sb.append(hoursTotal).append("h "); + } + appendUnit |= minTotal > 0; + if (appendUnit) { + sb.append(minTotal - hoursTotal * 60).append("min "); + } + appendUnit |= secTotal > 0; + if (appendUnit) { + sb.append(secTotal - minTotal * 60).append("sec "); + } + sb.append(totalMs - secTotal * 1000).append("ms"); + log.info(sb.toString()); + } + + private void logProcessedArtifact(String prefix, GAV artifact, AtomicInteger artifactCounter, int artifactsTotal) { + var sb = new StringBuilder(180); + var formatter = new Formatter(sb); + var artifactIndex = artifactCounter.incrementAndGet(); + final double percents = ((double) artifactIndex * 100) / artifactsTotal; + formatter.format(FORMAT_BASE, artifactIndex, artifactsTotal, percents); + sb.append(prefix).append(artifact.toGapvc()); + log.info(sb.toString()); + } + + private void addNonPncArtifact(ArtifactVisit artifactVisit, Cachi2Lockfile.Cachi2Artifact cachi2Artifact) { + var gav = artifactVisit.getGav(); + cachi2Artifact.setGroupId(gav.getGroupId()); + cachi2Artifact.setArtifactId(gav.getArtifactId()); + if (gav.getClassifier() != null && !gav.getClassifier().isEmpty()) { + cachi2Artifact.setClassifier(gav.getClassifier()); + } + cachi2Artifact.setArtifactType(gav.getPackaging()); + cachi2Artifact.setVersion(gav.getVersion()); + cachi2Artifact.setRepositoryUrl(defaultRepositoryUrl); + var checksums = artifactVisit.getChecksums(); + if (!checksums.isEmpty()) { + String checksum = null; + if (preferredChecksumAlg != null) { + checksum = checksums.get(preferredChecksumAlg); + if (checksum != null) { + checksum = preferredChecksumAlg + ":" + checksum; + } + } + if (checksum == null) { + checksum = getPreferredChecksum(checksums); + } + cachi2Artifact.setChecksum(checksum); + } + } + + /** + * Selects the preferred checksum out of the available ones. The current implementation will look for an SHA + * algorithm with the highest number. If an SHA algorithm was not found among the available ones, the first checksum + * will be returned. + *

+ * The returned value will follow the format {@code :}. + * + * @param checksums checksums to choose from + * @return preferred checksum value + */ + private static String getPreferredChecksum(Map checksums) { + String strongestAlg = null; + String strongestValue = null; + int strongestAlgNumber = 0; + for (var e : checksums.entrySet()) { + if (strongestAlg == null) { + strongestAlg = e.getKey(); + strongestValue = e.getValue(); + } + if (e.getKey().startsWith(SHA)) { + final int algNumber = Integer.parseInt(e.getKey().substring(SHA.length())); + if (algNumber > strongestAlgNumber) { + strongestAlg = e.getKey(); + strongestValue = e.getValue(); + strongestAlgNumber = algNumber; + } + } + } + return strongestAlg + ":" + strongestValue; + } + + private static void assertNoErrors(Collection errors) { + if (!errors.isEmpty()) { + var sb = new StringBuilder("The following errors were encountered while querying for artifact info:"); + log.error(sb.toString()); + var i = 1; + for (var error : errors) { + var prefix = i++ + ")"; + log.error(prefix, error); + sb.append(System.lineSeparator()).append(prefix).append(" ").append(error.getLocalizedMessage()); + for (var e : error.getStackTrace()) { + sb.append(System.lineSeparator()); + for (int j = 0; j < prefix.length(); ++j) { + sb.append(" "); + } + sb.append("at ").append(e); + if (e.getClassName().contains(Cachi2LockfileGenerator.class.getName())) { + sb.append(System.lineSeparator()); + for (int j = 0; j < prefix.length(); ++j) { + sb.append(" "); + } + sb.append("..."); + break; + } + } + } + throw new RuntimeException(sb.toString()); + } + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/YamlUtil.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/YamlUtil.java new file mode 100644 index 00000000..c1c63910 --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/addons/cachi2/YamlUtil.java @@ -0,0 +1,17 @@ +package org.jboss.pnc.bacon.pig.impl.addons.cachi2; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +class YamlUtil { + + static ObjectMapper initYamlMapper() { + return new ObjectMapper(new YAMLFactory()).enable(SerializationFeature.INDENT_OUTPUT) + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoDescriptor.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoDescriptor.java index 4c0bd091..d7be9c56 100644 --- a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoDescriptor.java +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoDescriptor.java @@ -42,8 +42,8 @@ public class RepoDescriptor { public static final String MAVEN_REPOSITORY = "maven-repository/"; /** - * Returns a collection of GAVs (that are actually groupId:artifactId:version, i.e. ignoring the classifier and the - * type attributes) found in a repository. + * Returns a collection of {@link GAV} (that are actually {@code groupId:artifactId:version}, i.e. ignoring the + * classifier and the type attributes) found in a repository. * * @param m2RepoDirectory local Maven repository directory * @return a list of GAVs found in a repository @@ -56,8 +56,8 @@ public static Collection listGavs(File m2RepoDirectory) { } /** - * Returns a collection of GAVs (that are include all the attributes of artifact coordinates, including the - * classifier and the type attributes). + * Returns a collection of {@link GAV} that include all the attributes of artifact coordinates, including the + * classifier and the type attributes. * * @param m2RepoDirectory local Maven repository directory * @return a list of GAVs found in a repository @@ -110,12 +110,16 @@ private static String getFileNameOrNull(Path p) { } private static boolean isInRepoDir(Path file) { - Path fileName = file == null ? null : file.getFileName(); - if (fileName == null) { + if (file == null) { return false; } - return fileName.toString().equals(MAVEN_REPOSITORY.substring(0, MAVEN_REPOSITORY.length() - 1)) - || isInRepoDir(file.getParent()); + for (int i = 0; i < file.getNameCount(); ++i) { + var name = file.getName(i); + if (name.toString().regionMatches(0, MAVEN_REPOSITORY, 0, MAVEN_REPOSITORY.length() - 1)) { + return true; + } + } + return false; } public static Collection listFiles(File m2RepoDirectory) { diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoManager.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoManager.java index d4e3b441..5dc6edca 100644 --- a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoManager.java +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/RepoManager.java @@ -43,6 +43,7 @@ import org.jboss.pnc.bacon.common.ObjectHelper; import org.jboss.pnc.bacon.common.exception.FatalException; import org.jboss.pnc.bacon.pig.impl.PigContext; +import org.jboss.pnc.bacon.pig.impl.addons.cachi2.Cachi2LockfileGenerator; import org.jboss.pnc.bacon.pig.impl.common.DeliverableManager; import org.jboss.pnc.bacon.pig.impl.config.AdditionalArtifactsFromBuild; import org.jboss.pnc.bacon.pig.impl.config.PigConfiguration; @@ -106,6 +107,7 @@ public class RepoManager extends DeliverableManager getResolvedArtifacts() { return resolvedArtifacts.values(); } + + public VisitableArtifactRepository toVisitableRepository() { + return new VisitableRepository(getRhArtifacts()); + } + + private Collection getRhArtifacts() { + Collection result = new ArrayList<>(); + for (var resolvedGav : getResolvedArtifacts()) { + if (!RhVersionPattern.isRhVersion(resolvedGav.getGav().getVersion())) { + continue; + } + for (var c : resolvedGav.getArtifacts()) { + result.add( + new org.jboss.pnc.bacon.pig.impl.utils.GAV( + c.getGroupId(), + c.getArtifactId(), + c.getVersion(), + c.getExtension(), + c.getClassifier())); + } + } + return result; + } + + private class VisitableRepository implements VisitableArtifactRepository { + private final Collection artifacts; + + public VisitableRepository(Collection artifacts) { + this.artifacts = artifacts; + } + + @Override + public void visit(ArtifactVisitor visitor) { + for (var gav : artifacts) { + visitor.visit(new ResolvedArtifactVisit(gav)); + } + } + + private Path getArtifactPath(org.jboss.pnc.bacon.pig.impl.utils.GAV artifact) { + var resolvedGav = resolvedArtifacts + .get(new GAV(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion())); + if (resolvedGav == null) { + throw new IllegalArgumentException( + "Failed to locate " + artifact.toGapvc() + " among the resolved artifacts"); + } + var path = resolvedGav.getArtifactDirectory().resolve(artifact.toFileName()); + if (!Files.exists(path)) { + throw new IllegalArgumentException("Failed to locate " + path); + } + return path; + } + + @Override + public int getArtifactsTotal() { + return artifacts.size(); + } + + private class ResolvedArtifactVisit implements ArtifactVisit { + private final org.jboss.pnc.bacon.pig.impl.utils.GAV gav; + + public ResolvedArtifactVisit(org.jboss.pnc.bacon.pig.impl.utils.GAV gav) { + this.gav = gav; + } + + @Override + public org.jboss.pnc.bacon.pig.impl.utils.GAV getGav() { + return gav; + } + + @Override + public Map getChecksums() { + return FileSystemArtifactVisit.readArtifactChecksums(getArtifactPath(gav)); + } + } + } } diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/ResolvedGav.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/ResolvedGav.java index 56f8e5d4..28d734ce 100644 --- a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/ResolvedGav.java +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/ResolvedGav.java @@ -33,7 +33,7 @@ public class ResolvedGav { this.gav = gav; } - GAV getGav() { + public GAV getGav() { return gav; } @@ -82,7 +82,7 @@ void addArtifact(Artifact a) { } } - Collection getArtifacts() { + public Collection getArtifacts() { return artifacts; } } diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/ArtifactVisit.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/ArtifactVisit.java new file mode 100644 index 00000000..4833a250 --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/ArtifactVisit.java @@ -0,0 +1,15 @@ +package org.jboss.pnc.bacon.pig.impl.repo.visitor; + +import org.jboss.pnc.bacon.pig.impl.utils.GAV; + +import java.util.Map; + +/** + * Information about a visited artifact + */ +public interface ArtifactVisit { + + GAV getGav(); + + Map getChecksums(); +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/ArtifactVisitor.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/ArtifactVisitor.java new file mode 100644 index 00000000..4020985a --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/ArtifactVisitor.java @@ -0,0 +1,14 @@ +package org.jboss.pnc.bacon.pig.impl.repo.visitor; + +/** + * Repository artifact visitor + */ +public interface ArtifactVisitor { + + /** + * Called for each artifact present in a repository + * + * @param visit visited artifact + */ + void visit(ArtifactVisit visit); +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/FileSystemArtifactRepository.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/FileSystemArtifactRepository.java new file mode 100644 index 00000000..1e05dfdd --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/FileSystemArtifactRepository.java @@ -0,0 +1,97 @@ +package org.jboss.pnc.bacon.pig.impl.repo.visitor; + +import org.jboss.pnc.bacon.pig.impl.repo.RepoDescriptor; +import org.jboss.pnc.bacon.pig.impl.utils.GAV; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +class FileSystemArtifactRepository implements VisitableArtifactRepository { + + /** + * A directory that contains Maven repository content. The Maven repository content may not start at the root though + * but be nested in some other directory. + */ + private final Path mavenRepoPath; + /** + * Maven repository directory relative to the mavenRepoPath + */ + private volatile Path mavenRepoDir; + + private final Collection artifacts; + + /** + * A directory that contains Maven repository content. The Maven repository content may not start at the root though + * but be nested in some other directory. + * + * @param mavenRepoPath directory containing Maven repository content + */ + FileSystemArtifactRepository(Path mavenRepoPath) { + this.mavenRepoPath = Objects.requireNonNull(mavenRepoPath, "Maven repository path is null"); + if (!Files.isDirectory(mavenRepoPath)) { + throw new IllegalArgumentException(mavenRepoPath + " is not a directory"); + } + artifacts = RepoDescriptor.listArtifacts(mavenRepoPath); + } + + @Override + public void visit(ArtifactVisitor visitor) { + for (var a : artifacts) { + visitor.visit(new FileSystemArtifactVisit(this, a)); + } + } + + @Override + public int getArtifactsTotal() { + return artifacts.size(); + } + + T processArtifact(GAV gav, Function func) { + return processArtifact(func, mavenRepoPath, gav.toUri()); + } + + private T processArtifact(Function func, Path baseDir, String artifactRelativePath) { + if (mavenRepoDir == null) { + mavenRepoDir = getMavenRepoDir(baseDir, artifactRelativePath); + } + final Path artifactPath = mavenRepoDir.resolve(artifactRelativePath); + if (!Files.exists(artifactPath)) { + throw new RuntimeException("Failed to locate " + artifactPath + " in " + mavenRepoPath); + } + return func.apply(artifactPath); + } + + private Path getMavenRepoDir(Path rootDir, String artifactRelativePath) { + final AtomicReference mavenRepoDirRef = new AtomicReference<>(); + try { + Files.walkFileTree(rootDir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + if (Files.exists(dir.resolve(artifactRelativePath))) { + mavenRepoDirRef.set(dir); + return FileVisitResult.TERMINATE; + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + var mavenRepoDir = mavenRepoDirRef.get(); + if (mavenRepoDir == null) { + throw new RuntimeException( + "Failed to locate Maven repository directory in " + mavenRepoPath + " containing " + + artifactRelativePath); + } + return mavenRepoDir; + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/FileSystemArtifactVisit.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/FileSystemArtifactVisit.java new file mode 100644 index 00000000..ee4774e1 --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/FileSystemArtifactVisit.java @@ -0,0 +1,73 @@ +package org.jboss.pnc.bacon.pig.impl.repo.visitor; + +import org.jboss.pnc.bacon.pig.impl.utils.GAV; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public class FileSystemArtifactVisit implements ArtifactVisit { + + private static final String[] CHECKSUM_PREFIXES = { "sha", "md5" }; + + private final FileSystemArtifactRepository repo; + private final GAV gav; + + FileSystemArtifactVisit(FileSystemArtifactRepository repo, GAV gav) { + this.repo = repo; + this.gav = gav; + } + + @Override + public GAV getGav() { + return gav; + } + + @Override + public Map getChecksums() { + return repo.processArtifact(gav, FileSystemArtifactVisit::readArtifactChecksums); + } + + /** + * Reads Maven checksum files for a given artifact file in a local Maven repository. + * + * @param artifactPath artifact file in a local Maven repository + * @return checksum map with algorithm name as a key and checksum as a value + */ + public static Map readArtifactChecksums(Path artifactPath) { + final String checksumFilePrefix = artifactPath.getFileName().toString() + "."; + try (Stream stream = Files.list(artifactPath.getParent())) { + Map checksums = new HashMap<>(2); + stream.forEach(file -> { + var fileName = file.getFileName().toString(); + if (!fileName.startsWith(checksumFilePrefix)) { + return; + } + for (var checksumPrefix : CHECKSUM_PREFIXES) { + if (fileName.regionMatches( + true, + checksumFilePrefix.length(), + checksumPrefix, + 0, + checksumPrefix.length())) { + final String algName = fileName.substring(checksumFilePrefix.length()).toLowerCase(); + final String value; + try { + value = Files.readString(file); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + file, e); + } + checksums.put(algName, value); + } + } + }); + return checksums; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/VisitableArtifactRepository.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/VisitableArtifactRepository.java new file mode 100644 index 00000000..496a24a6 --- /dev/null +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/repo/visitor/VisitableArtifactRepository.java @@ -0,0 +1,27 @@ +package org.jboss.pnc.bacon.pig.impl.repo.visitor; + +import java.nio.file.Path; + +/** + * Visitable Maven artifact repository + */ +public interface VisitableArtifactRepository { + + static VisitableArtifactRepository of(Path mavenRepo) { + return new FileSystemArtifactRepository(mavenRepo); + } + + /** + * Visits artifacts present in the repository + * + * @param visitor artifact visitor + */ + void visit(ArtifactVisitor visitor); + + /** + * Total number of artifacts found in this repository. + * + * @return total number of artifacts found in this repository + */ + int getArtifactsTotal(); +} diff --git a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/utils/GAV.java b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/utils/GAV.java index 887114f8..bb4d5b0e 100644 --- a/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/utils/GAV.java +++ b/pig/src/main/java/org/jboss/pnc/bacon/pig/impl/utils/GAV.java @@ -106,7 +106,8 @@ public GAV(String groupId, String artifactId, String version, String packaging, this.groupId = groupId; this.artifactId = artifactId; this.version = version; - this.classifier = classifier; + // other methods assume that a non-null classifer is a non-empty classifier + this.classifier = classifier == null || classifier.isEmpty() ? null : classifier; } public static GAV fromXml(Element xml, Map properties) { diff --git a/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/BomMultiStepRepositoryStrategyTest.java b/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/BomMultiStepRepositoryStrategyTest.java index d045ea8f..2ef10c27 100644 --- a/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/BomMultiStepRepositoryStrategyTest.java +++ b/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/BomMultiStepRepositoryStrategyTest.java @@ -4,6 +4,11 @@ public class BomMultiStepRepositoryStrategyTest extends MultiStepBomBasedRepositoryTestBase { + @Override + protected void assertOutcome() { + assertCachi2LockFile(CACHI2_LOCKFILE_NAME); + } + @Override protected RepoGenerationStrategy getRepoGenerationStrategy() { return RepoGenerationStrategy.BOM; diff --git a/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/MultiStepBomBasedRepositoryTestBase.java b/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/MultiStepBomBasedRepositoryTestBase.java index a9f68b93..fab3cdea 100644 --- a/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/MultiStepBomBasedRepositoryTestBase.java +++ b/pig/src/test/java/org/jboss/pnc/bacon/pig/impl/repo/MultiStepBomBasedRepositoryTestBase.java @@ -1,16 +1,19 @@ package org.jboss.pnc.bacon.pig.impl.repo; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.fs.util.ZipUtils; import org.apache.maven.settings.Profile; import org.apache.maven.settings.Repository; import org.apache.maven.settings.RepositoryPolicy; import org.apache.maven.settings.Settings; import org.apache.maven.settings.io.DefaultSettingsReader; import org.apache.maven.settings.io.DefaultSettingsWriter; -import org.assertj.core.api.Assertions; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.jboss.pnc.bacon.pig.impl.PigContext; +import org.jboss.pnc.bacon.pig.impl.addons.AddOnFactory; +import org.jboss.pnc.bacon.pig.impl.addons.cachi2.Cachi2Lockfile; import org.jboss.pnc.bacon.pig.impl.config.Flow; import org.jboss.pnc.bacon.pig.impl.config.PigConfiguration; import org.jboss.pnc.bacon.pig.impl.config.ProductConfig; @@ -19,6 +22,7 @@ import org.jboss.pnc.bacon.pig.impl.documents.Deliverables; import org.jboss.pnc.bacon.pig.impl.pnc.BuildInfoCollector; import org.jboss.pnc.bacon.pig.impl.pnc.PncBuild; +import org.jboss.pnc.bacon.pig.impl.repo.visitor.VisitableArtifactRepository; import org.jboss.pnc.bacon.pig.impl.utils.ResourceUtils; import org.jboss.pnc.bacon.pig.impl.utils.indy.Indy; import org.junit.jupiter.api.AfterAll; @@ -33,7 +37,9 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -47,6 +53,7 @@ import java.util.TreeSet; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; public abstract class MultiStepBomBasedRepositoryTestBase { @@ -56,6 +63,8 @@ public abstract class MultiStepBomBasedRepositoryTestBase { private static final String TEST_PLATFORM_ARTIFACTS = "test-platform-artifacts"; private static final String EXPECTED_ARTIFACT_LIST_TXT = "resolve-and-repackage-repo-artifact-list.txt"; private static final String EXTENSIONS_LIST_URL = "http://gitlab.cee.com"; + private static final String MOCK_REPO_URL = "https://mock.indy.org/maven"; + static final String CACHI2_LOCKFILE_NAME = "bom-strategy-generated-lockfile.yaml"; private Path workDir; private File testMavenSettings; @@ -88,6 +97,161 @@ void resolveAndRepackageShouldGenerateRepository() throws Exception { mockPigContextAndMethods(); mockIndySettingsFile(); + buildQuarkusPlatform(); + + PigConfiguration pigConfiguration = mockPigConfigurationAndMethods(); + + RepoGenerationData generationDataSpy = mockRepoGenerationDataAndMethods(pigConfiguration); + + Map buildsSpy = mockBuildsAndMethods(generationDataSpy); + + Path configurationDirectory = Mockito.mock(Path.class); + + mockResourceUtilsMethods(configurationDirectory); + + Deliverables deliverables = mockDeliverables(pigConfiguration); + + BuildInfoCollector buildInfoCollectorMock = Mockito.mock(BuildInfoCollector.class); + + final String releasePath = workDir.toString(); + RepoManager repoManager = new RepoManager( + pigConfiguration, + releasePath, + deliverables, + buildsSpy, + configurationDirectory, + false, + false, + false, + buildInfoCollectorMock, + false); + + RepoManager repoManagerSpy = Mockito.spy(repoManager); + + prepareFakeExtensionArtifactList(repoManagerSpy); + + RepositoryData repoData = repoManagerSpy.prepare(); + + // run the addons + PigContext pigCtx = PigContext.get(); + doReturn(repoData).when(pigCtx).getRepositoryData(); + for (var addon : AddOnFactory.listAddOns( + pigConfiguration, + buildsSpy, + releasePath, + workDir.resolve("extras").toString(), + deliverables)) { + if (addon.shouldRun()) { + addon.trigger(); + } + } + + assertThat(repoData.getRepositoryPath()).isEqualTo(getGeneratedMavenRepoZip()); + + assertRepoZipContent(repoData); + assertCachi2LockFile("extras/artifacts.lock.yaml"); + assertOutcome(); + } + + protected void assertCachi2LockFile(String lockFilePath) { + var zipArtifactChecksums = getZipArtifactChecksums(); + + var lockfilePath = getOutcomeResource(lockFilePath); + assertThat(lockfilePath).exists(); + var lockfile = Cachi2Lockfile.readFrom(lockfilePath); + assertThat(lockfile).isNotNull(); + + for (var cachi2Artifact : lockfile.getArtifacts()) { + assertThat(cachi2Artifact.getType()).isEqualTo("maven"); + final Map attributes = cachi2Artifact.getAttributes(); + var groupId = attributes.get("group_id"); + var artifactId = attributes.get("artifact_id"); + var version = attributes.get("version"); + var type = attributes.get("type"); + var classifier = attributes.get("classifier"); + + var key = groupId + ":" + artifactId + ":" + type + ":" + version; + if (classifier != null) { + key += ":" + classifier; + } + var zipArtifactChecksum = zipArtifactChecksums.remove(key); + assertThat(zipArtifactChecksum).isNotNull(); + assertThat(cachi2Artifact.getChecksum()).isEqualTo("sha1:" + zipArtifactChecksum); + assertThat(attributes.get("repository_url")).isEqualTo(MOCK_REPO_URL); + } + + // make sure no more ZIP artifacts left + assertThat(zipArtifactChecksums).isEmpty(); + } + + private Map getZipArtifactChecksums() { + try (FileSystem zipFs = ZipUtils.newZip(getGeneratedMavenRepoZip())) { + final VisitableArtifactRepository visitableRepo = VisitableArtifactRepository.of(zipFs.getPath("")); + final Map zipArtifacts = new HashMap<>(visitableRepo.getArtifactsTotal()); + visitableRepo.visit(visit -> { + var sha1 = visit.getChecksums().get("sha1"); + assertThat(sha1).isNotBlank(); + zipArtifacts.put(visit.getGav().toGapvc(), sha1); + }); + return zipArtifacts; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Returns the path to the generated Maven repository ZIP. + * + * @return path to the generated Maven repository ZIP + */ + protected Path getGeneratedMavenRepoZip() { + return getOutcomeResource("rh-sample-maven-repository.zip"); + } + + /** + * Returns a path to a resource produced by the pig run. The returned path may or may not exist. It's the + * responsibility of the caller to perform the assertions. + * + * @param name name of the resource + * @return path to a resource, which may or may not exist + */ + protected Path getOutcomeResource(String name) { + return workDir.resolve(name); + } + + /** + * Allows subclasses to assert target strategy-specific outcomes + */ + protected void assertOutcome() { + } + + private void assertRepoZipContent(RepositoryData repoData) { + final Set expectedFiles = repoZipContentList(); + + final Set actualFiles = repoData.getFiles() + .stream() + .map(file -> file.getAbsolutePath().replaceAll(".+/deliverable-generation\\d+/", "").replace('\\', '/')) + .collect(Collectors.toCollection(TreeSet::new)); + + final Path actualArtifactList = workDir.resolve("resolve-and-repackage-repo-artifact-list-actual.txt"); + try { + Files.write( + actualArtifactList, + (actualFiles.stream().collect(Collectors.joining("\n")) + "\n").getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Could not write to " + actualArtifactList, e); + } + + if (!actualFiles.equals(expectedFiles)) { + System.out.printf( + "\nThe zipped repository has unexpected content. You may want to compare src/test/resources/%s with %s\n\n", + EXPECTED_ARTIFACT_LIST_TXT, + actualArtifactList); + } + assertThat(actualFiles).containsExactlyElementsOf(expectedFiles); + } + + private void buildQuarkusPlatform() throws BootstrapMavenException { // initialize a Maven artifact resolver with the local Maven repo pointing // to a location we want to install the platform related artifacts below final MavenArtifactResolver resolver = MavenArtifactResolver.builder() @@ -181,65 +345,6 @@ void resolveAndRepackageShouldGenerateRepository() throws Exception { .installArtifact(IO_QUARKUS_PLATFORM_TEST, "quarkus-maven-plugin", "1.1.1.redhat-00001") .setMavenResolver(resolver) .build(); - - PigConfiguration pigConfiguration = mockPigConfigurationAndMethods(); - - RepoGenerationData generationDataSpy = mockRepoGenerationDataAndMethods(pigConfiguration); - - Map buildsSpy = mockBuildsAndMethods(generationDataSpy); - - Path configurationDirectory = Mockito.mock(Path.class); - - mockResourceUtilsMethods(configurationDirectory); - - Deliverables deliverables = mockDeliverables(pigConfiguration); - - BuildInfoCollector buildInfoCollectorMock = Mockito.mock(BuildInfoCollector.class); - - RepoManager repoManager = new RepoManager( - pigConfiguration, - workDir.toString(), - deliverables, - buildsSpy, - configurationDirectory, - false, - false, - false, - buildInfoCollectorMock, - false); - - RepoManager repoManagerSpy = Mockito.spy(repoManager); - - prepareFakeExtensionArtifactList(repoManagerSpy); - - RepositoryData repoData = repoManagerSpy.prepare(); - - Assertions.assertThat(repoData.getRepositoryPath()) - .isEqualTo(workDir.resolve("rh-sample-maven-repository.zip")); - - final Set expectedFiles = repoZipContentList(); - - final Set actualFiles = repoData.getFiles() - .stream() - .map(file -> file.getAbsolutePath().replaceAll(".+/deliverable-generation\\d+/", "").replace('\\', '/')) - .collect(Collectors.toCollection(TreeSet::new)); - - final Path actualArtifactList = workDir.resolve("resolve-and-repackage-repo-artifact-list-actual.txt"); - try { - Files.write( - actualArtifactList, - (actualFiles.stream().collect(Collectors.joining("\n")) + "\n").getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException("Could not write to " + actualArtifactList, e); - } - - if (!actualFiles.equals(expectedFiles)) { - System.out.printf( - "\nThe zipped repository has unexpected content. You may want to compare src/test/resources/%s with %s\n\n", - EXPECTED_ARTIFACT_LIST_TXT, - actualArtifactList); - } - Assertions.assertThat(actualFiles).containsExactlyElementsOf(expectedFiles); } private void mockPigContextAndMethods() { @@ -282,6 +387,7 @@ private void mockIndySettingsFile() { String pathToTestSettingsFile = testMavenSettings.getAbsolutePath(); MockedStatic indyMockedStatic = Mockito.mockStatic(Indy.class); indyMockedStatic.when(() -> Indy.getConfiguredIndySettingsXmlPath(false)).thenReturn(pathToTestSettingsFile); + indyMockedStatic.when(() -> Indy.getIndyUrl()).thenReturn("https://mock.indy.org/maven"); } private Repository addLocalRepo(Settings settings, String id, Path localPath) { @@ -326,6 +432,7 @@ private PigConfiguration mockPigConfigurationAndMethods() { ProductConfig productConfig = Mockito.mock(ProductConfig.class); doReturn(productConfig).when(pigConfiguration).getProduct(); doReturn("sample").when(productConfig).getName(); + doReturn(Map.of("cachi2LockFile", Map.of())).when(pigConfiguration).getAddons(); return pigConfiguration; } @@ -348,7 +455,9 @@ private RepoGenerationData mockRepoGenerationDataAndMethods(PigConfiguration pig IO_QUARKUS_PLATFORM_TEST + ":quarkus-bom:1.1.1.redhat-00001", // this will resolve dependencies of the annotation processor w/o enforcing platform BOMs "nonManagedDependencies", - "org.acme:acme-annotation-processor:1.2.3.redhat-30303")); + "org.acme:acme-annotation-processor:1.2.3.redhat-30303", + "cachi2LockFile", + CACHI2_LOCKFILE_NAME)); // this quarkus-bom step is simply to generate the repo for the quarkus-bom final RepoGenerationData quarkusBomStep = new RepoGenerationData();