From 82fd2849b10189fe9444f7545141a3405601ce44 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 27 Jan 2021 13:07:17 +0200 Subject: [PATCH] Allow AppCDS generation from using a builder image The is useful when the building JVM version is not the same as the runtime JVM version --- .../quarkus/deployment/pkg/PackageConfig.java | 11 ++ .../AppCDSContainerImageBuildItem.java | 19 ++ .../deployment/pkg/steps/AppCDSBuildStep.java | 165 ++++++++++++------ 3 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSContainerImageBuildItem.java 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 ce29c80900b3b..ab9208b9532bf 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 @@ -89,6 +89,17 @@ public class PackageConfig { @ConfigItem public boolean createAppcds; + /** + * When AppCDS generation is enabled, if this property is set, then the JVM used to generate the AppCDS file + * will be the JVM present in the container image. The builder image is expected to have have the 'java' binary + * on its PATH. + * This flag is useful when the JVM to be used at runtime is not the same exact JVM version as the one used to build + * the jar. + * Note that this property is consulted only when {@code quarkus.package.create-appcds=true}. + */ + @ConfigItem + public Optional appcdsBuilderImage; + /** * This is an advanced option that only takes effect for the mutable-jar format. * diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSContainerImageBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSContainerImageBuildItem.java new file mode 100644 index 0000000000000..45f5e6aff633b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSContainerImageBuildItem.java @@ -0,0 +1,19 @@ +package io.quarkus.deployment.pkg.builditem; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Indicates that a specific container image should be used to generate the AppCDS file + */ +public final class AppCDSContainerImageBuildItem extends SimpleBuildItem { + + private final String containerImage; + + public AppCDSContainerImageBuildItem(String containerImage) { + this.containerImage = containerImage; + } + + public String getContainerImage() { + return containerImage; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java index ae3071e813865..1abf2c2eaf879 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -18,6 +17,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.builditem.AppCDSContainerImageBuildItem; import io.quarkus.deployment.pkg.builditem.AppCDSRequestedBuildItem; import io.quarkus.deployment.pkg.builditem.AppCDSResultBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; @@ -31,8 +31,10 @@ public class AppCDSBuildStep { private static final Logger log = Logger.getLogger(AppCDSBuildStep.class); - public static final String JDK_CLASSLIST_FILE = "classlist"; + public static final String CLASSES_LIST_FILE_NAME = "classes.lst"; + private static final String CONTAINER_IMAGE_BASE_BUILD_DIR = "/tmp/quarkus"; + private static final String CONTAINER_IMAGE_APPCDS_DIR = CONTAINER_IMAGE_BASE_BUILD_DIR + "/appcds"; @BuildStep(onlyIf = AppCDSRequired.class) public void requested(OutputTargetBuildItem outputTarget, BuildProducer producer) @@ -45,33 +47,31 @@ public void requested(OutputTargetBuildItem outputTarget, BuildProducer appCDsRequested, - JarBuildItem jarResult, PackageConfig packageConfig, + JarBuildItem jarResult, OutputTargetBuildItem outputTarget, PackageConfig packageConfig, + Optional appCDSContainerImage, BuildProducer appCDS, BuildProducer artifactResult) throws Exception { if (!appCDsRequested.isPresent()) { return; } - Path appCDSDir = appCDsRequested.get().getAppCDSDir(); - String javaHomeStr = System.getProperty("java.home"); - Path javaHomeDir = Paths.get(javaHomeStr); - Path jdkClassList = javaHomeDir.resolve("lib").resolve(JDK_CLASSLIST_FILE); - if (!jdkClassList.toFile().exists()) { - log.warnf( - "In order to create AppCDS the JDK used to build the Quarkus application must contain a file named '%s' in the its 'lib' directory.", - JDK_CLASSLIST_FILE); - return; - } - String javaExecutableStr = JavaBinFinder.simpleBinaryName(); - String javaBinStr = javaHomeStr + File.separator + "bin" + File.separator + javaExecutableStr; - if (!new File(javaBinStr).canExecute()) { - log.warnf( - "In order to create AppCDS the JDK used to build the Quarkus application must contain an executable named '%s' in its 'bin' directory.", - javaBinStr); - return; + // to actually execute the commands needed to generate the AppCDS file, either the JVM in the container image will be used + // (if specified), or the JVM running the build + String containerImage = determineContainerImage(packageConfig, appCDSContainerImage); + String javaBinPath = null; + if (containerImage == null) { + javaBinPath = System.getProperty("java.home") + File.separator + "bin" + File.separator + + JavaBinFinder.simpleBinaryName(); + if (!new File(javaBinPath).canExecute()) { + log.warnf( + "In order to create AppCDS the JDK used to build the Quarkus application must contain an executable named '%s' in its 'bin' directory.", + javaBinPath); + return; + } } - Path classesLstPath = createClassesLst(packageConfig, jarResult, javaBinStr, appCDSDir); + Path classesLstPath = createClassesLst(jarResult, outputTarget, javaBinPath, containerImage, + appCDsRequested.get().getAppCDSDir(), packageConfig.isFastJar()); if (classesLstPath == null) { log.warnf("Unable to create AppCDS because '%s' was not created.", CLASSES_LIST_FILE_NAME); return; @@ -80,38 +80,71 @@ public void build(Optional appCDsRequested, log.debugf("'%s' successfully created.", CLASSES_LIST_FILE_NAME); log.info("Launching AppCDS creation process."); - Path appCDSPath = createAppCDS(jarResult, javaBinStr, classesLstPath, packageConfig.isFastJar()); + Path appCDSPath = createAppCDS(jarResult, outputTarget, javaBinPath, containerImage, classesLstPath, + packageConfig.isFastJar()); if (appCDSPath == null) { log.warn("Unable to create AppCDS."); return; } - log.infof( - "AppCDS successfully created at: '%s'.\nTo ensure they are loaded properly, " + - "run the application jar from its directory and also add the '-XX:SharedArchiveFile=app-cds.jsa' " + - "JVM flag.\nMoreover, make sure to use the exact same Java version (%s) to run the application as was used to build it.", - appCDSPath.toAbsolutePath().toString(), System.getProperty("java.version")); + log.infof("AppCDS successfully created at: '%s'.", appCDSPath.toAbsolutePath().toString()); + if (containerImage == null) { + log.infof( + "To ensure they are loaded properly, " + + "run the application jar from its directory and also add the '-XX:SharedArchiveFile=app-cds.jsa' " + + "JVM flag.\nMoreover, make sure to use the exact same Java version (%s) to run the application as was used to build it.", + System.getProperty("java.version")); + } appCDS.produce(new AppCDSResultBuildItem(appCDSPath)); artifactResult.produce(new ArtifactResultBuildItem(appCDSPath, "appCDS", Collections.emptyMap())); } + private String determineContainerImage(PackageConfig packageConfig, + Optional appCDSContainerImage) { + if (packageConfig.appcdsBuilderImage.isPresent()) { + return packageConfig.appcdsBuilderImage.get(); + } else if (appCDSContainerImage.isPresent()) { + return appCDSContainerImage.get().getContainerImage(); + } + return null; + } + /** * @return The path of the created classes.lst file or null if the file was not created */ - private Path createClassesLst(PackageConfig packageConfig, JarBuildItem jarResult, - String javaBin, Path appCDSDir) { - - List command = new ArrayList<>(5); - command.add(javaBin); - command.add("-XX:DumpLoadedClassList=" + CLASSES_LIST_FILE_NAME); - command.add(String.format("-D%s=true", MainClassBuildStep.GENERATE_APP_CDS_SYSTEM_PROPERTY)); - command.add("-jar"); - if (packageConfig.isFastJar()) { - command.add(jarResult.getLibraryDir().getParent().resolve(JarResultBuildStep.QUARKUS_RUN_JAR).toAbsolutePath() - .toString()); + private Path createClassesLst(JarBuildItem jarResult, + OutputTargetBuildItem outputTarget, String javaBinPath, String containerImage, Path appCDSDir, boolean isFastJar) { + + List commonJavaArgs = new ArrayList<>(3); + commonJavaArgs.add("-XX:DumpLoadedClassList=" + CLASSES_LIST_FILE_NAME); + commonJavaArgs.add(String.format("-D%s=true", MainClassBuildStep.GENERATE_APP_CDS_SYSTEM_PROPERTY)); + commonJavaArgs.add("-jar"); + + List command; + if (containerImage != null) { + List dockerRunCommand = dockerRunCommands(outputTarget, containerImage, CONTAINER_IMAGE_APPCDS_DIR); + command = new ArrayList<>(dockerRunCommand.size() + 1 + commonJavaArgs.size()); + command.addAll(dockerRunCommand); + command.add("java"); + command.addAll(commonJavaArgs); + if (isFastJar) { + command.add(CONTAINER_IMAGE_BASE_BUILD_DIR + "/" + JarResultBuildStep.DEFAULT_FAST_JAR_DIRECTORY_NAME + "/" + + JarResultBuildStep.QUARKUS_RUN_JAR); + } else { + command.add(CONTAINER_IMAGE_BASE_BUILD_DIR + "/" + jarResult.getPath().getFileName().toString()); + } } else { - command.add(jarResult.getPath().toAbsolutePath().toString()); + command = new ArrayList<>(2 + commonJavaArgs.size()); + command.add(javaBinPath); + command.addAll(commonJavaArgs); + if (isFastJar) { + command + .add(jarResult.getLibraryDir().getParent().resolve(JarResultBuildStep.QUARKUS_RUN_JAR).toAbsolutePath() + .toString()); + } else { + command.add(jarResult.getPath().toAbsolutePath().toString()); + } } if (log.isDebugEnabled()) { @@ -142,11 +175,30 @@ private Path createClassesLst(PackageConfig packageConfig, JarBuildItem jarResul return appCDSDir.resolve(CLASSES_LIST_FILE_NAME); } + // the idea here is to use 'docker run -v ... java ...' in order to utilize the JVM of the builder image to + // generate the classes file on the host + private List dockerRunCommands(OutputTargetBuildItem outputTarget, String containerImage, + String containerWorkingDir) { + List command = new ArrayList<>(10); + command.add("docker"); + command.add("run"); + command.add("-v"); + command.add(outputTarget.getOutputDirectory().toAbsolutePath().toString() + ":" + CONTAINER_IMAGE_BASE_BUILD_DIR + + ":z"); + command.add("--user"); + command.add("1000:1000"); + command.add("-w"); + command.add(containerWorkingDir); + command.add("--rm"); + command.add(containerImage); + return command; + } + /** * @return The path of the created app-cds.jsa file or null if the file was not created */ - private Path createAppCDS(JarBuildItem jarResult, String javaBin, - Path classesLstPath, boolean isFastFar) { + private Path createAppCDS(JarBuildItem jarResult, OutputTargetBuildItem outputTarget, String javaBinPath, + String containerImage, Path classesLstPath, boolean isFastFar) { Path workingDirectory = jarResult.getPath().getParent(); Path appCDSPath = workingDirectory.resolve("app-cds.jsa"); @@ -158,20 +210,35 @@ private Path createAppCDS(JarBuildItem jarResult, String javaBin, } } - List command = new ArrayList<>(6); - command.add(javaBin); - command.add("-Xshare:dump"); - command.add("-XX:SharedClassListFile=" + classesLstPath.toAbsolutePath().toString()); + List javaArgs = new ArrayList<>(5); + javaArgs.add("-Xshare:dump"); + javaArgs.add("-XX:SharedClassListFile=" + + ((containerImage != null) ? CONTAINER_IMAGE_APPCDS_DIR + "/" + classesLstPath.getFileName().toString() + : classesLstPath.toAbsolutePath().toString())); // We use the relative paths because at runtime 'java -XX:SharedArchiveFile=... -jar ...' expects the AppCDS and jar files // to match exactly what was used at build time. // For that reason we also run the creation process from inside the output directory, // The end result is that users can simply use 'java -XX:SharedArchiveFile=app-cds.jsa -jar app.jar' - command.add("-XX:SharedArchiveFile=" + appCDSPath.getFileName().toString()); - command.add("--class-path"); + javaArgs.add("-XX:SharedArchiveFile=" + appCDSPath.getFileName().toString()); + javaArgs.add("--class-path"); if (isFastFar) { - command.add(JarResultBuildStep.QUARKUS_RUN_JAR); + javaArgs.add(JarResultBuildStep.QUARKUS_RUN_JAR); + } else { + javaArgs.add(jarResult.getPath().getFileName().toString()); + } + + List command; + if (containerImage != null) { + List dockerRunCommand = dockerRunCommands(outputTarget, containerImage, + CONTAINER_IMAGE_BASE_BUILD_DIR + "/" + JarResultBuildStep.DEFAULT_FAST_JAR_DIRECTORY_NAME); + command = new ArrayList<>(dockerRunCommand.size() + 1 + javaArgs.size()); + command.addAll(dockerRunCommand); + command.add("java"); + command.addAll(javaArgs); } else { - command.add(jarResult.getPath().getFileName().toString()); + command = new ArrayList<>(1 + javaArgs.size()); + command.add(javaBinPath); + command.addAll(javaArgs); } if (log.isDebugEnabled()) {