diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/UberJarIgnoredResourceBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/UberJarIgnoredResourceBuildItem.java new file mode 100644 index 0000000000000..00551260490b1 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/UberJarIgnoredResourceBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.deployment.pkg.builditem; + +import io.quarkus.builder.item.MultiBuildItem; +import io.smallrye.common.constraint.Assert; + +/** + * Ignore resources when building an Uber Jar + */ +public final class UberJarIgnoredResourceBuildItem extends MultiBuildItem { + + private final String path; + + public UberJarIgnoredResourceBuildItem(String path) { + this.path = Assert.checkNotEmptyParam("UberJarIgnoredResourceBuildItem.path", path); + } + + public String getPath() { + return path; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/UberJarMergedResourceBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/UberJarMergedResourceBuildItem.java new file mode 100644 index 0000000000000..d38546c55316d --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/UberJarMergedResourceBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.deployment.pkg.builditem; + +import io.quarkus.builder.item.MultiBuildItem; +import io.smallrye.common.constraint.Assert; + +/** + * Merge duplicate resources from multiple JARs when building an Uber Jar + */ +public final class UberJarMergedResourceBuildItem extends MultiBuildItem { + + private final String path; + + public UberJarMergedResourceBuildItem(String path) { + this.path = Assert.checkNotEmptyParam("UberJarMergedResourceBuildItem.path", path); + } + + public String getPath() { + return path; + } +} 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 adb633fcbd372..75e74d17a8576 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 @@ -83,6 +83,8 @@ import io.quarkus.deployment.pkg.builditem.LegacyJarRequiredBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.pkg.builditem.UberJarIgnoredResourceBuildItem; +import io.quarkus.deployment.pkg.builditem.UberJarMergedResourceBuildItem; import io.quarkus.deployment.pkg.builditem.UberJarRequiredBuildItem; import io.quarkus.deployment.util.FileUtil; @@ -141,24 +143,41 @@ public boolean test(String path) { }; private static final Logger log = Logger.getLogger(JarResultBuildStep.class); + // we shouldn't have to specify these flags when opening a ZipFS (since they are the default ones), but failure to do so // makes a subsequent uberJar creation fail in java 8 (but works fine in Java 11) private static final StandardOpenOption[] DEFAULT_OPEN_OPTIONS = { TRUNCATE_EXISTING, WRITE, CREATE }; + private static final BiPredicate IS_JSON_FILE_PREDICATE = new IsJsonFilePredicate(); + public static final String DEPLOYMENT_CLASS_PATH_DAT = "deployment-class-path.dat"; + public static final String BUILD_SYSTEM_PROPERTIES = "build-system.properties"; + public static final String DEPLOYMENT_LIB = "deployment"; + public static final String APPMODEL_DAT = "appmodel.dat"; + public static final String QUARKUS_RUN_JAR = "quarkus-run.jar"; + public static final String QUARKUS_APP_DEPS = "quarkus-app-dependencies.txt"; + public static final String BOOT_LIB = "boot"; + public static final String LIB = "lib"; + public static final String MAIN = "main"; + public static final String GENERATED_BYTECODE_JAR = "generated-bytecode.jar"; + public static final String TRANSFORMED_BYTECODE_JAR = "transformed-bytecode.jar"; + public static final String APP = "app"; + public static final String QUARKUS = "quarkus"; + public static final String DEFAULT_FAST_JAR_DIRECTORY_NAME = "quarkus-app"; + public static final String MP_CONFIG_FILE = "META-INF/microprofile-config.properties"; @BuildStep @@ -189,6 +208,8 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem List generatedClasses, List generatedResources, List uberJarRequired, + List uberJarMergedResourceBuildItems, + List uberJarIgnoredResourceBuildItems, List legacyJarRequired, QuarkusBuildCloseablesBuildItem closeablesBuildItem, List additionalApplicationArchiveBuildItems, @@ -207,7 +228,7 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem || packageConfig.type.equalsIgnoreCase(PackageConfig.UBER_JAR))) { return buildUberJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem, packageConfig, applicationInfo, generatedClasses, generatedResources, closeablesBuildItem, - mainClassBuildItem); + uberJarMergedResourceBuildItems, uberJarIgnoredResourceBuildItems, mainClassBuildItem); } else if (!legacyJarRequired.isEmpty() || packageConfig.isLegacyJar() || packageConfig.type.equalsIgnoreCase(PackageConfig.LEGACY)) { return buildLegacyThinJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, @@ -255,6 +276,8 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, List generatedClasses, List generatedResources, QuarkusBuildCloseablesBuildItem closeablesBuildItem, + List mergeResources, + List ignoredResources, MainClassBuildItem mainClassBuildItem) throws Exception { //we use the -runner jar name, unless we are building both types @@ -269,6 +292,8 @@ private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, applicationInfo, generatedClasses, generatedResources, + mergeResources, + ignoredResources, mainClassBuildItem, runnerJar); @@ -293,6 +318,8 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem, ApplicationInfoBuildItem applicationInfo, List generatedClasses, List generatedResources, + List mergedResources, + List ignoredResources, MainClassBuildItem mainClassBuildItem, Path runnerJar) throws Exception { try (FileSystem runnerZipFs = ZipUtils.newZip(runnerJar)) { @@ -302,8 +329,14 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem, final Map seen = new HashMap<>(); final Map> duplicateCatcher = new HashMap<>(); final Map> concatenatedEntries = new HashMap<>(); + final Set mergeResourcePaths = mergedResources.stream() + .map(UberJarMergedResourceBuildItem::getPath) + .collect(Collectors.toSet()); Set finalIgnoredEntries = new HashSet<>(IGNORED_ENTRIES); packageConfig.userConfiguredIgnoredEntries.ifPresent(finalIgnoredEntries::addAll); + ignoredResources.stream() + .map(UberJarIgnoredResourceBuildItem::getPath) + .forEach(finalIgnoredEntries::add); final List appDeps = curateOutcomeBuildItem.getEffectiveModel().getUserDependencies(); @@ -328,12 +361,13 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem, try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedDep)) { for (final Path root : artifactFs.getRootDirectories()) { walkFileDependencyForDependency(root, runnerZipFs, seen, duplicateCatcher, concatenatedEntries, - finalIgnoredEntries, appDep, transformedFromThisArchive); + finalIgnoredEntries, appDep, transformedFromThisArchive, mergeResourcePaths); } } } else { walkFileDependencyForDependency(resolvedDep, runnerZipFs, seen, duplicateCatcher, - concatenatedEntries, finalIgnoredEntries, appDep, transformedFromThisArchive); + concatenatedEntries, finalIgnoredEntries, appDep, transformedFromThisArchive, + mergeResourcePaths); } } } @@ -360,7 +394,8 @@ private boolean isAppDepAJar(AppArtifact artifact) { private void walkFileDependencyForDependency(Path root, FileSystem runnerZipFs, Map seen, Map> duplicateCatcher, Map> concatenatedEntries, - Set finalIgnoredEntries, AppDependency appDep, Set transformedFromThisArchive) throws IOException { + Set finalIgnoredEntries, AppDependency appDep, Set transformedFromThisArchive, + Set mergeResourcePaths) throws IOException { final Path metaInfDir = root.resolve("META-INF"); Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor() { @@ -392,7 +427,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) boolean transformed = transformedFromThisArchive != null && transformedFromThisArchive.contains(relativePath); if (!transformed) { - if (CONCATENATED_ENTRIES_PREDICATE.test(relativePath)) { + if (CONCATENATED_ENTRIES_PREDICATE.test(relativePath) + || mergeResourcePaths.contains(relativePath)) { concatenatedEntries.computeIfAbsent(relativePath, (u) -> new ArrayList<>()) .add(Files.readAllBytes(file)); return FileVisitResult.CONTINUE; @@ -818,7 +854,6 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) /** * Native images are built from a specially created jar file. This allows for changes in how the jar file is generated. - * */ @BuildStep public NativeImageSourceJarBuildItem buildNativeImageJar(CurateOutcomeBuildItem curateOutcomeBuildItem, @@ -831,7 +866,9 @@ public NativeImageSourceJarBuildItem buildNativeImageJar(CurateOutcomeBuildItem List nativeImageResources, List generatedResources, MainClassBuildItem mainClassBuildItem, - List uberJarRequired) throws Exception { + List uberJarRequired, + List mergeResources, + List ignoreResources) throws Exception { Path targetDirectory = outputTargetBuildItem.getOutputDirectory() .resolve(outputTargetBuildItem.getBaseName() + "-native-image-source-jar"); IoUtils.createOrEmptyDir(targetDirectory); @@ -850,7 +887,9 @@ public NativeImageSourceJarBuildItem buildNativeImageJar(CurateOutcomeBuildItem final NativeImageSourceJarBuildItem nativeImageSourceJarBuildItem = buildNativeImageUberJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem, - packageConfig, applicationInfo, allClasses, generatedResources, mainClassBuildItem, targetDirectory); + packageConfig, applicationInfo, allClasses, generatedResources, mergeResources, + ignoreResources, mainClassBuildItem, + targetDirectory); // additionally copy any json config files to a location accessible by native-image tool during // native-image generation copyJsonConfigFiles(applicationArchivesBuildItem, targetDirectory); @@ -898,6 +937,8 @@ private NativeImageSourceJarBuildItem buildNativeImageUberJar(CurateOutcomeBuild ApplicationInfoBuildItem applicationInfo, List generatedClasses, List generatedResources, + List mergeResources, + List ignoreResources, MainClassBuildItem mainClassBuildItem, Path targetDirectory) throws Exception { //we use the -runner jar name, unless we are building both types @@ -911,6 +952,8 @@ private NativeImageSourceJarBuildItem buildNativeImageUberJar(CurateOutcomeBuild applicationInfo, generatedClasses, generatedResources, + mergeResources, + ignoreResources, mainClassBuildItem, runnerJar); @@ -1093,6 +1136,7 @@ private void copyCommonContent(FileSystem runnerZipFs, Map> for (Map.Entry> entry : concatenatedEntries.entrySet()) { try (final OutputStream os = wrapForJDK8232879( Files.newOutputStream(runnerZipFs.getPath(entry.getKey()), DEFAULT_OPEN_OPTIONS))) { + // TODO: Handle merging of XMLs for (byte[] i : entry.getValue()) { os.write(i); os.write('\n'); diff --git a/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/UberJarConfigBuildStep.java b/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/UberJarConfigBuildStep.java new file mode 100644 index 0000000000000..5299b7832ebb8 --- /dev/null +++ b/core/test-extension/deployment/src/main/java/io/quarkus/extest/deployment/UberJarConfigBuildStep.java @@ -0,0 +1,22 @@ +package io.quarkus.extest.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.pkg.builditem.UberJarIgnoredResourceBuildItem; +import io.quarkus.deployment.pkg.builditem.UberJarMergedResourceBuildItem; + +/** + * Used in UberJarMergedResourceBuildItemTest + */ +public class UberJarConfigBuildStep { + + @BuildStep + UberJarMergedResourceBuildItem uberJarMergedResourceBuildItem() { + return new UberJarMergedResourceBuildItem("META-INF/cxf/bus-extensions.txt"); + } + + @BuildStep + UberJarIgnoredResourceBuildItem uberJarIgnoredResourceBuildItem() { + return new UberJarIgnoredResourceBuildItem("META-INF/cxf/cxf.fixml"); + } + +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/deployment/pkg/builditem/UberJarIgnoredResourceBuildItemTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/deployment/pkg/builditem/UberJarIgnoredResourceBuildItemTest.java new file mode 100644 index 0000000000000..a29373de19fa0 --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/deployment/pkg/builditem/UberJarIgnoredResourceBuildItemTest.java @@ -0,0 +1,52 @@ +package io.quarkus.deployment.pkg.builditem; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.runtime.annotations.QuarkusMain; +import io.quarkus.test.QuarkusProdModeTest; + +class UberJarIgnoredResourceBuildItemTest { + + @RegisterExtension + static final QuarkusProdModeTest runner = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties") + .addClass(UberJarMain.class)) + .setApplicationName("uber-jar-ignored") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true) + .setExpectExit(true) + .overrideConfigKey("quarkus.package.type", "uber-jar") + .setForcedDependencies( + Collections.singletonList( + // META-INF/cxf/cxf.fixml should be present in the cxf-rt-transports-http and cxf-core JARs + new AppArtifact("org.apache.cxf", "cxf-rt-transports-http", "3.4.3"))); + + @Test + public void testResourceWasIgnored() throws IOException { + assertThat(runner.getStartupConsoleOutput()).contains("RESOURCES: 0"); + assertThat(runner.getExitCode()).isZero(); + } + + @QuarkusMain + public static class UberJarMain { + + public static void main(String[] args) throws IOException { + List resources = Collections + .list(UberJarMain.class.getClassLoader().getResources("META-INF/cxf/cxf.fixml")); + System.out.println("RESOURCES: " + resources.size()); + } + + } +} diff --git a/core/test-extension/deployment/src/test/java/io/quarkus/deployment/pkg/builditem/UberJarMergedResourceBuildItemTest.java b/core/test-extension/deployment/src/test/java/io/quarkus/deployment/pkg/builditem/UberJarMergedResourceBuildItemTest.java new file mode 100644 index 0000000000000..4491a952a92fc --- /dev/null +++ b/core/test-extension/deployment/src/test/java/io/quarkus/deployment/pkg/builditem/UberJarMergedResourceBuildItemTest.java @@ -0,0 +1,78 @@ +package io.quarkus.deployment.pkg.builditem; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.runtime.annotations.QuarkusMain; +import io.quarkus.test.QuarkusProdModeTest; + +class UberJarMergedResourceBuildItemTest { + + @RegisterExtension + static final QuarkusProdModeTest runner = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsManifestResource("application.properties", "microprofile-config.properties") + .addClass(UberJarMain.class)) + .setApplicationName("uber-jar-merged") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true) + .setExpectExit(true) + .overrideConfigKey("quarkus.package.type", "uber-jar") + .setForcedDependencies( + Collections.singletonList( + // META-INF/cxf/bus-extensions.txt should be present in the cxf-rt-transports-http and cxf-core JARs + new AppArtifact("org.apache.cxf", "cxf-rt-transports-http", "3.4.3"))); + + @Test + public void testResourceWasMerged() throws IOException { + assertThat(runner.getStartupConsoleOutput()).contains("RESOURCES: 1", + "org.apache.cxf.transport.http.HTTPTransportFactory::true", + "org.apache.cxf.transport.http.HTTPWSDLExtensionLoader::true:true", + "org.apache.cxf.transport.http.policy.HTTPClientAssertionBuilder::true:true", + "org.apache.cxf.transport.http.policy.HTTPServerAssertionBuilder::true:true", + "org.apache.cxf.transport.http.policy.NoOpPolicyInterceptorProvider::true:true", + "org.apache.cxf.bus.managers.PhaseManagerImpl:org.apache.cxf.phase.PhaseManager:true", + "org.apache.cxf.bus.managers.WorkQueueManagerImpl:org.apache.cxf.workqueue.WorkQueueManager:true", + "org.apache.cxf.bus.managers.CXFBusLifeCycleManager:org.apache.cxf.buslifecycle.BusLifeCycleManager:true", + "org.apache.cxf.bus.managers.ServerRegistryImpl:org.apache.cxf.endpoint.ServerRegistry:true", + "org.apache.cxf.bus.managers.EndpointResolverRegistryImpl:org.apache.cxf.endpoint.EndpointResolverRegistry:true", + "org.apache.cxf.bus.managers.HeaderManagerImpl:org.apache.cxf.headers.HeaderManager:true", + "org.apache.cxf.service.factory.FactoryBeanListenerManager::true", + "org.apache.cxf.bus.managers.ServerLifeCycleManagerImpl:org.apache.cxf.endpoint.ServerLifeCycleManager:true", + "org.apache.cxf.bus.managers.ClientLifeCycleManagerImpl:org.apache.cxf.endpoint.ClientLifeCycleManager:true", + "org.apache.cxf.bus.resource.ResourceManagerImpl:org.apache.cxf.resource.ResourceManager:true", + "org.apache.cxf.catalog.OASISCatalogManager:org.apache.cxf.catalog.OASISCatalogManager:true", + "org.apache.cxf.common.util.ASMHelperImpl:org.apache.cxf.common.util.ASMHelper:true", + "org.apache.cxf.common.spi.ClassLoaderProxyService:org.apache.cxf.common.spi.ClassLoaderService:true"); + assertThat(runner.getExitCode()).isZero(); + } + + @QuarkusMain + public static class UberJarMain { + + public static void main(String[] args) throws IOException { + List resources = Collections + .list(UberJarMain.class.getClassLoader().getResources("META-INF/cxf/bus-extensions.txt")); + System.out.println("RESOURCES: " + resources.size()); + try (InputStream is = resources.get(0).openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + reader.lines().forEach(System.out::println); + } + } + + } +}