From 75ebde9bd9d75f18565a1ca95672dbf26f3a70a7 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Sun, 27 Oct 2024 20:51:55 +0100 Subject: [PATCH] Ability to configure extension Dev mode JVM arguments --- .../deployment/dev/DevModeCommandLine.java | 35 + .../dev/DevModeCommandLineBuilder.java | 663 ++++++++++++++++++ .../deployment/dev/DevModeContext.java | 2 +- .../dev/ExtensionDevModeJvmOptionFilter.java | 65 ++ .../dev/QuarkusDevModeLauncher.java | 576 --------------- .../dev/DevModeCommandLineBuilderTest.java | 244 +++++++ .../gradle/tasks/GradleDevModeLauncher.java | 52 -- .../tasks/QuarkusApplicationModelTask.java | 6 +- .../io/quarkus/gradle/tasks/QuarkusDev.java | 47 +- .../gradle/tasks/QuarkusRemoteDev.java | 3 +- .../io/quarkus/gradle/tasks/QuarkusTest.java | 3 +- .../GradleApplicationModelBuilder.java | 6 +- .../main/java/io/quarkus/maven/DevMojo.java | 84 ++- .../quarkus/maven/MavenDevModeLauncher.java | 52 -- .../java/io/quarkus/maven/RemoteDevMojo.java | 4 +- .../main/java/io/quarkus/maven/TestMojo.java | 3 +- .../src/main/asciidoc/extension-metadata.adoc | 83 +++ docs/src/main/asciidoc/gradle-tooling.adoc | 85 +++ docs/src/main/asciidoc/maven-tooling.adoc | 57 ++ .../quarkus/bootstrap/BootstrapConstants.java | 17 + .../bootstrap/model/ApplicationModel.java | 7 + .../model/ApplicationModelBuilder.java | 163 +++-- .../model/DefaultApplicationModel.java | 17 +- .../model/ExtensionDevModeConfig.java | 50 ++ .../io/quarkus/bootstrap/model/JvmOption.java | 48 ++ .../quarkus/bootstrap/model/JvmOptions.java | 57 ++ .../bootstrap/model/JvmOptionsBuilder.java | 203 ++++++ .../bootstrap/model/MutableBaseJvmOption.java | 73 ++ .../model/MutableStandardJvmOption.java | 128 ++++ .../bootstrap/model/MutableXxJvmOption.java | 61 ++ .../model/MutableStandardJvmOptionTest.java | 50 ++ .../model/MutableXxJvmOptionTest.java | 42 ++ .../ApplicationDependencyTreeResolver.java | 2 +- .../IncubatingApplicationModelResolver.java | 8 +- .../maven/ExtensionDescriptorMojo.java | 312 +++++---- .../maven/ExtensionDevModeMavenConfig.java | 129 ++++ 36 files changed, 2505 insertions(+), 932 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLine.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLineBuilder.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/ExtensionDevModeJvmOptionFilter.java delete mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java create mode 100644 core/deployment/src/test/java/io/quarkus/deployment/dev/DevModeCommandLineBuilderTest.java delete mode 100644 devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/GradleDevModeLauncher.java delete mode 100644 devtools/maven/src/main/java/io/quarkus/maven/MavenDevModeLauncher.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOption.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptions.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptionsBuilder.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableBaseJvmOption.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableStandardJvmOption.java create mode 100644 independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableXxJvmOption.java create mode 100644 independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableStandardJvmOptionTest.java create mode 100644 independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableXxJvmOptionTest.java create mode 100644 independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLine.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLine.java new file mode 100644 index 0000000000000..867477beb087e --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLine.java @@ -0,0 +1,35 @@ +package io.quarkus.deployment.dev; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +public class DevModeCommandLine { + + public static DevModeCommandLineBuilder builder(String java) { + return new DevModeCommandLineBuilder(java); + } + + private final List args; + private final String debugPort; + private final Collection buildFiles; + + public DevModeCommandLine(List args, String debugPort, Collection buildFiles) { + this.args = args; + this.debugPort = debugPort; + this.buildFiles = buildFiles; + } + + public List getArguments() { + return args; + } + + public Collection getWatchedBuildFiles() { + return buildFiles; + } + + public String getDebugPort() { + return debugPort; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLineBuilder.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLineBuilder.java new file mode 100644 index 0000000000000..25050037360a2 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeCommandLineBuilder.java @@ -0,0 +1,663 @@ +package io.quarkus.deployment.dev; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.UnknownHostException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.model.ExtensionDevModeConfig; +import io.quarkus.bootstrap.model.JvmOptions; +import io.quarkus.bootstrap.model.JvmOptionsBuilder; +import io.quarkus.deployment.util.CommandLineUtil; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.runtime.logging.JBossVersion; +import io.quarkus.runtime.util.JavaVersionUtil; +import io.quarkus.utilities.JavaBinFinder; + +public class DevModeCommandLineBuilder { + + private static final Logger log = Logger.getLogger(DevModeCommandLineBuilder.class); + + /** + * Logs a warning about extensions enabling the C2 compiler + * + * @param extensions extensions enabling the C2 compiler + */ + private static void extensionsEnableC2Warning(List extensions) { + var sb = new StringBuilder().append("Extension"); + if (extensions.size() > 1) { + sb.append("s"); + } + sb.append(" ").append(extensions.get(0).toGacString()); + for (int i = 1; i < extensions.size(); ++i) { + sb.append(", ").append(extensions.get(i).toGacString()); + } + sb.append(" enable"); + if (extensions.size() == 1) { + sb.append("s"); + } + sb.append(" the C2 compiler which is disabled by default in Dev mode for optimal performance."); + log.info(sb.toString()); + } + + /** + * Logs a warning about extensions disabling debug mode + * + * @param extensions extensions disabling debug mode + */ + private static void extensionsDisablingDebugWarning(List extensions) { + var sb = new StringBuilder().append("Extension"); + if (extensions.size() > 1) { + sb.append("s"); + } + sb.append(" ").append(extensions.get(0).toGacString()); + for (int i = 1; i < extensions.size(); ++i) { + sb.append(", ").append(extensions.get(i).toGacString()); + } + sb.append(" disable"); + if (extensions.size() == 1) { + sb.append("s"); + } + sb.append( + " the Debug mode for optimal performance. Debugging can still be enabled in the Quarkus plugin configuration or with -Ddebug on the command line."); + log.info(sb.toString()); + } + + private static final String TIERED_STOP_AT_LEVEL = "TieredStopAtLevel"; + private static final String AGENTLIB_JDWP = "agentlib:jdwp"; + + final Pattern validDebug = Pattern.compile("^(true|false|client|[0-9]+)$"); + final Pattern validPort = Pattern.compile("^-?[0-9]+$"); + + private List args = new ArrayList<>(); + private JvmOptionsBuilder jvmOptionsBuilder = JvmOptions.builder(); + private String debug; + private String suspend; + private String debugHost = "localhost"; + private String debugPort = "5005"; + private String actualDebugPort; + private File projectDir; + private File buildDir; + private File outputDir; + private Map buildSystemProperties = new HashMap<>(0); + private String applicationName; + private String applicationVersion; + private String sourceEncoding; + private Map> compilerOptions = new HashMap<>(1); + private List compilerPluginArtifacts; + private List compilerPluginOptions; + private String releaseJavaVersion; + private String sourceJavaVersion; + private String targetJavaVersion; + private Set buildFiles = new HashSet<>(0); + private boolean deleteDevJar = true; + private Boolean forceC2; + private String baseName; + private Consumer entryPointCustomizer; + private String applicationArgs; + private Set localArtifacts = new HashSet<>(); + private DevModeContext.ModuleInfo main; + private List dependencies = new ArrayList<>(0); + private LinkedHashMap classpath = new LinkedHashMap<>(); + private Set processorPaths; + private List processors; + private Collection extDevModeConfig; + private ExtensionDevModeJvmOptionFilter extDevModeJvmOptionFilter; + + protected DevModeCommandLineBuilder(String java) { + final String javaTool = java == null ? JavaBinFinder.findBin() : java; + log.debugf("Using javaTool: %s", javaTool); + args.add(javaTool); + } + + public DevModeCommandLineBuilder preventnoverify(boolean preventnoverify) { + if (!preventnoverify) { + // in Java 13 and up, preventing verification is deprecated - see https://bugs.openjdk.java.net/browse/JDK-8218003 + // this test isn't absolutely correct in the sense that depending on the user setup, the actual Java binary + // that is used might be different that the one running Maven, but given how small of an impact this has + // it's probably better than running an extra command on 'javaTool' just to figure out the version + if (!JavaVersionUtil.isJava13OrHigher()) { + args.add("-Xverify:none"); + } + } + return this; + } + + public DevModeCommandLineBuilder forceC2(Boolean force) { + forceC2 = force; + return this; + } + + public DevModeCommandLineBuilder jvmArgs(String jvmArgs) { + args.add(jvmArgs); + return this; + } + + public DevModeCommandLineBuilder jvmArgs(List jvmArgs) { + args.addAll(jvmArgs); + return this; + } + + public DevModeCommandLineBuilder debug(String debug) { + this.debug = debug; + return this; + } + + public DevModeCommandLineBuilder suspend(String suspend) { + this.suspend = suspend; + return this; + } + + public DevModeCommandLineBuilder projectDir(File projectDir) { + this.projectDir = projectDir; + return this; + } + + public DevModeCommandLineBuilder buildDir(File buildDir) { + this.buildDir = buildDir; + return this; + } + + public DevModeCommandLineBuilder outputDir(File outputDir) { + this.outputDir = outputDir; + return this; + } + + public DevModeCommandLineBuilder buildSystemProperties(Map buildSystemProperties) { + this.buildSystemProperties = buildSystemProperties; + return this; + } + + public DevModeCommandLineBuilder buildSystemProperty(String name, String value) { + this.buildSystemProperties.put(name, value); + return this; + } + + public DevModeCommandLineBuilder applicationName(String appName) { + this.applicationName = appName; + return this; + } + + public DevModeCommandLineBuilder applicationVersion(String appVersion) { + this.applicationVersion = appVersion; + return this; + } + + public DevModeCommandLineBuilder applicationArgs(String appArgs) { + this.applicationArgs = appArgs; + return this; + } + + public DevModeCommandLineBuilder sourceEncoding(String srcEncoding) { + this.sourceEncoding = srcEncoding; + return this; + } + + public DevModeCommandLineBuilder compilerOptions(String name, List options) { + compilerOptions.compute(name, (key, value) -> { + if (value == null) { + return new HashSet<>(options); + } + value.addAll(options); + return value; + }); + return this; + } + + public DevModeCommandLineBuilder compilerOptions(Map> options) { + compilerOptions.putAll(options); + return this; + } + + public DevModeCommandLineBuilder compilerPluginArtifacts(List artifacts) { + compilerPluginArtifacts = artifacts; + return this; + } + + public DevModeCommandLineBuilder compilerPluginOptions(List options) { + compilerPluginOptions = options; + return this; + } + + public DevModeCommandLineBuilder annotationProcessorPaths(Set processorPaths) { + this.processorPaths = processorPaths; + return this; + } + + public DevModeCommandLineBuilder annotationProcessors(List processors) { + this.processors = processors; + return this; + } + + public DevModeCommandLineBuilder releaseJavaVersion(String releaseJavaVersion) { + this.releaseJavaVersion = releaseJavaVersion; + return this; + } + + public DevModeCommandLineBuilder sourceJavaVersion(String sourceJavaVersion) { + this.sourceJavaVersion = sourceJavaVersion; + return this; + } + + public DevModeCommandLineBuilder targetJavaVersion(String targetJavaVersion) { + this.targetJavaVersion = targetJavaVersion; + return this; + } + + public DevModeCommandLineBuilder watchedBuildFile(Path buildFile) { + this.buildFiles.add(buildFile); + return this; + } + + public DevModeCommandLineBuilder deleteDevJar(boolean deleteDevJar) { + this.deleteDevJar = deleteDevJar; + return this; + } + + public DevModeCommandLineBuilder baseName(String baseName) { + this.baseName = baseName; + return this; + } + + public DevModeCommandLineBuilder remoteDev(boolean remoteDev) { + this.entryPointCustomizer = devModeContext -> { + devModeContext.setMode(QuarkusBootstrap.Mode.REMOTE_DEV_CLIENT); + devModeContext.setAlternateEntryPoint(IsolatedRemoteDevModeMain.class.getName()); + }; + return this; + } + + public DevModeCommandLineBuilder entryPointCustomizer(Consumer consumer) { + this.entryPointCustomizer = consumer; + return this; + } + + public DevModeCommandLineBuilder localArtifact(ArtifactKey localArtifact) { + localArtifacts.add(localArtifact); + return this; + } + + public boolean isLocal(ArtifactKey artifact) { + return localArtifacts.contains(artifact); + } + + public DevModeCommandLineBuilder mainModule(DevModeContext.ModuleInfo mainModule) { + main = mainModule; + return this; + } + + public DevModeCommandLineBuilder dependency(DevModeContext.ModuleInfo module) { + dependencies.add(module); + return this; + } + + public DevModeCommandLineBuilder classpathEntry(ArtifactKey key, File f) { + final File prev = classpath.put(key, f); + if (prev != null && !f.equals(prev)) { + Logger.getLogger(getClass()).warn(key + " classpath entry " + prev + " was overriden with " + f); + } + return this; + } + + public DevModeCommandLineBuilder debugHost(String host) { + if ((null != host) && !host.isEmpty()) { + this.debugHost = host; + } + return this; + } + + public DevModeCommandLineBuilder debugPort(String port) { + if ((null != port) && !port.isEmpty()) { + this.debugPort = port; + } + return this; + } + + public DevModeCommandLineBuilder addOpens(String value) { + jvmOptionsBuilder.add("add-opens", value); + return this; + } + + public DevModeCommandLineBuilder addModules(Collection modules) { + jvmOptionsBuilder.addAll("add-modules", modules); + return this; + } + + public DevModeCommandLineBuilder extensionDevModeConfig(Collection extDevModeConfig) { + this.extDevModeConfig = extDevModeConfig; + return this; + } + + public DevModeCommandLineBuilder extensionDevModeJvmOptionFilter( + ExtensionDevModeJvmOptionFilter extDevModeJvmOptionFilter) { + this.extDevModeJvmOptionFilter = extDevModeJvmOptionFilter; + return this; + } + + public DevModeCommandLine build() throws Exception { + JBossVersion.disableVersionLogging(); + + //build a class-path string for the base platform + //this stuff does not change + // Do not include URIs in the manifest, because some JVMs do not like that + final DevModeContext devModeContext = new DevModeContext(); + for (Map.Entry e : System.getProperties().entrySet()) { + devModeContext.getSystemProperties().put(e.getKey().toString(), (String) e.getValue()); + } + devModeContext.setProjectDir(projectDir); + devModeContext.getBuildSystemProperties().putAll(buildSystemProperties); + + // this is a minor hack to allow ApplicationConfig to be populated with defaults + devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.application.name", applicationName); + devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.application.version", applicationVersion); + + devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.live-reload.ignore-module-info", "true"); + + devModeContext.setSourceEncoding(sourceEncoding); + devModeContext.setCompilerOptions(compilerOptions); + + if (compilerPluginArtifacts != null) { + devModeContext.setCompilerPluginArtifacts(compilerPluginArtifacts); + } + if (compilerPluginOptions != null) { + devModeContext.setCompilerPluginsOptions(compilerPluginOptions); + } + if (processorPaths != null) { + devModeContext.setAnnotationProcessorPaths(processorPaths); + } + if (processors != null) { + devModeContext.setAnnotationProcessors(processors); + } + + devModeContext.setReleaseJavaVersion(releaseJavaVersion); + devModeContext.setSourceJavaVersion(sourceJavaVersion); + devModeContext.setTargetJvmVersion(targetJavaVersion); + devModeContext.getLocalArtifacts().addAll(localArtifacts); + devModeContext.setApplicationRoot(main); + devModeContext.getAdditionalModules().addAll(dependencies); + + devModeContext.setBaseName(baseName); + devModeContext.setCacheDir(new File(buildDir, "transformer-cache").getAbsoluteFile()); + + if (entryPointCustomizer != null) { + entryPointCustomizer.accept(devModeContext); + } + + // if the --enable-preview flag was set, then we need to enable it when launching dev mode as well + if (devModeContext.isEnablePreview()) { + System.out.println("ADDING enable-preview"); + jvmOptionsBuilder.add("enable-preview"); + } + + setJvmOptions(); + args.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); + + outputDir.mkdirs(); + + args.add("-jar"); + args.add(createDevJar(devModeContext).getAbsolutePath()); + if (applicationArgs != null) { + args.addAll(Arrays.asList(CommandLineUtil.translateCommandline(applicationArgs))); + } + + return new DevModeCommandLine(args, actualDebugPort, buildFiles); + } + + private File createDevJar(DevModeContext devModeContext) throws IOException { + + final File tempFile = new File(buildDir, applicationName + "-dev.jar"); + tempFile.delete(); + // Only delete the -dev.jar on exit if requested + if (deleteDevJar) { + tempFile.deleteOnExit(); + } + log.debugf("Executable jar: %s", tempFile.getAbsolutePath()); + + // this is the jar file we will use to launch the dev mode main class + devModeContext.setDevModeRunnerJarFile(tempFile); + + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tempFile))) { + out.putNextEntry(new ZipEntry("META-INF/")); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + + final StringBuilder classPathManifest = new StringBuilder(); + classpath.values().forEach(file -> { + final URI uri = file.toPath().toAbsolutePath().toUri(); + classPathManifest.append(uri).append(" "); + }); + + manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, classPathManifest.toString()); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, DevModeMain.class.getName()); + out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + manifest.write(out); + + out.putNextEntry(new ZipEntry(DevModeMain.DEV_MODE_CONTEXT)); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ObjectOutputStream obj = new ObjectOutputStream(new DataOutputStream(bytes)); + obj.writeObject(devModeContext); + obj.close(); + out.write(bytes.toByteArray()); + } + return tempFile; + } + + private void setJvmOptions() throws Exception { + final Map> lockedJvmOptions = addExtensionJvmOptions(); + if (isDisableC2(lockedJvmOptions)) { + // prevent C2 compiler for kicking in - makes startup a little faster + // it only makes sense in dev-mode but it is not available when GraalVM is used as the JDK + args.add("-XX:" + TIERED_STOP_AT_LEVEL + "=1"); + } + + List extensionsDisablingDebug; + if (debug == null && (extensionsDisablingDebug = lockedJvmOptions.get(AGENTLIB_JDWP)) != null) { + extensionsDisablingDebugWarning(extensionsDisablingDebug); + } else { + configureDebugging(); + } + + for (var jvmOption : jvmOptionsBuilder.build()) { + if (forceC2 != null && jvmOption.getName().equals(TIERED_STOP_AT_LEVEL)) { + continue; + } + args.addAll(jvmOption.toCliOptions()); + } + } + + /** + * Checks user and extension config options to decide whether to disable C2. + *

+ * By default, the C2 compiler is disabled for dev mode to make startup a little faster. + * It only makes sense in dev-mode but it is not available when GraalVM is used as the JDK. + * + * @param lockedJvmOptions JVM option locked by extensions + * @return whether to disable the C2 compiler + */ + private boolean isDisableC2(Map> lockedJvmOptions) { + // a user's choice + if (forceC2 != null) { + return !forceC2; + } + // an extension configured it + if (jvmOptionsBuilder.contains(TIERED_STOP_AT_LEVEL)) { + return false; + } + // if it's locked, don't set it + final List lockingExtensions = lockedJvmOptions.get(TIERED_STOP_AT_LEVEL); + if (lockingExtensions != null) { + extensionsEnableC2Warning(lockingExtensions); + return false; + } + return !JavaVersionUtil.isGraalvmJdk(); + } + + private void configureDebugging() throws Exception { + if (suspend != null) { + switch (suspend.toLowerCase(Locale.ENGLISH)) { + case "n": + case "false": { + suspend = "n"; + break; + } + case "y": + case "true": { + suspend = "y"; + break; + } + default: { + log.warn("Ignoring invalid value \"" + suspend + "\" for \"suspend\" param and defaulting to \"n\""); + suspend = "n"; + break; + } + } + } else { + suspend = "n"; + } + + int port = 5005; + + if (debugPort != null && validPort.matcher(debugPort).matches()) { + port = Integer.parseInt(debugPort); + } + if (debug != null) { + if (!validDebug.matcher(debug).matches()) { + throw new Exception( + "Invalid value for debug parameter: " + debug + " must be true|false|client|{port}"); + } + if (validPort.matcher(debug).matches()) { + port = Integer.parseInt(debug); + } + } + int originalPort = port; + if (port <= 0) { + port = getRandomPort(); + } + + if (debug != null && debug.equalsIgnoreCase("client")) { + args.add("-" + AGENTLIB_JDWP + "=transport=dt_socket,address=" + debugHost + ":" + port + ",server=n,suspend=" + + suspend); + actualDebugPort = String.valueOf(port); + } else if (debug == null || !debug.equalsIgnoreCase("false")) { + // if the debug port is used, we want to make an effort to pick another one + // if we can't find an open port, we don't fail the process launch, we just don't enable debugging + // Furthermore, we don't check this on restarts, as the previous process is still running + boolean warnAboutChange = false; + if (actualDebugPort == null) { + int tries = 0; + while (true) { + boolean isPortUsed; + try (Socket socket = new Socket(getInetAddress(debugHost), port)) { + // we can make a connection, that means the port is in use + isPortUsed = true; + warnAboutChange = warnAboutChange || (originalPort != 0); // we only want to warn if the user had not configured a random port + } catch (IOException e) { + // no connection made, so the port is not in use + isPortUsed = false; + } + if (!isPortUsed) { + actualDebugPort = String.valueOf(port); + break; + } + if (++tries >= 5) { + break; + } else { + port = getRandomPort(); + } + } + } + if (actualDebugPort != null) { + if (warnAboutChange) { + log.warn("Changed debug port to " + actualDebugPort + " because of a port conflict"); + } + args.add("-" + AGENTLIB_JDWP + "=transport=dt_socket,address=" + debugHost + ":" + port + ",server=y,suspend=" + + suspend); + } else { + log.error("Port " + port + " in use, not starting in debug mode"); + } + } + } + + /** + * Add JVM arguments configured by extensions. + */ + private Map> addExtensionJvmOptions() { + if (extDevModeConfig == null || extDevModeJvmOptionFilter != null && extDevModeJvmOptionFilter.isDisableAll()) { + return Map.of(); + } + Map> mergedLockedOptions = Map.of(); + for (var extDevConfig : extDevModeConfig) { + if (extDevModeJvmOptionFilter != null && extDevModeJvmOptionFilter.isDisabled(extDevConfig.getExtensionKey())) { + log.debugf("Skipped JVM options from %s", extDevConfig.getExtensionKey()); + continue; + } + final JvmOptions jvmOptions = extDevConfig.getJvmOptions(); + if (jvmOptions != null && !jvmOptions.isEmpty()) { + jvmOptionsBuilder.addAll(jvmOptions); + if (log.isDebugEnabled()) { + log.debugf("Adding JVM options from %s", extDevConfig.getExtensionKey()); + for (var arg : jvmOptions.asCollection()) { + log.debug(" " + arg.getName() + ": " + arg.getValues()); + } + } + } + if (!extDevConfig.getLockJvmOptions().isEmpty()) { + mergedLockedOptions = collectLockedOptions(mergedLockedOptions, extDevConfig); + } + } + return mergedLockedOptions; + } + + private static Map> collectLockedOptions(Map> allLockedOptions, + ExtensionDevModeConfig extDevConfig) { + final Collection extLockedOptions = extDevConfig.getLockJvmOptions(); + if (allLockedOptions.isEmpty()) { + allLockedOptions = new HashMap<>(extLockedOptions.size()); + } + for (var option : extLockedOptions) { + allLockedOptions.computeIfAbsent(option, k -> new ArrayList<>(1)).add(extDevConfig.getExtensionKey()); + } + log.debugf("%s locks JVM options %s", extDevConfig.getExtensionKey(), extLockedOptions); + return allLockedOptions; + } + + private int getRandomPort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private InetAddress getInetAddress(String host) throws UnknownHostException { + if ("localhost".equals(host)) { + return InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }); + } + return InetAddress.getByName(host); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java index 6d0f4c107d9a6..cadabb28826f6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java @@ -32,7 +32,7 @@ public class DevModeContext implements Serializable { public static final CompilationUnit EMPTY_COMPILATION_UNIT = new CompilationUnit(PathList.of(), null, null, null, null); - public static final String ENABLE_PREVIEW_FLAG = "--enable-preview"; + private static final String ENABLE_PREVIEW_FLAG = "--enable-preview"; private ModuleInfo applicationRoot; private final List additionalModules = new ArrayList<>(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/ExtensionDevModeJvmOptionFilter.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/ExtensionDevModeJvmOptionFilter.java new file mode 100644 index 0000000000000..4349679f3021f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/ExtensionDevModeJvmOptionFilter.java @@ -0,0 +1,65 @@ +package io.quarkus.deployment.dev; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.maven.dependency.ArtifactCoordsPattern; +import io.quarkus.maven.dependency.ArtifactKey; + +/** + * Extension Dev mode JVM argument filter configuration + */ +public class ExtensionDevModeJvmOptionFilter { + + private boolean disableAll; + + private List disableFor = List.of(); + private List disableForPatterns; + + public boolean isDisableAll() { + return disableAll; + } + + public void setDisableAll(boolean disableAll) { + this.disableAll = disableAll; + resetPatterns(); + } + + public List getDisableFor() { + return disableFor; + } + + public void setDisableFor(List disableFor) { + this.disableFor = disableFor; + resetPatterns(); + } + + private void resetPatterns() { + disableForPatterns = null; + } + + List getDisableForPatterns() { + if (disableFor.isEmpty()) { + return List.of(); + } + if (disableForPatterns == null) { + var result = new ArrayList(disableFor.size()); + for (var s : disableFor) { + result.add(ArtifactCoordsPattern.of(s)); + } + disableForPatterns = result; + } + return disableForPatterns; + } + + boolean isDisabled(ArtifactKey extensionKey) { + for (var pattern : getDisableForPatterns()) { + if (pattern.matches(extensionKey.getGroupId(), extensionKey.getArtifactId(), extensionKey.getClassifier(), "jar", + null)) { + return true; + } + } + return false; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java deleted file mode 100644 index 4f99d00ef5eb6..0000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java +++ /dev/null @@ -1,576 +0,0 @@ -package io.quarkus.deployment.dev; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URI; -import java.net.UnknownHostException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.jar.Attributes; -import java.util.jar.Manifest; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import org.jboss.logging.Logger; - -import io.quarkus.bootstrap.app.QuarkusBootstrap; -import io.quarkus.deployment.dev.DevModeContext.ModuleInfo; -import io.quarkus.deployment.util.CommandLineUtil; -import io.quarkus.maven.dependency.ArtifactKey; -import io.quarkus.runtime.logging.JBossVersion; -import io.quarkus.runtime.util.JavaVersionUtil; -import io.quarkus.utilities.JavaBinFinder; - -public abstract class QuarkusDevModeLauncher { - final Pattern validDebug = Pattern.compile("^(true|false|client|[0-9]+)$"); - final Pattern validPort = Pattern.compile("^-?[0-9]+$"); - - public class Builder> { - - protected Builder(String java) { - args = new ArrayList<>(); - final String javaTool = java == null ? JavaBinFinder.findBin() : java; - QuarkusDevModeLauncher.this.debug("Using javaTool: %s", javaTool); - args.add(javaTool); - - } - - @SuppressWarnings("unchecked") - public B preventnoverify(boolean preventnoverify) { - if (!preventnoverify) { - // in Java 13 and up, preventing verification is deprecated - see https://bugs.openjdk.java.net/browse/JDK-8218003 - // this test isn't absolutely correct in the sense that depending on the user setup, the actual Java binary - // that is used might be different that the one running Maven, but given how small of an impact this has - // it's probably better than running an extra command on 'javaTool' just to figure out the version - if (!JavaVersionUtil.isJava13OrHigher()) { - args.add("-Xverify:none"); - } - } - return (B) this; - } - - @SuppressWarnings("unchecked") - public B forceC2(boolean force) { - forceC2 = force; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B jvmArgs(String jvmArgs) { - args.add(jvmArgs); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B jvmArgs(List jvmArgs) { - args.addAll(jvmArgs); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B debug(String debug) { - QuarkusDevModeLauncher.this.debug = debug; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B suspend(String suspend) { - QuarkusDevModeLauncher.this.suspend = suspend; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B projectDir(File projectDir) { - QuarkusDevModeLauncher.this.projectDir = projectDir; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B buildDir(File buildDir) { - QuarkusDevModeLauncher.this.buildDir = buildDir; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B outputDir(File outputDir) { - QuarkusDevModeLauncher.this.outputDir = outputDir; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B buildSystemProperties(Map buildSystemProperties) { - QuarkusDevModeLauncher.this.buildSystemProperties = buildSystemProperties; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B buildSystemProperty(String name, String value) { - QuarkusDevModeLauncher.this.buildSystemProperties.put(name, value); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B applicationName(String appName) { - QuarkusDevModeLauncher.this.applicationName = appName; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B applicationVersion(String appVersion) { - QuarkusDevModeLauncher.this.applicationVersion = appVersion; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B applicationArgs(String appArgs) { - QuarkusDevModeLauncher.this.applicationArgs = appArgs; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B sourceEncoding(String srcEncoding) { - QuarkusDevModeLauncher.this.sourceEncoding = srcEncoding; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B compilerOptions(String name, List options) { - compilerOptions.compute(name, (key, value) -> { - if (value == null) { - return new HashSet<>(options); - } - value.addAll(options); - return value; - }); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B compilerOptions(Map> options) { - compilerOptions.putAll(options); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B compilerPluginArtifacts(List artifacts) { - compilerPluginArtifacts = artifacts; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B compilerPluginOptions(List options) { - compilerPluginOptions = options; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B annotationProcessorPaths(Set processorPaths) { - QuarkusDevModeLauncher.this.processorPaths = processorPaths; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B annotationProcessors(List processors) { - QuarkusDevModeLauncher.this.processors = processors; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B releaseJavaVersion(String releaseJavaVersion) { - QuarkusDevModeLauncher.this.releaseJavaVersion = releaseJavaVersion; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B sourceJavaVersion(String sourceJavaVersion) { - QuarkusDevModeLauncher.this.sourceJavaVersion = sourceJavaVersion; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B targetJavaVersion(String targetJavaVersion) { - QuarkusDevModeLauncher.this.targetJavaVersion = targetJavaVersion; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B watchedBuildFile(Path buildFile) { - QuarkusDevModeLauncher.this.buildFiles.add(buildFile); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B deleteDevJar(boolean deleteDevJar) { - QuarkusDevModeLauncher.this.deleteDevJar = deleteDevJar; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B baseName(String baseName) { - QuarkusDevModeLauncher.this.baseName = baseName; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B remoteDev(boolean remoteDev) { - QuarkusDevModeLauncher.this.entryPointCustomizer = new Consumer() { - @Override - public void accept(DevModeContext devModeContext) { - devModeContext.setMode(QuarkusBootstrap.Mode.REMOTE_DEV_CLIENT); - devModeContext.setAlternateEntryPoint(IsolatedRemoteDevModeMain.class.getName()); - } - }; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B entryPointCustomizer(Consumer consumer) { - QuarkusDevModeLauncher.this.entryPointCustomizer = consumer; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B localArtifact(ArtifactKey localArtifact) { - localArtifacts.add(localArtifact); - return (B) this; - } - - public boolean isLocal(ArtifactKey artifact) { - return localArtifacts.contains(artifact); - } - - @SuppressWarnings("unchecked") - public B mainModule(ModuleInfo mainModule) { - main = mainModule; - return (B) this; - } - - @SuppressWarnings("unchecked") - public B dependency(ModuleInfo module) { - dependencies.add(module); - return (B) this; - } - - @SuppressWarnings("unchecked") - public B classpathEntry(ArtifactKey key, File f) { - final File prev = classpath.put(key, f); - if (prev != null && !f.equals(prev)) { - Logger.getLogger(getClass()).warn(key + " classpath entry " + prev + " was overriden with " + f); - } - return (B) this; - } - - @SuppressWarnings("unchecked") - public B debugHost(String host) { - if ((null != host) && !host.isEmpty()) { - QuarkusDevModeLauncher.this.debugHost = host; - } - return (B) this; - } - - @SuppressWarnings("unchecked") - public B debugPort(String port) { - if ((null != port) && !port.isEmpty()) { - QuarkusDevModeLauncher.this.debugPort = port; - } - return (B) this; - } - - @SuppressWarnings("unchecked") - public R build() throws Exception { - prepare(); - return (R) QuarkusDevModeLauncher.this; - } - } - - private List args = new ArrayList<>(0); - private String debug; - private String suspend; - private String debugHost = "localhost"; - private String debugPort = "5005"; - private String actualDebugPort; - private File projectDir; - private File buildDir; - private File outputDir; - private Map buildSystemProperties = new HashMap<>(0); - private String applicationName; - private String applicationVersion; - private String sourceEncoding; - private Map> compilerOptions = new HashMap<>(1); - private List compilerPluginArtifacts; - private List compilerPluginOptions; - private String releaseJavaVersion; - private String sourceJavaVersion; - private String targetJavaVersion; - private Set buildFiles = new HashSet<>(0); - private boolean deleteDevJar = true; - private boolean forceC2 = false; - private String baseName; - private Consumer entryPointCustomizer; - private String applicationArgs; - private Set localArtifacts = new HashSet<>(); - private ModuleInfo main; - private List dependencies = new ArrayList<>(0); - private LinkedHashMap classpath = new LinkedHashMap<>(); - private Set processorPaths; - private List processors; - - protected QuarkusDevModeLauncher() { - } - - /** - * Attempts to prepare the dev mode runner. - */ - protected void prepare() throws Exception { - JBossVersion.disableVersionLogging(); - - if (!JavaVersionUtil.isGraalvmJdk() && !forceC2) { - // prevent C2 compiler for kicking in - makes startup a little faster - // it only makes sense in dev-mode but it is not available when GraalVM is used as the JDK - args.add("-XX:TieredStopAtLevel=1"); - } - - if (suspend != null) { - switch (suspend.toLowerCase(Locale.ENGLISH)) { - case "n": - case "false": { - suspend = "n"; - break; - } - case "y": - case "true": { - suspend = "y"; - break; - } - default: { - warn("Ignoring invalid value \"" + suspend + "\" for \"suspend\" param and defaulting to \"n\""); - suspend = "n"; - break; - } - } - } else { - suspend = "n"; - } - - int port = 5005; - - if (debugPort != null && validPort.matcher(debugPort).matches()) { - port = Integer.parseInt(debugPort); - } - if (debug != null) { - if (!validDebug.matcher(debug).matches()) { - throw new Exception( - "Invalid value for debug parameter: " + debug + " must be true|false|client|{port}"); - } - if (validPort.matcher(debug).matches()) { - port = Integer.parseInt(debug); - } - } - int originalPort = port; - if (port <= 0) { - port = getRandomPort(); - } - - if (debug != null && debug.equalsIgnoreCase("client")) { - args.add("-agentlib:jdwp=transport=dt_socket,address=" + debugHost + ":" + port + ",server=n,suspend=" + suspend); - actualDebugPort = String.valueOf(port); - } else if (debug == null || !debug.equalsIgnoreCase("false")) { - // if the debug port is used, we want to make an effort to pick another one - // if we can't find an open port, we don't fail the process launch, we just don't enable debugging - // Furthermore, we don't check this on restarts, as the previous process is still running - boolean warnAboutChange = false; - if (actualDebugPort == null) { - int tries = 0; - while (true) { - boolean isPortUsed; - try (Socket socket = new Socket(getInetAddress(debugHost), port)) { - // we can make a connection, that means the port is in use - isPortUsed = true; - warnAboutChange = warnAboutChange || (originalPort != 0); // we only want to warn if the user had not configured a random port - } catch (IOException e) { - // no connection made, so the port is not in use - isPortUsed = false; - } - if (!isPortUsed) { - actualDebugPort = String.valueOf(port); - break; - } - if (++tries >= 5) { - break; - } else { - port = getRandomPort(); - } - } - } - if (actualDebugPort != null) { - if (warnAboutChange) { - warn("Changed debug port to " + actualDebugPort + " because of a port conflict"); - } - args.add("-agentlib:jdwp=transport=dt_socket,address=" + debugHost + ":" + port + ",server=y,suspend=" - + suspend); - } else { - error("Port " + port + " in use, not starting in debug mode"); - } - } - - //build a class-path string for the base platform - //this stuff does not change - // Do not include URIs in the manifest, because some JVMs do not like that - StringBuilder classPathManifest = new StringBuilder(); - final DevModeContext devModeContext = new DevModeContext(); - for (Map.Entry e : System.getProperties().entrySet()) { - devModeContext.getSystemProperties().put(e.getKey().toString(), (String) e.getValue()); - } - devModeContext.setProjectDir(projectDir); - devModeContext.getBuildSystemProperties().putAll(buildSystemProperties); - - // this is a minor hack to allow ApplicationConfig to be populated with defaults - devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.application.name", applicationName); - devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.application.version", applicationVersion); - - devModeContext.getBuildSystemProperties().putIfAbsent("quarkus.live-reload.ignore-module-info", "true"); - - devModeContext.setSourceEncoding(sourceEncoding); - devModeContext.setCompilerOptions(compilerOptions); - - if (compilerPluginArtifacts != null) { - devModeContext.setCompilerPluginArtifacts(compilerPluginArtifacts); - } - if (compilerPluginOptions != null) { - devModeContext.setCompilerPluginsOptions(compilerPluginOptions); - } - if (processorPaths != null) { - devModeContext.setAnnotationProcessorPaths(processorPaths); - } - if (processors != null) { - devModeContext.setAnnotationProcessors(processors); - } - - devModeContext.setReleaseJavaVersion(releaseJavaVersion); - devModeContext.setSourceJavaVersion(sourceJavaVersion); - devModeContext.setTargetJvmVersion(targetJavaVersion); - devModeContext.getLocalArtifacts().addAll(localArtifacts); - devModeContext.setApplicationRoot(main); - devModeContext.getAdditionalModules().addAll(dependencies); - - args.add("-Djava.util.logging.manager=org.jboss.logmanager.LogManager"); - - File tempFile = new File(buildDir, applicationName + "-dev.jar"); - tempFile.delete(); - // Only delete the -dev.jar on exit if requested - if (deleteDevJar) { - tempFile.deleteOnExit(); - } - debug("Executable jar: %s", tempFile.getAbsolutePath()); - - devModeContext.setBaseName(baseName); - devModeContext.setCacheDir(new File(buildDir, "transformer-cache").getAbsoluteFile()); - - // this is the jar file we will use to launch the dev mode main class - devModeContext.setDevModeRunnerJarFile(tempFile); - - if (entryPointCustomizer != null) { - entryPointCustomizer.accept(devModeContext); - } - - try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tempFile))) { - out.putNextEntry(new ZipEntry("META-INF/")); - Manifest manifest = new Manifest(); - manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); - - classpath.values().forEach(file -> { - final URI uri = file.toPath().toAbsolutePath().toUri(); - classPathManifest.append(uri).append(" "); - }); - - manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, classPathManifest.toString()); - manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, DevModeMain.class.getName()); - out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); - manifest.write(out); - - out.putNextEntry(new ZipEntry(DevModeMain.DEV_MODE_CONTEXT)); - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - ObjectOutputStream obj = new ObjectOutputStream(new DataOutputStream(bytes)); - obj.writeObject(devModeContext); - obj.close(); - out.write(bytes.toByteArray()); - } - - outputDir.mkdirs(); - // if the --enable-preview flag was set, then we need to enable it when launching dev mode as well - if (devModeContext.isEnablePreview()) { - args.add(DevModeContext.ENABLE_PREVIEW_FLAG); - } - - args.add("-jar"); - args.add(tempFile.getAbsolutePath()); - if (applicationArgs != null) { - args.addAll(Arrays.asList(CommandLineUtil.translateCommandline(applicationArgs))); - } - } - - private int getRandomPort() throws IOException { - try (ServerSocket socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } - } - - private InetAddress getInetAddress(String host) throws UnknownHostException { - if ("localhost".equals(host)) { - return InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }); - } - return InetAddress.getByName(host); - } - - public Collection watchedBuildFiles() { - return buildFiles; - } - - public List args() { - return args; - } - - public String getActualDebugPort() { - return actualDebugPort; - } - - protected abstract boolean isDebugEnabled(); - - protected void debug(Object msg, Object... args) { - if (!isDebugEnabled()) { - return; - } - if (msg == null) { - return; - } - if (args.length == 0) { - debug(msg); - return; - } - debug(String.format(msg.toString(), args)); - } - - protected abstract void debug(Object msg); - - protected abstract void error(Object msg); - - protected abstract void warn(Object msg); -} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/dev/DevModeCommandLineBuilderTest.java b/core/deployment/src/test/java/io/quarkus/deployment/dev/DevModeCommandLineBuilderTest.java new file mode 100644 index 0000000000000..a741565671b40 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/dev/DevModeCommandLineBuilderTest.java @@ -0,0 +1,244 @@ +package io.quarkus.deployment.dev; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import io.quarkus.bootstrap.model.ExtensionDevModeConfig; +import io.quarkus.bootstrap.model.JvmOptions; +import io.quarkus.maven.dependency.ArtifactKey; + +public class DevModeCommandLineBuilderTest { + + @TempDir + File outputDir; + + @Test + public void extensionSetsJvmOptionWoValue() throws Exception { + + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .add("enable-preview") + .build(), + Set.of()); + + var args = getCliArguments(acmeMagic); + System.out.println(args); + assertThat(args).contains("--enable-preview"); + } + + @Test + public void extensionSetsJvmOptionWithValue() throws Exception { + + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .add("enable-native-access", "ALL-UNNAMED") + .build(), + Set.of()); + + var args = getCliArguments(acmeMagic); + assertThat(args).contains("--enable-native-access=ALL-UNNAMED"); + } + + @Test + public void extensionAddModules() throws Exception { + + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .add("add-modules", "jdk.incubator.vector") + .addAll("add-modules", List.of("jdk.incubator.vector", "java.management")) + .build(), + Set.of()); + + var args = getCliArguments(acmeMagic); + assertThat(args).contains("--add-modules=java.management,jdk.incubator.vector"); + } + + @Test + public void extensionAddOpens() throws Exception { + + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .add("add-opens", "java.base/java.util=ALL-UNNAMED") + .add("add-opens", "java.base/java.io=ALL-UNNAMED") + .add("add-opens", "java.base/java.nio=ALL-UNNAMED") + .build(), + Set.of()); + + var args = getCliArguments(acmeMagic); + assertThat(args).containsSequence("--add-opens", "java.base/java.io=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.nio=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.util=ALL-UNNAMED"); + } + + @Test + public void extensionEnablingC2() throws Exception { + + var args = getCliArguments(); + // C2 is disabled by default + assertThat(args).contains("-XX:TieredStopAtLevel=1"); + + // extension locking the default value of TieredStopAtLevel + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder().build(), + Set.of("TieredStopAtLevel")); + args = getCliArguments(acmeMagic); + assertThat(args).doesNotContain("-XX:TieredStopAtLevel=1"); + + // user disabling C2 explicitly + args = getCliBuilder(acmeMagic).forceC2(false).build().getArguments(); + assertThat(args).contains("-XX:TieredStopAtLevel=1"); + } + + @Test + public void extensionDisablingDebugMode() throws Exception { + + final String agentlibJdwpArg = "-agentlib:jdwp=transport=dt_socket,address=localhost:5005,server=y,suspend=n"; + + // in case the debug option is not set, it's expected to be enabled + var args = getCliBuilder().debug(null).build().getArguments(); + assertThat(args).contains(agentlibJdwpArg); + + // extension locking the default value of -agentlib:jdwp + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder().build(), + Set.of("agentlib:jdwp")); + args = getCliBuilder(acmeMagic).debug(null).build().getArguments(); + assertThat(args).doesNotContain(agentlibJdwpArg); + + // user explicitly enables debug + args = getCliBuilder(acmeMagic).debug("true").build().getArguments(); + assertThat(args).contains(agentlibJdwpArg); + } + + @Test + public void extensionSetsXxBooleanOption() throws Exception { + + // false + var args = getCliArguments(new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .addXxOption("UseThreadPriorities", "false") + .build(), + Set.of())); + assertThat(args).contains("-XX:-UseThreadPriorities"); + + // true + args = getCliArguments(new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .addXxOption("UseThreadPriorities", "true") + .build(), + Set.of())); + assertThat(args).contains("-XX:+UseThreadPriorities"); + } + + @Test + public void extensionSetsXxNumericOption() throws Exception { + + var args = getCliArguments(new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .addXxOption("AllocatePrefetchStyle", "1") + .build(), + Set.of())); + assertThat(args).contains("-XX:AllocatePrefetchStyle=1"); + } + + @Test + public void disableAllExtensionArgs() throws Exception { + + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .add("add-opens", "java.base/java.io=ALL-UNNAMED") + .add("add-opens", "java.base/java.nio=ALL-UNNAMED") + .build(), + Set.of("TieredStopAtLevel")); + + final ExtensionDevModeConfig otherMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.other:other-magic"), + JvmOptions.builder() + .add("add-opens", "java.base/java.util=ALL-UNNAMED") + .add("add-opens", "java.base/java.io=ALL-UNNAMED") + .build(), + Set.of()); + + // all enabled + var args = getCliArguments(acmeMagic, otherMagic); + assertThat(args).containsSequence("--add-opens", "java.base/java.io=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.nio=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.util=ALL-UNNAMED"); + assertThat(args).doesNotContain("-XX:TieredStopAtLevel=1"); + + // all disabled + final ExtensionDevModeJvmOptionFilter filter = new ExtensionDevModeJvmOptionFilter(); + filter.setDisableAll(true); + args = getCliBuilder(acmeMagic, otherMagic) + .extensionDevModeJvmOptionFilter(filter) + .build().getArguments(); + assertThat(args).doesNotContain("--add-opens"); + assertThat(args).contains("-XX:TieredStopAtLevel=1"); + } + + @Test + public void disableParticularExtensionArgs() throws Exception { + + final ExtensionDevModeConfig acmeMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.acme:acme-magic"), + JvmOptions.builder() + .add("add-opens", "java.base/java.io=ALL-UNNAMED") + .add("add-opens", "java.base/java.nio=ALL-UNNAMED") + .build(), + Set.of("TieredStopAtLevel")); + + final ExtensionDevModeConfig otherMagic = new ExtensionDevModeConfig( + ArtifactKey.fromString("org.other:other-magic"), + JvmOptions.builder() + .add("add-opens", "java.base/java.util=ALL-UNNAMED") + .add("add-opens", "java.base/java.io=ALL-UNNAMED") + .build(), + Set.of()); + + // all enabled + var args = getCliArguments(acmeMagic, otherMagic); + assertThat(args).containsSequence("--add-opens", "java.base/java.io=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.nio=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.util=ALL-UNNAMED"); + assertThat(args).doesNotContain("-XX:TieredStopAtLevel=1"); + + // all disabled + final ExtensionDevModeJvmOptionFilter filter = new ExtensionDevModeJvmOptionFilter(); + filter.setDisableFor(List.of("org.acme:acme-magic")); + args = getCliBuilder(acmeMagic, otherMagic) + .extensionDevModeJvmOptionFilter(filter) + .build().getArguments(); + assertThat(args).containsSequence("--add-opens", "java.base/java.io=ALL-UNNAMED"); + assertThat(args).containsSequence("--add-opens", "java.base/java.util=ALL-UNNAMED"); + assertThat(args).contains("-XX:TieredStopAtLevel=1"); + } + + private List getCliArguments(ExtensionDevModeConfig... extensionDevModeConfigs) throws Exception { + return getCliBuilder(extensionDevModeConfigs).build().getArguments(); + } + + private DevModeCommandLineBuilder getCliBuilder(ExtensionDevModeConfig... extensionDevModeConfigs) { + return DevModeCommandLine.builder("java") + .applicationName("test") + .outputDir(outputDir) + .buildDir(outputDir) + .debug("false") // disable debug port check + .extensionDevModeConfig(List.of(extensionDevModeConfigs)); + } +} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/GradleDevModeLauncher.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/GradleDevModeLauncher.java deleted file mode 100644 index afe2f0d7f8cd5..0000000000000 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/GradleDevModeLauncher.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.quarkus.gradle.tasks; - -import org.gradle.api.logging.Logger; - -import io.quarkus.deployment.dev.QuarkusDevModeLauncher; - -public class GradleDevModeLauncher extends QuarkusDevModeLauncher { - - /** - * Initializes the launcher builder - * - * @param logger the logger - * @param java java binary to use - * @return launcher builder - */ - public static Builder builder(Logger logger, String java) { - return new GradleDevModeLauncher(logger).new Builder(java); - } - - public class Builder extends QuarkusDevModeLauncher.Builder { - - private Builder(String java) { - super(java); - } - } - - private final Logger logger; - - private GradleDevModeLauncher(Logger logger) { - this.logger = logger; - } - - @Override - protected boolean isDebugEnabled() { - return logger.isDebugEnabled(); - } - - @Override - protected void debug(Object msg) { - logger.warn(msg == null ? "null" : msg.toString()); - } - - @Override - protected void error(Object msg) { - logger.error(msg == null ? "null" : msg.toString()); - } - - @Override - protected void warn(Object msg) { - logger.warn(msg == null ? "null" : msg.toString()); - } -} diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java index 6d21c80b275b2..c505daef136b9 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusApplicationModelTask.java @@ -609,13 +609,13 @@ private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuild return false; } artifactBuilder.setRuntimeExtensionArtifact(); - final String extensionCoords = artifactBuilder.toGACTVString(); - modelBuilder.handleExtensionProperties(extProps, extensionCoords); + modelBuilder.handleExtensionProperties(extProps, artifactBuilder.getKey()); final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); if (providesCapabilities != null) { modelBuilder - .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + .addExtensionCapabilities( + CapabilityContract.of(artifactBuilder.toGACTVString(), providesCapabilities, null)); } return true; } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index 5cc8b1319ac2f..97c7cc1055b8f 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -67,9 +67,11 @@ import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.deployment.dev.DevModeCommandLine; +import io.quarkus.deployment.dev.DevModeCommandLineBuilder; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.DevModeMain; -import io.quarkus.deployment.dev.QuarkusDevModeLauncher; +import io.quarkus.deployment.dev.ExtensionDevModeJvmOptionFilter; import io.quarkus.gradle.dependency.ApplicationDeploymentClasspathBuilder; import io.quarkus.gradle.dsl.CompilerOption; import io.quarkus.gradle.dsl.CompilerOptions; @@ -88,6 +90,7 @@ public abstract class QuarkusDev extends QuarkusTask { private final SourceSet mainSourceSet; private final CompilerOptions compilerOptions = new CompilerOptions(); + private final ExtensionDevModeJvmOptionFilter extensionJvmOptions = new ExtensionDevModeJvmOptionFilter(); private final Property workingDirectory; private final MapProperty environmentVariables; @@ -130,7 +133,6 @@ public QuarkusDev( preventNoVerify.convention(false); forceC2 = objectFactory.property(Boolean.class); - forceC2.convention(false); shouldPropagateJavaCompilerArgs = objectFactory.property(Boolean.class); shouldPropagateJavaCompilerArgs.convention(true); @@ -227,6 +229,7 @@ public boolean isPreventnoverify() { } @Input + @Optional public Property getForceC2() { return forceC2; } @@ -328,6 +331,18 @@ public QuarkusDev compilerOptions(Action action) { return this; } + @SuppressWarnings("unused") + @Internal + public ExtensionDevModeJvmOptionFilter getExtensionJvmOptions() { + return this.extensionJvmOptions; + } + + @SuppressWarnings("unused") + public QuarkusDev extensionJvmOptions(Action action) { + action.execute(extensionJvmOptions); + return this; + } + @TaskAction public void startDev() { if (!sourcesExist()) { @@ -357,11 +372,11 @@ public void close() throws IOException { }); try { - QuarkusDevModeLauncher runner = newLauncher(analyticsService); + final DevModeCommandLine runner = newLauncher(analyticsService); String outputFile = System.getProperty(IO_QUARKUS_DEVMODE_ARGS); if (outputFile == null) { getProject().exec(action -> { - action.commandLine(runner.args()).workingDir(getWorkingDirectory().get()); + action.commandLine(runner.getArguments()).workingDir(getWorkingDirectory().get()); action.environment(getEnvVars()); action.setStandardInput(System.in) .setErrorOutput(System.out) @@ -369,7 +384,7 @@ public void close() throws IOException { }); } else { try (BufferedWriter is = Files.newBufferedWriter(Paths.get(outputFile))) { - for (String i : runner.args()) { + for (String i : runner.getArguments()) { is.write(i); is.newLine(); } @@ -407,7 +422,7 @@ private boolean classesExist() { return false; } - private QuarkusDevModeLauncher newLauncher(final AnalyticsService analyticsService) throws Exception { + private DevModeCommandLine newLauncher(final AnalyticsService analyticsService) throws Exception { final Project project = getProject(); final JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); @@ -421,9 +436,9 @@ private QuarkusDevModeLauncher newLauncher(final AnalyticsService analyticsServi java = javaLauncher.get().getExecutablePath().getAsFile().getAbsolutePath(); } } - GradleDevModeLauncher.Builder builder = GradleDevModeLauncher.builder(getLogger(), java) + DevModeCommandLineBuilder builder = DevModeCommandLine.builder(java) .preventnoverify(getPreventNoVerify().getOrElse(false)) - .forceC2(getForceC2().getOrElse(false)) + .forceC2(getForceC2().getOrNull()) .projectDir(projectDir) .buildDir(buildDir) .outputDir(buildDir) @@ -441,14 +456,11 @@ private QuarkusDevModeLauncher newLauncher(final AnalyticsService analyticsServi } if (getOpenJavaLang().isPresent() && getOpenJavaLang().get()) { - builder.jvmArgs("--add-opens"); - builder.jvmArgs("java.base/java.lang=ALL-UNNAMED"); + builder.addOpens("java.base/java.lang=ALL-UNNAMED"); } if (getModules().isPresent() && !getModules().get().isEmpty()) { - String mods = String.join(",", getModules().get()); - builder.jvmArgs("--add-modules"); - builder.jvmArgs(mods); + builder.addModules(getModules().get()); } for (Map.Entry e : project.getProperties().entrySet()) { @@ -464,6 +476,8 @@ private QuarkusDevModeLauncher newLauncher(final AnalyticsService analyticsServi builder.sourceEncoding(getSourceEncoding()); final ApplicationModel appModel = extension().getApplicationModel(LaunchMode.DEVELOPMENT); + builder.extensionDevModeConfig(appModel.getExtensionDevModeConfig()) + .extensionDevModeJvmOptionFilter(extensionJvmOptions); analyticsService.sendAnalytics( DEV_MODE, @@ -511,7 +525,6 @@ private QuarkusDevModeLauncher newLauncher(final AnalyticsService analyticsServi SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); builder.annotationProcessorPaths(mainSourceSet.getAnnotationProcessorPath().getFiles()); - // builder.annotationProcessors(SOME); for (CompilerOption compilerOptions : compilerOptions.getCompilerOptions()) { builder.compilerOptions(compilerOptions.getName(), compilerOptions.getArgs()); @@ -545,11 +558,11 @@ private QuarkusDevModeLauncher newLauncher(final AnalyticsService analyticsServi return builder.build(); } - protected void modifyDevModeContext(GradleDevModeLauncher.Builder builder) { + protected void modifyDevModeContext(DevModeCommandLineBuilder builder) { } - private void addQuarkusDevModeDeps(GradleDevModeLauncher.Builder builder, ApplicationModel appModel) { + private void addQuarkusDevModeDeps(DevModeCommandLineBuilder builder, ApplicationModel appModel) { ResolvedDependency coreDeployment = null; for (ResolvedDependency d : appModel.getDependencies()) { @@ -620,7 +633,7 @@ private void addQuarkusDevModeDeps(GradleDevModeLauncher.Builder builder, Applic } } - private void addLocalProject(ResolvedDependency project, GradleDevModeLauncher.Builder builder, Set addeDeps, + private void addLocalProject(ResolvedDependency project, DevModeCommandLineBuilder builder, Set addeDeps, boolean root) { addeDeps.add(project.getKey()); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRemoteDev.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRemoteDev.java index 9891ced032d1c..acdc0ff0f436a 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRemoteDev.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusRemoteDev.java @@ -4,6 +4,7 @@ import org.gradle.api.artifacts.Configuration; +import io.quarkus.deployment.dev.DevModeCommandLineBuilder; import io.quarkus.gradle.extension.QuarkusPluginExtension; public abstract class QuarkusRemoteDev extends QuarkusDev { @@ -16,7 +17,7 @@ public QuarkusRemoteDev(Configuration quarkusDevConfiguration, QuarkusPluginExte extension); } - protected void modifyDevModeContext(GradleDevModeLauncher.Builder builder) { + protected void modifyDevModeContext(DevModeCommandLineBuilder builder) { builder.remoteDev(true); } } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTest.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTest.java index 1c3632fb68e7a..8f46fb02fff54 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTest.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusTest.java @@ -6,6 +6,7 @@ import org.gradle.api.artifacts.Configuration; +import io.quarkus.deployment.dev.DevModeCommandLineBuilder; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.IsolatedTestModeMain; import io.quarkus.gradle.extension.QuarkusPluginExtension; @@ -20,7 +21,7 @@ public QuarkusTest(Configuration quarkusDevConfiguration, QuarkusPluginExtension extension); } - protected void modifyDevModeContext(GradleDevModeLauncher.Builder builder) { + protected void modifyDevModeContext(DevModeCommandLineBuilder builder) { builder.entryPointCustomizer(new Consumer() { @Override public void accept(DevModeContext devModeContext) { diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java index 204c6e2664153..da2b33824d86f 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -489,13 +489,13 @@ private static boolean processQuarkusDir(ResolvedDependencyBuilder artifactBuild return false; } artifactBuilder.setRuntimeExtensionArtifact(); - final String extensionCoords = artifactBuilder.toGACTVString(); - modelBuilder.handleExtensionProperties(extProps, extensionCoords); + modelBuilder.handleExtensionProperties(extProps, artifactBuilder.getKey()); final String providesCapabilities = extProps.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); if (providesCapabilities != null) { modelBuilder - .addExtensionCapabilities(CapabilityContract.of(extensionCoords, providesCapabilities, null)); + .addExtensionCapabilities( + CapabilityContract.of(artifactBuilder.toGACTVString(), providesCapabilities, null)); } return true; } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index f605744e82af5..f3ec06c6cd83b 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -102,10 +102,11 @@ import io.quarkus.bootstrap.util.BootstrapUtils; import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.deployment.dev.DevModeCommandLine; +import io.quarkus.deployment.dev.DevModeCommandLineBuilder; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.DevModeMain; -import io.quarkus.deployment.dev.QuarkusDevModeLauncher; -import io.quarkus.maven.MavenDevModeLauncher.Builder; +import io.quarkus.deployment.dev.ExtensionDevModeJvmOptionFilter; import io.quarkus.maven.components.CompilerOptions; import io.quarkus.maven.components.MavenVersionEnforcer; import io.quarkus.maven.components.QuarkusWorkspaceProvider; @@ -302,8 +303,8 @@ public class DevMojo extends AbstractMojo { * Setting this will likely have a small negative effect on startup time and should only be done when it absolutely * makes sense. */ - @Parameter(defaultValue = "${forceC2}") - private boolean forceC2 = false; + @Parameter(property = "forceC2") + private Boolean forceC2; /** * Whether changes in the projects that appear to be dependencies of the project containing the application to be launched @@ -386,6 +387,35 @@ public class DevMojo extends AbstractMojo { @Parameter(defaultValue = "org.codehaus.mojo:flatten-maven-plugin") Set skipPlugins; + /** + * Extension dev mode JVM option filter configuration. + *

+ * Allows disabling all JVM options configured by extensions, for example + * + *

{@code
+     *     
+     *         
+     *         true
+     *     *
+     * }
+ * + * or specifying a {@code groupId:artifactId:classifier} artifact pattern + * to disable options provided by the matching subset of extensions, for example + * + *
{@code
+     *     
+     *         
+     *             
+     *             org.acme
+     *             
+     *             io.quarkiverse:quarkus-magic
+     *         
+     *     
+     * }
+ */ + @Parameter + ExtensionDevModeJvmOptionFilter extensionJvmOptions; + /** * console attributes, used to restore the console state */ @@ -499,7 +529,7 @@ public void close() throws IOException { final DevModeRunner newRunner; try { bootstrapId = handleAutoCompile(); - newRunner = new DevModeRunner(runner.launcher.getActualDebugPort(), bootstrapId); + newRunner = new DevModeRunner(runner.commandLine.getDebugPort(), bootstrapId); } catch (Exception e) { getLog().info("Could not load changed pom.xml file, changes not applied", e); continue; @@ -1016,7 +1046,7 @@ private String getSourceEncoding() { return null; } - private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency module, boolean root) throws Exception { + private void addProject(DevModeCommandLineBuilder builder, ResolvedDependency module, boolean root) throws Exception { if (!module.isJar()) { return; } @@ -1181,19 +1211,19 @@ private void addProject(MavenDevModeLauncher.Builder builder, ResolvedDependency private class DevModeRunner { - final QuarkusDevModeLauncher launcher; + final DevModeCommandLine commandLine; private Process process; private DevModeRunner(String bootstrapId) throws Exception { - launcher = newLauncher(null, bootstrapId); + commandLine = newLauncher(null, bootstrapId); } private DevModeRunner(String actualDebugPort, String bootstrapId) throws Exception { - launcher = newLauncher(actualDebugPort, bootstrapId); + commandLine = newLauncher(actualDebugPort, bootstrapId); } Collection pomFiles() { - return launcher.watchedBuildFiles(); + return commandLine.getWatchedBuildFiles(); } boolean alive() { @@ -1212,9 +1242,9 @@ boolean isExpectedExitValue() { void run() throws Exception { // Display the launch command line in dev mode if (getLog().isDebugEnabled()) { - getLog().debug("Launching JVM with command line: " + String.join(" ", launcher.args())); + getLog().debug("Launching JVM with command line: " + String.join(" ", commandLine.getArguments())); } - final ProcessBuilder processBuilder = new ProcessBuilder(launcher.args()) + final ProcessBuilder processBuilder = new ProcessBuilder(commandLine.getArguments()) .redirectErrorStream(true) .inheritIO() .directory(workingDir == null ? project.getBasedir() : workingDir); @@ -1243,7 +1273,7 @@ void stop() throws InterruptedException { } } - private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootstrapId) throws Exception { + private DevModeCommandLine newLauncher(String actualDebugPort, String bootstrapId) throws Exception { String java = null; // See if a toolchain is configured if (toolchainManager != null) { @@ -1254,7 +1284,7 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst } } - final MavenDevModeLauncher.Builder builder = MavenDevModeLauncher.builder(java, getLog()) + final DevModeCommandLineBuilder builder = DevModeCommandLine.builder(java) .preventnoverify(preventnoverify) .forceC2(forceC2) .buildDir(buildDir) @@ -1271,14 +1301,11 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst } if (openJavaLang) { - builder.jvmArgs("--add-opens"); - builder.jvmArgs("java.base/java.lang=ALL-UNNAMED"); + builder.addOpens("java.base/java.lang=ALL-UNNAMED"); } if (modules != null && !modules.isEmpty()) { - String mods = String.join(",", this.modules); - builder.jvmArgs("--add-modules"); - builder.jvmArgs(mods); + builder.addModules(this.modules); } builder.projectDir(project.getFile().getParentFile()); @@ -1392,6 +1419,9 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst .resolveModel(mvnCtx.getCurrentProject().getAppArtifact()); } + builder.extensionDevModeConfig(appModel.getExtensionDevModeConfig()) + .extensionDevModeJvmOptionFilter(extensionJvmOptions); + // serialize the app model to avoid re-resolving it in the dev process BootstrapUtils.serializeAppModel(appModel, appModelLocation); builder.jvmArgs("-D" + BootstrapConstants.SERIALIZED_APP_MODEL + "=" + appModelLocation); @@ -1448,7 +1478,13 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst return builder.build(); } - private void setJvmArgs(Builder builder) throws Exception { + private void debug(String msg, Object... args) { + if (getLog().isDebugEnabled()) { + getLog().debug(String.format(msg, args)); + } + } + + private void setJvmArgs(DevModeCommandLineBuilder builder) throws Exception { String jvmArgs = this.jvmArgs; if (!systemProperties.isEmpty()) { final StringBuilder buf = new StringBuilder(); @@ -1476,7 +1512,7 @@ private void applyCompilerFlag(Optional compilerPluginConfiguration, St .ifPresent(builderCall); } - private void addQuarkusDevModeDeps(MavenDevModeLauncher.Builder builder, ApplicationModel appModel) + private void addQuarkusDevModeDeps(DevModeCommandLineBuilder builder, ApplicationModel appModel) throws MojoExecutionException, DependencyResolutionException { ResolvedDependency coreDeployment = null; @@ -1569,7 +1605,7 @@ private List getProjectAetherDependencyMana return managed; } - private void setKotlinSpecificFlags(MavenDevModeLauncher.Builder builder) { + private void setKotlinSpecificFlags(DevModeCommandLineBuilder builder) { Plugin kotlinMavenPlugin = null; for (Plugin plugin : project.getBuildPlugins()) { if (plugin.getArtifactId().equals(KOTLIN_MAVEN_PLUGIN) && plugin.getGroupId().equals(ORG_JETBRAINS_KOTLIN)) { @@ -1615,7 +1651,7 @@ private void setKotlinSpecificFlags(MavenDevModeLauncher.Builder builder) { builder.compilerPluginOptions(options); } - private void setAnnotationProcessorFlags(MavenDevModeLauncher.Builder builder) { + private void setAnnotationProcessorFlags(DevModeCommandLineBuilder builder) { Plugin compilerMavenPlugin = null; for (Plugin plugin : project.getBuildPlugins()) { if (plugin.getArtifactId().equals("maven-compiler-plugin") @@ -1649,7 +1685,7 @@ private void setAnnotationProcessorFlags(MavenDevModeLauncher.Builder builder) { builder.compilerPluginOptions(options); } - protected void modifyDevModeContext(MavenDevModeLauncher.Builder builder) { + protected void modifyDevModeContext(DevModeCommandLineBuilder builder) { } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/MavenDevModeLauncher.java b/devtools/maven/src/main/java/io/quarkus/maven/MavenDevModeLauncher.java deleted file mode 100644 index d50eb108099b8..0000000000000 --- a/devtools/maven/src/main/java/io/quarkus/maven/MavenDevModeLauncher.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.quarkus.maven; - -import org.apache.maven.plugin.logging.Log; - -import io.quarkus.deployment.dev.QuarkusDevModeLauncher; - -public class MavenDevModeLauncher extends QuarkusDevModeLauncher { - - /** - * Initializes the launcher builder - * - * @param java path to the java, may be null - * @param log the logger - * @return launcher builder - */ - public static Builder builder(String java, Log log) { - return new MavenDevModeLauncher(log).new Builder(java); - } - - public class Builder extends QuarkusDevModeLauncher.Builder { - - private Builder(String java) { - super(java); - } - } - - private final Log log; - - private MavenDevModeLauncher(Log log) { - this.log = log; - } - - @Override - protected boolean isDebugEnabled() { - return log.isDebugEnabled(); - } - - @Override - protected void debug(Object msg) { - log.error(msg == null ? "null" : msg.toString()); - } - - @Override - protected void error(Object msg) { - log.error(msg == null ? "null" : msg.toString()); - } - - @Override - protected void warn(Object msg) { - log.warn(msg == null ? "null" : msg.toString()); - } -} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java index 73bbb078e98fe..acf7c3f1ced5c 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java @@ -4,13 +4,15 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; +import io.quarkus.deployment.dev.DevModeCommandLineBuilder; + /** * The dev mojo, that connects to a remote host. */ @Mojo(name = "remote-dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.TEST) public class RemoteDevMojo extends DevMojo { @Override - protected void modifyDevModeContext(MavenDevModeLauncher.Builder builder) { + protected void modifyDevModeContext(DevModeCommandLineBuilder builder) { builder.remoteDev(true); } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java index bfbf3eff00656..3f64fb0a2ed34 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java @@ -7,6 +7,7 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.deployment.dev.DevModeCommandLineBuilder; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.IsolatedTestModeMain; import io.quarkus.runtime.LaunchMode; @@ -23,7 +24,7 @@ protected LaunchMode getLaunchModeClasspath() { } @Override - protected void modifyDevModeContext(MavenDevModeLauncher.Builder builder) { + protected void modifyDevModeContext(DevModeCommandLineBuilder builder) { builder.entryPointCustomizer(new Consumer() { @Override public void accept(DevModeContext devModeContext) { diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 2bd3af6873a81..b4bb30f6ad2a8 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -169,6 +169,22 @@ The following properties may appear in this file: | Optional | https://quarkus.io/guides/conditional-extension-dependencies[Dependency condition] that has to be satisfied for a conditional dependency on this extension to be activated +| `dev-mode.jvm-option.std.=` +| Optional +| Standard Java command line option that should be added to command line launching an application in dev mode + +| `dev-mode.jvm-option.xx.=` +| Optional +| `-XX:` Java command line option that should be added to command line launching an application in dev mode + +| `dev-mode.lock.jvm-options` +| Optional +| A comma-separated list of standard Java command line options that should not be overridden by values that are pre-configured by the Quarkus Maven and Gradle plugins by default + +| `dev-mode.lock.xx-jvm-options` +| Optional +| A comma-separated list of `-XX:` Java command line options that should not be overridden by values that are pre-configured by the Quarkus Maven and Gradle plugins by default + |=== [[quarkus-config-roots]] @@ -185,3 +201,70 @@ Same as `quarkus-config-roots.list`, this file may appear in a runtime extension == META-INF/quarkus-build-steps.list This file may appear in a deployment extension artifact. It contains a list of classes that implement Quarkus build steps (methods annotated with `io.quarkus.deployment.annotations.BuildStep`). This file is generated as part of the extension project build process and must not be edited manually. + +[[quarkus-extension-maven-plugin]] +== Quarkus Extension Maven Plugin + +The `quarkus-extension-maven-plugin` is configured in the runtime module of a Quarkus extension Maven project and serves the following purposes: + +* validate extension metadata configuration; +* generate extension metadata; +* check whether extension dependencies conform to the Quarkus extension dependency model. + +=== Dev mode JVM options + +An extension may pre-configure certain Java command line options that should be added to the command line +launching an application in Dev mode. Here is how such options can be configured in the `quarkus-extension-maven-plugin` configuration: + +[source,xml,subs=attributes+] +---- + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + + + jdk.incubator.vector + + ALL-UNNAMED + + + false + + agentlib:jdwp + TieredStopAtLevel + + + + + +---- + +The `extension-descriptor` goal will generate `META-INF/quarkus-extension.properties` with the corresponding properties. +And once an application that has a dependency on this extension is launched in Dev mode, the logs will look like +[source,bash] +---- + +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) Adding JVM options from org.acme:quarkus-blue::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) enable-native-access: [ALL-UNNAMED] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) add-modules: [jdk.incubator.vector] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) enable-preview: [] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) UseThreadPriorities: [false] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) org.acme:quarkus-blue::jar locks JVM options [TieredStopAtLevel, agentlib:jdwp] +[INFO] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] Extension org.acme:quarkus-blue enables the C2 compiler which is disabled by default in Dev mode for optimal performance. +[INFO] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] Extension org.acme:quarkus-blue disables the Debug mode for optimal performance. Debugging can still be enabled in the Quarkus plugin configuration or with -Ddebug on the command line. +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) Executable jar: /home//app/target/acme-app-dev.jar +[DEBUG] Launching JVM with command line: /home//jdk/bin/java -Dquarkus-internal.serialized-app-model.path=/home//app/target/quarkus/bootstrap/dev-app-model.dat -javaagent:/home//.m2/repository/io/quarkus/quarkus-class-change-agent/{quarkus-verion}/quarkus-class-change-agent-{quarkus-version}.jar --enable-native-access=ALL-UNNAMED --add-modules=jdk.incubator.vector --enable-preview -XX:-UseThreadPriorities -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar /home//app/target/acme-app-dev.jar +---- + +Note, there are a couple `INFO` messages informing the user that the values for certain options Quarkus uses +by default in Dev mode were overridden by the extension preferences. Specifically, the C2 compiler was re-enabled +and the debug mode was disabled. A user can still choose to enable the debug agent and disable the C2 compilter +if necessary by explicitly setting the corresponding Quarkus Maven or Gradle plugin parameter values. \ No newline at end of file diff --git a/docs/src/main/asciidoc/gradle-tooling.adoc b/docs/src/main/asciidoc/gradle-tooling.adoc index 0ba1a19ae1e34..89251c264e359 100644 --- a/docs/src/main/asciidoc/gradle-tooling.adoc +++ b/docs/src/main/asciidoc/gradle-tooling.adoc @@ -315,6 +315,91 @@ All the config options are shown below: include::{generated-dir}/config/quarkus-core_quarkus.live-reload.adoc[opts=optional, leveloffset=+1] +=== Extension provided Dev mode Java options + +Some extensions may provide pre-configured Java options that should be added to the command line launching an application in Dev mode. + +Let's suppose there are couple of extensions `quarkus-blue` and `quarkus-red` in an application that provide Java options for Dev mode. +The logs may look something like this +[source,bash] +---- +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] Adding JVM options from org.acme:quarkus-blue::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] enable-native-access: [ALL-UNNAMED] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] add-modules: [jdk.incubator.vector] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] enable-preview: [] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] Adding JVM options from org.acme:quarkus-red::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] add-opens: [java.base/java.io=ALL-UNNAMED, java.base/java.nio=ALL-UNNAMED] +[INFO] [org.gradle.process.internal.DefaultExecHandle] Starting process 'command '/home//jdk/bin/java''. Working directory: /home//gradle-app/build/classes/java/main Command: /home//jdk/bin/java -Dquarkus.console.basic=true -Dio.quarkus.force-color-support=true -javaagent:/home//.m2/repository/io/quarkus/quarkus-class-change-agent/{quarkus-version}/quarkus-class-change-agent-{quarkus-version}.jar -Dquarkus-internal.serialized-app-model.path=/home//gradle-app/build/tmp/quarkusDev/quarkus-app-model.dat -Dquarkus-internal-test.serialized-app-model.path=/home//gradle-app/build/tmp/quarkusDev/quarkus-app-test-model.dat -XX:TieredStopAtLevel=1 -agentlib:jdwp=transport=dt_socket,address=localhost:5005,server=y,suspend=n --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --enable-native-access=ALL-UNNAMED --add-modules=jdk.incubator.vector --enable-preview -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar /home//gradle-app/build/gradle-app-dev.jar +---- + +A user may choose to disable all the Java options provided by extensions by configuring `disableAll` parameter such as + +[role="primary asciidoc-tabs-sync-groovy"] +.Groovy DSL +**** +[source,groovy] +---- +quarkusDev { + extensionJvmOptions{ + disableAll = true + } +} +---- +**** + +[role="secondary asciidoc-tabs-sync-kotlin"] +.Kotlin DSL +**** +[source,kotlin] +---- +tasks.quarkusDev { + extensionJvmOptions{ + setDisableAll(true) + } +} +---- +**** + +Or disable Java options provided by specific extensions by configuring Maven coordinates patterns, such as + +[role="primary asciidoc-tabs-sync-groovy"] +.Groovy DSL +**** +[source,groovy] +---- +quarkusDev { + extensionJvmOptions{ + disableFor = ["org.acme:quarkus-red"] + } +} +---- +**** + +[role="secondary asciidoc-tabs-sync-kotlin"] +.Kotlin DSL +**** +[source,kotlin] +---- +tasks.quarkusDev { + extensionJvmOptions{ + setDisableFor(mutableListOf("org.acme:quarkus-red")) + } +} +---- +**** + +With this configuration the logs will look like + +[source,bash] +---- +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] Adding JVM options from org.acme:quarkus-blue::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] enable-native-access: [ALL-UNNAMED] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] add-modules: [jdk.incubator.vector] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] enable-preview: [] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] Skipped JVM options from org.acme:quarkus-red::jar +[INFO] [org.gradle.process.internal.DefaultExecHandle] Starting process 'command '/home//jdk/bin/java''. Working directory: /home//gradle-app/build/classes/java/main Command: /home//jdk/bin/java -Dquarkus.console.basic=true -Dio.quarkus.force-color-support=true -javaagent:/home//.m2/repository/io/quarkus/quarkus-class-change-agent/{quarkus-version}/quarkus-class-change-agent-{quarkus-version}.jar -Dquarkus-internal.serialized-app-model.path=/home//gradle-app/build/tmp/quarkusDev/quarkus-app-model.dat -Dquarkus-internal-test.serialized-app-model.path=/home//gradle-app/build/tmp/quarkusDev/quarkus-app-test-model.dat -XX:TieredStopAtLevel=1 -agentlib:jdwp=transport=dt_socket,address=localhost:5005,server=y,suspend=n --enable-native-access=ALL-UNNAMED --add-modules=jdk.incubator.vector --enable-preview -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar /home//gradle-app/build/gradle-app-kotlin-dev.jar +---- + == Debugging In development mode, Quarkus starts by default with debug mode enabled, listening to port `5005` without suspending the JVM. diff --git a/docs/src/main/asciidoc/maven-tooling.adoc b/docs/src/main/asciidoc/maven-tooling.adoc index 2491ec6f31847..67d7578f40f22 100644 --- a/docs/src/main/asciidoc/maven-tooling.adoc +++ b/docs/src/main/asciidoc/maven-tooling.adoc @@ -269,6 +269,63 @@ your password is never sent directly over the wire. For the initial connection r initial state data, and subsequent requests hash it with a random session id generated by the server and any body contents for POST requests, and the path for DELETE requests, as well as an incrementing counter to prevent replay attacks. +=== Extension provided Dev mode Java options + +Some extensions may provide pre-configured Java options that should be added to the command line launching an application in Dev mode. + +Let's suppose there are couple of extensions `quarkus-blue` and `quarkus-red` in an application that provide Java options for Dev mode. +The logs may look something like this +[source,bash] +---- +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) Adding JVM options from org.acme:quarkus-red::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) add-opens: [java.base/java.io=ALL-UNNAMED, java.base/java.nio=ALL-UNNAMED] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) Adding JVM options from org.acme:quarkus-blue::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) enable-native-access: [ALL-UNNAMED] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) add-modules: [jdk.incubator.vector] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) enable-preview: [] +[DEBUG] Launching JVM with command line: /home//jdk/bin/java -Dquarkus-internal.serialized-app-model.path=/home//app/target/quarkus/bootstrap/dev-app-model.dat -javaagent:/home//.m2/repository/io/quarkus/quarkus-class-change-agent/{quarkus-version}/quarkus-class-change-agent-{quarkus-version}.jar -XX:TieredStopAtLevel=1 -agentlib:jdwp=transport=dt_socket,address=localhost:5005,server=y,suspend=n --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --enable-native-access=ALL-UNNAMED --add-modules=jdk.incubator.vector --enable-preview -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar /home//app/target/acme-app-dev.jar +---- + +A user may choose to disable all the Java options provided by extensions by configuring `disableAll` parameter such as +[source,xml,subs=attributes+] +---- + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + + + false + + + +---- +Or disable Java options provided by specific extensions by configuring Maven coordinates patterns, such as +[source,xml,subs=attributes+] +---- + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + + + + org.acme:quarkus-red + + + + +---- +With this configuration the logs will look like +[source,bash] +---- +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) Adding JVM options from org.acme:quarkus-blue::jar +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) enable-native-access: [ALL-UNNAMED] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) add-modules: [jdk.incubator.vector] +[DEBUG] [io.quarkus.deployment.dev.DevModeCommandLineBuilder] (main) enable-preview: [] +[DEBUG] Launching JVM with command line: /home//jdk/bin/java -Dquarkus-internal.serialized-app-model.path=/home//app/target/quarkus/bootstrap/dev-app-model.dat -javaagent:/home//.m2/repository/io/quarkus/quarkus-class-change-agent/{quarkus-version}/quarkus-class-change-agent-{quarkus-version}.jar -XX:TieredStopAtLevel=1 -agentlib:jdwp=transport=dt_socket,address=localhost:5005,server=y,suspend=n --enable-native-access=ALL-UNNAMED --add-modules=jdk.incubator.vector --enable-preview -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar /home//app/target/acme-app-dev.jar +---- + == Debugging In development mode, Quarkus starts by default with debug mode enabled, listening to port `5005` without suspending the JVM. diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java index 6c2aa72680a47..c5c84b316922b 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java @@ -46,4 +46,21 @@ public interface BootstrapConstants { String PLATFORM_PROPERTY_PREFIX = "platform."; String QUARKUS_BOOTSTRAP_WORKSPACE_DISCOVERY = "quarkus.bootstrap.workspace-discovery"; + + /** + * Prefix for properties configuring extension Dev mode JVM arguments + */ + String EXT_DEV_MODE_JVM_OPTION_PREFIX = "dev-mode.jvm-option."; + + /** + * {@code quarkus-extension.properties} property listing JVM options whose values shouldn't change by + * the default parameters values of the Quarkus Maven and Gradle plugins launching an application in dev mode + */ + String EXT_DEV_MODE_LOCK_JVM_OPTIONS = "dev-mode.lock.jvm-options"; + + /** + * {@code quarkus-extension.properties} property listing JVM XX options whose values shouldn't change by + * the default parameters values of the Quarkus Maven and Gradle plugins launching an application in dev mode + */ + String EXT_DEV_MODE_LOCK_XX_JVM_OPTIONS = "dev-mode.lock.xx-jvm-options"; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java index 8695f93573fdc..d9eb54f92c73e 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModel.java @@ -156,4 +156,11 @@ private static void collectModules(WorkspaceModule module, Map getExtensionDevModeConfig(); } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index c4e0331313581..7689f9aa6eb9e 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -15,6 +15,7 @@ import org.jboss.logging.Logger; +import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.workspace.WorkspaceModule; import io.quarkus.bootstrap.workspace.WorkspaceModuleId; import io.quarkus.maven.dependency.ArtifactCoords; @@ -35,6 +36,8 @@ public class ApplicationModelBuilder { private static final Logger log = Logger.getLogger(ApplicationModelBuilder.class); + private static final String COMMA = ","; + ResolvedDependencyBuilder appArtifact; final Map dependencies = new LinkedHashMap<>(); @@ -47,6 +50,7 @@ public class ApplicationModelBuilder { final Collection extensionCapabilities = new ConcurrentLinkedDeque<>(); PlatformImports platformImports; final Map projectModules = new HashMap<>(); + final Collection extensionDevConfig = new ConcurrentLinkedDeque<>(); public ApplicationModelBuilder() { // we never include the ide launcher in the final app model @@ -166,81 +170,137 @@ public WorkspaceModule.Mutable getOrCreateProjectModule(WorkspaceModuleId id, Fi } /** - * Sets the parent first and excluded artifacts from a descriptor properties file + * Collects extension properties from the {@code META-INF/quarkus-extension.properties} * - * @param props The quarkus-extension.properties file + * @param props extension properties + * @param extensionKey extension dependency key */ - public void handleExtensionProperties(Properties props, String extension) { + public void handleExtensionProperties(Properties props, ArtifactKey extensionKey) { + JvmOptionsBuilder jvmOptionsBuilder = null; + Set lockJvmOptions = Set.of(); for (Map.Entry prop : props.entrySet()) { if (prop.getValue() == null) { continue; } - final String value = prop.getValue().toString(); + final String name = prop.getKey().toString(); + final String value = prop.getValue().toString().trim(); + + if (JvmOptionsBuilder.isExtensionDevModeJvmOptionProperty(name)) { + log.debugf("Extension %s configures JVM option %s=%s in dev mode", extensionKey, name, value); + if (jvmOptionsBuilder == null) { + jvmOptionsBuilder = JvmOptions.builder(); + } + jvmOptionsBuilder.addFromQuarkusExtensionProperty(name, value); + continue; + } + if (value.isBlank()) { continue; } - final String name = prop.getKey().toString(); switch (name) { case PARENT_FIRST_ARTIFACTS: - for (String artifact : value.split(",")) { - parentFirstArtifacts.add(new GACT(artifact.split(":"))); - } + addParentFirstArtifacts(value); break; case RUNNER_PARENT_FIRST_ARTIFACTS: - for (String artifact : value.split(",")) { - runnerParentFirstArtifacts.add(new GACT(artifact.split(":"))); - } + addRunnerParentFirstArtifacts(value); break; case EXCLUDED_ARTIFACTS: - for (String artifact : value.split(",")) { - excludedArtifacts.add(ArtifactCoordsPattern.of(artifact)); - log.debugf("Extension %s is excluding %s", extension, artifact); - } + addExcludedArtifacts(extensionKey, value); break; case LESSER_PRIORITY_ARTIFACTS: - String[] artifacts = value.split(","); - for (String artifact : artifacts) { - lesserPriorityArtifacts.add(new GACT(artifact.split(":"))); - log.debugf("Extension %s is making %s a lesser priority artifact", extension, artifact); - } + addLesserPriorityArtifacts(extensionKey, value); + break; + case BootstrapConstants.EXT_DEV_MODE_LOCK_XX_JVM_OPTIONS: + case BootstrapConstants.EXT_DEV_MODE_LOCK_JVM_OPTIONS: + lockJvmOptions = splitByCommaAndAddAll(value, lockJvmOptions); break; default: if (name.startsWith(REMOVED_RESOURCES_DOT)) { - final String keyStr = name.substring(REMOVED_RESOURCES_DOT.length()); - if (!keyStr.isBlank()) { - ArtifactKey key = null; - try { - key = ArtifactKey.fromString(keyStr); - } catch (IllegalArgumentException e) { - log.warnf("Failed to parse artifact key %s in %s from descriptor of extension %s", keyStr, name, - extension); - } - if (key != null) { - final Set resources; - Collection existingResources = excludedResources.get(key); - if (existingResources == null || existingResources.isEmpty()) { - resources = Set.of(value.split(",")); - } else { - final String[] split = value.split(","); - resources = new HashSet<>(existingResources.size() + split.length); - resources.addAll(existingResources); - for (String s : split) { - resources.add(s); - } - } - log.debugf("Extension %s is excluding resources %s from artifact %s", extension, resources, - key); - excludedResources.put(key, resources); - } - } + addRemovedResources(extensionKey, name, value); } } } + if (jvmOptionsBuilder != null || lockJvmOptions != null) { + extensionDevConfig.add(new ExtensionDevModeConfig(extensionKey, + jvmOptionsBuilder == null ? JvmOptions.builder().build() : jvmOptionsBuilder.build(), + lockJvmOptions)); + } + } + + private static Set splitByCommaAndAddAll(String commaList, Set set) { + var arr = commaList.split(COMMA); + if (arr.length == 0) { + return set; + } + if (set.isEmpty()) { + return Set.of(arr); + } + set = new HashSet<>(set); + for (int i = 0; i < arr.length; ++i) { + set.add(arr[i]); + } + return set; + } + + private void addRemovedResources(ArtifactKey extension, String name, String value) { + final String keyStr = name.substring(REMOVED_RESOURCES_DOT.length()); + if (keyStr.isBlank()) { + return; + } + final ArtifactKey key; + try { + key = ArtifactKey.fromString(keyStr); + } catch (IllegalArgumentException e) { + log.warnf("Failed to parse artifact key %s in %s from descriptor of extension %s", keyStr, name, extension); + return; + } + final Set resources; + final Collection existingResources = excludedResources.get(key); + if (existingResources == null || existingResources.isEmpty()) { + resources = Set.of(value.split(COMMA)); + } else { + final String[] split = value.split(COMMA); + resources = new HashSet<>(existingResources.size() + split.length); + resources.addAll(existingResources); + resources.addAll(List.of(split)); + } + log.debugf("Extension %s is excluding resources %s from artifact %s", extension, resources, key); + excludedResources.put(key, resources); + } + + private void addLesserPriorityArtifacts(ArtifactKey extension, String value) { + for (String artifact : value.split(COMMA)) { + lesserPriorityArtifacts.add(toArtifactKey(artifact)); + log.debugf("Extension %s is making %s a lesser priority artifact", extension, artifact); + } + } + + private void addExcludedArtifacts(ArtifactKey extension, String value) { + for (String artifact : value.split(COMMA)) { + excludedArtifacts.add(ArtifactCoordsPattern.of(artifact)); + log.debugf("Extension %s is excluding %s", extension, artifact); + } + } + + private void addRunnerParentFirstArtifacts(String value) { + for (String artifact : value.split(COMMA)) { + runnerParentFirstArtifacts.add(toArtifactKey(artifact)); + } + } + + private void addParentFirstArtifacts(String value) { + for (String artifact : value.split(COMMA)) { + parentFirstArtifacts.add(toArtifactKey(artifact)); + } + } + + private static GACT toArtifactKey(String artifact) { + return new GACT(artifact.split(":")); } - private boolean isExcluded(ArtifactCoords coords) { - for (var pattern : excludedArtifacts) { - if (pattern.matches(coords)) { + private static boolean matches(ArtifactCoordsPattern[] patterns, ArtifactCoords coords) { + for (int i = 0; i < patterns.length; ++i) { + if (patterns[i].matches(coords)) { return true; } } @@ -268,8 +328,9 @@ List buildDependencies() { } final List result = new ArrayList<>(dependencies.size()); + final ArtifactCoordsPattern[] excludePatterns = excludedArtifacts.toArray(new ArtifactCoordsPattern[0]); for (ResolvedDependencyBuilder db : this.dependencies.values()) { - if (!isExcluded(db.getArtifactCoords())) { + if (!matches(excludePatterns, db.getArtifactCoords())) { result.add(db.build()); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java index 1796b1ceff51a..3e6cc090723d6 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java @@ -16,7 +16,7 @@ public class DefaultApplicationModel implements ApplicationModel, Serializable { - private static final long serialVersionUID = -3878782344578748234L; + private static final long serialVersionUID = -5247678201356725379L; private final ResolvedDependency appArtifact; private final List dependencies; @@ -24,6 +24,7 @@ public class DefaultApplicationModel implements ApplicationModel, Serializable { private final List capabilityContracts; private final Set localProjectArtifacts; private final Map> excludedResources; + private final List extensionDevConfig; public DefaultApplicationModel(ApplicationModelBuilder builder) { this.appArtifact = builder.appArtifact.build(); @@ -31,7 +32,8 @@ public DefaultApplicationModel(ApplicationModelBuilder builder) { this.platformImports = builder.platformImports; this.capabilityContracts = List.copyOf(builder.extensionCapabilities); this.localProjectArtifacts = Set.copyOf(builder.reloadableWorkspaceModules); - this.excludedResources = builder.excludedResources; + this.excludedResources = Map.copyOf(builder.excludedResources); + this.extensionDevConfig = List.copyOf(builder.extensionDevConfig); } @Override @@ -93,6 +95,11 @@ public Map> getRemovedResources() { return excludedResources; } + @Override + public Collection getExtensionDevModeConfig() { + return extensionDevConfig; + } + private Collection collectDependencies(int flags) { var result = new ArrayList(); for (var d : getDependencies(flags)) { @@ -121,7 +128,7 @@ private FlagDependencyIterator(int[] flags) { public Iterator iterator() { return new Iterator<>() { - final Iterator i = dependencies.iterator(); + int index = 0; ResolvedDependency next; { @@ -145,8 +152,8 @@ public ResolvedDependency next() { private void moveOn() { next = null; - while (i.hasNext()) { - var d = i.next(); + while (index < dependencies.size()) { + var d = dependencies.get(index++); if (d.hasAnyFlag(flags)) { next = d; break; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java new file mode 100644 index 0000000000000..5a5a4cc4d4597 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ExtensionDevModeConfig.java @@ -0,0 +1,50 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.util.Set; + +import io.quarkus.maven.dependency.ArtifactKey; + +/** + * Extension Dev mode configuration options + */ +public class ExtensionDevModeConfig implements Serializable { + + private final ArtifactKey extensionKey; + private final JvmOptions jvmOptions; + private final Set lockJvmOptions; + + public ExtensionDevModeConfig(ArtifactKey extensionKey, JvmOptions jvmOptions, Set lockDefaultJvmOptions) { + this.extensionKey = extensionKey; + this.jvmOptions = jvmOptions; + this.lockJvmOptions = lockDefaultJvmOptions; + } + + /** + * Extension key + * + * @return extension key + */ + public ArtifactKey getExtensionKey() { + return extensionKey; + } + + /** + * JVM options that should be added to the command line launching an application in Dev mode. + * + * @return JVM options to be added to the command line launching an application in Dev mode + */ + public JvmOptions getJvmOptions() { + return jvmOptions; + } + + /** + * JVM options whose default values should not be overridden with values that otherwise would be recommended + * as defaults for Quarkus dev mode by the Quarkus Maven and Gradle plugins. + * + * @return JVM options that shouldn't be overridden + */ + public Set getLockJvmOptions() { + return lockJvmOptions; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOption.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOption.java new file mode 100644 index 0000000000000..28de35927ff87 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOption.java @@ -0,0 +1,48 @@ +package io.quarkus.bootstrap.model; + +import java.util.Collection; +import java.util.List; +import java.util.Properties; + +/** + * JVM option + */ +public interface JvmOption { + + /** + * Simple option name without dashes + * + * @return simple option name without dashes + */ + String getName(); + + /** + * Checks whether an option has a value + * + * @return true if an option has a value, otherwise - false + */ + default boolean hasValue() { + return !getValues().isEmpty(); + } + + /** + * All the configured option values. + * + * @return all the configured values + */ + Collection getValues(); + + /** + * Adds an option with its values as a property. + * + * @param props properties to add an argument to + */ + void addToQuarkusExtensionProperties(Properties props); + + /** + * Returns command line representation of this option. + * + * @return Java command line representation of this option + */ + List toCliOptions(); +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptions.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptions.java new file mode 100644 index 0000000000000..bec2383d444d4 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptions.java @@ -0,0 +1,57 @@ +package io.quarkus.bootstrap.model; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Properties; + +/** + * Collection of JVM arguments + */ +public interface JvmOptions extends Iterable { + + /** + * Creates a new JVM arguments builder instance. + * + * @return JVM arguments builder + */ + static JvmOptionsBuilder builder() { + return new JvmOptionsBuilder(); + } + + /** + * Collection of JVM arguments. + * + * @return collection of JVM arguments + */ + Collection asCollection(); + + /** + * Iterator over the JVM arguments. + * + * @return iterator over the JVM arguments + */ + @Override + default Iterator iterator() { + return asCollection().iterator(); + } + + /** + * Checks whether any JVM arguments have been configured. + * + * @return true if no argument was configured, otherwise - false + */ + default boolean isEmpty() { + return asCollection().isEmpty(); + } + + /** + * Adds all the configured JVM arguments as properties. + * + * @param props properties to add the JVM arguments to + */ + default void setAsExtensionDevModeProperties(Properties props) { + for (var a : asCollection()) { + a.addToQuarkusExtensionProperties(props); + } + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptionsBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptionsBuilder.java new file mode 100644 index 0000000000000..e7dfde8fdcc17 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/JvmOptionsBuilder.java @@ -0,0 +1,203 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.bootstrap.BootstrapConstants; + +/** + * JVM arguments builder + */ +public class JvmOptionsBuilder { + + public static boolean isExtensionDevModeJvmOptionProperty(String name) { + return name.startsWith(BootstrapConstants.EXT_DEV_MODE_JVM_OPTION_PREFIX); + } + + private static String getGroupPrefixForPropertyName(String propName) { + final int i = propName.indexOf('.', BootstrapConstants.EXT_DEV_MODE_JVM_OPTION_PREFIX.length() + 1); + if (i < 0) { + throw new IllegalArgumentException("Failed to determine JVM option group given property name " + propName); + } + return propName.substring(BootstrapConstants.EXT_DEV_MODE_JVM_OPTION_PREFIX.length(), i + 1); + } + + private Map> options = Map.of(); + + JvmOptionsBuilder() { + } + + public void addFromQuarkusExtensionProperty(String propertyName, String value) { + final String groupPrefix = getGroupPrefixForPropertyName(propertyName); + final String optionName = propertyName + .substring(BootstrapConstants.EXT_DEV_MODE_JVM_OPTION_PREFIX.length() + groupPrefix.length()); + addToGroup(groupPrefix, optionName, value); + } + + private JvmOptionsBuilder addToGroup(String optionGroupPrefix, String optionName, String value) { + if (options.isEmpty()) { + options = new HashMap<>(); + } + var option = options.computeIfAbsent(optionName, n -> { + switch (optionGroupPrefix) { + case MutableStandardJvmOption.PROPERTY_GROUP_PREFIX: + return MutableStandardJvmOption.newInstance(optionName); + case MutableXxJvmOption.PROPERTY_GROUP_PREFIX: + return MutableXxJvmOption.newInstance(optionName); + } + throw new IllegalArgumentException("Unrecognized JVM option group prefix " + optionGroupPrefix); + }); + if (!value.isBlank()) { + option.addValue(value); + } + return this; + } + + /** + * Adds a standard option without a value. + * + * @param name option name without the starting dashes + * @return this builder instance + */ + public JvmOptionsBuilder add(String name) { + if (options.isEmpty()) { + options = new HashMap<>(); + } + options.computeIfAbsent(name, n -> MutableStandardJvmOption.newInstance(name)); + return this; + } + + /** + * Adds a standard option with a value. + * + * @param name option name without the starting dashes + * @param value option value, must not be null or black + * @return this builder instance + */ + public JvmOptionsBuilder add(String name, String value) { + if (options.isEmpty()) { + options = new HashMap<>(); + } + var arg = options.get(name); + if (arg == null) { + options.put(name, MutableStandardJvmOption.newInstance(name, value)); + } else { + arg.addValue(value); + } + return this; + } + + /** + * Adds a standard option with multiple values. + * + * @param name option name without the starting dashes + * @param values option values + * @return this builder instance + */ + public JvmOptionsBuilder addAll(String name, Collection values) { + if (options.isEmpty()) { + options = new HashMap<>(); + } + var option = options.computeIfAbsent(name, n -> MutableStandardJvmOption.newInstance(name)); + for (var value : values) { + option.addValue(value); + } + return this; + } + + /** + * Adds a non-standard option that should be prefixed with {@code -XX:} when added to the command line with a value. + * + * @param name option name without the starting dashes + * @param value option value, must not be null or black + * @return this builder instance + */ + public JvmOptionsBuilder addXxOption(String name, String value) { + if (options.isEmpty()) { + options = new HashMap<>(); + } + var arg = options.get(name); + if (arg == null) { + options.put(name, MutableXxJvmOption.newInstance(name, value)); + } else { + arg.addValue(value); + } + return this; + } + + /** + * Adds JVM options with their values. + * + * @param options options to add + * @return this builder instance + */ + public JvmOptionsBuilder addAll(JvmOptions options) { + if (this.options.isEmpty()) { + this.options = new HashMap<>(); + } + for (var option : options.asCollection()) { + if (option.hasValue()) { + var existing = this.options.putIfAbsent(option.getName(), (MutableBaseJvmOption) option); + if (existing != null) { + for (var value : option.getValues()) { + existing.addValue(value); + } + } + addAll(option.getName(), option.getValues()); + } else { + this.options.putIfAbsent(option.getName(), (MutableBaseJvmOption) option); + } + } + return this; + } + + /** + * Collection of currently configured options. + * + * @return collection of currently configured options + */ + public Collection getOptions() { + return List.copyOf(options.values()); + } + + /** + * Checks whether an option with a given name is set. + * + * @param optionName option name + * @return true in case the option is set, otherwise - false + */ + public boolean contains(String optionName) { + for (var option : options.values()) { + if (option.getName().equals(optionName)) { + return true; + } + } + return false; + } + + /** + * Finalizes a collection of JVM options by creating an instance of {@link JvmOptions}. + * + * @return new instance of {@link JvmOptions} with all the configured options + */ + public JvmOptions build() { + return new JvmOptionsImpl(options.isEmpty() ? List.of() : List.copyOf(options.values())); + } + + private static class JvmOptionsImpl implements JvmOptions, Serializable { + + private final List args; + + private JvmOptionsImpl(List args) { + this.args = args; + } + + @Override + public Collection asCollection() { + return args; + } + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableBaseJvmOption.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableBaseJvmOption.java new file mode 100644 index 0000000000000..99742725f494f --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableBaseJvmOption.java @@ -0,0 +1,73 @@ +package io.quarkus.bootstrap.model; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +public abstract class MutableBaseJvmOption> implements JvmOption, Serializable { + + private static final String EMPTY_STR = ""; + private static final String PROPERTY_VALUE_SEPARATOR = "|"; + + private String name; + private Set values = Set.of(); + + @Override + public String getName() { + return name; + } + + @Override + public Collection getValues() { + return values; + } + + protected void setName(String name) { + this.name = name; + } + + public T addValue(String value) { + if (value == null) { + throw new IllegalArgumentException("value is null"); + } + if (value.isBlank()) { + throw new IllegalArgumentException("value is blank"); + } + if (values.isEmpty()) { + values = new HashSet<>(1); + } + for (String v : value.split("\\|")) { + values.add(v); + } + return (T) this; + } + + protected abstract String getQuarkusExtensionPropertyPrefix(); + + @Override + public void addToQuarkusExtensionProperties(Properties props) { + props.setProperty(getQuarkusExtensionPropertyPrefix() + name, toPropertyValue()); + } + + protected String toPropertyValue() { + if (values.isEmpty()) { + return EMPTY_STR; + } + var i = values.iterator(); + if (values.size() == 1) { + return i.next(); + } + final StringBuilder sb = new StringBuilder(); + sb.append(i.next()); + while (i.hasNext()) { + sb.append(PROPERTY_VALUE_SEPARATOR).append(i.next()); + } + return sb.toString(); + } + + public String toString() { + return name + "=" + values; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableStandardJvmOption.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableStandardJvmOption.java new file mode 100644 index 0000000000000..f81ebdf80de9e --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableStandardJvmOption.java @@ -0,0 +1,128 @@ +package io.quarkus.bootstrap.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.quarkus.bootstrap.BootstrapConstants; + +public class MutableStandardJvmOption extends MutableBaseJvmOption { + + public static final String PROPERTY_GROUP_PREFIX = "std."; + + private static final String COMPLETE_PROPERTY_PREFIX = BootstrapConstants.EXT_DEV_MODE_JVM_OPTION_PREFIX + + PROPERTY_GROUP_PREFIX; + + private static final String DASH_DASH = "--"; + private static final String COMMA = ","; + private static final String EQUALS = "="; + + public static MutableStandardJvmOption fromQuarkusExtensionProperty(String propertyName, String value) { + final String optionName = propertyName.substring(COMPLETE_PROPERTY_PREFIX.length()); + return value.isBlank() ? newInstance(optionName) : newInstance(optionName, value); + } + + public static MutableStandardJvmOption newInstance(String name) { + return newInstance(name, null); + } + + public static MutableStandardJvmOption newInstance(String name, String value) { + var result = new MutableStandardJvmOption(); + result.setName(name); + if (value != null) { + result.addValue(value); + } + return result; + } + + @Override + protected String getQuarkusExtensionPropertyPrefix() { + return COMPLETE_PROPERTY_PREFIX; + } + + @Override + public List toCliOptions() { + switch (getName()) { + case "add-modules": + return toCliSortedValueList(COMMA); + case "add-opens": + return toCliModulePackageList(); + default: + return toCliGenericArgument(); + } + } + + private List toCliModulePackageList() { + if (!hasValue()) { + return List.of(); + } + var name = getName(); + var values = getValues(); + if (values.size() == 1) { + return List.of(DASH_DASH + name + EQUALS + EQUALS + values.iterator().next()); + } + final Map> modulePackages = new HashMap<>(values.size()); + for (String value : values) { + final int slash = value.indexOf('='); + if (slash < 1) { + throw new IllegalArgumentException( + "Value '" + value + "' does not follow module/package=target-module(,target-module) format"); + } + final Set targetModules = modulePackages.computeIfAbsent(value.substring(0, slash), k -> new HashSet<>()); + final String[] packageNames = value.substring(slash + 1).split(COMMA); + for (String packageName : packageNames) { + targetModules.add(packageName); + } + } + final String[] modulePackageList = toSortedArray(modulePackages.keySet()); + final List result = new ArrayList<>(modulePackageList.length * 2); + for (String modulePackage : modulePackageList) { + final Set targetModules = modulePackages.get(modulePackage); + if (!targetModules.isEmpty()) { + result.add(DASH_DASH + name); + final StringBuilder sb = new StringBuilder() + .append(modulePackage).append(EQUALS); + final String[] targetModuleNames = toSortedArray(targetModules); + appendItems(sb, targetModuleNames, COMMA); + result.add(sb.toString()); + } + } + return result; + } + + private List toCliSortedValueList(String valueSeparator) { + if (!hasValue()) { + return List.of(); + } + var sb = new StringBuilder().append(DASH_DASH).append(getName()).append(EQUALS); + var values = getValues(); + if (values.size() == 1) { + sb.append(values.iterator().next()); + return List.of(sb.toString()); + } + appendItems(sb, toSortedArray(values), valueSeparator); + return List.of(sb.toString()); + } + + private List toCliGenericArgument() { + return getValues().isEmpty() ? List.of(DASH_DASH + getName()) : toCliSortedValueList(COMMA); + } + + private static void appendItems(StringBuilder sb, String[] arr, String itemSeparator) { + sb.append(arr[0]); + for (int i = 1; i < arr.length; ++i) { + sb.append(itemSeparator).append(arr[i]); + } + } + + private static String[] toSortedArray(Collection value) { + final String[] arr = value.toArray(new String[0]); + Arrays.sort(arr); + return arr; + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableXxJvmOption.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableXxJvmOption.java new file mode 100644 index 0000000000000..ce9322789d667 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableXxJvmOption.java @@ -0,0 +1,61 @@ +package io.quarkus.bootstrap.model; + +import java.util.List; + +import io.quarkus.bootstrap.BootstrapConstants; + +public class MutableXxJvmOption extends MutableBaseJvmOption { + + public static final String PROPERTY_GROUP_PREFIX = "xx."; + + private static final String COMPLETE_PROPERTY_PREFIX = BootstrapConstants.EXT_DEV_MODE_JVM_OPTION_PREFIX + + PROPERTY_GROUP_PREFIX; + + private static final String DASH_XX_COLLON = "-XX:"; + + public static MutableXxJvmOption fromQuarkusExtensionProperty(String propertyName, String value) { + final String optionName = propertyName.substring(COMPLETE_PROPERTY_PREFIX.length()); + return value.isBlank() ? newInstance(optionName) : newInstance(optionName, value); + } + + public static MutableXxJvmOption newInstance(String name) { + return newInstance(name, null); + } + + public static MutableXxJvmOption newInstance(String name, String value) { + var result = new MutableXxJvmOption(); + result.setName(name); + if (value != null) { + result.addValue(value); + } + return result; + } + + @Override + protected String getQuarkusExtensionPropertyPrefix() { + return COMPLETE_PROPERTY_PREFIX; + } + + @Override + public List toCliOptions() { + if (!hasValue()) { + return toBooleanOption(true); + } + if (getValues().size() == 1) { + var value = getValues().iterator().next(); + if ("true".equalsIgnoreCase(value) || "+".equals(value)) { + return toBooleanOption(true); + } + if ("false".equalsIgnoreCase(value) || "-".equals(value)) { + return toBooleanOption(false); + } + return List.of(DASH_XX_COLLON + getName() + "=" + value); + } + throw new IllegalArgumentException( + "Failed to format option " + DASH_XX_COLLON + getName() + " with values " + getValues()); + } + + private List toBooleanOption(boolean enabled) { + return List.of(DASH_XX_COLLON + (enabled ? "+" : "-") + getName()); + } +} diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableStandardJvmOptionTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableStandardJvmOptionTest.java new file mode 100644 index 0000000000000..6ce5cf19169ce --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableStandardJvmOptionTest.java @@ -0,0 +1,50 @@ +package io.quarkus.bootstrap.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class MutableStandardJvmOptionTest { + + @Test + public void testHasNoValue() { + assertThat(MutableStandardJvmOption.newInstance("enable-preview").hasValue()).isFalse(); + } + + @Test + public void testHasValue() { + assertThat(MutableStandardJvmOption.newInstance("module-path", "value").hasValue()).isTrue(); + } + + @Test + public void testCliArgumentEnablePreview() { + assertThat(MutableStandardJvmOption.newInstance("enable-preview").toCliOptions()).containsExactly("--enable-preview"); + } + + @Test + public void testCliArgumentIllegalAccess() { + assertThat(MutableStandardJvmOption.newInstance("illegal-access", "warn").toCliOptions()) + .containsExactly("--illegal-access=warn"); + } + + @Test + public void testCliArgumentAddModules() { + assertThat(MutableStandardJvmOption.newInstance("add-modules") + .addValue("java.compiler") + .addValue("java.net") + .toCliOptions()) + .containsExactly("--add-modules=java.compiler,java.net"); + } + + @Test + public void testCliArgumentAddOpens() { + assertThat(MutableStandardJvmOption.newInstance("add-opens") + .addValue("java.base/java.util=ALL-UNNAMED") + .addValue("java.base/java.io=org.acme.one,org.acme.two") + .addValue("java.base/java.io=org.acme.three") + .toCliOptions()) + .containsExactly("--add-opens", "java.base/java.io=org.acme.one,org.acme.three,org.acme.two", + "--add-opens", "java.base/java.util=ALL-UNNAMED"); + } + +} diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableXxJvmOptionTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableXxJvmOptionTest.java new file mode 100644 index 0000000000000..f7a51c98801e2 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/MutableXxJvmOptionTest.java @@ -0,0 +1,42 @@ +package io.quarkus.bootstrap.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class MutableXxJvmOptionTest { + + @Test + public void testHasNoValue() { + assertThat(MutableXxJvmOption.newInstance("AllowUserSignalHandlers").hasValue()).isFalse(); + } + + @Test + public void testHasValue() { + assertThat(MutableXxJvmOption.newInstance("AllowUserSignalHandlers", "true").hasValue()).isTrue(); + } + + @Test + public void testCliBooleanOptionWithNoValue() { + assertThat(MutableXxJvmOption.newInstance("AllowUserSignalHandlers").toCliOptions()) + .containsExactly("-XX:+AllowUserSignalHandlers"); + } + + @Test + public void testCliBooleanOptionWithTrueValue() { + assertThat(MutableXxJvmOption.newInstance("AllowUserSignalHandlers", "true").toCliOptions()) + .containsExactly("-XX:+AllowUserSignalHandlers"); + } + + @Test + public void testCliBooleanOptionWithFalseValue() { + assertThat(MutableXxJvmOption.newInstance("AllowUserSignalHandlers", "false").toCliOptions()) + .containsExactly("-XX:-AllowUserSignalHandlers"); + } + + @Test + public void testCliOptionWithSingleStringValue() { + assertThat(MutableXxJvmOption.newInstance("ErrorFile", "./hs_err_pid.log").toCliOptions()) + .containsExactly("-XX:ErrorFile=./hs_err_pid.log"); + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java index a5d2ce58cde78..e65df264dbdac 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyTreeResolver.java @@ -820,7 +820,7 @@ void ensureActivated() { return; } activated = true; - appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); + appBuilder.handleExtensionProperties(props, getKey(runtimeArtifact)); final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java index b569ce9c5d500..747c976b5eda3 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java @@ -525,9 +525,8 @@ void scheduleRuntimeVisit(ModelResolutionTaskRunner taskRunner) { void visitRuntimeDependency() { Artifact artifact = node.getArtifact(); - final ArtifactKey key = getKey(artifact); if (resolvedDep == null) { - resolvedDep = appBuilder.getDependency(key); + resolvedDep = appBuilder.getDependency(getKey(artifact)); } // in case it was relocated it might not survive conflict resolution in the deployment graph @@ -571,8 +570,7 @@ void scheduleChildVisits(ModelResolutionTaskRunner taskRunner, var winner = getWinner(childNode); if (winner == null) { allDeps.add(getCoords(childNode.getArtifact())); - var child = new AppDep(this, childNode); - children.add(child); + children.add(new AppDep(this, childNode)); if (filtered != null) { filtered.add(childNode); } @@ -880,7 +878,7 @@ void ensureActivated() { return; } activated = true; - appBuilder.handleExtensionProperties(props, runtimeArtifact.toString()); + appBuilder.handleExtensionProperties(props, getKey(runtimeArtifact)); final String providesCapabilities = props.getProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES); final String requiresCapabilities = props.getProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES); diff --git a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java index 61fdffab18d13..3f31cf926c925 100644 --- a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java +++ b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDescriptorMojo.java @@ -8,15 +8,7 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import org.apache.maven.artifact.Artifact; @@ -100,6 +92,7 @@ public static class RemovedResources { private static final String GROUP_ID = "group-id"; private static final String ARTIFACT_ID = "artifact-id"; private static final String METADATA = "metadata"; + private static final String COMMA = ","; /** * The entry point to Aether, i.e. the component doing all the work. @@ -250,6 +243,12 @@ public static class RemovedResources { @Parameter(property = "requiresQuarkusCore") String requiresQuarkusCore; + /** + * Extension Dev mode configuration options + */ + @Parameter + ExtensionDevModeMavenConfig devMode; + ArtifactCoords deploymentCoords; CollectResult collectedDeploymentDeps; DependencyResult runtimeDeps; @@ -263,139 +262,17 @@ public void execute() throws MojoExecutionException { validateExtensionDeps(); } - if (conditionalDependencies.isEmpty()) { - // if conditional dependencies haven't been configured - // we check whether there are direct optional dependencies on extensions - // that are configured with a dependency condition - // such dependencies will be registered as conditional - StringBuilder buf = null; - for (org.apache.maven.model.Dependency d : project.getDependencies()) { - if (!d.isOptional()) { - continue; - } - if (!d.getScope().isEmpty() - && !(d.getScope().equals(JavaScopes.COMPILE) || d.getScope().equals(JavaScopes.RUNTIME))) { - continue; - } - final Properties props = getExtensionDescriptor( - new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion()), - false); - if (props == null || !props.containsKey(BootstrapConstants.DEPENDENCY_CONDITION)) { - continue; - } - if (buf == null) { - buf = new StringBuilder(); - } else { - buf.setLength(0); - } - buf.append(d.getGroupId()).append(':').append(d.getArtifactId()).append(':'); - if (d.getClassifier() != null) { - buf.append(d.getClassifier()); - } - buf.append(':').append(d.getType()).append(':').append(d.getVersion()); - conditionalDependencies.add(buf.toString()); - } - } - - String quarkusCoreVersion = findQuarkusCoreVersion(); - String quarkusCoreVersionRange = requiresQuarkusCore == null ? toVersionRange(quarkusCoreVersion) : requiresQuarkusCore; - final Properties props = new Properties(); props.setProperty(BootstrapConstants.PROP_DEPLOYMENT_ARTIFACT, deployment); - if (!conditionalDependencies.isEmpty()) { - final StringBuilder buf = new StringBuilder(); - int i = 0; - buf.append(ArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); - while (i < conditionalDependencies.size()) { - buf.append(' ').append(ArtifactCoords.fromString(conditionalDependencies.get(i++)).toString()); - } - props.setProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString()); - } - if (!dependencyCondition.isEmpty()) { - final StringBuilder buf = new StringBuilder(); - int i = 0; - buf.append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); - while (i < dependencyCondition.size()) { - buf.append(' ').append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); - } - props.setProperty(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); - - } - - if (!capabilities.getProvides().isEmpty()) { - final StringBuilder buf = new StringBuilder(); - final Iterator i = capabilities.getProvides().iterator(); - appendCapability(i.next(), buf); - while (i.hasNext()) { - appendCapability(i.next(), buf.append(',')); - } - props.setProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES, buf.toString()); - } - if (!capabilities.getRequires().isEmpty()) { - final StringBuilder buf = new StringBuilder(); - final Iterator i = capabilities.getRequires().iterator(); - appendCapability(i.next(), buf); - while (i.hasNext()) { - appendCapability(i.next(), buf.append(',')); - } - props.setProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES, buf.toString()); - } - - if (parentFirstArtifacts != null && !parentFirstArtifacts.isEmpty()) { - String val = String.join(",", parentFirstArtifacts); - props.put(ApplicationModelBuilder.PARENT_FIRST_ARTIFACTS, val); - } - - if (runnerParentFirstArtifacts != null && !runnerParentFirstArtifacts.isEmpty()) { - String val = String.join(",", runnerParentFirstArtifacts); - props.put(ApplicationModelBuilder.RUNNER_PARENT_FIRST_ARTIFACTS, val); - } - - if (excludedArtifacts != null && !excludedArtifacts.isEmpty()) { - String val = String.join(",", excludedArtifacts); - props.put(ApplicationModelBuilder.EXCLUDED_ARTIFACTS, val); - } - - if (!removedResources.isEmpty()) { - for (RemovedResources entry : removedResources) { - final ArtifactKey key; - try { - key = ArtifactKey.fromString(entry.key); - } catch (IllegalArgumentException e) { - throw new MojoExecutionException( - "Failed to parse removed resource '" + entry.key + '=' + entry.resources + "'", e); - } - if (entry.resources == null || entry.resources.isBlank()) { - continue; - } - final String[] resources = entry.resources.split(","); - if (resources.length == 0) { - continue; - } - final String value; - if (resources.length == 1) { - value = resources[0]; - } else { - final StringBuilder sb = new StringBuilder(); - sb.append(resources[0]); - for (int i = 1; i < resources.length; ++i) { - final String resource = resources[i]; - if (!resource.isBlank()) { - sb.append(',').append(resource); - } - } - value = sb.toString(); - } - props.setProperty(ApplicationModelBuilder.REMOVED_RESOURCES_DOT + key.toString(), value); - } - } - - if (lesserPriorityArtifacts != null && !lesserPriorityArtifacts.isEmpty()) { - String val = String.join(",", lesserPriorityArtifacts); - props.put(ApplicationModelBuilder.LESSER_PRIORITY_ARTIFACTS, val); - } + recordConditionalDeps(props); + recordCapabilities(props); + recordClassLoadingConfig(props); + recordDevModeConfig(props); + final String quarkusCoreVersion = findQuarkusCoreVersion(); + final String quarkusCoreVersionRange = requiresQuarkusCore == null ? toVersionRange(quarkusCoreVersion) + : requiresQuarkusCore; if (quarkusCoreVersionRange != null) { props.put("requires-quarkus-version", quarkusCoreVersionRange); } @@ -491,6 +368,165 @@ public void execute() throws MojoExecutionException { } } + private void recordDevModeConfig(Properties props) { + if (devMode == null) { + return; + } + var jvmArgs = devMode.getJvmOptions(); + if (jvmArgs != null && !jvmArgs.isEmpty()) { + jvmArgs.setAsExtensionDevModeProperties(props); + } + jvmArgs = devMode.getXxJvmOptions(); + if (jvmArgs != null && !jvmArgs.isEmpty()) { + jvmArgs.setAsExtensionDevModeProperties(props); + } + if (devMode.hasLockedXxJvmOptions()) { + props.setProperty(BootstrapConstants.EXT_DEV_MODE_LOCK_XX_JVM_OPTIONS, + String.join(COMMA, devMode.getLockXxJvmOptions())); + } + if (devMode.hasLockedJvmOptions()) { + props.setProperty(BootstrapConstants.EXT_DEV_MODE_LOCK_JVM_OPTIONS, + String.join(COMMA, devMode.getLockJvmOptions())); + } + } + + private void recordClassLoadingConfig(Properties props) throws MojoExecutionException { + if (parentFirstArtifacts != null && !parentFirstArtifacts.isEmpty()) { + String val = String.join(COMMA, parentFirstArtifacts); + props.put(ApplicationModelBuilder.PARENT_FIRST_ARTIFACTS, val); + } + + if (runnerParentFirstArtifacts != null && !runnerParentFirstArtifacts.isEmpty()) { + String val = String.join(COMMA, runnerParentFirstArtifacts); + props.put(ApplicationModelBuilder.RUNNER_PARENT_FIRST_ARTIFACTS, val); + } + + if (excludedArtifacts != null && !excludedArtifacts.isEmpty()) { + String val = String.join(COMMA, excludedArtifacts); + props.put(ApplicationModelBuilder.EXCLUDED_ARTIFACTS, val); + } + + if (!removedResources.isEmpty()) { + for (RemovedResources entry : removedResources) { + final ArtifactKey key; + try { + key = ArtifactKey.fromString(entry.key); + } catch (IllegalArgumentException e) { + throw new MojoExecutionException( + "Failed to parse removed resource '" + entry.key + '=' + entry.resources + "'", e); + } + if (entry.resources == null || entry.resources.isBlank()) { + continue; + } + final String[] resources = entry.resources.split(COMMA); + if (resources.length == 0) { + continue; + } + final String value; + if (resources.length == 1) { + value = resources[0]; + } else { + final StringBuilder sb = new StringBuilder(); + sb.append(resources[0]); + for (int i = 1; i < resources.length; ++i) { + final String resource = resources[i]; + if (!resource.isBlank()) { + sb.append(',').append(resource); + } + } + value = sb.toString(); + } + props.setProperty(ApplicationModelBuilder.REMOVED_RESOURCES_DOT + key, value); + } + } + + if (lesserPriorityArtifacts != null && !lesserPriorityArtifacts.isEmpty()) { + String val = String.join(COMMA, lesserPriorityArtifacts); + props.put(ApplicationModelBuilder.LESSER_PRIORITY_ARTIFACTS, val); + } + } + + private void recordCapabilities(Properties props) { + if (!capabilities.getProvides().isEmpty()) { + final StringBuilder buf = new StringBuilder(); + final Iterator i = capabilities.getProvides().iterator(); + appendCapability(i.next(), buf); + while (i.hasNext()) { + appendCapability(i.next(), buf.append(',')); + } + props.setProperty(BootstrapConstants.PROP_PROVIDES_CAPABILITIES, buf.toString()); + } + if (!capabilities.getRequires().isEmpty()) { + final StringBuilder buf = new StringBuilder(); + final Iterator i = capabilities.getRequires().iterator(); + appendCapability(i.next(), buf); + while (i.hasNext()) { + appendCapability(i.next(), buf.append(',')); + } + props.setProperty(BootstrapConstants.PROP_REQUIRES_CAPABILITIES, buf.toString()); + } + } + + private void recordConditionalDeps(Properties props) { + lookForConditionalDeps(); + if (!conditionalDependencies.isEmpty()) { + final StringBuilder buf = new StringBuilder(); + int i = 0; + buf.append(ArtifactCoords.fromString(conditionalDependencies.get(i++))); + while (i < conditionalDependencies.size()) { + buf.append(' ').append(ArtifactCoords.fromString(conditionalDependencies.get(i++))); + } + props.setProperty(BootstrapConstants.CONDITIONAL_DEPENDENCIES, buf.toString()); + } + if (!dependencyCondition.isEmpty()) { + final StringBuilder buf = new StringBuilder(); + int i = 0; + buf.append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); + while (i < dependencyCondition.size()) { + buf.append(' ').append(ArtifactKey.fromString(dependencyCondition.get(i++)).toGacString()); + } + props.setProperty(BootstrapConstants.DEPENDENCY_CONDITION, buf.toString()); + + } + } + + private void lookForConditionalDeps() { + if (!conditionalDependencies.isEmpty()) { + return; + } + // if conditional dependencies haven't been configured + // we check whether there are direct optional dependencies on extensions + // that are configured with a dependency condition + // such dependencies will be registered as conditional + StringBuilder buf = null; + for (org.apache.maven.model.Dependency d : project.getDependencies()) { + if (!d.isOptional()) { + continue; + } + if (!d.getScope().isEmpty() + && !(d.getScope().equals(JavaScopes.COMPILE) || d.getScope().equals(JavaScopes.RUNTIME))) { + continue; + } + final Properties props = getExtensionDescriptor( + new DefaultArtifact(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getType(), d.getVersion()), + false); + if (props == null || !props.containsKey(BootstrapConstants.DEPENDENCY_CONDITION)) { + continue; + } + if (buf == null) { + buf = new StringBuilder(); + } else { + buf.setLength(0); + } + buf.append(d.getGroupId()).append(':').append(d.getArtifactId()).append(':'); + if (d.getClassifier() != null) { + buf.append(d.getClassifier()); + } + buf.append(':').append(d.getType()).append(':').append(d.getVersion()); + conditionalDependencies.add(buf.toString()); + } + } + private void setRequiresQuarkusCoreVersion(String compatibilityRange, ObjectNode extObject) { ObjectNode metadata = getMetadataNode(extObject); if (!metadata.has("requires-quarkus-core") && compatibilityRange != null) { diff --git a/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java new file mode 100644 index 0000000000000..9edb8dd8545e3 --- /dev/null +++ b/independent-projects/extension-maven-plugin/src/main/java/io/quarkus/maven/ExtensionDevModeMavenConfig.java @@ -0,0 +1,129 @@ +package io.quarkus.maven; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import io.quarkus.bootstrap.model.JvmOptions; +import io.quarkus.bootstrap.model.JvmOptionsBuilder; + +/** + * Extension Dev mode configuration options + */ +public class ExtensionDevModeMavenConfig { + + private JvmOptionsMap jvmOptions; + private XxJvmOptionsMap xxJvmOptions; + private List lockJvmOptions = List.of(); + private List lockXxJvmOptions = List.of(); + + /** + * Standard JVM options that should be added to the command line launching an application in Dev mode. + * + * @return standard JVM options that should be added to the command line launching an application in Dev mode + */ + public JvmOptions getJvmOptions() { + return jvmOptions == null ? null : jvmOptions.builder.build(); + } + + public void setJvmOptions(JvmOptionsMap jvmOptions) { + this.jvmOptions = jvmOptions; + } + + /** + * Non-standard JVM options that should be added to the command line launching an application in Dev mode. + * + * @return non-standard JVM options that should be added to the command line launching an application in Dev mode + */ + public JvmOptions getXxJvmOptions() { + return xxJvmOptions == null ? null : xxJvmOptions.builder.build(); + } + + public void setXxJvmOptions(XxJvmOptionsMap xxJvmOptions) { + this.xxJvmOptions = xxJvmOptions; + } + + /** + * JVM options whose default values should not be overridden by non-default values that would be set + * by default by Quarkus Maven and Gradle plugins for dev mode. + * + * @return JVM option names + */ + public List getLockJvmOptions() { + return lockJvmOptions; + } + + public void setLockJvmOptions(List lockJvmOptions) { + this.lockJvmOptions = lockJvmOptions; + } + + public boolean hasLockedJvmOptions() { + return !lockJvmOptions.isEmpty(); + } + + /** + * XX JVM options whose default values should not be overridden by non-default values that would be set + * by default by Quarkus Maven and Gradle plugins for dev mode. + * + * @return XX JVM option names + */ + public List getLockXxJvmOptions() { + return lockXxJvmOptions; + } + + public void setLockXxJvmOptions(List lockXxJvmOptions) { + this.lockXxJvmOptions = lockXxJvmOptions; + } + + public boolean hasLockedXxJvmOptions() { + return !lockXxJvmOptions.isEmpty(); + } + + /** + * This {@link java.util.Map} implementation overrides the {@link java.util.Map#put(Object, Object)} method + * to allow the users configuring a parameter with the same name more than once, merging all the configured values. + */ + public static class JvmOptionsMap extends AbstractMap { + + protected final JvmOptionsBuilder builder = JvmOptions.builder(); + + public JvmOptionsMap() { + super(); + } + + @Override + public String put(String name, String value) { + builder.add(name, nullOrTrim(value)); + return null; + } + + @Override + public Set> entrySet() { + // this is just to make Maven debug mode work with the type + var options = builder.getOptions(); + var map = new HashMap(options.size()); + for (var arg : options) { + map.put(arg.getName(), arg.getValues().toString()); + } + return map.entrySet(); + } + + static String nullOrTrim(Object value) { + return value == null ? null : String.valueOf(value).trim(); + } + } + + public static class XxJvmOptionsMap extends JvmOptionsMap { + + public XxJvmOptionsMap() { + super(); + } + + @Override + public String put(String name, String value) { + builder.addXxOption(name, nullOrTrim(value)); + return null; + } + } +}