From 5a36e09babc92969240f3ba2eff712efbe1fc398 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Tue, 1 Sep 2026 23:06:13 +0200 Subject: [PATCH] fix: disambiguate duplicate dependency JARs by content hash instead of file size When multiple dependency JARs share the same filename, jib renames the duplicates by appending a suffix so they don't overwrite each other in the image. Until now that suffix was the file size (Files.size), which is not a reliable discriminator: two genuinely different JARs that happen to share a filename can also happen to have the exact same size, in which case one still silently overwrites the other, leading to NoClassDefFoundError at runtime. Use a short prefix of the SHA-256 of the JAR contents instead, computed via jib-core's existing Digests utility. A content hash distinguishes distinct JARs regardless of size, while remaining deterministic across builds. The digest is truncated to 12 hex chars to keep filenames reasonable. The two call sites (JavaContainerBuilder and PluginConfigurationProcessor, which must stay in sync) are both updated, along with the affected tests and the jib-core / jib-maven-plugin / jib-gradle-plugin changelogs. See https://github.com/GoogleContainerTools/jib/issues/3331 --- jib-core/CHANGELOG.md | 1 + .../tools/jib/api/JavaContainerBuilder.java | 47 +++++++++++++++---- .../jib/api/JavaContainerBuilderTest.java | 4 +- jib-gradle-plugin/CHANGELOG.md | 1 + jib-maven-plugin/CHANGELOG.md | 1 + .../common/PluginConfigurationProcessor.java | 37 +++++++++++++-- .../PluginConfigurationProcessorTest.java | 16 ++++--- 7 files changed, 86 insertions(+), 21 deletions(-) diff --git a/jib-core/CHANGELOG.md b/jib-core/CHANGELOG.md index 7d94150be4..b359a6788a 100644 --- a/jib-core/CHANGELOG.md +++ b/jib-core/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. ### Changed ### Fixed +- fix: disambiguate duplicate dependency JARs by a content hash instead of the file size, so distinct JARs that share a filename no longer collide when they happen to have the same size ([#3331](https://github.com/GoogleContainerTools/jib/issues/3331)) ## 0.28.2 diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java index 148131d91f..3725ae4230 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java @@ -22,10 +22,12 @@ import com.google.cloud.tools.jib.api.buildplan.ModificationTimeProvider; import com.google.cloud.tools.jib.api.buildplan.RelativeUnixPath; import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import com.google.cloud.tools.jib.hash.Digests; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Streams; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; @@ -114,6 +116,12 @@ public static JavaContainerBuilder fromDistroless() { */ @Deprecated public static final String DEFAULT_WEB_APP_ROOT = "/jetty/webapps/ROOT"; + /** + * Number of hexadecimal characters of the content hash appended to disambiguate dependency JARs + * that share a filename. See {@link #computeContentHash(Path)}. + */ + private static final int DUPLICATE_JAR_HASH_LENGTH = 12; + /** * Creates a new {@link JavaContainerBuilder} that uses distroless jetty as the base image. For * more information on {@code gcr.io/distroless/java}, see dependencyFiles) throws I /** * Adds dependency JARs to the image. Duplicate JAR filenames across all dependencies are renamed - * with the filesize in order to avoid collisions. + * with a short content hash in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -320,7 +328,7 @@ public JavaContainerBuilder addDependencies(Path... dependencyFiles) throws IOEx /** * Adds snapshot dependency JARs to the image. Duplicate JAR filenames across all dependencies are - * renamed with the filesize in order to avoid collisions. + * renamed with a short content hash in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -341,7 +349,7 @@ public JavaContainerBuilder addSnapshotDependencies(List dependencyFiles) /** * Adds snapshot dependency JARs to the image. Duplicate JAR filenames across all dependencies are - * renamed with the filesize in order to avoid collisions. + * renamed with a short content hash in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -354,7 +362,7 @@ public JavaContainerBuilder addSnapshotDependencies(Path... dependencyFiles) thr /** * Adds project dependency JARs to the image. Generally, project dependency are jars produced from * source in this project as part of other modules/sub-projects. Duplicate JAR filenames across - * all dependencies are renamed with the filesize in order to avoid collisions. + * all dependencies are renamed with a short content hash in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -376,7 +384,7 @@ public JavaContainerBuilder addProjectDependencies(List dependencyFiles) /** * Adds project dependency JARs to the image. Generally, project dependency are jars produced from * source in this project as part of other modules/sub-projects. Duplicate JAR filenames across - * all dependencies are renamed with the filesize in order to avoid collisions. + * all dependencies are renamed with a short content hash in order to avoid collisions. * * @param dependencyFiles the list of dependency JARs to add to the image * @return this @@ -598,12 +606,12 @@ public JibContainerBuilder toContainerBuilder() throws IOException { LayerType.PROJECT_DEPENDENCIES, addedProjectDependencies); for (Map.Entry> entry : layerMap.entrySet()) { for (Path file : Preconditions.checkNotNull(entry.getValue())) { - // Handle duplicates by appending filesize to the end of the file. This renaming logic - // must be in sync with the code that does the same in the other place. See + // Handle duplicates by appending a short content hash to the end of the file. This renaming + // logic must be in sync with the code that does the same in the other place. See // https://github.com/GoogleContainerTools/jib/issues/3331 String jarName = file.getFileName().toString(); if (duplicates.contains(jarName)) { - jarName = jarName.replaceFirst("\\.jar$", "-" + Files.size(file)) + ".jar"; + jarName = jarName.replaceFirst("\\.jar$", "-" + computeContentHash(file)) + ".jar"; } // Add dependencies to layer configuration addFileToLayer( @@ -721,4 +729,25 @@ private void addDirectoryContentsToLayer( builder.addEntry(path, pathOnContainer, modificationTime); }); } + + /** + * Computes a short, deterministic hash of a file's contents, used to disambiguate dependency JARs + * that share a filename. A content hash is used rather than the file size so that distinct JARs + * never collide, even if they happen to have the same size. The (SHA-256) digest is truncated + * because the full hash is unnecessarily long for a filename suffix and a short prefix is more + * than enough to tell apart the handful of same-named JARs in a single build. + * + * @param file the file to hash + * @return the first {@value #DUPLICATE_JAR_HASH_LENGTH} hexadecimal characters of the file's + * SHA-256 content digest + * @throws IOException if reading the file fails + */ + private static String computeContentHash(Path file) throws IOException { + try (InputStream inputStream = Files.newInputStream(file)) { + return Digests.computeDigest(inputStream) + .getDigest() + .getHash() + .substring(0, DUPLICATE_JAR_HASH_LENGTH); + } + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java index a75b6209ef..614d7eda63 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java @@ -94,8 +94,8 @@ public void testToJibContainerBuilder_all() // Check dependencies List expectedDependencies = ImmutableList.of( - AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-770.jar"), - AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-200.jar")); + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-6f67b9a71e4e.jar"), + AbsoluteUnixPath.get("/hello/different-libs/dependency-1.0.0-cfdfca50d5aa.jar")); Assert.assertEquals(expectedDependencies, getExtractionPaths(buildContext, "dependencies")); // Check snapshots diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 0916ff372c..9cc8c4b79d 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. ### Changed ### Fixed +- fix: disambiguate duplicate dependency JARs by a content hash instead of the file size, so distinct JARs that share a filename no longer collide when they happen to have the same size ([#3331](https://github.com/GoogleContainerTools/jib/issues/3331)) ## 3.5.4 diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 4e613dafd2..6acacb40f8 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. ### Changed ### Fixed +- fix: disambiguate duplicate dependency JARs by a content hash instead of the file size, so distinct JARs that share a filename no longer collide when they happen to have the same size ([#3331](https://github.com/GoogleContainerTools/jib/issues/3331)) ## 3.5.2 diff --git a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java index 007784607a..18101a2907 100644 --- a/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java +++ b/jib-plugins-common/src/main/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessor.java @@ -37,6 +37,7 @@ import com.google.cloud.tools.jib.api.buildplan.Platform; import com.google.cloud.tools.jib.frontend.CredentialRetrieverFactory; import com.google.cloud.tools.jib.global.JibSystemProperties; +import com.google.cloud.tools.jib.hash.Digests; import com.google.cloud.tools.jib.plugins.common.RawConfiguration.CredHelperConfiguration; import com.google.cloud.tools.jib.plugins.common.RawConfiguration.ExtraDirectoriesConfiguration; import com.google.cloud.tools.jib.plugins.common.RawConfiguration.PlatformConfiguration; @@ -50,6 +51,7 @@ import com.google.common.collect.Multimaps; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -95,6 +97,13 @@ public class PluginConfigurationProcessor { private static final String JIB_MAIN_CLASS_FILE = "jib-main-class-file"; private static final Path DEFAULT_JIB_DIR = Paths.get("src").resolve("main").resolve("jib"); + /** + * Number of hexadecimal characters of the content hash appended to disambiguate dependency JARs + * that share a filename. Must be kept in sync with {@code JavaContainerBuilder}. See {@link + * #computeContentHash(Path)}. + */ + private static final int DUPLICATE_JAR_HASH_LENGTH = 12; + private PluginConfigurationProcessor() {} /** @@ -659,12 +668,12 @@ static List computeEntrypoint( .collect(Collectors.toList()); for (Path jar : jars) { - // Handle duplicates by appending filesize to the end of the file. This renaming logic - // must be in sync with the code that does the same in the other place. See + // Handle duplicates by appending a short content hash to the end of the file. This renaming + // logic must be in sync with the code that does the same in the other place. See // https://github.com/GoogleContainerTools/jib/issues/3331 String jarName = jar.getFileName().toString(); if (duplicates.contains(jarName)) { - jarName = jarName.replaceFirst("\\.jar$", "-" + Files.size(jar)) + ".jar"; + jarName = jarName.replaceFirst("\\.jar$", "-" + computeContentHash(jar)) + ".jar"; } classpath.add(appRoot.resolve("libs").resolve(jarName).toString()); } @@ -1128,4 +1137,26 @@ private static boolean isKnownJava21Image(String imageReference) { private static boolean isKnownJava25Image(String imageReference) { return imageReference.startsWith("eclipse-temurin:25"); } + + /** + * Computes a short, deterministic hash of a file's contents, used to disambiguate dependency JARs + * that share a filename. A content hash is used rather than the file size so that distinct JARs + * never collide, even if they happen to have the same size. The (SHA-256) digest is truncated + * because the full hash is unnecessarily long for a filename suffix and a short prefix is more + * than enough to tell apart the handful of same-named JARs in a single build. This must stay in + * sync with the equivalent logic in {@code JavaContainerBuilder}. + * + * @param file the file to hash + * @return the first {@value #DUPLICATE_JAR_HASH_LENGTH} hexadecimal characters of the file's + * SHA-256 content digest + * @throws IOException if reading the file fails + */ + private static String computeContentHash(Path file) throws IOException { + try (InputStream inputStream = Files.newInputStream(file)) { + return Digests.computeDigest(inputStream) + .getDigest() + .getHash() + .substring(0, DUPLICATE_JAR_HASH_LENGTH); + } + } } diff --git a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java index 74b68dea47..808b4e042c 100644 --- a/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java +++ b/jib-plugins-common/src/test/java/com/google/cloud/tools/jib/plugins/common/PluginConfigurationProcessorTest.java @@ -437,18 +437,19 @@ public void testComputeEntrypoint_expandClasspathDependencies() } @Test - public void testComputeEntrypoint_expandClasspathDependencies_sizeAddedForDuplicateJars() + public void testComputeEntrypoint_expandClasspathDependencies_contentHashAddedForDuplicateJars() throws MainClassInferenceException, InvalidAppRootException, IOException, InvalidContainerizingModeException { - Path libFoo13 = temporaryFolder.newFolder().toPath().resolve("foo-1.jar"); - Path libFoo45 = temporaryFolder.newFolder().toPath().resolve("foo-1.jar"); - Files.write(libFoo13, new byte[13]); - Files.write(libFoo45, new byte[45]); + Path libFooA = temporaryFolder.newFolder().toPath().resolve("foo-1.jar"); + Path libFooB = temporaryFolder.newFolder().toPath().resolve("foo-1.jar"); + Files.write(libFooA, new byte[13]); + Files.write(libFooB, new byte[45]); when(rawConfiguration.getExpandClasspathDependencies()).thenReturn(true); when(projectProperties.getDependencies()) - .thenReturn(Arrays.asList(libFoo13, Paths.get("/home/libs/bar-2.jar"), libFoo45)); + .thenReturn(Arrays.asList(libFooA, Paths.get("/home/libs/bar-2.jar"), libFooB)); + // SHA-256 prefixes of 13 and 45 zero bytes, respectively. assertThat( PluginConfigurationProcessor.computeEntrypoint( rawConfiguration, projectProperties, jibContainerBuilder)) @@ -456,7 +457,8 @@ public void testComputeEntrypoint_expandClasspathDependencies_sizeAddedForDuplic "java", "-cp", "/app/resources:/app/classes:" - + "/app/libs/foo-1-13.jar:/app/libs/bar-2.jar:/app/libs/foo-1-45.jar", + + "/app/libs/foo-1-dd46c3eebb18.jar:/app/libs/bar-2.jar:" + + "/app/libs/foo-1-8a1020634191.jar", "java.lang.Object") .inOrder(); }