Skip to content

Commit

Permalink
Introduce the ability to generate AppCDS
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Jun 17, 2020
1 parent 60c4c99 commit 4fadfb9
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.microprofile.config.spi.ConfigBuilder;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
Expand All @@ -37,6 +35,7 @@
import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.builditem.DeploymentResultBuildItem;
import io.quarkus.deployment.util.JavaVersionUtil;
import io.quarkus.runtime.LaunchMode;

public class QuarkusAugmentor {
Expand Down Expand Up @@ -78,9 +77,7 @@ public class QuarkusAugmentor {
}

public BuildResult run() throws Exception {
Pattern pattern = Pattern.compile("(?:1\\.)?(\\d+)(?:\\..*)?");
Matcher matcher = pattern.matcher(System.getProperty("java.version", ""));
if (matcher.matches() && Integer.parseInt(matcher.group(1)) < 11) {
if (!JavaVersionUtil.isJava11OrHigher()) {
log.warn("Using Java versions older than 11 to build"
+ " Quarkus applications is deprecated and will be disallowed in a future release!");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,29 @@ public class PackageConfig {
*/
@ConfigItem
public Optional<String> outputName;

/**
* Whether to automate the creation of AppCDS. This has not effect when a native binary is needed and will be ignored in
* that case.
* Furthermore, this options only works for Java 11+.
* Finally, care must be taken to use the same exact JVM version when building and running the application.
*/
@ConfigItem
public boolean createAppcds;

public boolean isAnyJarType() {
return (type.equalsIgnoreCase(PackageConfig.LEGACY) ||
type.equalsIgnoreCase(PackageConfig.JAR) ||
type.equalsIgnoreCase(PackageConfig.FAST_JAR) ||
type.equalsIgnoreCase(PackageConfig.UBER_JAR));
}

public boolean isFastJar() {
return type.equalsIgnoreCase(PackageConfig.FAST_JAR);
}

public boolean isLegacyJar() {
return (type.equalsIgnoreCase(PackageConfig.LEGACY) ||
type.equalsIgnoreCase(PackageConfig.JAR));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.deployment.pkg.builditem;

import java.nio.file.Path;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* Build item to notify the indicate to the the various steps that AppCDS generation
* has been requested
*/
public final class AppCDSRequestedBuildItem extends SimpleBuildItem {

/**
* Directory where various files needed for AppCDS generation will reside
*/
private final Path appCDSDir;

public AppCDSRequestedBuildItem(Path appCDSDir) {
this.appCDSDir = appCDSDir;
}

public Path getAppCDSDir() {
return appCDSDir;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.deployment.pkg.builditem;

import java.nio.file.Path;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* A build item containing the result of the AppCDS generation process
*/
public final class AppCDSResultBuildItem extends SimpleBuildItem {

/**
* The file containing the generated AppCDS
*/
private final Path appCDS;

public AppCDSResultBuildItem(Path appCDS) {
this.appCDS = appCDS;
}

public Path getAppCDS() {
return appCDS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package io.quarkus.deployment.pkg.steps;

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;
import java.util.Optional;
import java.util.function.BooleanSupplier;

import org.jboss.logging.Logger;

import io.quarkus.bootstrap.util.IoUtils;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.pkg.PackageConfig;
import io.quarkus.deployment.pkg.builditem.AppCDSRequestedBuildItem;
import io.quarkus.deployment.pkg.builditem.AppCDSResultBuildItem;
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
import io.quarkus.deployment.pkg.builditem.JarBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.steps.MainClassBuildStep;
import io.quarkus.deployment.util.JavaVersionUtil;
import io.quarkus.runtime.LaunchMode;

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";

@BuildStep(onlyIf = AppCDSRequired.class)
public void requested(OutputTargetBuildItem outputTarget, BuildProducer<AppCDSRequestedBuildItem> producer)
throws IOException {
Path appCDSDir = outputTarget.getOutputDirectory().resolve("appcds");
IoUtils.recursiveDelete(appCDSDir);
Files.createDirectories(appCDSDir);

producer.produce(new AppCDSRequestedBuildItem(outputTarget.getOutputDirectory().resolve("appcds")));
}

@BuildStep
public void build(Optional<AppCDSRequestedBuildItem> appCDsRequested,
JarBuildItem jarResult, PackageConfig packageConfig,
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 = "java";
String javaBinStr = javaHomeStr + File.separator + "bin" + File.separator + javaExecutableStr; //TODO do we need to add '.exe' here for Windows?
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;
}

Path classesLstPath = createClassesLst(packageConfig, jarResult, javaBinStr, appCDSDir);
if (classesLstPath == null) {
log.warn("Unable to create AppCDS because " + CLASSES_LIST_FILE_NAME + " was not created.");
return;
}

log.debug("'" + CLASSES_LIST_FILE_NAME + "' successfully created.");

log.info("Launching AppCDS creation process.");
Path appCDSPath = createAppCDS(jarResult, javaBinStr, 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 same java version to run the application as was used to build it.",
appCDSPath.toAbsolutePath().toString());

appCDS.produce(new AppCDSResultBuildItem(appCDSPath));
artifactResult.produce(new ArtifactResultBuildItem(appCDSPath, "appCDS", Collections.emptyMap()));
}

/**
* @return The path of the created classes.lst file or null of the file was not created
*/
private Path createClassesLst(PackageConfig packageConfig, JarBuildItem jarResult,
String javaBin, Path appCDSDir) {

boolean isFastFar = packageConfig.isFastJar();

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 (isFastFar) {
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));
}

int exitCode;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command)
.directory(appCDSDir.toFile());
if (log.isDebugEnabled()) {
processBuilder.inheritIO();
} else {
processBuilder.redirectError(NULL_FILE);
processBuilder.redirectOutput(NULL_FILE);
}
exitCode = processBuilder.start().waitFor();
} catch (Exception e) {
log.debug("Failed to launch process used to create '" + CLASSES_LIST_FILE_NAME + "'.", e);
return null;
}

if (exitCode != 0) {
log.debugf("The process that was supposed to create AppCDS exited with error code: %d.", exitCode);
return null;
}

return appCDSDir.resolve(CLASSES_LIST_FILE_NAME);
}

/**
* @return The path of the created app-cds.jsa file or null of the file was not created
*/
private Path createAppCDS(JarBuildItem jarResult, String javaBin,
Path classesLstPath, boolean isFastFar) {

Path workingDirectory = isFastFar ? jarResult.getPath().getParent().getParent() : jarResult.getPath().getParent();
Path appCDSPath = workingDirectory.resolve("app-cds.jsa");
if (appCDSPath.toFile().exists()) {
try {
Files.delete(appCDSPath);
} catch (IOException e) {
log.debug("Unable to delete existing 'app-cds.jsa' file.", e);
}
}

List<String> command = new ArrayList<>(6);
command.add(javaBin);
command.add("-Xshare:dump");
command.add("-XX:SharedClassListFile=" + 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");
if (isFastFar) {
command.add(JarResultBuildStep.QUARKUS_RUN_JAR);
} else {
command.add(jarResult.getPath().getFileName().toString());
}

if (log.isDebugEnabled()) {
log.debugf("Launching command: '%s' to create final AppCDS.", String.join(" ", command));
}

int exitCode;
try {
ProcessBuilder processBuilder = new ProcessBuilder(command)
.directory(workingDirectory.toFile());
if (log.isDebugEnabled()) {
processBuilder.inheritIO();
} else {
processBuilder.redirectError(NULL_FILE);
processBuilder.redirectOutput(NULL_FILE);
}
exitCode = processBuilder.start().waitFor();
} catch (Exception e) {
log.debug("Failed to launch process used to create AppCDS.", e);
return null;
}

if (exitCode != 0) {
log.debugf("The process that was supposed to create AppCDS exited with error code: %d.", exitCode);
return null;
}

if (!appCDSPath.toFile().exists()) { // shouldn't happen, but let's avoid any surprises
return null;
}

return appCDSPath;
}

static class AppCDSRequired implements BooleanSupplier {

private final PackageConfig packageConfig;
private final LaunchMode launchMode;

AppCDSRequired(PackageConfig packageConfig, LaunchMode launchMode) {
this.packageConfig = packageConfig;
this.launchMode = launchMode;
}

@Override
public boolean getAsBoolean() {
if (launchMode != LaunchMode.NORMAL) {
return false;
}

if (!packageConfig.createAppcds || !packageConfig.isAnyJarType()) {
return false;
}

if (!JavaVersionUtil.isJava11OrHigher()) {
log.warn("AppCDS can only be used with Java 11+.");
return false;
}
return true;
}
}

// copied from Java 9
// TODO remove when we move to Java 11

private static final File NULL_FILE = new File(
(System.getProperty("os.name")
.startsWith("Windows") ? "NUL" : "/dev/null"));
}
Loading

0 comments on commit 4fadfb9

Please sign in to comment.