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 5ca434cc1bcf3a..4d61721473743c 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 @@ -79,4 +79,31 @@ 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+ and is considered experimental for the time being. + * 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)) || + type.equalsIgnoreCase(PackageConfig.MUTABLE_JAR); + } + + public boolean isFastJar() { + return type.equalsIgnoreCase(PackageConfig.FAST_JAR) || + type.equalsIgnoreCase(PackageConfig.MUTABLE_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/AppCDSRequestedBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSRequestedBuildItem.java new file mode 100644 index 00000000000000..054cb5a6132d17 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSRequestedBuildItem.java @@ -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; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSResultBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSResultBuildItem.java new file mode 100644 index 00000000000000..c603d7fb13a3ac --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/builditem/AppCDSResultBuildItem.java @@ -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; + } +} 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..7a6c6b3855cab8 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/AppCDSBuildStep.java @@ -0,0 +1,242 @@ +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 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 appCDsRequested, + JarBuildItem jarResult, PackageConfig packageConfig, + BuildProducer appCDS, + BuildProducer 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 exact same Java version (%s) to run the application as was used to build it.", + appCDSPath.toAbsolutePath().toString(), System.getProperty("java.version")); + + 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) { + + List 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()); + } 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 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")); +} 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 66a0d81dd439e4..9613f8fa3fb55b 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 @@ -5,6 +5,7 @@ import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; +import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.BooleanSupplier; @@ -58,6 +60,7 @@ import io.quarkus.deployment.builditem.QuarkusBuildCloseablesBuildItem; import io.quarkus.deployment.builditem.TransformedClassesBuildItem; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.builditem.AppCDSRequestedBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; @@ -160,14 +163,18 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem List generatedResources, List uberJarRequired, QuarkusBuildCloseablesBuildItem closeablesBuildItem, - MainClassBuildItem mainClassBuildItem) throws Exception { + MainClassBuildItem mainClassBuildItem, Optional appCDS) throws Exception { + + if (appCDS.isPresent()) { + handleAppCDSSupportFileGeneration(transformedClasses, generatedClasses, appCDS.get()); + } + if (!uberJarRequired.isEmpty() || packageConfig.uberJar || packageConfig.type.equalsIgnoreCase(PackageConfig.UBER_JAR)) { 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); @@ -177,6 +184,32 @@ public JarBuildItem buildRunnerJar(CurateOutcomeBuildItem curateOutcomeBuildItem } } + // the idea here is to just dump the class names of the generated and transformed classes into a file + // that is read at runtime when AppCDS generation is requested + private void handleAppCDSSupportFileGeneration(TransformedClassesBuildItem transformedClasses, + List generatedClasses, AppCDSRequestedBuildItem appCDS) throws IOException { + Path appCDsDir = appCDS.getAppCDSDir(); + Path generatedClassesFile = appCDsDir.resolve("generatedAndTransformed.lst"); + try (BufferedWriter writer = Files.newBufferedWriter(generatedClassesFile, StandardOpenOption.CREATE)) { + StringBuilder classes = new StringBuilder(); + for (GeneratedClassBuildItem generatedClass : generatedClasses) { + classes.append(generatedClass.getName().replace('/', '.')).append(System.lineSeparator()); + } + + for (Set transformedClassesSet : transformedClasses + .getTransformedClassesByJar().values()) { + for (TransformedClassesBuildItem.TransformedClass transformedClass : transformedClassesSet) { + classes.append(transformedClass.getFileName().replace('/', '.').replace(".class", "")) + .append(System.lineSeparator()); + } + } + + if (classes.length() != 0) { + writer.write(classes.toString()); + } + } + } + private JarBuildItem buildUberJar(CurateOutcomeBuildItem curateOutcomeBuildItem, OutputTargetBuildItem outputTargetBuildItem, TransformedClassesBuildItem transformedClasses, @@ -1098,11 +1131,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) || - packageConfig.type.equalsIgnoreCase(PackageConfig.MUTABLE_JAR); + return packageConfig.isAnyJarType(); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index de58e41982fd1a..3400b5b1b6fe8f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -52,6 +52,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.configuration.RunTimeConfigurationGenerator; import io.quarkus.deployment.pkg.PackageConfig; +import io.quarkus.deployment.pkg.builditem.AppCDSRequestedBuildItem; import io.quarkus.deployment.recording.BytecodeRecorderImpl; import io.quarkus.dev.appstate.ApplicationStateNotification; import io.quarkus.gizmo.BytecodeCreator; @@ -64,6 +65,7 @@ import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.TryBlock; import io.quarkus.runtime.Application; +import io.quarkus.runtime.ApplicationLifecycleManager; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.NativeImageRuntimePropertiesRecorder; import io.quarkus.runtime.Quarkus; @@ -71,9 +73,10 @@ import io.quarkus.runtime.StartupContext; import io.quarkus.runtime.StartupTask; import io.quarkus.runtime.annotations.QuarkusMain; +import io.quarkus.runtime.appcds.AppCDSUtil; import io.quarkus.runtime.configuration.ProfileManager; -class MainClassBuildStep { +public class MainClassBuildStep { static final String MAIN_CLASS = "io.quarkus.runner.GeneratedMain"; static final String STARTUP_CONTEXT = "STARTUP_CONTEXT"; @@ -89,6 +92,8 @@ class MainClassBuildStep { JAVAX_NET_SSL_TRUST_STORE_TYPE, JAVAX_NET_SSL_TRUST_STORE_PROVIDER, JAVAX_NET_SSL_TRUST_STORE_PASSWORD)); + public static final String GENERATE_APP_CDS_SYSTEM_PROPERTY = "quarkus.appcds.generate"; + private static final FieldDescriptor STARTUP_CONTEXT_FIELD = FieldDescriptor.of(Application.APP_CLASS_NAME, STARTUP_CONTEXT, StartupContext.class); @@ -104,7 +109,8 @@ void build(List staticInitTasks, BuildProducer generatedClass, LaunchModeBuildItem launchMode, LiveReloadBuildItem liveReloadBuildItem, - ApplicationInfoBuildItem applicationInfo) { + ApplicationInfoBuildItem applicationInfo, + Optional appCDSRequested) { appClassNameProducer.produce(new ApplicationClassNameBuildItem(Application.APP_CLASS_NAME)); @@ -166,6 +172,21 @@ void build(List staticInitTasks, mv = file.getMethodCreator("doStart", void.class, String[].class); mv.setModifiers(Modifier.PROTECTED | Modifier.FINAL); + // if AppCDS generation was requested, we ensure that the application simply loads some classes from a file and terminates + if (appCDSRequested.isPresent()) { + ResultHandle createAppCDsSysProp = mv.invokeStaticMethod( + ofMethod(System.class, "getProperty", String.class, String.class, String.class), + mv.load(GENERATE_APP_CDS_SYSTEM_PROPERTY), mv.load("false")); + ResultHandle createAppCDSBool = mv.invokeStaticMethod( + ofMethod(Boolean.class, "parseBoolean", boolean.class, String.class), createAppCDsSysProp); + BytecodeCreator createAppCDS = mv.ifTrue(createAppCDSBool).trueBranch(); + + createAppCDS.invokeStaticMethod(ofMethod(AppCDSUtil.class, "loadGeneratedClasses", void.class)); + + createAppCDS.invokeStaticMethod(ofMethod(ApplicationLifecycleManager.class, "exit", void.class)); + createAppCDS.returnValue(null); + } + // very first thing is to set system props (for run time, which use substitutions for a different // storage from build-time) for (SystemPropertyBuildItem i : properties) { 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/core/runtime/src/main/java/io/quarkus/runtime/appcds/AppCDSUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/appcds/AppCDSUtil.java new file mode 100644 index 00000000000000..6f242cb68f1433 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/appcds/AppCDSUtil.java @@ -0,0 +1,28 @@ +package io.quarkus.runtime.appcds; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Paths; + +public class AppCDSUtil { + + /** + * This is never meant to be used in a regular application run. + * It is only referenced by the generated main with the purpose of + * loading all the generated and transformed classes in order to give the AppCDS + * generation process a larger set of classes to work with + */ + public static void loadGeneratedClasses() throws IOException, ClassNotFoundException { + try (BufferedReader br = new BufferedReader(new FileReader(Paths.get("generatedAndTransformed.lst").toFile()))) { + String line; + while ((line = br.readLine()) != null) { + Class.forName(line, true, Thread.currentThread().getContextClassLoader()); + } + } catch (ClassNotFoundException | IOException e) { + System.err.println("Improperly configured AppCDS generation process launched"); + e.printStackTrace(); + throw e; + } + } +} 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 5661036e213331..759e4ac31aa353 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,13 +89,10 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji JibContainerBuilder jibContainerBuilder; String packageType = packageConfig.type; - if (packageType.equalsIgnoreCase(PackageConfig.LEGACY) - || packageType.equalsIgnoreCase(PackageConfig.JAR) - || packageType.equalsIgnoreCase(PackageConfig.UBER_JAR)) { + if (packageConfig.isLegacyJar() || packageType.equalsIgnoreCase(PackageConfig.UBER_JAR)) { jibContainerBuilder = createContainerBuilderFromLegacyJar(jibConfig, sourceJar, outputTarget, mainClass, containerImageLabels); - } else if (packageType.equalsIgnoreCase(PackageConfig.FAST_JAR) - || packageType.equalsIgnoreCase(PackageConfig.MUTABLE_JAR)) { + } else if (packageConfig.isFastJar()) { jibContainerBuilder = createContainerBuilderFromFastJar(jibConfig, sourceJar, containerImageLabels); } else { throw new IllegalArgumentException( diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java index e9cf6a28a56fc3..12f0d7d4626956 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/JarRunnerIT.java @@ -17,6 +17,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; @@ -28,6 +29,8 @@ import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import io.quarkus.maven.it.verifier.MavenProcessInvocationResult; import io.quarkus.maven.it.verifier.RunningInvoker; @@ -52,7 +55,7 @@ public void testThatJarRunnerConsoleOutputWorksCorrectly() throws MavenInvocatio File output = new File(testDir, "target/output.log"); output.createNewFile(); - Process process = doLaunch(jar, output); + Process process = doLaunch(jar, output).start(); try { // Wait until server up await() @@ -94,7 +97,7 @@ public void testThatFastJarFormatWorks() throws Exception { File output = new File(testDir, "target/output.log"); output.createNewFile(); - Process process = doLaunch(jar, output); + Process process = doLaunch(jar, output).start(); try { // Wait until server up dumpFileContentOnFailure(() -> { @@ -139,7 +142,7 @@ public void testThatMutableFastJarWorks() throws Exception { File output = new File(testDir, "target/output.log"); output.createNewFile(); - Process process = doLaunch(jar, output); + Process process = doLaunch(jar, output).start(); try { // Wait until server up dumpFileContentOnFailure(() -> { @@ -171,7 +174,7 @@ public void testThatMutableFastJarWorks() throws Exception { processBuilder.redirectError(output); Assertions.assertEquals(0, processBuilder.start().waitFor()); - process = doLaunch(jar, output); + process = doLaunch(jar, output).start(); try { // Wait until server up await() @@ -191,6 +194,48 @@ public void testThatMutableFastJarWorks() throws Exception { } } + @Test + @EnabledForJreRange(min = JRE.JAVA_11) + public void testThatAppCDSAreUsable() throws Exception { + File testDir = initProject("projects/classic", "projects/project-classic-console-output-appcds"); + RunningInvoker running = new RunningInvoker(testDir, false); + + MavenProcessInvocationResult result = running + .execute(Arrays.asList("package", "-DskipTests", "-Dquarkus.package.create-appcds=true"), + Collections.emptyMap()); + + await().atMost(1, TimeUnit.MINUTES).until(() -> result.getProcess() != null && !result.getProcess().isAlive()); + assertThat(running.log()).containsIgnoringCase("BUILD SUCCESS"); + running.stop(); + + Path jar = testDir.toPath().toAbsolutePath() + .resolve(Paths.get("target/acme-1.0-SNAPSHOT-runner.jar")); + File output = new File(testDir, "target/output.log"); + output.createNewFile(); + + // by using '-Xshare:on' we ensure that the JVM will fail if for any reason is cannot use the AppCDS + // '-Xlog:class+path=info' will print diagnostic information that is useful for debugging if something goes wrong + Process process = doLaunch(jar.getFileName(), output, + Arrays.asList("-XX:SharedArchiveFile=app-cds.jsa", "-Xshare:on", "-Xlog:class+path=info")) + .directory(jar.getParent().toFile()).start(); + try { + // Wait until server up + dumpFileContentOnFailure(() -> { + await() + .pollDelay(1, TimeUnit.SECONDS) + .atMost(1, TimeUnit.MINUTES).until(() -> DevModeTestUtils.getHttpResponse("/app/hello/package", 200)); + return null; + }, output, ConditionTimeoutException.class); + + String logs = FileUtils.readFileToString(output, "UTF-8"); + + assertThatOutputWorksCorrectly(logs); + } finally { + process.destroy(); + } + + } + /** * Tests that quarkus.arc.exclude-dependency.* can be used for modules in a multimodule project */ @@ -210,7 +255,7 @@ public void testArcExcludeDependencyOnLocalModule() throws Exception { File output = new File(targetDir, "output.log"); output.createNewFile(); - Process process = doLaunch(jar, output); + Process process = doLaunch(jar, output).start(); try { // Wait until server up AtomicReference response = new AtomicReference<>(); @@ -229,9 +274,14 @@ public void testArcExcludeDependencyOnLocalModule() throws Exception { } } - private Process doLaunch(Path jar, File output) throws IOException { + private ProcessBuilder doLaunch(Path jar, File output) throws IOException { + return doLaunch(jar, output, Collections.emptyList()); + } + + private ProcessBuilder doLaunch(Path jar, File output, Collection vmArgs) throws IOException { List commands = new ArrayList<>(); commands.add(JavaBinFinder.findBin()); + commands.addAll(vmArgs); commands.add("-jar"); commands.add(jar.toString()); // write out the command used to launch the process, into the log file @@ -239,7 +289,7 @@ private Process doLaunch(Path jar, File output) throws IOException { ProcessBuilder processBuilder = new ProcessBuilder(commands.toArray(new String[0])); processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(output)); processBuilder.redirectError(ProcessBuilder.Redirect.appendTo(output)); - return processBuilder.start(); + return processBuilder; } static void assertResourceReadingFromClassPathWorksCorrectly(String path) {