-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce the ability to generate AppCDS
- Loading branch information
Showing
7 changed files
with
330 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSBuildItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
260 changes: 260 additions & 0 deletions
260
core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
core/deployment/src/main/java/io/quarkus/deployment/util/JavaVersionUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters