Skip to content

Commit

Permalink
Allow AppCDS generation from using a builder image
Browse files Browse the repository at this point in the history
The is useful when the building JVM version is not the same
as the runtime JVM version
  • Loading branch information
geoand committed Jan 28, 2021
1 parent ebe5e9a commit 82fd284
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> appcdsBuilderImage;

/**
* This is an advanced option that only takes effect for the mutable-jar format.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<AppCDSRequestedBuildItem> producer)
Expand All @@ -45,33 +47,31 @@ public void requested(OutputTargetBuildItem outputTarget, BuildProducer<AppCDSRe

@BuildStep
public void build(Optional<AppCDSRequestedBuildItem> appCDsRequested,
JarBuildItem jarResult, PackageConfig packageConfig,
JarBuildItem jarResult, OutputTargetBuildItem outputTarget, PackageConfig packageConfig,
Optional<AppCDSContainerImageBuildItem> appCDSContainerImage,
BuildProducer<AppCDSResultBuildItem> appCDS,
BuildProducer<ArtifactResultBuildItem> 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;
Expand All @@ -80,38 +80,71 @@ public void build(Optional<AppCDSRequestedBuildItem> 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<AppCDSContainerImageBuildItem> 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<String> 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<String> 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<String> command;
if (containerImage != null) {
List<String> 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()) {
Expand Down Expand Up @@ -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<String> dockerRunCommands(OutputTargetBuildItem outputTarget, String containerImage,
String containerWorkingDir) {
List<String> 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");
Expand All @@ -158,20 +210,35 @@ private Path createAppCDS(JarBuildItem jarResult, String javaBin,
}
}

List<String> command = new ArrayList<>(6);
command.add(javaBin);
command.add("-Xshare:dump");
command.add("-XX:SharedClassListFile=" + classesLstPath.toAbsolutePath().toString());
List<String> 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<String> command;
if (containerImage != null) {
List<String> 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()) {
Expand Down

0 comments on commit 82fd284

Please sign in to comment.