diff --git a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarProcessor.java b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarProcessor.java index a1e8443fae..076e6e7673 100644 --- a/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarProcessor.java +++ b/jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarProcessor.java @@ -22,6 +22,7 @@ import com.google.cloud.tools.jib.filesystem.DirectoryWalker; import com.google.cloud.tools.jib.plugins.common.ZipUtil; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -37,6 +38,10 @@ public class JarProcessor { private static final AbsoluteUnixPath APP_ROOT = AbsoluteUnixPath.get("/app"); + private static final String CLASSES = "classes"; + private static final String RESOURCES = "resources"; + private static final String DEPENDENCIES = "dependencies"; + private static final String SNAPSHOT_DEPENDENCIES = "snapshot dependencies"; /** * Jar Type. @@ -76,7 +81,7 @@ public static JarType determineJarType(Path jarPath) throws IOException { * @throws IOException if I/O error occurs when opening the jar file or if temporary directory * provided doesn't exist */ - public static List explodeStandardJar(Path jarPath, Path tempDirPath) + static List explodeStandardJar(Path jarPath, Path tempDirPath) throws IOException { Path localExplodedJarRoot = tempDirPath; ZipUtil.unzip(jarPath, localExplodedJarRoot); @@ -84,12 +89,11 @@ public static List explodeStandardJar(Path jarPath, Path tempD // Get dependencies from Class-Path in the jar's manifest and add a layer each for non-snapshot // and snapshot dependencies. If Class-Path is not present in the jar's manifest then skip - // adding a dependencies layer. + // adding the dependencies layers. String classPath = null; try (JarFile jarFile = new JarFile(jarPath.toFile())) { classPath = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.CLASS_PATH); } - if (classPath != null) { Predicate isSnapshot = name -> name.contains("SNAPSHOT"); List allDependencies = Splitter.onPattern("\\s+").splitToList(classPath.trim()); @@ -103,7 +107,7 @@ public static List explodeStandardJar(Path jarPath, Path tempD allDependencies.stream().filter(isSnapshot).map(Paths::get).collect(Collectors.toList()); if (!nonSnapshotDependencies.isEmpty()) { FileEntriesLayer.Builder nonSnapshotDependenciesLayerBuilder = - FileEntriesLayer.builder().setName("dependencies"); + FileEntriesLayer.builder().setName(DEPENDENCIES); nonSnapshotDependencies.forEach( path -> nonSnapshotDependenciesLayerBuilder.addEntry( @@ -112,7 +116,7 @@ public static List explodeStandardJar(Path jarPath, Path tempD } if (!snapshotDependencies.isEmpty()) { FileEntriesLayer.Builder snapshotDependenciesLayerBuilder = - FileEntriesLayer.builder().setName("snapshot dependencies"); + FileEntriesLayer.builder().setName(SNAPSHOT_DEPENDENCIES); snapshotDependencies.forEach( path -> snapshotDependenciesLayerBuilder.addEntry( @@ -121,21 +125,20 @@ public static List explodeStandardJar(Path jarPath, Path tempD } } - Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); - Predicate isResourceFile = isClassFile.negate(); - // Determine class and resource files in the directory containing jar contents and create // FileEntriesLayer for each type of layer (classes or resources), while maintaining the // file's original project structure. + Predicate isClassFile = path -> path.getFileName().toString().endsWith(".class"); + Predicate isResourceFile = isClassFile.negate(); FileEntriesLayer classesLayer = addDirectoryContentsToLayer( - "classes", + CLASSES, localExplodedJarRoot, isClassFile, APP_ROOT.resolve(RelativeUnixPath.get("explodedJar"))); FileEntriesLayer resourcesLayer = addDirectoryContentsToLayer( - "resources", + RESOURCES, localExplodedJarRoot, isResourceFile, APP_ROOT.resolve(RelativeUnixPath.get("explodedJar"))); @@ -145,6 +148,27 @@ public static List explodeStandardJar(Path jarPath, Path tempD return layers; } + /** + * Computes the entrypoint for a standard jar in exploded mode. + * + * @param jarPath path to jar file + * @return list of {@link String} representing entrypoint + * @throws IOException if I/O error occurs when opening the jar file + */ + static ImmutableList computeEntrypointForExplodedStandard(Path jarPath) + throws IOException { + try (JarFile jarFile = new JarFile(jarPath.toFile())) { + String mainClass = + jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + if (mainClass == null) { + throw new IllegalArgumentException( + "`Main-Class:` attribute for an application main class not defined in the input Jar's manifest (`META-INF/MANIFEST.MF` in the Jar)."); + } + String classpath = APP_ROOT + "/explodedJar:" + APP_ROOT + "/dependencies/*"; + return ImmutableList.of("java", "-cp", classpath, mainClass); + } + } + private static FileEntriesLayer addDirectoryContentsToLayer( String layerName, Path sourceRoot, diff --git a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarProcessorTest.java b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarProcessorTest.java index 647a15174b..b2b0468a40 100644 --- a/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarProcessorTest.java +++ b/jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarProcessorTest.java @@ -17,6 +17,7 @@ package com.google.cloud.tools.jib.cli.jar; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.cloud.tools.jib.api.buildplan.AbsoluteUnixPath; import com.google.cloud.tools.jib.api.buildplan.FileEntriesLayer; @@ -281,4 +282,44 @@ public void testExplodeMode_standard_withoutClassPathInManifest_containsOnlyClas AbsoluteUnixPath.get("/app/explodedJar/class1.class"), AbsoluteUnixPath.get("/app/explodedJar/class2.class")); } + + @Test + public void testExplodeMode_standard_computeEntrypoint_allLayersPresent() + throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITH_CLASS_PATH_MANIFEST).toURI()); + ImmutableList actualEntrypoint = + JarProcessor.computeEntrypointForExplodedStandard(standardJar); + + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of("java", "-cp", "/app/explodedJar:/app/dependencies/*", "HelloWorld")); + } + + @Test + public void testExplodedMode_standard_computeEntrypoint_noDependenciesLayers() + throws IOException, URISyntaxException { + Path standardJar = + Paths.get(Resources.getResource(STANDARD_JAR_WITHOUT_CLASS_PATH_MANIFEST).toURI()); + ImmutableList actualEntrypoint = + JarProcessor.computeEntrypointForExplodedStandard(standardJar); + + assertThat(actualEntrypoint) + .isEqualTo( + ImmutableList.of("java", "-cp", "/app/explodedJar:/app/dependencies/*", "HelloWorld")); + } + + @Test + public void testExplodedMode_standard_computeEntrypoint_noMainClass() throws URISyntaxException { + Path standardJar = Paths.get(Resources.getResource(STANDARD_JAR_EMPTY).toURI()); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> JarProcessor.computeEntrypointForExplodedStandard(standardJar)); + + assertThat(ex) + .hasMessageThat() + .isEqualTo( + "`Main-Class:` attribute for an application main class not defined in the input Jar's manifest (`META-INF/MANIFEST.MF` in the Jar)."); + } }