diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java index 37a24b90c6a305..2546e96ad42d88 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -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; @@ -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 { @@ -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!"); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java index d1320dff856536..b48eaf80c9ded7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java @@ -88,4 +88,29 @@ public class PackageConfig { */ @ConfigItem public Optional 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)); + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSBuildItem.java new file mode 100644 index 00000000000000..63f8530c0278b4 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSBuildItem.java @@ -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; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java new file mode 100644 index 00000000000000..1f83ea045c3034 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -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 appCDS, + BuildProducer 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 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 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 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 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 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")); +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java index f26f7d29e2f99b..ed97ad9da40d9e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java @@ -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); @@ -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(); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/JavaVersionUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/JavaVersionUtil.java new file mode 100644 index 00000000000000..d7cac86ddc062c --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/JavaVersionUtil.java @@ -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; + } +} diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index a56997bb053607..7c956595e5c5e0 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -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(