Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for AppCDS generation in Jib built container images #14655

Merged
merged 2 commits into from
Feb 3, 2021
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ 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} and it requires having
* docker available during the build.
*/
@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
@@ -1,10 +1,11 @@
package io.quarkus.deployment.pkg.steps;

import static io.quarkus.deployment.pkg.steps.LinuxIDUtil.getLinuxID;

import java.io.File;
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 +19,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 +33,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,77 +49,107 @@ 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;
}

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()) {
log.debugf("Launching command: '%s' to create '" + CLASSES_LIST_FILE_NAME + "' AppCDS.", String.join(" ", command));
log.debugf("Launching command: '%s' to create '" + CLASSES_LIST_FILE_NAME + "' file.", String.join(" ", command));
}

int exitCode;
Expand All @@ -139,14 +173,45 @@ private Path createClassesLst(PackageConfig packageConfig, JarBuildItem jarResul
return null;
}

return appCDSDir.resolve(CLASSES_LIST_FILE_NAME);
Path result = appCDSDir.resolve(CLASSES_LIST_FILE_NAME);
if (!Files.exists(result)) {
log.warnf("Unable to create AppCDS because '%s' was not created. Check the logs for details",
CLASSES_LIST_FILE_NAME);
return null;
}
return result;
}

// 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");
if (SystemUtils.IS_OS_LINUX) {
String uid = getLinuxID("-ur");
String gid = getLinuxID("-gr");
if (uid != null && gid != null && !uid.isEmpty() && !gid.isEmpty()) {
command.add("--user");
command.add(uid + ":" + gid);
}
}
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 +223,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkus.deployment.pkg.steps;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

final class LinuxIDUtil {

private LinuxIDUtil() {
}

static String getLinuxID(String option) {
Process process;

try {
StringBuilder responseBuilder = new StringBuilder();
String line;

ProcessBuilder idPB = new ProcessBuilder().command("id", option);
idPB.redirectError(new File("/dev/null"));
idPB.redirectInput(new File("/dev/null"));

process = idPB.start();
try (InputStream inputStream = process.getInputStream()) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
while ((line = reader.readLine()) != null) {
responseBuilder.append(line);
}
safeWaitFor(process);
return responseBuilder.toString();
}
} catch (Throwable t) {
safeWaitFor(process);
throw t;
}
} catch (IOException e) { //from process.start()
//swallow and return null id
return null;
}
}

private static void safeWaitFor(Process process) {
boolean intr = false;
try {
for (;;)
try {
process.waitFor();
return;
} catch (InterruptedException ex) {
intr = true;
}
} finally {
if (intr) {
Thread.currentThread().interrupt();
}
}
}
}
Loading