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 16, 2020
1 parent 6308736 commit 27561cc
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 14 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,18 @@
package io.quarkus.deployment.pkg.builditem;

import java.nio.file.Path;

import io.quarkus.builder.item.SimpleBuildItem;

public final class AppCDSBuildItem extends SimpleBuildItem {

private final Path appCDS;

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

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

import java.io.BufferedWriter;
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.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

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.AppCDSBuildItem;
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.util.JavaVersionUtil;

public class AppCDSBuildStep {

private static final Logger log = Logger.getLogger(AppCDSBuildStep.class);
public static final String JDK_CLASSLIST_FILE = "classlist";

@BuildStep(onlyIf = AppCDSRequired.class)
public void build(OutputTargetBuildItem outputTarget, JarBuildItem jarResult, PackageConfig packageConfig,
BuildProducer<AppCDSBuildItem> appCDS,
BuildProducer<ArtifactResultBuildItem> artifactResult) throws Exception {
Path appCDSDir = outputTarget.getOutputDirectory().resolve("appcds");
IoUtils.recursiveDelete(appCDSDir);
Files.createDirectories(appCDSDir);

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, jdkClassList, appCDSDir);
if (classesLstPath == null) {
log.warn("Unable to create AppCDS because classes.lst was not created.");
return;
}

log.debug("'classes.lst' 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 AppCDSBuildItem(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, Path jdkClassList,
Path appCDSDir) {

// seed the file with the contents of the JDKs classes list
Path classesLstPath = appCDSDir.resolve("classes.lst");
try {
Files.copy(jdkClassList, classesLstPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
log.debug("Failed to create 'classes.lst'.", e);
return null;
}

// go through each dependency, get all class files and write them to the 'classes.lst' file
if (packageConfig.isLegacyJar()) {
Path libraryDir = jarResult.getLibraryDir();
try {
File[] libJars = libraryDir.toFile().listFiles();
List<File> jarFiles = new ArrayList<>(libJars.length + 1);
jarFiles.addAll(Arrays.asList(libJars));
jarFiles.add(jarResult.getPath().toFile());
addJarContentToClassLst(classesLstPath, jarFiles);
} catch (IOException e) {
log.debug("Failed to create 'classes.lst'.", e);
return null;
}
} else if (packageConfig.isFastJar()) {
// 'fast-jar' doesn't play well with AppCDS generation process that uses '-Xshare:dump' as its classpath
// isn't known to the dumping process.
// So for now all we do is add the boot-libs which are part of the classpath that the dump process knows about
Path libraryDir = jarResult.getLibraryDir();
Path fastJarOutputDir = libraryDir.getParent();
try {
File[] bootLibJars = fastJarOutputDir.resolve(JarResultBuildStep.BOOT_LIB).toFile().listFiles();
List<File> jarFiles = new ArrayList<>(bootLibJars.length);
jarFiles.addAll(Arrays.asList(bootLibJars));
jarFiles.add(jarResult.getPath().toFile());
addJarContentToClassLst(classesLstPath, jarFiles);
} catch (IOException e) {
log.debug("Failed to create 'classes.lst'.", e);
return null;
}
} else if (packageConfig.type.equalsIgnoreCase(PackageConfig.UBER_JAR)) {
try {
addJarContentToClassLst(classesLstPath, Collections.singletonList(jarResult.getPath().toFile()));
} catch (IOException e) {
log.debug("Failed to create 'classes.lst'.", e);
return null;
}
}

return classesLstPath;
}

private void addJarContentToClassLst(Path classesLstPath, List<File> jarFiles) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(classesLstPath, StandardOpenOption.APPEND)) {
for (File file : jarFiles) {
if (!file.getName().endsWith(".jar")) {
continue;
}
StringBuilder classes = new StringBuilder();
try (JarFile zip = new JarFile(file)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().endsWith(".class")) {
classes.append(entry.getName().replace(".class", "")).append(System.lineSeparator());
}
}
}

if (classes.length() != 0) {
writer.write(classes.toString());
}
}
}
}

/**
* @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;

AppCDSRequired(PackageConfig packageConfig) {
this.packageConfig = packageConfig;
}

@Override
public boolean getAsBoolean() {
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"));
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem
return buildUberJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses, applicationArchivesBuildItem,
packageConfig, applicationInfo, generatedClasses, generatedResources, closeablesBuildItem,
mainClassBuildItem);
} else if (packageConfig.type.equalsIgnoreCase(PackageConfig.LEGACY)
|| packageConfig.type.equalsIgnoreCase(PackageConfig.JAR)) {
} else if (packageConfig.isLegacyJar()) {
return buildLegacyThinJar(curateOutcomeBuildItem, outputTargetBuildItem, transformedClasses,
applicationArchivesBuildItem,
packageConfig, applicationInfo, generatedClasses, generatedResources, mainClassBuildItem);
Expand Down Expand Up @@ -1098,10 +1097,7 @@ static class JarRequired implements BooleanSupplier {

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.deployment.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class JavaVersionUtil {

private static final Pattern PATTERN = Pattern.compile("(?:1\\.)?(\\d+)(?:\\..*)?");

private static boolean IS_JAVA_11_OR_NEWER;

static {
Matcher matcher = PATTERN.matcher(System.getProperty("java.version", ""));
IS_JAVA_11_OR_NEWER = !matcher.matches() || Integer.parseInt(matcher.group(1)) >= 11;

}

public static boolean isJava11OrHigher() {
return IS_JAVA_11_OR_NEWER;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,10 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji

JibContainerBuilder jibContainerBuilder;
String packageType = packageConfig.type;
if (packageType.equalsIgnoreCase(PackageConfig.LEGACY)
|| packageType.equalsIgnoreCase(PackageConfig.JAR)) {
if (packageConfig.isLegacyJar()) {
jibContainerBuilder = createContainerBuilderFromLegacyJar(jibConfig,
sourceJar, outputTarget, mainClass, containerImageLabels);
} else if (packageType.equalsIgnoreCase(PackageConfig.FAST_JAR)) {
} else if (packageConfig.isFastJar()) {
jibContainerBuilder = createContainerBuilderFromFastJar(jibConfig, sourceJar, containerImageLabels);
} else {
throw new IllegalArgumentException(
Expand Down

0 comments on commit 27561cc

Please sign in to comment.