Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jib-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <a
Expand Down Expand Up @@ -288,7 +296,7 @@ public JavaContainerBuilder setOthersDestination(RelativeUnixPath othersDestinat

/**
* 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
Expand All @@ -308,7 +316,7 @@ public JavaContainerBuilder addDependencies(List<Path> 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
Expand All @@ -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
Expand All @@ -341,7 +349,7 @@ public JavaContainerBuilder addSnapshotDependencies(List<Path> 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
Expand All @@ -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
Expand All @@ -376,7 +384,7 @@ public JavaContainerBuilder addProjectDependencies(List<Path> 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
Expand Down Expand Up @@ -598,12 +606,12 @@ public JibContainerBuilder toContainerBuilder() throws IOException {
LayerType.PROJECT_DEPENDENCIES, addedProjectDependencies);
for (Map.Entry<LayerType, List<Path>> 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(
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ public void testToJibContainerBuilder_all()
// Check dependencies
List<AbsoluteUnixPath> 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
Expand Down
1 change: 1 addition & 0 deletions jib-gradle-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions jib-maven-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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() {}

/**
Expand Down Expand Up @@ -659,12 +668,12 @@ static List<String> 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());
}
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -437,26 +437,28 @@ 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))
.containsExactly(
"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();
}
Expand Down