diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index 9a06dbf1fe0f9..6a63a6f9417ec 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -1,6 +1,7 @@ package io.quarkus.deployment.pkg; import java.nio.file.Path; +import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; @@ -49,6 +50,13 @@ public interface PackageConfig { */ Optional outputName(); + /** + * The timestamp used as a reference for generating the packages (e.g. for the creation timestamp of ZIP entries). + *

+ * The approach is similar to what is done by the maven-jar-plugin with `project.build.outputTimestamp`. + */ + Optional outputTimestamp(); + /** * Setting this switch to {@code true} will cause Quarkus to write the transformed application bytecode * to the build tool's output directory. diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index ac478d68902a5..69ff7091cf025 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -36,6 +36,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.function.BiPredicate; @@ -64,6 +65,7 @@ import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.builditem.QuarkusBuildCloseablesBuildItem; import io.quarkus.deployment.builditem.TransformedClassesBuildItem; +import io.quarkus.deployment.builditem.TransformedClassesBuildItem.TransformedClass; import io.quarkus.deployment.configuration.ClassLoadingConfig; import io.quarkus.deployment.pkg.JarUnsigner; import io.quarkus.deployment.pkg.PackageConfig; @@ -350,7 +352,7 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem, MainClassBuildItem mainClassBuildItem, ClassLoadingConfig classLoadingConfig, Path runnerJar) throws Exception { - try (FileSystem runnerZipFs = createNewZip(runnerJar, packageConfig)) { + try (FileSystem runnerZipFs = createNewReproducibleZipFileSystem(runnerJar, packageConfig)) { log.info("Building uber jar: " + runnerJar); @@ -549,7 +551,7 @@ private JarBuildItem buildLegacyThinJar(CurateOutcomeBuildItem curateOutcomeBuil Files.deleteIfExists(runnerJar); IoUtils.createOrEmptyDir(libDir); - try (FileSystem runnerZipFs = createNewZip(runnerJar, packageConfig)) { + try (FileSystem runnerZipFs = createNewReproducibleZipFileSystem(runnerJar, packageConfig)) { log.info("Building thin jar: " + runnerJar); @@ -647,10 +649,14 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, if (!transformedClasses.getTransformedClassesByJar().isEmpty()) { Path transformedZip = quarkus.resolve(TRANSFORMED_BYTECODE_JAR); fastJarJarsBuilder.setTransformed(transformedZip); - try (FileSystem out = createNewZip(transformedZip, packageConfig)) { - for (Set transformedSet : transformedClasses - .getTransformedClassesByJar().values()) { - for (TransformedClassesBuildItem.TransformedClass transformed : transformedSet) { + try (FileSystem out = createNewReproducibleZipFileSystem(transformedZip, packageConfig)) { + // we make sure the entries are added in a reproducible order + // we use Path#toString() to get a reproducible order on both Unix-based OSes and Windows + for (Entry> transformedClassEntry : transformedClasses + .getTransformedClassesByJar().entrySet().stream() + .sorted(Comparator.comparing(e -> e.getKey().toString())).toList()) { + for (TransformedClass transformed : transformedClassEntry.getValue().stream() + .sorted(Comparator.comparing(TransformedClass::getFileName)).toList()) { Path target = out.getPath(transformed.getFileName()); if (transformed.getData() != null) { if (target.getParent() != null) { @@ -668,9 +674,11 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, //now generated classes and resources Path generatedZip = quarkus.resolve(GENERATED_BYTECODE_JAR); fastJarJarsBuilder.setGenerated(generatedZip); - try (FileSystem out = createNewZip(generatedZip, packageConfig)) { - for (GeneratedClassBuildItem i : generatedClasses) { - String fileName = fromClassNameToResourceName(i.getName()); + try (FileSystem out = createNewReproducibleZipFileSystem(generatedZip, packageConfig)) { + // make sure we write the elements in order + for (GeneratedClassBuildItem i : generatedClasses.stream() + .sorted(Comparator.comparing(GeneratedClassBuildItem::binaryName)).toList()) { + String fileName = fromClassNameToResourceName(i.internalName()); Path target = out.getPath(fileName); if (target.getParent() != null) { Files.createDirectories(target.getParent()); @@ -678,7 +686,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, Files.write(target, i.getClassData()); } - for (GeneratedResourceBuildItem i : generatedResources) { + // make sure we write the elements in order + for (GeneratedResourceBuildItem i : generatedResources.stream() + .sorted(Comparator.comparing(GeneratedResourceBuildItem::getName)).toList()) { Path target = out.getPath(i.getName()); if (target.getParent() != null) { Files.createDirectories(target.getParent()); @@ -703,7 +713,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, .setResolvedDependency(applicationArchivesBuildItem.getRootArchive().getResolvedDependency()) .setPath(runnerJar)); Predicate ignoredEntriesPredicate = getThinJarIgnoredEntriesPredicate(packageConfig); - try (FileSystem runnerZipFs = createNewZip(runnerJar, packageConfig)) { + try (FileSystem runnerZipFs = createNewReproducibleZipFileSystem(runnerJar, packageConfig)) { copyFiles(applicationArchivesBuildItem.getRootArchive(), runnerZipFs, null, ignoredEntriesPredicate); } } @@ -793,7 +803,7 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem, } } if (!rebuild) { - try (FileSystem runnerZipFs = createNewZip(initJar, packageConfig)) { + try (FileSystem runnerZipFs = createNewReproducibleZipFileSystem(initJar, packageConfig)) { ResolvedDependency appArtifact = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); generateManifest(runnerZipFs, classPath.toString(), packageConfig, appArtifact, QuarkusEntryPoint.class.getName(), @@ -997,7 +1007,7 @@ public static String getJarFileName(ResolvedDependency dep, Path resolvedPath) { } private void packageClasses(Path resolvedDep, final Path targetPath, PackageConfig packageConfig) throws IOException { - try (FileSystem runnerZipFs = createNewZip(targetPath, packageConfig)) { + try (FileSystem runnerZipFs = createNewReproducibleZipFileSystem(targetPath, packageConfig)) { Files.walkFileTree(resolvedDep, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor() { @Override @@ -1063,7 +1073,7 @@ private NativeImageSourceJarBuildItem buildNativeImageThinJar(CurateOutcomeBuild Path libDir = targetDirectory.resolve(LIB); Files.createDirectories(libDir); - try (FileSystem runnerZipFs = ZipUtils.newZip(runnerJar)) { + try (FileSystem runnerZipFs = createNewReproducibleZipFileSystem(runnerJar, packageConfig)) { log.info("Building native image source jar: " + runnerJar); @@ -1657,12 +1667,13 @@ public boolean decompile(Path jarToDecompile) { } } - private static FileSystem createNewZip(Path runnerJar, PackageConfig config) throws IOException { + private static FileSystem createNewReproducibleZipFileSystem(Path runnerJar, PackageConfig config) throws IOException { boolean useUncompressedJar = !config.jar().compress(); if (useUncompressedJar) { - return ZipUtils.newZip(runnerJar, Map.of("compressionMethod", "STORED")); + return ZipUtils.createNewReproducibleZipFileSystem(runnerJar, Map.of("compressionMethod", "STORED"), + config.outputTimestamp().orElse(null)); } - return ZipUtils.newZip(runnerJar); + return ZipUtils.createNewReproducibleZipFileSystem(runnerJar, config.outputTimestamp().orElse(null)); } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index a16ba48fad7ba..15bb944206421 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -348,6 +348,11 @@ public Properties getBuildSystemProperties(QuarkusBootstrapMojo mojo, boolean qu effectiveProperties.putIfAbsent("quarkus.application.name", mojo.mavenProject().getArtifactId()); effectiveProperties.putIfAbsent("quarkus.application.version", mojo.mavenProject().getVersion()); + // pass the project.build.outputTimestamp to Quarkus packaging subsystem + if (mojo.mavenProject().getProperties().containsKey("project.build.outputTimestamp")) { + effectiveProperties.putIfAbsent("quarkus.package.output-timestamp", + mojo.mavenProject().getProperties().getProperty("project.build.outputTimestamp")); + } for (Map.Entry attribute : mojo.manifestEntries().entrySet()) { if (attribute.getValue() == null) {