diff --git a/common/utils/src/main/java/org/graalvm/buildtools/agent/AgentConfiguration.java b/common/utils/src/main/java/org/graalvm/buildtools/agent/AgentConfiguration.java index 7e3dfe404..105311a43 100644 --- a/common/utils/src/main/java/org/graalvm/buildtools/agent/AgentConfiguration.java +++ b/common/utils/src/main/java/org/graalvm/buildtools/agent/AgentConfiguration.java @@ -49,21 +49,33 @@ public class AgentConfiguration implements Serializable { private final Collection callerFilterFiles; private final Collection accessFilterFiles; - private final boolean builtinCallerFilter; - private final boolean builtinHeuristicFilter; - private final boolean experimentalPredefinedClasses; - private final boolean experimentalUnsafeAllocationTracing; - private final boolean trackReflectionMetadata; + private final Boolean builtinCallerFilter; + private final Boolean builtinHeuristicFilter; + private final Boolean experimentalPredefinedClasses; + private final Boolean experimentalUnsafeAllocationTracing; + private final Boolean trackReflectionMetadata; private final AgentMode agentMode; + // This constructor should be used only to specify that we have instance of agent that is disabled (to avoid using null for agent enable check) + public AgentConfiguration(AgentMode ...modes) { + this.callerFilterFiles = null; + this.accessFilterFiles = null; + this.builtinCallerFilter = null; + this.builtinHeuristicFilter = null; + this.experimentalPredefinedClasses = null; + this.experimentalUnsafeAllocationTracing = null; + this.trackReflectionMetadata = null; + this.agentMode = modes.length == 1 ? modes[0] : new DisabledAgentMode(); + } + public AgentConfiguration(Collection callerFilterFiles, Collection accessFilterFiles, - boolean builtinCallerFilter, - boolean builtinHeuristicFilter, - boolean experimentalPredefinedClasses, - boolean experimentalUnsafeAllocationTracing, - boolean trackReflectionMetadata, + Boolean builtinCallerFilter, + Boolean builtinHeuristicFilter, + Boolean experimentalPredefinedClasses, + Boolean experimentalUnsafeAllocationTracing, + Boolean trackReflectionMetadata, AgentMode agentMode) { this.callerFilterFiles = callerFilterFiles; this.accessFilterFiles = accessFilterFiles; @@ -79,11 +91,11 @@ public List getAgentCommandLine() { List cmdLine = new ArrayList<>(agentMode.getAgentCommandLine()); appendOptionToValues("caller-filter-file=", callerFilterFiles, cmdLine); appendOptionToValues("access-filter-file=", accessFilterFiles, cmdLine); - cmdLine.add("builtin-caller-filter=" + builtinCallerFilter); - cmdLine.add("builtin-heuristic-filter=" + builtinHeuristicFilter); - cmdLine.add("experimental-class-define-support=" + experimentalPredefinedClasses); - cmdLine.add("experimental-unsafe-allocation-support=" + experimentalUnsafeAllocationTracing); - cmdLine.add("track-reflection-metadata=" + trackReflectionMetadata); + addToCmd("builtin-caller-filter=", builtinCallerFilter, cmdLine); + addToCmd("builtin-heuristic-filter=", builtinHeuristicFilter, cmdLine); + addToCmd("experimental-class-define-support=", experimentalPredefinedClasses, cmdLine); + addToCmd("experimental-unsafe-allocation-support=", experimentalUnsafeAllocationTracing, cmdLine); + addToCmd("track-reflection-metadata=", trackReflectionMetadata, cmdLine); return cmdLine; } @@ -100,10 +112,19 @@ public boolean isEnabled() { } public static void appendOptionToValues(String option, Collection values, Collection target) { - values.stream().map(value -> option + value).forEach(target::add); + if (values != null) { + values.stream().map(value -> option + value).forEach(target::add); + } } public AgentMode getAgentMode() { return agentMode; } + + private void addToCmd(String option, Boolean value, List cmdLine) { + if (value != null) { + cmdLine.add(option + value); + } + } + } diff --git a/docs/src/docs/asciidoc/maven-plugin.adoc b/docs/src/docs/asciidoc/maven-plugin.adoc index d95ee0342..f8ab0af31 100644 --- a/docs/src/docs/asciidoc/maven-plugin.adoc +++ b/docs/src/docs/asciidoc/maven-plugin.adoc @@ -478,7 +478,7 @@ The Native Image Maven plugin simplifies generation of the required configuratio injecting the agent automatically for you (this includes, but is not limited to the reflection file). -The agent generates the native configuration files in a subdirectory of +The agent generates the native configuration files in subdirectories of `target/native/agent-output`. Although those files will be automatically used if you run your build with the agent enabled, you should consider reviewing the generated files and adding them to your sources instead. @@ -514,47 +514,56 @@ line by supplying the `-Dagent=false` flag. === Configuring agent options If you would like to configure the options for the agent -- for example, to configure -experimental features such as `experimental-class-loader-support` or advanced features +agent mode or advanced features such as -https://www.graalvm.org/reference-manual/native-image/Agent/#caller-based-filters[Caller-based Filters] -and https://www.graalvm.org/reference-manual/native-image/Agent/#access-filters[Access Filters] --- you can include `` within the `` block of the configuration of the -`native-maven-plugin` in your POM. - -* You can supply multiple sets of ``. -* You can declare an unnamed `` element which will always be used whenever the - agent is enabled. This should be used to declare common options that will be used for - all executions with the agent. -* Additional `` elements must declare a unique `name` attribute. - - To configure options for your application, use the name `main`. - - To configure options for your tests, use the name `test`. - - To configure additional sets of options, declare each with a unique name other than - `main` or `test`. -* The `main` options are enabled automatically whenever your application is run with the - agent. -* The `test` options are enabled automatically whenever your tests are run with the agent. -* To enable any other set of named ``, supply `-DagentOptions=` as a - command-line argument for Maven, where `` corresponds to the `name` attribute of - the `` element. - -[WARNING] -==== -The Native Image Maven plugin automatically configures the `config-output-dir` for the -agent. An attempt to configure a custom value for the `config-output-dir` option will -therefore result in a build failure. -==== +https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/#caller-based-filters[Caller-based Filters] +and https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/#access-filters[Access Filters] +- you can specify them in your POM as described bellow. + +[source,xml,indent=0] +include::../snippets/maven/pom.xml[tag=native-plugin-agent-configuration] + +This example shows all possibilities you can use for native agent configuration. If you don't need some of the options, +just remove them. -The following example is likely more complex than anything you would do in your own -projects, but it demonstrates how to configure four sets of ``. +Agent can be run in one of the following modes: -* The unnamed set is always active. -* The `main` set is automatically active for application execution. -* The `test` set is automatically active for test execution. -* The `periodic-config` set is never active by default, but it can be enabled via - `-DagentOptions=periodic-config` on the command line. +* `standard` - in this mode you run agent only with options provided in `options` section. +* `direct` - in this mode you can provide command line that you want to be executed as an agent configuration. In this + mode, user is fully responsible for agent configuration, and the rest of the agent configuration, provided in pom.xml file, will be ignored. +* `conditional` - in this mode you can provide additional files that can be used as a filter for the agent output. You +can read more about conditional mode https://www.graalvm.org/latest/reference-manual/native-image/metadata/ExperimentalAgentOptions/[here]. + +Each option and how you should use it is described bellow: + +* `enabled` - a simple flag that specifies whether the agent is enabled or not. Can be set to true or false. +* `defaultMode` - agent mode switcher. Can be set to: standard, direct or conditional. +* `modes` - list of additional mode options, specific for certain mode type. Inside this tag, you can specify options for + direct or conditional modes. Standard mode doesn't have any specific options. + ** in case of `direct` mode you can specify `` tag, with the agent command line as its value + ** in case of `conditional` mode you can specify `` tag, and set additional https://github.com/oracle/graalvm-reachability-metadata/blob/master/docs/CollectingMetadata.md[filter files] + inside `` and ``. Also, you can set value for `` tag. If the value is true, agent will create + partial-config file, and merge it with conditional merge, otherwise agent will generate same kind of output as other modes. +* `options` - list of options that can be specify independent from agent mode. More about common options can be found +https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/[here]. + + +[[metadata-copy]] +==== Metadata copy +`metadataCopy` provides additional options for manipulating the agent output after agent finishes its job. [source,xml,indent=0] -include::../../../../samples/java-application-with-reflection/pom.xml[tag=native-plugin-agent-options] +include::../snippets/maven/metadataCopy.xml[tag=native-plugin-agent-metadata-copy] + +You can set values for the following tags: + +* `` - where you want to copy agent output. +* `` - in case you already have some other config files inside `output directory`, you can choose whether you want to override those files or + merge new files with the existing once (set merge value to true). +* `` - in case you don't want to copy output of the certain stage (main or test) you can disable them and metadataCopy will not look at + the agent output for that stage. For example, if you want to copy only config files generated in tests, you can disable main stage. Also, if you want + to copy only files generated in main phase, you can disable test stage. Therefore, if you skip both stages, metadataCopy will not be executed. + [[agent-support-running-tests]] === Running tests with the agent @@ -572,13 +581,14 @@ When the `agent` system property is set to `true` (or when the agent is to your Maven Surefire test execution, and the generated files can be found in the `target/native/agent-output/test` directory. -To run your tests with custom agent options, supply the `-DagentOptions=` -command-line argument to Maven as follows. See the documentation for -<> for details. - +[TIP] +==== +If you want to run metadataCopy as well, first define its configuration as described <> and add `native:metadata-copy` +at the end of the agent invocation command. Example: ```bash -mvn -Pnative -Dagent=true -DagentOptions=periodic-config test +mvn -Pnative -Dagent=true test native:metadata-copy ``` +==== [[agent-support-running-application]] === Running your application with the agent @@ -597,14 +607,6 @@ Then you can execute your application with the agent by running: mvn -Pnative -Dagent=true -DskipTests -DskipNativeBuild=true package exec:exec@java-agent ``` -To execute your application with custom agent options, supply the `-DagentOptions=` -command-line argument to Maven as follows. See the documentation for -<> for details. - -```bash -mvn -Pnative -Dagent=true -DagentOptions=periodic-config -DskipTests -DskipNativeBuild=true package exec:exec@java-agent -``` - Both of the above commands will generate configuration files in the `target/native/agent-output/main` directory. If you want to run your native application with those configuration files, you then need to execute the following command: diff --git a/docs/src/docs/snippets/maven/metadataCopy.xml b/docs/src/docs/snippets/maven/metadataCopy.xml new file mode 100644 index 000000000..fbd30ccf5 --- /dev/null +++ b/docs/src/docs/snippets/maven/metadataCopy.xml @@ -0,0 +1,9 @@ + + + + main + + true + /tmp/test-output-dir + + \ No newline at end of file diff --git a/docs/src/docs/snippets/maven/pom.xml b/docs/src/docs/snippets/maven/pom.xml new file mode 100644 index 000000000..18a398ec6 --- /dev/null +++ b/docs/src/docs/snippets/maven/pom.xml @@ -0,0 +1,36 @@ + + + + true + Standard + + config-output-dir=${project.build.directory}/native/agent-output + + user-code-filter.json + extra-filter.json + true + + + + + caller-filter-file1.json + caller-filter-file2.json + + + access-filter-file1.json + access-filter-file2.json + + true + true + true + + true + + true + + + + + + + diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithAgentFunctionalTest.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithAgentFunctionalTest.groovy index a0db0dcf8..26c9d08dc 100644 --- a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithAgentFunctionalTest.groovy +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JavaApplicationWithAgentFunctionalTest.groovy @@ -78,218 +78,107 @@ class JavaApplicationWithAgentFunctionalTest extends AbstractGraalVMMavenFunctio outputDoesNotContain "containers found" } - def "agent is used for tests when enabled in POM without custom options"() { + def "test agent with metadata copy task"() { given: withSample("java-application-with-reflection") + mvn'-Pnative', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' when: - // Run Maven in debug mode (-X) in order to capture the command line arguments - // used to launch Surefire with the agent. - mvnDebug '-Pnative', 'test' + mvn'-Pnative', '-DskipNativeTests', 'native:metadata-copy' then: - outputContains """ -[ 4 containers found ] -[ 0 containers skipped ] -[ 4 containers started ] -[ 0 containers aborted ] -[ 4 containers successful ] -[ 0 containers failed ] -[ 7 tests found ] -[ 0 tests skipped ] -[ 7 tests started ] -[ 0 tests aborted ] -[ 7 tests successful ] -[ 0 tests failed ] -""".trim() - - and: - ['jni', 'proxy', 'reflect', 'resource', 'serialization'].each { name -> - assert file("target/native/agent-output/test/${name}-config.json").exists() - } - - and: - outputContains '-agentlib:native-image-agent' - // From shared, unnamed config: - outputContains '=experimental-class-loader-support' - // From test config: - outputContains ',access-filter-file=' - outputContains '/src/test/resources/access-filter.json'.replace('/', java.io.File.separator) - // Always configured: - outputContains ',config-output-dir=' - outputContains '/target/native/agent-output/test'.replace("/", java.io.File.separator) - - and: - // If the custom access-filter.json is applied, we should not see any warnings about Surefire types. - // The actual warning would be something like: - // Warning: Could not resolve org.apache.maven.surefire.junitplatform.JUnitPlatformProvider for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.maven.surefire.junitplatform.JUnitPlatformProvider. - outputDoesNotContain 'Warning: Could not resolve org.apache.maven.surefire' - // From periodic-config: - outputDoesNotContain ',config-write-period-secs=30,config-write-initial-delay-secs=5' + buildSucceeded + outputDoesNotContain "Cannot merge agent files because native-image-configure is not installed. Please upgrade to a newer version of GraalVM." + outputDoesNotContain "returned non-zero result" + outputDoesNotContain "Agent files cannot be copied." + outputDoesNotContain "Cannot collect agent configuration." + outputContains "Metadata copy process finished." } - def "agent is not used for tests when enabled in POM but disabled via the command line"() { + def "test agent with metadata copy task and disabled stages"() { given: withSample("java-application-with-reflection") + mvn'-PagentConfigurationWithDisabledStages', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' when: - mvn '-Pnative', '-Dagent=false', 'test' + mvn '-PagentConfigurationWithDisabledStages', '-DskipNativeTests', 'native:metadata-copy' then: - outputContains """ -[ 4 containers found ] -[ 0 containers skipped ] -[ 4 containers started ] -[ 0 containers aborted ] -[ 4 containers successful ] -[ 0 containers failed ] -[ 7 tests found ] -[ 0 tests skipped ] -[ 7 tests started ] -[ 0 tests aborted ] -[ 6 tests successful ] -[ 1 tests failed ] -""".trim() - - and: - outputContains 'expected: but was: ' + buildSucceeded + outputDoesNotContain "Cannot collect agent configuration." + outputDoesNotContain "Cannot merge agent files because native-image-configure is not installed. Please upgrade to a newer version of GraalVM." + outputDoesNotContain "returned non-zero result" + outputDoesNotContain "Agent files cannot be copied." + outputContains "Copying files from: test" + outputContains "Metadata copy process finished." } - def "agent is used for tests when enabled in POM with custom options"() { + def "test agent in direct mode with metadata copy task"() { given: withSample("java-application-with-reflection") + mvn'-PagentConfigurationDirectMode', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' when: - // Run Maven in debug mode (-X) in order to capture the command line arguments - // used to launch Surefire with the agent. - mvnDebug '-Pnative', '-DagentOptions=periodic-config', 'test' + mvn '-PagentConfigurationDirectMode', '-DskipNativeTests', 'native:metadata-copy' then: - outputContains """ -[ 4 containers found ] -[ 0 containers skipped ] -[ 4 containers started ] -[ 0 containers aborted ] -[ 4 containers successful ] -[ 0 containers failed ] -[ 7 tests found ] -[ 0 tests skipped ] -[ 7 tests started ] -[ 0 tests aborted ] -[ 7 tests successful ] -[ 0 tests failed ] -""".trim() - - and: - ['jni', 'proxy', 'reflect', 'resource', 'serialization'].each { name -> - assert file("target/native/agent-output/test/${name}-config.json").exists() - } - - and: - // If custom agent options are processed, the debug output for Surefire - // should include the following segments of the agent command line argument. - outputContains '-agentlib:native-image-agent' - // From shared, unnamed config: - outputContains '=experimental-class-loader-support' - // From test config: - outputContains ',access-filter-file=' - outputContains '/src/test/resources/access-filter.json'.replace('/', java.io.File.separator) - // From periodic-config: - outputContains ',config-write-period-secs=30,config-write-initial-delay-secs=5' - // Always configured: - outputContains ',config-output-dir=' - outputContains '/target/native/agent-output/test'.replace("/", java.io.File.separator) - - and: - // If the custom access-filter.json is applied, we should not see any warnings about Surefire types. - // The actual warning would be something like: - // Warning: Could not resolve org.apache.maven.surefire.junitplatform.JUnitPlatformProvider for reflection configuration. Reason: java.lang.ClassNotFoundException: org.apache.maven.surefire.junitplatform.JUnitPlatformProvider. - outputDoesNotContain 'Warning: Could not resolve org.apache.maven.surefire' + buildSucceeded + outputDoesNotContain "Cannot collect agent configuration." + outputDoesNotContain "Cannot merge agent files because native-image-configure is not installed. Please upgrade to a newer version of GraalVM." + outputDoesNotContain "returned non-zero result" + outputContains "You are running agent in direct mode. Skipping both merge and metadata copy tasks." } - @Issue("https://github.com/graalvm/native-build-tools/issues/134") - @Unroll("generated agent files are added when building native image on Maven #version with JUnit Platform #junitVersion") - def "generated agent files are used when building native image"() { + def "test agent in conditional mode with metadata copy task"() { given: withSample("java-application-with-reflection") + mvn '-PagentConfigurationConditionalMode', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' when: - mvnDebug '-Pnative', '-DskipTests=true', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' + mvn '-PagentConfigurationConditionalMode', '-DskipNativeTests', 'native:metadata-copy' then: - ['jni', 'proxy', 'reflect', 'resource', 'serialization'].each { name -> - assert file("target/native/agent-output/main/${name}-config.json").exists() - } - - and: - // If custom agent options are not used, the Maven debug output should include - // the following segments of the agent command line argument. - outputContains '-agentlib:native-image-agent' - // From shared, unnamed config: - outputContains '=experimental-class-loader-support' - // From main config: - outputContains ',access-filter-file=' - outputContains '/src/main/resources/access-filter.json'.replace('/', java.io.File.separator) - // Always configured: - outputContains ',config-output-dir=' - outputContains '/target/native/agent-output/main'.replace("/", java.io.File.separator) - - when: - mvn '-Pnative', '-DskipTests=true', 'package', 'exec:exec@native' - - then: - outputContains "Application message: Hello, native!" + buildSucceeded + outputDoesNotContain "Cannot collect agent configuration." + outputDoesNotContain "Cannot merge agent files because native-image-configure is not installed. Please upgrade to a newer version of GraalVM." + outputDoesNotContain "returned non-zero result" } - def "generated agent files are not used when building native image when agent is enabled in POM but disabled via the command line"() { + def "test without agent configuration"() { given: withSample("java-application-with-reflection") when: - mvnDebug '-Pnative', '-Dagent=false', '-DskipTests=true', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' + mvn'-PnoAgentConfiguration', 'package' then: - outputDoesNotContain '-agentlib:native-image-agent' - - when: - mvn '-Pnative', '-DskipTests=true', 'package', 'exec:exec@native' - - then: - outputContains "Application message: null" + buildSucceeded } - def "custom options and generated agent files are used when building native image"() { + def "agent is not used for tests when enabled in POM but disabled via the command line"() { given: withSample("java-application-with-reflection") when: - mvnDebug '-Pnative', '-DagentOptions=periodic-config', '-DskipTests=true', '-DskipNativeBuild=true', 'package', 'exec:exec@java-agent' + mvn '-Pnative', '-Dagent=false', 'test' then: - ['jni', 'proxy', 'reflect', 'resource', 'serialization'].each { name -> - assert file("target/native/agent-output/main/${name}-config.json").exists() - } + outputContains """ +[ 4 containers found ] +[ 0 containers skipped ] +[ 4 containers started ] +[ 0 containers aborted ] +[ 4 containers successful ] +[ 0 containers failed ] +[ 7 tests found ] +[ 0 tests skipped ] +[ 7 tests started ] +[ 0 tests aborted ] +[ 6 tests successful ] +[ 1 tests failed ] +""".trim() and: - // If custom agent options are used, the Maven debug output should include - // the following segments of the agent command line argument. - outputContains '-agentlib:native-image-agent' - // From shared, unnamed config: - outputContains '=experimental-class-loader-support' - // From main config: - outputContains ',access-filter-file=' - outputContains '/src/main/resources/access-filter.json'.replace('/', java.io.File.separator) - // From periodic-config: - outputContains ',config-write-period-secs=30,config-write-initial-delay-secs=5' - // Always configured: - outputContains ',config-output-dir=' - outputContains '/target/native/agent-output/main'.replace("/", java.io.File.separator) - - when: - mvn '-Pnative', '-DskipTests=true', 'package', 'exec:exec@native' - - then: - outputContains "Application message: Hello, native!" + outputContains 'expected: but was: ' } - } diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java index ed71a1564..cdd58e386 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java @@ -41,43 +41,42 @@ package org.graalvm.buildtools.maven; -import java.io.BufferedReader; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.toolchain.ToolchainManager; +import org.graalvm.buildtools.maven.config.ExcludeConfigConfiguration; +import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; +import org.graalvm.buildtools.utils.NativeImageUtils; +import org.graalvm.buildtools.utils.SharedConstants; + +import javax.inject.Inject; import java.io.File; -import java.io.IOException; import java.io.InputStream; +import java.io.BufferedReader; import java.io.InputStreamReader; +import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.inject.Inject; - -import org.apache.maven.artifact.Artifact; -import org.apache.maven.execution.MavenSession; -import org.apache.maven.plugin.MojoExecution; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.descriptor.PluginDescriptor; -import org.apache.maven.plugins.annotations.Component; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.toolchain.ToolchainManager; -import org.graalvm.buildtools.Utils; -import org.graalvm.buildtools.maven.config.ExcludeConfigConfiguration; -import org.graalvm.buildtools.utils.NativeImageUtils; -import org.graalvm.buildtools.utils.SharedConstants; - /** * @author Sebastien Deleuze */ @@ -383,7 +382,7 @@ protected String getClasspath() throws MojoExecutionException { protected void buildImage() throws MojoExecutionException { checkRequiredVersionIfNeeded(); - Path nativeImageExecutable = Utils.getNativeImage(logger); + Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger); try { ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString()); @@ -419,7 +418,7 @@ protected void checkRequiredVersionIfNeeded() throws MojoExecutionException { if (requiredVersion == null) { return; } - Path nativeImageExecutable = Utils.getNativeImage(logger); + Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger); try { ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString()); processBuilder.command().add("--version"); diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MergeAgentFilesMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MergeAgentFilesMojo.java index ad4707015..d5b5a19f4 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MergeAgentFilesMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MergeAgentFilesMojo.java @@ -40,32 +40,29 @@ */ package org.graalvm.buildtools.maven; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; -import org.codehaus.plexus.logging.Logger; import org.codehaus.plexus.util.FileUtils; -import org.graalvm.buildtools.Utils; -import org.graalvm.buildtools.utils.NativeImageUtils; +import org.graalvm.buildtools.maven.config.AbstractMergeAgentFilesMojo; +import org.graalvm.buildtools.maven.config.agent.AgentConfiguration; +import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.Collections; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.graalvm.buildtools.maven.NativeExtension.agentOutputDirectoryFor; -import static org.graalvm.buildtools.utils.NativeImageUtils.nativeImageConfigureFileName; - @Mojo(name = "merge-agent-files", defaultPhase = LifecyclePhase.TEST) -public class MergeAgentFilesMojo extends AbstractMojo { +public class MergeAgentFilesMojo extends AbstractMergeAgentFilesMojo { @Parameter(defaultValue = "${project}", readonly = true, required = true) protected MavenProject project; @@ -75,48 +72,63 @@ public class MergeAgentFilesMojo extends AbstractMojo { @Parameter(property = "native.agent.merge.context", required = true) protected String context; - @Component - protected Logger logger; + @Parameter(alias = "agent") + private AgentConfiguration agentConfiguration; + private static int numberOfExecutions = 0; @Override public void execute() throws MojoExecutionException { - String agentOutputDirectory = agentOutputDirectoryFor(target, NativeExtension.Context.valueOf(context)); + // we need this mojo to be executed only once + numberOfExecutions++; + if (numberOfExecutions > 1) { + return; + } + + // if we reached here and agent config is null, agent is enabled but there is no configuration in pom.xml + // that means that we enabled agent from command line, so we are using default agent configuration + if (agentConfiguration == null) { + agentConfiguration = new AgentConfiguration(); + } + + + if (agentConfiguration.getDefaultMode().equalsIgnoreCase("direct")) { + logger.info("Skipping files merge mojo since we are in direct mode"); + return; + } + + List disabledPhases = agentConfiguration.getMetadataCopyConfiguration().getDisabledStages(); + if (disabledPhases.size() == 2) { + logger.info("Both phases are skipped."); + return; + } + + Set dirs = new HashSet(2); + dirs.addAll(Arrays.asList("main", "test")); + dirs.removeAll(disabledPhases); + + for (String dir : dirs) { + String agentOutputDirectory = (target + "/native/agent-output/" + dir).replace('/', File.separatorChar); + mergeForGivenDir(agentOutputDirectory); + } + } + + private void mergeForGivenDir(String agentOutputDirectory) throws MojoExecutionException { File baseDir = new File(agentOutputDirectory); if (baseDir.exists()) { - Path nativeImageExecutable = Utils.getNativeImage(logger); - File mergerExecutable = tryInstall(nativeImageExecutable); + Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger); + tryInstallMergeExecutable(nativeImageExecutable); List sessionDirectories = sessionDirectoriesFrom(baseDir.listFiles()).collect(Collectors.toList()); + if (sessionDirectories.size() == 0) { + sessionDirectories = Collections.singletonList(baseDir); + } + invokeMerge(mergerExecutable, sessionDirectories, baseDir); } else { getLog().debug("Agent output directory " + baseDir + " doesn't exist. Skipping merge."); } } - private File tryInstall(Path nativeImageExecutablePath) { - File nativeImageExecutable = nativeImageExecutablePath.toAbsolutePath().toFile(); - File mergerExecutable = new File(nativeImageExecutable.getParentFile(), nativeImageConfigureFileName()); - if (!mergerExecutable.exists()) { - getLog().info("Installing native image merger to " + mergerExecutable); - ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString()); - processBuilder.command().add("--macro:native-image-configure-launcher"); - processBuilder.directory(mergerExecutable.getParentFile()); - processBuilder.inheritIO(); - - try { - Process installProcess = processBuilder.start(); - if (installProcess.waitFor() != 0) { - getLog().warn("Installation of native image merging tool failed"); - } - NativeImageUtils.maybeCreateConfigureUtilSymlink(mergerExecutable, nativeImageExecutablePath); - } catch (IOException | InterruptedException e) { - // ignore since we will handle that if the installer doesn't exist later - } - - } - return mergerExecutable; - } - private static Stream sessionDirectoriesFrom(File[] files) { return Arrays.stream(files) .filter(File::isDirectory) @@ -133,28 +145,33 @@ private void invokeMerge(File mergerExecutable, List inputDirectories, Fil getLog().warn("Skipping merging of agent files since there are no input directories."); return; } + getLog().info("Merging agent " + inputDirectories.size() + " files into " + outputDirectory); - List args = new ArrayList<>(inputDirectories.size() + 2); - args.add("generate"); - inputDirectories.stream() - .map(f -> "--input-dir=" + f.getAbsolutePath()) - .forEach(args::add); - args.add("--output-dir=" + outputDirectory.getAbsolutePath()); + List optionsInputDirs = inputDirectories.stream().map(File::getAbsolutePath).collect(Collectors.toList()); + List optionsOutputDirs = Collections.singletonList(outputDirectory.getAbsolutePath()); + List args = agentConfiguration.getAgentMode().getNativeImageConfigureOptions(optionsInputDirs, optionsOutputDirs); + ProcessBuilder processBuilder = new ProcessBuilder(mergerExecutable.toString()); processBuilder.command().addAll(args); processBuilder.inheritIO(); - String commandString = String.join(" ", processBuilder.command()); Process imageBuildProcess = processBuilder.start(); if (imageBuildProcess.waitFor() != 0) { throw new MojoExecutionException("Execution of " + commandString + " returned non-zero result"); } - for (File inputDirectory : inputDirectories) { - FileUtils.deleteDirectory(inputDirectory); + + // in case inputDirectories has only one value which is the same as outputDirectory + // we shouldn't delete that directory, because we will delete outputDirectory + if (!(inputDirectories.size() == 1 && inputDirectories.get(0).equals(outputDirectory))) { + for (File inputDirectory : inputDirectories) { + FileUtils.deleteDirectory(inputDirectory); + } } + getLog().debug("Agent output: " + Arrays.toString(outputDirectory.listFiles())); } catch (IOException | InterruptedException e) { throw new MojoExecutionException("Merging agent files with " + mergerExecutable + " failed", e); } } + } diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MetadataCopyMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MetadataCopyMojo.java new file mode 100644 index 000000000..8b4150873 --- /dev/null +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/MetadataCopyMojo.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.buildtools.maven; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.graalvm.buildtools.agent.StandardAgentMode; +import org.graalvm.buildtools.maven.config.AbstractMergeAgentFilesMojo; +import org.graalvm.buildtools.maven.config.agent.AgentConfiguration; +import org.graalvm.buildtools.maven.config.agent.MetadataCopyConfiguration; +import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Objects; +import java.util.stream.Collectors; + +@Mojo(name = "metadata-copy", defaultPhase = LifecyclePhase.PREPARE_PACKAGE) +public class MetadataCopyMojo extends AbstractMergeAgentFilesMojo { + + private static final String DEFAULT_OUTPUT_DIRECTORY = "/META-INF/native-image"; + private static final List FILES_REQUIRED_FOR_MERGE = Arrays.asList("reflect-config.json", "jni-config.json", "proxy-config.json", "resource-config.json"); + + @Parameter(alias = "agent") + private AgentConfiguration agentConfiguration; + + @Parameter(defaultValue = "${project}", readonly = true, required = true) + private MavenProject project; + + @Override + public void execute() throws MojoExecutionException { + if (agentConfiguration != null && agentConfiguration.isEnabled()) { + // in direct mode user is fully responsible for agent configuration, and we will not execute anything besides line that user provided + if (agentConfiguration.getDefaultMode().equalsIgnoreCase("direct")) { + logger.info("You are running agent in direct mode. Skipping both merge and metadata copy tasks."); + logger.info("In direct mode, user takes full responsibility for agent configuration."); + return; + } + + MetadataCopyConfiguration config = agentConfiguration.getMetadataCopyConfiguration(); + if (config == null) { + getLog().info("Metadata copy config not provided. Skipping this task."); + return; + } + + String buildDirectory = project.getBuild().getDirectory() + "/native/agent-output/"; + String destinationDir = config.getOutputDirectory(); + if (destinationDir == null) { + destinationDir = project.getBuild().getOutputDirectory() + DEFAULT_OUTPUT_DIRECTORY; + } + + if (!Files.isDirectory(Paths.get(destinationDir))) { + logger.warn("Destination directory " + destinationDir + " doesn't exist."); + logger.warn("Creating directory at: " + destinationDir); + boolean success = new File(destinationDir).mkdirs(); + if (!success) { + throw new MojoExecutionException("Cannot create directory at the given location: " + destinationDir); + } + } + + Path nativeImageExecutable = NativeImageConfigurationUtils.getNativeImage(logger); + tryInstallMergeExecutable(nativeImageExecutable); + executeCopy(buildDirectory, destinationDir); + getLog().info("Metadata copy process finished."); + } + } + + private void executeCopy(String buildDirectory, String destinationDir) throws MojoExecutionException { + MetadataCopyConfiguration config = agentConfiguration.getMetadataCopyConfiguration(); + List sourceDirectories = getSourceDirectories(config.getDisabledStages(), buildDirectory); + + // in case we have both main and test phase disabled, we don't need to copy anything + if (sourceDirectories.isEmpty()) { + logger.warn("Skipping metadata copy task. Both main and test stages are disabled in metadata copy configuration."); + return; + } + + // In case user wants to merge agent-output files with some existing files in output directory, we need to check if there are some + // files in outputDirectory that can be merged. If the output directory is empty, we ignore user instruction to merge files. + if (config.shouldMerge() && !isDirectoryEmpty(destinationDir)) { + // If output directory contains some files, we need to check if the directory contains all necessary files for merge + if (!dirContainsFilesForMerge(destinationDir)) { + List destinationDirContent = Arrays.stream(Objects.requireNonNull(new File(destinationDir).listFiles())).map(File::getName).collect(Collectors.toList()); + List missingFiles = getListDiff(FILES_REQUIRED_FOR_MERGE, destinationDirContent); + + throw new MojoExecutionException("There are missing files for merge in output directory. If you want to merge agent files with " + + "existing files in output directory, please make sure that output directory contains all of the following files: " + + "reflect-config.json, jni-config.json, proxy-config.json, resource-config.json. Currently the output directory is " + + "missing: " + missingFiles); + } + + sourceDirectories.add(destinationDir); + } + + if (!checkIfSourcesExists(sourceDirectories)) { + return; + } + + String sourceDirsInfo = sourceDirectories.stream().map(File::new).map(File::getName).collect(Collectors.joining(", ")); + logger.info("Copying files from: " + sourceDirsInfo); + + List nativeImageConfigureOptions = new StandardAgentMode().getNativeImageConfigureOptions(sourceDirectories, Collections.singletonList(destinationDir)); + nativeImageConfigureOptions.add(0, mergerExecutable.getAbsolutePath()); + ProcessBuilder processBuilder = new ProcessBuilder(nativeImageConfigureOptions); + + try { + Process start = processBuilder.start(); + int retCode = start.waitFor(); + if (retCode != 0) { + getLog().error("Metadata copy process failed with code: " + retCode); + throw new MojoExecutionException("Metadata copy process failed."); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private boolean checkIfSourcesExists(List sourceDirectories) { + for (String source : sourceDirectories) { + File dir = new File(source); + if (!dir.isDirectory() || !dir.exists()) { + logger.warn("Cannot find source directory " + source + " for metadata copy. Please check if you configured agent" + + " properly and it generates all necessary directories. If you want to skipp copy from some source, please " + + "configure metadataCopy with disable stage you want to skipp."); + return false; + } + } + + return true; + } + + private List getSourceDirectories(List disabledStages, String buildDirectory) { + List sourceDirectories = new ArrayList<>(); + + sourceDirectories.add(buildDirectory + NativeExtension.Context.main); + sourceDirectories.add(buildDirectory + NativeExtension.Context.test); + + for (String disabledStage : disabledStages) { + sourceDirectories.remove(buildDirectory + disabledStage); + } + + return sourceDirectories; + } + + private boolean isDirectoryEmpty(String dirName) { + File directory = new File(dirName); + File[] content = directory.listFiles(); + + return content == null || content.length == 0; + } + + //check if we have all files needed for native-image-configure generate tool + private boolean dirContainsFilesForMerge(String dir) { + File baseDir = new File(dir); + File[] content = baseDir.listFiles(); + if (content == null) { + return false; + } + List dirContent = Arrays.stream(content).map(File::getName).collect(Collectors.toList()); + + return getListDiff(FILES_REQUIRED_FOR_MERGE, dirContent).isEmpty(); + } + + private List getListDiff(List list1, List list2) { + List diff = new ArrayList<>(list1); + diff.removeAll(list2); + return diff; + } + +} 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 56d81d58b..cae7fba8d 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 @@ -52,7 +52,8 @@ import org.codehaus.plexus.logging.LogEnabled; import org.codehaus.plexus.logging.Logger; import org.codehaus.plexus.util.xml.Xpp3Dom; -import org.graalvm.buildtools.Utils; +import org.graalvm.buildtools.agent.AgentConfiguration; +import org.graalvm.buildtools.utils.AgentUtils; import org.graalvm.buildtools.utils.SharedConstants; import java.io.File; @@ -61,6 +62,9 @@ import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.graalvm.buildtools.utils.NativeImageConfigurationUtils.getNativeImage; /** * This extension is responsible for configuring the Surefire plugin to enable @@ -105,7 +109,8 @@ static String buildAgentArgument(String baseDir, Context context, List a // the same unit of work !). effectiveOutputDir = effectiveOutputDir + File.separator + SharedConstants.AGENT_SESSION_SUBDIR; } - options.add("config-output-dir=" + effectiveOutputDir); + String finalEffectiveOutputDir = effectiveOutputDir; + options = options.stream().map(option -> option.contains("{output_dir}") ? option.replace("{output_dir}", finalEffectiveOutputDir) : option).collect(Collectors.toList()); return "-agentlib:native-image-agent=" + String.join(",", options); } @@ -120,20 +125,26 @@ public void afterProjectsRead(MavenSession session) { withPlugin(build, "native-maven-plugin", nativePlugin -> { String target = build.getDirectory(); String testIdsDir = testIdsDirectory(target); - boolean isAgentEnabled = isAgentEnabled(session, nativePlugin); - String selectedOptionsName = getSelectedOptionsName(session); + + Xpp3Dom configurationRoot = (Xpp3Dom) nativePlugin.getConfiguration(); + AgentConfiguration agent; + try { + agent = AgentUtils.collectAgentProperties(session, configurationRoot); + } catch (Exception e) { + throw new RuntimeException(e); + } // Test configuration withPlugin(build, "maven-surefire-plugin", surefirePlugin -> { configureJunitListener(surefirePlugin, testIdsDir); - if (isAgentEnabled) { - List agentOptions = getAgentOptions(nativePlugin, Context.test, selectedOptionsName); + if (agent.isEnabled()) { + List agentOptions = agent.getAgentCommandLine(); configureAgentForSurefire(surefirePlugin, buildAgentArgument(target, Context.test, agentOptions)); } }); // Main configuration - if (isAgentEnabled) { + if (agent.isEnabled()) { withPlugin(build, "exec-maven-plugin", execPlugin -> updatePluginConfiguration(execPlugin, (exec, config) -> { if ("java-agent".equals(exec.getId())) { @@ -147,7 +158,7 @@ public void afterProjectsRead(MavenSession session) { // Agent argument Xpp3Dom arg = new Xpp3Dom("argument"); - List agentOptions = getAgentOptions(nativePlugin, Context.main, selectedOptionsName); + List agentOptions = agent.getAgentCommandLine(); arg.setValue(buildAgentArgument(target, Context.main, agentOptions)); children.add(0, arg); @@ -167,9 +178,7 @@ public void afterProjectsRead(MavenSession session) { Context context = exec.getGoals().stream().anyMatch("test"::equals) ? Context.test : Context.main; Xpp3Dom agentResourceDirectory = findOrAppend(configuration, "agentResourceDirectory"); agentResourceDirectory.setValue(agentOutputDirectoryFor(target, context)); - if (context == Context.test) { - setupMergeAgentFiles(exec, configuration, context); - } + setupMergeAgentFiles(exec, configuration, context); }); } }); @@ -193,118 +202,6 @@ private static void withPlugin(Build build, String artifactId, Consumer", enabled.getValue()); - } - } - - return false; - } - - private static String getSelectedOptionsName(MavenSession session) { - String selectedOptionsName = session.getSystemProperties().getProperty("agentOptions"); - if (selectedOptionsName == null) { - return null; - } - return assertNotEmptyAndTrim(selectedOptionsName, "agentOptions system property must have a value"); - } - - /** - * Parses the configuration block for the {@code native-maven-plugin}, searching for - * {@code } elements whose names match the name of the supplied {@code context} - * or {@code selectedOptionsName} and for unnamed, shared {@code } elements, - * and returns a list of the collected agent options. - * - * @param nativePlugin the {@code native-maven-plugin}; never null - * @param context the current execution context; never null - * @param selectedOptionsName the name of the set of custom agent options activated - * by the user via the {@code agentOptions} system property; may be null if not - * supplied via a system property - */ - private static List getAgentOptions(Plugin nativePlugin, Context context, String selectedOptionsName) { - // - // - // true - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - - List optionsList = new ArrayList<>(); - Xpp3Dom agent = getAgentNode(nativePlugin); - if (agent != null) { - for (Xpp3Dom options : agent.getChildren("options")) { - String name = options.getAttribute("name"); - if (name != null) { - name = assertNotEmptyAndTrim(name, " must declare a non-empty name attribute or omit the name attribute"); - } - // If unnamed/shared options, or options for the current context (main/test), or user-selected options: - if (name == null || name.equals(context.name()) || name.equals(selectedOptionsName)) { - processOptionNodes(options, optionsList); - } - } - } - return optionsList; - } - - private static void processOptionNodes(Xpp3Dom options, List optionsList) { - for (Xpp3Dom option : options.getChildren("option")) { - String value = assertNotEmptyAndTrim(option.getValue(), "