diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java index 2ad7ad8d7..a86965ecf 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java @@ -41,6 +41,7 @@ package org.graalvm.buildtools.gradle; +import java.util.List; import org.graalvm.buildtools.VersionInfo; import org.graalvm.buildtools.gradle.dsl.AgentConfiguration; import org.graalvm.buildtools.gradle.dsl.GraalVMExtension; @@ -74,6 +75,7 @@ import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.internal.provider.AbstractMinimalProvider; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.ApplicationPlugin; import org.gradle.api.plugins.JavaApplication; @@ -97,7 +99,9 @@ import javax.inject.Inject; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; @@ -507,7 +511,7 @@ private static void configureAgent(Project project, AgentCommandLineProvider cliProvider = project.getObjects().newInstance(AgentCommandLineProvider.class); Provider agent = agents.get(nativeImageOptions.getName()); cliProvider.getEnabled().set(agent); - Provider outputDir = project.getLayout().getBuildDirectory().dir(AGENT_OUTPUT_FOLDER + "/" + instrumentedTask.getName()); + Provider outputDir = agentOutputDirectoryFor(project, nativeImageOptions, instrumentedTask); cliProvider.getOutputDirectory().set(outputDir); cliProvider.getAgentOptions().set(nativeImageOptions.getAgent().getOptions()); instrumentedTask.get().getJvmArgumentProviders().add(cliProvider); @@ -528,6 +532,37 @@ private static void configureAgent(Project project, nativeImageOptions.getConfigurationFileDirectories().from(files); } + private static Provider agentOutputDirectoryFor(Project project, NativeImageOptions nativeImageOptions, TaskProvider instrumentedTask) { + return new AbstractMinimalProvider() { + @Override + public Class getType() { + return Directory.class; + } + + @Override + protected Value calculateOwnValue(ValueConsumer consumer) { + final List options = nativeImageOptions.getAgent().getOptions().getOrElse(Collections.emptyList()); + final String outputDirUnresolved = new ArrayList<>(options).stream() + .filter(option -> option.startsWith("config-output-dir")) + .findFirst() + .map(option -> { + final int firstEqualsPos = option.indexOf('='); + if (firstEqualsPos == -1) { + throw new IllegalArgumentException("agent option 'config-output-dir' is missing its value assignment '=...'."); + } + final String path = option.substring(firstEqualsPos + 1).trim(); + if (path.isEmpty()) { + throw new IllegalArgumentException("value of agent option 'config-output-dir' must not be empty."); + } + return path; + }) + .orElse(AGENT_OUTPUT_FOLDER + "/" + instrumentedTask.getName()); + + return Value.of(project.getLayout().getBuildDirectory().dir(outputDirUnresolved).get()); + } + }; + } + private static void injectTestPluginDependencies(Project project, Property testSupportEnabled) { project.afterEvaluate(p -> { if (testSupportEnabled.get()) { diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/AgentCommandLineProvider.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/AgentCommandLineProvider.java index b5e77e7bf..3d7ce05eb 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/AgentCommandLineProvider.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/AgentCommandLineProvider.java @@ -79,10 +79,14 @@ public Iterable asArguments() { if (getEnabled().get()) { File outputDir = getOutputDirectory().getAsFile().get(); List agentOptions = new ArrayList<>(getAgentOptions().getOrElse(Collections.emptyList())); - if (agentOptions.stream().map(s -> s.split("=")[0]).anyMatch(s -> s.contains("config-output-dir"))) { - throw new IllegalStateException("config-output-dir cannot be supplied as an agent option"); + + // Do not add config-output-dir when a conflicting option is already present. Otherwise, this happens: + // native-image-agent: can only once specify exactly one of trace-output=, config-output-dir= or config-merge-dir=. + final List mutuallyExclusiveOptions = Arrays.asList("config-output-dir", "trace-output", "config-merge-dir"); + if (agentOptions.stream().allMatch(option -> mutuallyExclusiveOptions.stream().noneMatch(option::startsWith))) { + agentOptions.add("config-output-dir=" + outputDir.getAbsolutePath()); } - agentOptions.add("config-output-dir=" + outputDir.getAbsolutePath()); + return Arrays.asList( "-agentlib:native-image-agent=" + String.join(",", agentOptions), "-Dorg.graalvm.nativeimage.imagecode=agent" diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java index 8699343a8..81bca9f07 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeExtension.java @@ -41,6 +41,8 @@ package org.graalvm.buildtools.maven; +import static org.graalvm.buildtools.utils.SharedConstants.AGENT_OUTPUT_FOLDER; + import org.apache.maven.AbstractMavenLifecycleParticipant; import org.apache.maven.MavenExecutionException; import org.apache.maven.execution.MavenSession; @@ -55,6 +57,7 @@ import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.BiConsumer; @@ -85,12 +88,35 @@ static String testIdsDirectory(String baseDir) { static String buildAgentArgument(String baseDir, Context context, List agentOptions) { List options = new ArrayList<>(agentOptions); - options.add("config-output-dir=" + agentOutputDirectoryFor(baseDir, context)); + // Do not add config-output-dir when a conflicting option is already present. Otherwise, this happens: + // native-image-agent: can only once specify exactly one of trace-output=, config-output-dir= or config-merge-dir=. + final List mutuallyExclusiveOptions = Arrays.asList("config-output-dir", "trace-output", "config-merge-dir"); + if (agentOptions.stream().allMatch(option -> mutuallyExclusiveOptions.stream().noneMatch(option::startsWith))) { + options.add("config-output-dir=" + agentDefaultOutputDirectoryFor(baseDir, context)); + } return "-agentlib:native-image-agent=" + String.join(",", options); } - private static String agentOutputDirectoryFor(String baseDir, Context context) { - return (baseDir + "/native/agent-output/" + context).replace('/', File.separatorChar); + private static String agentDefaultOutputDirectoryFor(String baseDir, Context context) { + return (baseDir + "/" + AGENT_OUTPUT_FOLDER + "/" + context).replace('/', File.separatorChar); + } + + private static String agentOutputDirectoryFor(List agentOptions, String baseDir, Context context) { + return agentOptions.stream() + .filter(option -> option.startsWith("config-output-dir")) + .findFirst() + .map(option -> { + int firstEqualsPos = option.indexOf('='); + if (firstEqualsPos == -1) { + throw new IllegalArgumentException("agent option 'config-output-dir' is missing its value assignment '=...'."); + } + final String path = option.substring(firstEqualsPos + 1).trim(); + if (path.isEmpty()) { + throw new IllegalArgumentException("value of agent option 'config-output-dir' must not be empty."); + } + return path; + }) + .orElseGet(() -> agentDefaultOutputDirectoryFor(baseDir, context)); } @Override @@ -114,6 +140,7 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti // Main configuration if (isAgentEnabled) { + List agentOptions = getAgentOptions(nativePlugin, Context.main, selectedOptionsName); withPlugin(build, "exec-maven-plugin", execPlugin -> updatePluginConfiguration(execPlugin, (exec, config) -> { if ("java-agent".equals(exec.getId())) { @@ -127,7 +154,6 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti // Agent argument Xpp3Dom arg = new Xpp3Dom("argument"); - List agentOptions = getAgentOptions(nativePlugin, Context.main, selectedOptionsName); arg.setValue(buildAgentArgument(target, Context.main, agentOptions)); children.add(0, arg); @@ -151,7 +177,7 @@ public void afterProjectsRead(MavenSession session) throws MavenExecutionExcepti updatePluginConfiguration(nativePlugin, (exec, configuration) -> { Context context = exec.getGoals().stream().anyMatch("test"::equals) ? Context.test : Context.main; Xpp3Dom agentResourceDirectory = findOrAppend(configuration, "agentResourceDirectory"); - agentResourceDirectory.setValue(agentOutputDirectoryFor(target, context)); + agentResourceDirectory.setValue(agentOutputDirectoryFor(agentOptions, target, context)); }); } }); @@ -242,11 +268,7 @@ private static List getAgentOptions(Plugin nativePlugin, Context context private static void processOptionNodes(Xpp3Dom options, List optionsList) { for (Xpp3Dom option : options.getChildren("option")) { - String value = assertNotEmptyAndTrim(option.getValue(), "