diff --git a/docs/reference-manual/native-image/Bundles.md b/docs/reference-manual/native-image/Bundles.md index 593e6669a7de..4231e248d31e 100644 --- a/docs/reference-manual/native-image/Bundles.md +++ b/docs/reference-manual/native-image/Bundles.md @@ -22,6 +22,7 @@ Using Native Image bundles is a safe solution to encapsulate all this input requ * [Building with Bundles](#building-with-bundles) * [Environment Variables](#capturing-environment-variables) * [Creating New Bundles from Existing Bundles](#combining---bundle-create-and---bundle-apply) +* [Executing the bundled application](#executing-the-bundled-application) * [Bundle File Format](#bundle-file-format) ## Creating Bundles @@ -31,13 +32,19 @@ This will cause `native-image` to create a _*.nib_ file in addition to the actua Here is the option description: ``` ---bundle-create[=new-bundle.nib] +--bundle-create[=new-bundle.nib][,dry-run][,container[=][,dockerfile=]] in addition to image building, create a Native Image bundle file (*.nib file) that allows rebuilding of that image again at a later point. If a bundle-file gets passed, the bundle will be created with the given name. Otherwise, the bundle-file name is derived from the image name. - Note both bundle options can be combined with --dry-run to only perform - the bundle operations without any actual image building. + Note both bundle options can be extended with ",dry-run" and ",container" + * 'dry-run': only perform the bundle operations without any actual image building. + * 'container': sets up a container image for image building and performs image building + from inside that container. Requires podman or rootless docker to be installed. + If available, 'podman' is preferred and rootless 'docker' is the fallback. Specifying + one or the other as '=' forces the use of a specific tool. + * 'dockerfile=': Use a user provided 'Dockerfile' instead of the default based on + Oracle Linux 8 base images for GraalVM (see https://github.com/graalvm/container) ``` For example, assuming a Micronaut application is built with Maven, make sure the `--bundle-create` option is used. @@ -82,6 +89,18 @@ $ jar tf micronautguide.nib META-INF/MANIFEST.MF META-INF/nibundle.properties output/default/micronautguide +com/oracle/svm/driver/launcher/BundleLauncherUtil.class +com/oracle/svm/driver/launcher/ContainerSupport$TargetPath.class +com/oracle/svm/driver/launcher/BundleLauncherHelp.txt +com/oracle/svm/driver/launcher/BundleLauncher.class +com/oracle/svm/driver/launcher/configuration/BundleConfigurationParser.class +com/oracle/svm/driver/launcher/configuration/BundleEnvironmentParser.class +com/oracle/svm/driver/launcher/configuration/BundleArgsParser.class +com/oracle/svm/driver/launcher/configuration/BundlePathMapParser.class +com/oracle/svm/driver/launcher/configuration/BundleContainerSettingsParser.class +com/oracle/svm/driver/launcher/ContainerSupport.class +com/oracle/svm/driver/launcher/json/BundleJSONParser.class +com/oracle/svm/driver/launcher/json/BundleJSONParserException.class input/classes/cp/micronaut-core-3.8.7.jar input/classes/cp/netty-buffer-4.1.87.Final.jar input/classes/cp/jackson-databind-2.14.1.jar @@ -93,10 +112,12 @@ input/classes/cp/micronaut-jdbc-4.7.2.jar input/classes/cp/jackson-core-2.14.0.jar input/classes/cp/micronaut-runtime-3.8.7.jar input/classes/cp/micronautguide-0.1.jar -input/stage/build.json -input/stage/environment.json input/stage/path_substitutions.json input/stage/path_canonicalizations.json +input/stage/build.json +input/stage/run.json +input/stage/environment.json +input/stage/Dockerfile ``` As you can see, a bundle is just a JAR file with a specific layout. @@ -113,11 +134,11 @@ target/micronautguide.output └── other ``` -### Combining --bundle-create with --dry-run +### Combining --bundle-create with dry-run As mentioned in the `--bundle-create` option description, it is also possible to let `native-image` build a bundle but not actually perform the image building. This might be useful if a user wants to move the bundle to a more powerful machine and build the image there. -Modify the above `native-maven-plugin` configuration to also contain the argument `--dry-run`. +Modify the `--bundle-create` argument in the `native-maven-plugin` configuration above to `--bundle-create,dry-run`. Then running `./mvnw package -Dpackaging=native-image` takes only seconds and the created bundle is much smaller: ``` Native Image Bundles: Bundle written to /home/testuser/micronaut-data-jdbc-repository-maven-java/target/micronautguide.nib @@ -147,7 +168,7 @@ Since no executable is created, no bundle build output is available. ## Building with Bundles -Assuming that the native executable is used in production and once in a while an unexpected exception is thrown at run time. +Assuming that the native executable is used in production and once in a while, an unexpected exception is thrown at run time. Since you still have the bundle that was used to create the executable, it is trivial to build a variant of that executable with debugging support. Use `--bundle-apply=micronautguide.nib` like this: ```shell @@ -186,24 +207,87 @@ You successfully rebuilt the application from the bundle with debug info enabled The full option help of `--bundle-apply` shows a more advanced use case that will be discussed [later](#combining---bundle-create-and---bundle-apply) in detail: ``` ---bundle-apply=some-bundle.nib +--bundle-apply=some-bundle.nib[,dry-run][,container[=][,dockerfile=]] an image will be built from the given bundle file with the exact same arguments and files that have been passed to native-image originally to create the bundle. Note that if an extra --bundle-create gets passed after --bundle-apply, a new bundle will be written based on the given - bundle args plus any additional arguments that haven been passed + bundle args plus any additional arguments that have been passed afterwards. For example: > native-image --bundle-apply=app.nib --bundle-create=app_dbg.nib -g creates a new bundle app_dbg.nib based on the given app.nib bundle. Both bundles are the same except the new one also uses the -g option. ``` + +### Building in a Container + +Another addition to the `--bundle-create` and `--bundle-apply` options, as mentioned above, is to perform image building inside a container image. +This ensures that during the image build `native-image` can not access any resources that were not explicitly specified via the classpath or module path. +Modify the `--bundle-create` argument in the `native-maven-plugin` configuration to `--bundle-create,container`. +This still creates the same bundle as before. +However, a container image is built and then used for building the native image executable. +If the container image is newly created, you can also see the build output from the container tool. +The name of the container image is the hash of the used Dockerfile. +If the container image already exists you will see the following line in the build output instead: + +```shell +Native Image Bundles: Reusing container image c253ca50f50b380da0e23b168349271976d57e4e. +``` + +For building in a container you require either _podman_ or _rootless docker_ to be available on your system. +Additionally, building in a container is currently only supported for Linux. +Using any other OS native image will not create and use a container image. +The container tool used for running the image build can be specified with `--bundle-create,container=podman` or `--bundle-create,container=docker`. +If not specified, `native-image` uses one of the supported tools. +If available, `podman` is preferred and rootless `docker` is the fallback. + +The Dockerfile used to build the container image may also be explicitly specified with `--bundle-create,container,dockerfile=`. +If no Dockerfile was specified, a default Dockerfile is used, which is based on the Oracle Linux 8 container images for GraalVM from [here](https://github.com/graalvm/container). +Whichever Dockerfile is finally used to build the container image is stored in the bundle. +Even if you do not use the `container` option, `native-image` creates a Dockerfile and stores it in the bundle. + +Other than creating a container image on the host system, building inside a container does not create any additional build output. +However, the created bundle contains some additional files: +```shell +$ jar tf micronautguide.nib +META-INF/MANIFEST.MF +META-INF/nibundle.properties +... +input/classes/cp/micronaut-management-3.8.7.jar +input/stage/path_substitutions.json +input/stage/path_canonicalizations.json +input/stage/build.json +input/stage/run.json +input/stage/environment.json +input/stage/Dockerfile +input/stage/container.json +``` +The bundle contains the Dockerfile used for building the container image and stores the used container tool, its version and the name of the container image in `container.json`: +```json +{ + "containerTool":"podman", + "containerToolVersion":"podman version 3.4.4", + "containerImage":"c253ca50f50b380da0e23b168349271976d57e4e" +} +``` + +The `container` option may also be combined with `dry-run`, in this case `native-image` does neither create an executable nor a container image. +It does not even check if the selected container tool is available. +In this case, _container.json_ is omitted, or, if you explicitly specified a container tool, just contains the _containerTool_ field without any additional information. + +Containerized builds are sticky, which means that if a bundle was created with `--bundle-create,container` the bundle is marked as a container build. +If you now use `--bundle-apply` with this bundle, it is automatically built in a container again. +However, this does not apply to [executing a bundle](#executing-the-bundled-application), a bundled application is still executed outside a container by default. + +The extended command line interface for containerized builds is shown in the option help texts for `--bundle-create` and `--bundle-apply` above. + ## Capturing Environment Variables Before bundle support was added, all environment variables were visible to the `native-image` builder. This approach does not work well with bundles and is problematic for image building without bundles. Consider having an environment variable that holds sensitive information from your build machine. -Due to Native Image's ability to run code at build time that can create data to be available at run time, it is very easy to build an image were you accidentally leak the contents of such variables. +Due to Native Image's ability to run code at build time that can create data to be available at run time, it is very easy to build an image where you accidentally leak the contents of such variables. Passing environment variables to `native-image` now requires explicit arguments. @@ -252,7 +336,7 @@ $ ls -lh *.iprof The file `default.iprof` contains the profiling information that was created because you ran the Micronaut application from the executable built with `--pgo-instrument`. Now you can create a new optimized bundle out of the existing one: ```shell -native-image --bundle-apply=micronautguide.nib --bundle-create=micronautguide-pgo-optimized.nib --dry-run --pgo +native-image --bundle-apply=micronautguide.nib --bundle-create=micronautguide-pgo-optimized.nib,dry-run --pgo ``` Now take a look how _micronautguide-pgo-optimized.nib_ is different from _micronautguide.nib_: @@ -297,6 +381,52 @@ $ native-image --bundle-apply=micronautguide-pgo-optimized.nib ... ``` +## Executing the bundled application + +As described later in [Bundle File Format](#bundle-file-format), a bundle file is a JAR file with a contained launcher for launching the bundled application. +This means you can use a native image bundle with any JDK and execute it as a JAR file with `/bin/java -jar [bundle-file.nib]`. +The launcher uses the command line arguments stored in _run.json_ and adds all JAR files and folders in _input/classes/cp_ and _input/classes/p_ to the classpath and module path respectively. + +The launcher also comes with a separate command line interface described in its help text: +``` +This native image bundle can be used to launch the bundled application. + +Usage: java -jar bundle-file [options] [bundle-application-options] + +where options include: + + --with-native-image-agent[,update-bundle[=]] + runs the application with a native-image-agent attached + 'update-bundle' adds the agents output to the bundle-files classpath. + '=' creates a new bundle with the agent output instead. + Note 'update-bundle' requires native-image to be installed + + --container[=][,dockerfile=] + sets up a container image for execution and executes the bundled application + from inside that container. Requires podman or rootless docker to be installed. + If available, 'podman' is preferred and rootless 'docker' is the fallback. Specifying + one or the other as '=' forces the use of a specific tool. + 'dockerfile=': Use a user provided 'Dockerfile' instead of the Dockerfile + bundled with the application + + --verbose enable verbose output + --help print this help message +``` + +Running the bundled application with the `--with-native-image-agent` argument requires a `native-image-agent` library to be available. +The output of the `native-image-agent` is written to _.output/launcher/META-INF/native-image/-agent_. +If native image agents output should be inserted into the bundle with `,update-bundle`, the launcher then also requires `native-image`. +The `update-bundle` option executes the command `native-image --bundle-apply=.nib --bundle-create=.nib -cp .output/launcher` after executing the bundled application with the `native-image-agent` attached. + +The `container` option realizes a similar behavior to [containerized image builds](#building-in-a-container). +However, the only exception is that in this case the application is executed inside the container instead of `native-image`. +Every bundle contains a Dockerfile which is used for executing the bundled application in a container. +However, this Dockerfile can be overwritten by adding `,dockerfile=` to the `--container` argument. + +The bundle launcher only consumes options it knows, all other arguments are passed on to the bundled application. +If the bundle launcher parses ` -- ` without a specified option, the launcher stops parsing arguments. +All remaining arguments are then also passed on to the bundled application. + ## Bundle File Format A bundle file is a JAR file with a well-defined internal layout. @@ -310,6 +440,7 @@ Inside a bundle you can find the following inner structure: │ * Bundle format version (BundleFileVersion{Major,Minor}) │ * Platform and architecture the bundle was created on │ * GraalVM / Native-image version used for bundle creation +├── com.oracle.svm.driver.launcher <- launcher for executing the bundled application ├── input <- All information required to rebuild the image │ ├── auxiliary <- Contains auxiliary files passed to native-image via arguments │ │ (e.g. external `config-*.json` files or PGO `*.iprof`-files) @@ -318,11 +449,16 @@ Inside a bundle you can find the following inner structure: │ │ └── p │ └── stage │ ├── build.json <- Full native-image command line (minus --bundle options) +│ ├── container.json <- Containerization tool, tool version and container +│ │ image name (not available information is omitted) +│ ├── Dockerfile <- Dockerfile used for building the container image │ ├── environment.json <- Environment variables used in the image build │ ├── path_canonicalizations.json <- Record of path-canonicalizations that happened -│ │ during bundle creation for the input files -│ └── path_substitutions.json <- Record of path-substitutions that happened -│ during bundle creation for the input files +│ │ during bundle creation for the input files +│ ├── path_substitutions.json <- Record of path-substitutions that happened +│ │ during bundle creation for the input files +│ └── run.json <- Full command line for executing the bundled application +│ (minus classpath and module path) └── output ├── default │ ├── myimage <- Created image and other output created by the image builder @@ -355,12 +491,12 @@ These include: * `--dry-run` The state of environment variables that are relevant for the build are captured in _input/stage/environment.json_. -For every `-E` argument that were seen when the bundle was created, a snapshot of its key-value pair is recorded in the file. +For every `-E` argument that was seen when the bundle was created, a snapshot of its key-value pair is recorded in the file. The remaining files _path_canonicalizations.json_ and _path_substitutions.json_ contain a record of the file-path transformations that were performed by the `native-image` tool based on the input file paths as specified by the original command line arguments. ### output -If a native executable is built as part of building the bundle (for example, the `--dry-run` option was not used), you also have an _output_ directory in the bundle. +If a native executable is built as part of building the bundle (for example, the `,dry-run` option was not used), you also have an _output_ directory in the bundle. It contains the executable that was built along with any other files that were generated as part of building. Most output files are located in the directory _output/default_ (the executable, its debug info, and debug sources). Builder output files, that would have been written to arbitrary absolute paths if the executable had not been built in the bundle mode, can be found in _output/other_. diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index 01abb6d5dba0..0d61c4dbd798 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -16,6 +16,8 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-46740) Add support for foreign downcalls (part of "Project Panama") on the AMD64 platform. * (GR-27034) Add `-H:ImageBuildID` option to generate Image Build ID, which is a 128-bit UUID string generated randomly, once per bundle or digest of input args when bundles are not used. * (GR-47647) Add `-H:±UnlockExperimentalVMOptions` for unlocking access to experimental options similar to HotSpot's `-XX:UnlockExperimentalVMOptions`. Explicit unlocking will be required in a future release, which can be tested with the env setting `NATIVE_IMAGE_EXPERIMENTAL_OPTIONS_ARE_FATAL=true`. For more details, see [issue #7105](https://github.com/oracle/graal/issues/7105). +* (GR-43920) Add support for executing native image bundles as jar files with extra options `--with-native-image-agent` and `--container`. +* (GR-43920) Add `,container[=]`, `,dockerfile=` and `,dry-run` options to `--bundle-create`and `--bundle-apply`. ## GraalVM for JDK 17 and GraalVM for JDK 20 (Internal Version 23.0.0) * (GR-40187) Report invalid use of SVM specific classes on image class- or module-path as error. As a temporary workaround, `-H:+AllowDeprecatedBuilderClassesOnImageClasspath` allows turning the error into a warning. diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 09807772bfa0..0c0abaa6437e 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -1016,6 +1016,7 @@ def _native_image_launcher_extra_jvm_args(): '--features=com.oracle.svm.driver.APIOptionFeature', '--initialize-at-build-time=com.oracle.svm.driver', '--link-at-build-time=com.oracle.svm.driver,com.oracle.svm.driver.metainf', + '-H:IncludeResources=com/oracle/svm/driver/launcher/.*', ] + svm_experimental_options([ '-H:-ParseRuntimeOptions', ]) diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index 28418b919c62..2c07703ca5ea 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -842,6 +842,7 @@ ], "dependencies": [ "com.oracle.svm.hosted", + "com.oracle.svm.driver.launcher", ], "requires" : [ "jdk.management", @@ -857,6 +858,23 @@ "jacoco" : "exclude", }, + "com.oracle.svm.driver.launcher": { + "subDir": "src", + "sourceDirs": [ + "src", + "resources" + ], + "checkstyle": "com.oracle.svm.hosted", + "workingSets": "SVM", + "annotationProcessors": [ + "compiler:GRAAL_PROCESSOR", + "SVM_PROCESSOR", + ], + "javaCompliance" : "17+", + "spotbugs": "false", + "jacoco" : "exclude", + }, + "com.oracle.svm.junit": { "subDir": "src", "sourceDirs": [ @@ -1690,6 +1708,7 @@ "mainClass": "com.oracle.svm.driver.NativeImage", "dependencies": [ "com.oracle.svm.driver", + "com.oracle.svm.driver.launcher", "svm-compiler-flags-builder", ], "distDependencies": [ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/NativeImageClassLoaderOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/NativeImageClassLoaderOptions.java index 20a51ab4def7..fc8993b0bffa 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/NativeImageClassLoaderOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/NativeImageClassLoaderOptions.java @@ -35,12 +35,12 @@ public class NativeImageClassLoaderOptions { public static final String AddReadsFormat = "=(,)*"; - @APIOption(name = "add-exports", extra = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})// + @APIOption(name = "add-exports", extra = true, launcherOption = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})// @Option(help = "Value " + AddExportsAndOpensFormat + " updates to export to , regardless of module declaration." + " can be ALL-UNNAMED to export to all unnamed modules.")// public static final HostedOptionKey AddExports = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); - @APIOption(name = "add-opens", extra = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})// + @APIOption(name = "add-opens", extra = true, launcherOption = true, valueSeparator = {APIOption.WHITESPACE_SEPARATOR, '='})// @Option(help = "Value " + AddExportsAndOpensFormat + " updates to open to , regardless of module declaration.")// public static final HostedOptionKey AddOpens = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/RuntimeAssertionsSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/RuntimeAssertionsSupport.java index 3f916a8c4767..84d15285e169 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/RuntimeAssertionsSupport.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/RuntimeAssertionsSupport.java @@ -91,15 +91,16 @@ public static class Options { private static final char VALUE_SEPARATOR = ':'; - @APIOption(name = {"-ea", "-enableassertions"}, valueSeparator = VALUE_SEPARATOR, valueTransformer = RuntimeAssertionsOptionTransformer.Enable.class, defaultValue = "", // + @APIOption(name = {"-ea", "-enableassertions"}, launcherOption = true, valueSeparator = VALUE_SEPARATOR, valueTransformer = RuntimeAssertionsOptionTransformer.Enable.class, defaultValue = "", // customHelp = "also -ea[:[packagename]...|:classname] or -enableassertions[:[packagename]...|:classname]. Enable assertions with specified granularity at run time.")// - @APIOption(name = {"-da", "-disableassertions"}, valueSeparator = VALUE_SEPARATOR, valueTransformer = RuntimeAssertionsOptionTransformer.Disable.class, defaultValue = "", // + @APIOption(name = {"-da", + "-disableassertions"}, launcherOption = true, valueSeparator = VALUE_SEPARATOR, valueTransformer = RuntimeAssertionsOptionTransformer.Disable.class, defaultValue = "", // customHelp = "also -da[:[packagename]...|:classname] or -disableassertions[:[packagename]...|:classname]. Disable assertions with specified granularity at run time.")// @Option(help = "Enable or disable Java assert statements at run time") // public static final HostedOptionKey RuntimeAssertions = new HostedOptionKey<>(LocatableMultiOptionValue.Strings.build()); - @APIOption(name = {"-esa", "-enablesystemassertions"}, customHelp = "also -enablesystemassertions. Enables assertions in all system classes at run time.") // - @APIOption(name = {"-dsa", "-disablesystemassertions"}, kind = APIOption.APIOptionKind.Negated, // + @APIOption(name = {"-esa", "-enablesystemassertions"}, launcherOption = true, customHelp = "also -enablesystemassertions. Enables assertions in all system classes at run time.") // + @APIOption(name = {"-dsa", "-disablesystemassertions"}, launcherOption = true, kind = APIOption.APIOptionKind.Negated, // customHelp = "also -disablesystemassertions. Disables assertions in all system classes at run time.") // @Option(help = "Enable or disable Java system assertions at run time") // public static final HostedOptionKey RuntimeSystemAssertions = new HostedOptionKey<>(false); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index 990309c15c6a..216b57fb3bf6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -116,6 +116,26 @@ public static boolean parseOnce() { @Option(help = "Build statically linked executable (requires static libc and zlib)")// public static final HostedOptionKey StaticExecutable = new HostedOptionKey<>(false); + @APIOption(name = "libc")// + @Option(help = "Selects the libc implementation to use. Available implementations: glibc, musl, bionic")// + public static final HostedOptionKey UseLibC = new HostedOptionKey<>(null) { + @Override + public String getValueOrDefault(UnmodifiableEconomicMap, Object> values) { + if (!values.containsKey(this)) { + return Platform.includedIn(Platform.ANDROID.class) + ? "bionic" + : System.getProperty("substratevm.HostLibC", "glibc"); + } + return (String) values.get(this); + } + + @Override + public String getValue(OptionValues values) { + assert checkDescriptorExists(); + return getValueOrDefault(values.getMap()); + } + }; + @APIOption(name = "target")// @Option(help = "Selects native-image compilation target (in - format). Defaults to host's OS-architecture pair.")// public static final HostedOptionKey TargetPlatform = new HostedOptionKey<>("") { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/APIOption.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/APIOption.java index 21200fd882fe..3392146d9fb9 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/APIOption.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/option/APIOption.java @@ -61,6 +61,12 @@ */ boolean extra() default false; + /** + * This option should be stored in a native image bundle and passed to the jvm when executed + * with {@code com.oracle.svm.driver.launcher.BundleLauncher}. + */ + boolean launcherOption() default false; + /** * Make a boolean option part of a group of boolean options. **/ diff --git a/substratevm/src/com.oracle.svm.driver.launcher/resources/com/oracle/svm/driver/launcher/BundleLauncherHelp.txt b/substratevm/src/com.oracle.svm.driver.launcher/resources/com/oracle/svm/driver/launcher/BundleLauncherHelp.txt new file mode 100644 index 000000000000..afe7f539be61 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/resources/com/oracle/svm/driver/launcher/BundleLauncherHelp.txt @@ -0,0 +1,22 @@ +This native image bundle can be used to launch the bundled application. + +Usage: java -jar bundle-file [options] [bundle-application-options] + +where options include: + + --with-native-image-agent[,update-bundle[=]] + runs the application with a native-image-agent attached + 'update-bundle' adds the agents output to the bundle-files classpath. + '=' creates a new bundle with the agent output instead. + Note 'update-bundle' requires native-image to be installed + + --container[=][,dockerfile=] + sets up a container image for execution and executes the bundled application + from inside that container. Requires podman or rootless docker to be installed. + If available, 'podman' is preferred and rootless 'docker' is the fallback. Specifying + one or the other as '=' forces the use of a specific tool. + 'dockerfile=': Use a user-provided 'Dockerfile' instead of the Dockerfile + bundled with the application + + --verbose enable verbose output + --help print this help message diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/BundleLauncher.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/BundleLauncher.java new file mode 100644 index 000000000000..70cb535cce9d --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/BundleLauncher.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.oracle.svm.driver.launcher.configuration.BundleArgsParser; +import com.oracle.svm.driver.launcher.configuration.BundleEnvironmentParser; + +public class BundleLauncher { + + static final String BUNDLE_INFO_MESSAGE_PREFIX = "Native Image Bundles: "; + private static final String BUNDLE_TEMP_DIR_PREFIX = "bundleRoot-"; + private static final String BUNDLE_FILE_EXTENSION = ".nib"; + private static final String HELP_TEXT = getResource("/com/oracle/svm/driver/launcher/BundleLauncherHelp.txt"); + + private static Path rootDir; + private static Path inputDir; + private static Path outputDir; + private static Path stageDir; + private static Path classPathDir; + private static Path modulePathDir; + + private static Path bundleFilePath; + private static String bundleName; + private static Path agentOutputDir; + + private static String newBundleName = null; + private static boolean updateBundle = false; + + private static boolean verbose = false; + + private static ContainerSupport containerSupport; + + private static final List launchArgs = new ArrayList<>(); + private static final List applicationArgs = new ArrayList<>(); + private static final Map launcherEnvironment = new HashMap<>(); + + static String getResource(String resourceName) { + try (InputStream input = BundleLauncher.class.getResourceAsStream(resourceName)) { + BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + return reader.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new Error(e); + } + } + + public static void main(String[] args) { + bundleFilePath = Paths.get(BundleLauncher.class.getProtectionDomain().getCodeSource().getLocation().getPath()); + bundleName = bundleFilePath.getFileName().toString().replace(BUNDLE_FILE_EXTENSION, ""); + agentOutputDir = bundleFilePath.getParent().resolve(Paths.get(bundleName + ".output", "launcher")); + unpackBundle(); + + // if we did not create a run.json bundle is not executable, e.g. shared library bundles + if (!Files.exists(stageDir.resolve("run.json"))) { + showMessage(BUNDLE_INFO_MESSAGE_PREFIX + "Bundle " + bundleFilePath + " is not executable!"); + System.exit(1); + } + + parseBundleLauncherArgs(args); + + ProcessBuilder pb = new ProcessBuilder(); + + Path environmentFile = stageDir.resolve("environment.json"); + if (Files.isReadable(environmentFile)) { + try (Reader reader = Files.newBufferedReader(environmentFile)) { + new BundleEnvironmentParser(launcherEnvironment).parseAndRegister(reader); + pb.environment().putAll(launcherEnvironment); + } catch (IOException e) { + throw new Error("Failed to read bundle-file " + environmentFile, e); + } + } + pb.command(createLaunchCommand()); + + if (verbose) { + List environmentList = pb.environment() + .entrySet() + .stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .sorted() + .toList(); + showMessage("Executing ["); + showMessage(String.join(" \\\n", environmentList)); + showMessage(String.join(" \\\n", pb.command())); + showMessage("]"); + } + + Process p = null; + int exitCode; + try { + p = pb.inheritIO().start(); + exitCode = p.waitFor(); + } catch (IOException | InterruptedException e) { + throw new Error("Failed to run bundled application"); + } finally { + if (p != null) { + p.destroy(); + } + } + + if (updateBundle) { + exitCode = updateBundle(); + } + + System.exit(exitCode); + } + + private static boolean useContainer() { + return containerSupport != null; + } + + private static List createLaunchCommand() { + List command = new ArrayList<>(); + + Path javaExecutable = getJavaExecutable().toAbsolutePath().normalize(); + + if (useContainer()) { + Path javaHome = javaExecutable.getParent().getParent(); + + Map mountMapping = ContainerSupport.mountMappingFor(javaHome, inputDir, outputDir); + if (Files.isDirectory(agentOutputDir)) { + mountMapping.put(agentOutputDir, ContainerSupport.TargetPath.of(agentOutputDir, false)); + launcherEnvironment.put("LD_LIBRARY_PATH", ContainerSupport.GRAAL_VM_HOME.resolve("lib").toString()); + } + + containerSupport.initializeImage(); + command.addAll(containerSupport.createCommand(launcherEnvironment, mountMapping)); + command.add(ContainerSupport.GRAAL_VM_HOME.resolve(javaHome.relativize(javaExecutable)).toString()); + } else { + command.add(javaExecutable.toString()); + } + + command.addAll(launchArgs); + + List classpath = new ArrayList<>(); + if (Files.isDirectory(classPathDir)) { + try (Stream walk = Files.walk(classPathDir, 1)) { + walk.filter(path -> path.toString().endsWith(".jar") || Files.isDirectory(path)) + .map(path -> useContainer() ? Paths.get("/").resolve(rootDir.relativize(path)) : path) + .map(Path::toString) + .forEach(classpath::add); + } catch (IOException e) { + throw new Error("Failed to iterate through directory " + classPathDir, e); + } + + command.add("-cp"); + command.add(String.join(File.pathSeparator, classpath)); + } + + List modulePath = new ArrayList<>(); + if (Files.isDirectory(modulePathDir)) { + try (Stream walk = Files.walk(modulePathDir, 1)) { + walk.filter(path -> (Files.isDirectory(path) && !path.equals(modulePathDir)) || path.toString().endsWith(".jar")) + .map(path -> useContainer() ? Paths.get("/").resolve(rootDir.relativize(path)) : path) + .map(Path::toString) + .forEach(modulePath::add); + } catch (IOException e) { + throw new Error("Failed to iterate through directory " + modulePathDir, e); + } + + if (!modulePath.isEmpty()) { + command.add("-p"); + command.add(String.join(File.pathSeparator, modulePath)); + } + } + + Path argsFile = stageDir.resolve("run.json"); + try (Reader reader = Files.newBufferedReader(argsFile)) { + List argsFromFile = new ArrayList<>(); + new BundleArgsParser(argsFromFile).parseAndRegister(reader); + command.addAll(argsFromFile); + } catch (IOException e) { + throw new Error("Failed to read bundle-file " + argsFile, e); + } + + command.addAll(applicationArgs); + + return command; + } + + private static int updateBundle() { + List command = new ArrayList<>(); + + Path nativeImageExecutable = getNativeImageExecutable().toAbsolutePath().normalize(); + command.add(nativeImageExecutable.toString()); + + Path newBundleFilePath = newBundleName == null ? bundleFilePath : bundleFilePath.getParent().resolve(newBundleName + BUNDLE_FILE_EXTENSION); + command.add("--bundle-apply=" + bundleFilePath); + command.add("--bundle-create=" + newBundleFilePath + ",dry-run"); + + command.add("-cp"); + command.add(agentOutputDir.toString()); + + ProcessBuilder pb = new ProcessBuilder(command); + Process p = null; + try { + p = pb.inheritIO().start(); + return p.waitFor(); + } catch (IOException | InterruptedException e) { + throw new Error("Failed to create updated bundle."); + } finally { + if (p != null) { + p.destroy(); + } + } + } + + private static void parseBundleLauncherArgs(String[] args) { + Deque argQueue = new ArrayDeque<>(Arrays.asList(args)); + + while (!argQueue.isEmpty()) { + String arg = argQueue.removeFirst(); + + if (arg.startsWith("--with-native-image-agent")) { + if (arg.indexOf(',') >= 0) { + String option = arg.substring(arg.indexOf(',') + 1); + if (option.startsWith("update-bundle")) { + updateBundle = true; + if (option.indexOf('=') >= 0) { + newBundleName = option.substring(option.indexOf('=')).replace(BUNDLE_FILE_EXTENSION, ""); + } + } else { + throw new Error(String.format("Unknown option %s. Valid option is: update-bundle[=].", option)); + } + } + + Path configOutputDir = agentOutputDir.resolve(Paths.get("META-INF", "native-image", bundleName + "-agent")); + try { + Files.createDirectories(configOutputDir); + showMessage(BUNDLE_INFO_MESSAGE_PREFIX + "Native image agent output written to " + agentOutputDir); + launchArgs.add("-agentlib:native-image-agent=config-output-dir=" + configOutputDir); + } catch (IOException e) { + throw new Error("Failed to create native image agent output dir"); + } + } else if (arg.startsWith("--container")) { + if (useContainer()) { + throw new Error("native-image launcher allows option container to be specified only once."); + } else if (!System.getProperty("os.name").equals("Linux")) { + throw new Error("container option is only supported for Linux"); + } + containerSupport = new ContainerSupport(stageDir, Error::new, BundleLauncher::showWarning, BundleLauncher::showMessage); + if (arg.indexOf(',') != -1) { + String option = arg.substring(arg.indexOf(',') + 1); + arg = arg.substring(0, arg.indexOf(',')); + + if (option.startsWith("dockerfile")) { + if (option.indexOf('=') != -1) { + containerSupport.dockerfile = Paths.get(option.substring(option.indexOf('=') + 1)); + if (!Files.isReadable(containerSupport.dockerfile)) { + throw new Error(String.format("Dockerfile '%s' is not readable", containerSupport.dockerfile.toAbsolutePath())); + } + } else { + throw new Error("container option dockerfile requires a dockerfile argument. E.g. dockerfile=path/to/Dockerfile."); + } + } else { + throw new Error(String.format("Unknown option %s. Valid option is: dockerfile=path/to/Dockerfile.", option)); + } + } + if (arg.indexOf('=') != -1) { + containerSupport.tool = arg.substring(arg.indexOf('=') + 1); + } + } else { + switch (arg) { + case "--help" -> { + showMessage(HELP_TEXT); + System.exit(0); + } + case "--verbose" -> verbose = true; + case "--" -> { + applicationArgs.addAll(argQueue); + argQueue.clear(); + } + default -> applicationArgs.add(arg); + } + } + } + } + + // Checkstyle: Allow raw info or warning printing - begin + private static void showMessage(String msg) { + System.out.println(msg); + } + + private static void showWarning(String msg) { + System.out.println("Warning: " + msg); + } + // Checkstyle: Allow raw info or warning printing - end + + private static final Path buildTimeJavaHome = Paths.get(System.getProperty("java.home")); + + private static Path getJavaExecutable() { + Path binJava = Paths.get("bin", System.getProperty("os.name").contains("Windows") ? "java.exe" : "java"); + if (Files.isExecutable(buildTimeJavaHome.resolve(binJava))) { + return buildTimeJavaHome.resolve(binJava); + } + + return getJavaHomeExecutable(binJava); + } + + private static Path getJavaHomeExecutable(Path executable) { + String javaHome = System.getenv("JAVA_HOME"); + if (javaHome == null) { + throw new Error("Environment variable JAVA_HOME is not set"); + } + Path javaHomeDir = Paths.get(javaHome); + if (!Files.isDirectory(javaHomeDir)) { + throw new Error("Environment variable JAVA_HOME does not refer to a directory"); + } + if (!Files.isExecutable(javaHomeDir.resolve(executable))) { + throw new Error("Environment variable JAVA_HOME does not refer to a directory with a " + executable + " executable"); + } + return javaHomeDir.resolve(executable); + } + + private static Path getNativeImageExecutable() { + Path binNativeImage = Paths.get("bin", System.getProperty("os.name").contains("Windows") ? "native-image.exe" : "native-image"); + if (Files.isExecutable(buildTimeJavaHome.resolve(binNativeImage))) { + return buildTimeJavaHome.resolve(binNativeImage); + } + + String graalVMHome = System.getenv("GRAALVM_HOME"); + if (graalVMHome != null) { + Path graalVMHomeDir = Paths.get(graalVMHome); + if (Files.isDirectory(graalVMHomeDir) && Files.isExecutable(graalVMHomeDir.resolve(binNativeImage))) { + return graalVMHomeDir.resolve(binNativeImage); + } + } + + return getJavaHomeExecutable(binNativeImage); + } + + private static final AtomicBoolean deleteBundleRoot = new AtomicBoolean(); + + private static Path createBundleRootDir() throws IOException { + Path bundleRoot = Files.createTempDirectory(BUNDLE_TEMP_DIR_PREFIX); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + deleteBundleRoot.set(true); + deleteAllFiles(bundleRoot); + })); + return bundleRoot; + } + + private static final String deletedFileSuffix = ".deleted"; + + private static boolean isDeletedPath(Path toDelete) { + return toDelete.getFileName().toString().endsWith(deletedFileSuffix); + } + + private static void deleteAllFiles(Path toDelete) { + try { + Path deletedPath = toDelete; + if (!isDeletedPath(deletedPath)) { + deletedPath = toDelete.resolveSibling(toDelete.getFileName() + deletedFileSuffix); + Files.move(toDelete, deletedPath); + } + try (Stream walk = Files.walk(deletedPath)) { + walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } + } catch (IOException e) { + showMessage("Could not recursively delete path: " + toDelete); + e.printStackTrace(); + } + } + + private static void unpackBundle() { + try { + rootDir = createBundleRootDir(); + inputDir = rootDir.resolve("input"); + + try (JarFile archive = new JarFile(bundleFilePath.toFile())) { + Enumeration jarEntries = archive.entries(); + while (jarEntries.hasMoreElements() && !deleteBundleRoot.get()) { + JarEntry jarEntry = jarEntries.nextElement(); + Path bundleEntry = rootDir.resolve(jarEntry.getName()); + try { + Path bundleFileParent = bundleEntry.getParent(); + if (bundleFileParent != null) { + Files.createDirectories(bundleFileParent); + } + Files.copy(archive.getInputStream(jarEntry), bundleEntry); + } catch (IOException e) { + throw new Error("Unable to copy " + jarEntry.getName() + " from bundle " + bundleEntry + " to " + bundleEntry, e); + } + } + } + } catch (IOException e) { + throw new Error("Unable to expand bundle directory layout from bundle file " + bundleFilePath, e); + } + + if (deleteBundleRoot.get()) { + /* Abort bundle run request without error message and exit with 0 */ + throw new Error(null, null); + } + + try { + stageDir = Files.createDirectories(inputDir.resolve("stage")); + Path classesDir = inputDir.resolve("classes"); + classPathDir = Files.createDirectories(classesDir.resolve("cp")); + modulePathDir = Files.createDirectories(classesDir.resolve("p")); + outputDir = Files.createDirectories(rootDir.resolve("output")); + } catch (IOException e) { + throw new Error("Unable to create bundle directory layout", e); + } + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/BundleLauncherUtil.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/BundleLauncherUtil.java new file mode 100644 index 000000000000..3b8f6f9c6318 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/BundleLauncherUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BundleLauncherUtil { + + private static final char[] HEX = "0123456789abcdef".toCharArray(); + + static String digest(String value) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(value.getBytes(StandardCharsets.UTF_8)); + return toHex(md.digest()); + } catch (NoSuchAlgorithmException ex) { + throw new Error(ex); + } + } + + static String toHex(byte[] data) { + StringBuilder r = new StringBuilder(data.length * 2); + for (byte b : data) { + r.append(HEX[(b >> 4) & 0xf]); + r.append(HEX[b & 0xf]); + } + return r.toString(); + } + + private static final Pattern SAFE_SHELL_ARG = Pattern.compile("[A-Za-z0-9@%_\\-+=:,./]+"); + + static String quoteShellArg(String arg) { + if (arg.isEmpty()) { + return "''"; + } + Matcher m = SAFE_SHELL_ARG.matcher(arg); + if (m.matches()) { + return arg; + } + return "'" + arg.replace("'", "'\"'\"'") + "'"; + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/ContainerSupport.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/ContainerSupport.java new file mode 100644 index 000000000000..a022b9a84517 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/ContainerSupport.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +import com.oracle.svm.driver.launcher.configuration.BundleContainerSettingsParser; + +public class ContainerSupport { + public String tool; + public String bundleTool; + public String toolVersion; + public String bundleToolVersion; + public String image; + public String bundleImage; + public Path dockerfile; + + public static final List SUPPORTED_TOOLS = List.of("podman", "docker"); + public static final String TOOL_JSON_KEY = "containerTool"; + public static final String TOOL_VERSION_JSON_KEY = "containerToolVersion"; + public static final String IMAGE_JSON_KEY = "containerImage"; + + public static final Path GRAAL_VM_HOME = Path.of("/graalvm"); + + private final BiFunction errorFunction; + private final Consumer warningPrinter; + private final Consumer messagePrinter; + + public ContainerSupport(Path bundleStageDir, BiFunction errorFunction, Consumer warningPrinter, Consumer messagePrinter) { + this.errorFunction = errorFunction; + this.warningPrinter = warningPrinter; + this.messagePrinter = messagePrinter; + + if (bundleStageDir != null) { + dockerfile = bundleStageDir.resolve("Dockerfile"); + Path containerFile = bundleStageDir.resolve("container.json"); + if (Files.exists(containerFile)) { + try (Reader reader = Files.newBufferedReader(containerFile)) { + Map containerSettings = new HashMap<>(); + new BundleContainerSettingsParser(containerSettings).parseAndRegister(reader); + bundleImage = containerSettings.getOrDefault(IMAGE_JSON_KEY, bundleImage); + bundleTool = containerSettings.getOrDefault(TOOL_JSON_KEY, bundleTool); + bundleToolVersion = containerSettings.getOrDefault(TOOL_VERSION_JSON_KEY, bundleToolVersion); + } catch (IOException e) { + throw errorFunction.apply("Failed to read bundle-file " + containerFile, e); + } + if (bundleTool != null) { + String containerToolVersionString = bundleToolVersion == null ? "" : String.format(" (%s)", bundleToolVersion); + messagePrinter.accept( + String.format("%sBundled native-image was created in a container with %s%s.%n", BundleLauncher.BUNDLE_INFO_MESSAGE_PREFIX, bundleTool, containerToolVersionString)); + } + } + } + } + + public int initializeImage() { + try { + image = BundleLauncherUtil.digest(Files.readString(dockerfile)); + } catch (IOException e) { + throw errorFunction.apply("Could not read Dockerfile " + dockerfile, e); + } + + if (bundleImage != null && !bundleImage.equals(image)) { + warningPrinter.accept("The bundled image was created with a different dockerfile."); + } + + if (bundleTool != null && tool == null) { + tool = bundleTool; + } + + if (tool != null) { + if (!isToolAvailable(tool)) { + throw errorFunction.apply("Configured container tool not available.", null); + } else if (tool.equals("docker") && !isRootlessDocker()) { + throw errorFunction.apply("Only rootless docker is supported for containerized builds.", null); + } + toolVersion = getToolVersion(); + + if (bundleTool != null) { + if (!tool.equals(bundleTool)) { + warningPrinter.accept(String.format("The bundled image was created with container tool '%s' (using '%s').%n", bundleTool, tool)); + } else if (toolVersion != null && bundleToolVersion != null && !toolVersion.equals(bundleToolVersion)) { + warningPrinter.accept(String.format("The bundled image was created with different %s version '%s' (installed '%s').%n", tool, bundleToolVersion, toolVersion)); + } + } + } else { + for (String supportedTool : SUPPORTED_TOOLS) { + if (isToolAvailable(supportedTool)) { + if (supportedTool.equals("docker") && !isRootlessDocker()) { + messagePrinter.accept(BundleLauncher.BUNDLE_INFO_MESSAGE_PREFIX + "Rootless context missing for docker."); + continue; + } + tool = supportedTool; + toolVersion = getToolVersion(); + break; + } + } + if (tool == null) { + throw errorFunction.apply(String.format("Please install one of the following tools before running containerized native image builds: %s", SUPPORTED_TOOLS), null); + } + } + + return createContainer(); + } + + private int createContainer() { + ProcessBuilder pbCheckForImage = new ProcessBuilder(tool, "images", "-q", image + ":latest"); + ProcessBuilder pb = new ProcessBuilder(tool, "build", "-f", dockerfile.toString(), "-t", image, "."); + + String imageId = getFirstProcessResultLine(pbCheckForImage); + if (imageId == null) { + pb.inheritIO(); + } else { + messagePrinter.accept(String.format("%sReusing container image %s.%n", BundleLauncher.BUNDLE_INFO_MESSAGE_PREFIX, image)); + } + + Process p = null; + try { + p = pb.start(); + int status = p.waitFor(); + if (status == 0 && imageId != null && !imageId.equals(getFirstProcessResultLine(pbCheckForImage))) { + try (var processResult = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + messagePrinter.accept(String.format("%sUpdated container image %s.%n", BundleLauncher.BUNDLE_INFO_MESSAGE_PREFIX, image)); + processResult.lines().forEach(messagePrinter); + } + } + return status; + } catch (IOException | InterruptedException e) { + throw errorFunction.apply(e.getMessage(), e); + } finally { + if (p != null) { + p.destroy(); + } + } + } + + private static boolean isToolAvailable(String toolName) { + return Arrays.stream(System.getenv("PATH").split(":")) + .map(str -> Path.of(str).resolve(toolName)) + .anyMatch(Files::isExecutable); + } + + private String getToolVersion() { + ProcessBuilder pb = new ProcessBuilder(tool, "--version"); + return getFirstProcessResultLine(pb); + } + + private boolean isRootlessDocker() { + ProcessBuilder pb = new ProcessBuilder("docker", "context", "show"); + return getFirstProcessResultLine(pb).equals("rootless"); + } + + private String getFirstProcessResultLine(ProcessBuilder pb) { + Process p = null; + try { + p = pb.start(); + p.waitFor(); + try (var processResult = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + return processResult.readLine(); + } + } catch (IOException | InterruptedException e) { + throw errorFunction.apply(e.getMessage(), e); + } finally { + if (p != null) { + p.destroy(); + } + } + } + + public record TargetPath(Path path, boolean readonly) { + public static TargetPath readonly(Path target) { + return of(target, true); + } + + public static TargetPath of(Path target, boolean readonly) { + return new TargetPath(target, readonly); + } + } + + public static Map mountMappingFor(Path javaHome, Path inputDir, Path outputDir) { + Map mountMapping = new HashMap<>(); + Path containerRoot = Paths.get("/"); + mountMapping.put(javaHome, TargetPath.readonly(containerRoot.resolve(GRAAL_VM_HOME))); + mountMapping.put(inputDir, ContainerSupport.TargetPath.readonly(containerRoot.resolve("input"))); + mountMapping.put(outputDir, ContainerSupport.TargetPath.of(containerRoot.resolve("output"), false)); + return mountMapping; + } + + public List createCommand(Map containerEnvironment, Map mountMapping) { + List containerCommand = new ArrayList<>(); + + // run docker tool without network access and remove container after image build is finished + containerCommand.add(tool); + containerCommand.add("run"); + containerCommand.add("--network=none"); + containerCommand.add("--rm"); + + // inject environment variables into container + containerEnvironment.forEach((key, value) -> { + containerCommand.add("-e"); + containerCommand.add(key + "=" + BundleLauncherUtil.quoteShellArg(value)); + }); + + // mount java home, input and output directories and argument files for native image build + mountMapping.forEach((source, target) -> { + containerCommand.add("--mount"); + List mountArgs = new ArrayList<>(); + mountArgs.add("type=bind"); + mountArgs.add("source=" + source); + mountArgs.add("target=" + target.path); + if (target.readonly) { + mountArgs.add("readonly"); + } + containerCommand.add(BundleLauncherUtil.quoteShellArg(String.join(",", mountArgs))); + }); + + // specify container name + containerCommand.add(image); + + return containerCommand; + } + + public static void replacePaths(List arguments, Path javaHome, Path bundleRoot) { + arguments.replaceAll(arg -> arg + .replace(javaHome.toString(), GRAAL_VM_HOME.toString()) + .replace(bundleRoot.toString(), "")); + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleArgsParser.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleArgsParser.java new file mode 100644 index 000000000000..ef2450d144c9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleArgsParser.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.configuration; + +import java.net.URI; +import java.util.List; + +public class BundleArgsParser extends BundleConfigurationParser { + + private final List args; + + public BundleArgsParser(List args) { + this.args = args; + } + + @Override + public void parseAndRegister(Object json, URI origin) { + for (var arg : asList(json, "Expected a list of arguments")) { + args.add(arg.toString()); + } + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleConfigurationParser.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleConfigurationParser.java new file mode 100644 index 000000000000..28d32162229e --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleConfigurationParser.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.configuration; + +import java.io.IOException; +import java.io.Reader; +import java.net.URI; +import java.util.List; +import java.util.Map; + +import com.oracle.svm.driver.launcher.json.BundleJSONParser; +import com.oracle.svm.driver.launcher.json.BundleJSONParserException; + +public abstract class BundleConfigurationParser { + + public void parseAndRegister(Reader reader) throws IOException { + parseAndRegister(new BundleJSONParser(reader).parse(), null); + } + + public abstract void parseAndRegister(Object json, URI origin) throws IOException; + + @SuppressWarnings("unchecked") + public static List asList(Object data, String errorMessage) { + if (data instanceof List) { + return (List) data; + } + throw new BundleJSONParserException(errorMessage); + } + + @SuppressWarnings("unchecked") + public static Map asMap(Object data, String errorMessage) { + if (data instanceof Map) { + return (Map) data; + } + throw new BundleJSONParserException(errorMessage); + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleContainerSettingsParser.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleContainerSettingsParser.java new file mode 100644 index 000000000000..d78921241b05 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleContainerSettingsParser.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.configuration; + +import java.net.URI; +import java.util.Map; + +public class BundleContainerSettingsParser extends BundleConfigurationParser { + private final Map containerSettings; + + public BundleContainerSettingsParser(Map containerSettings) { + this.containerSettings = containerSettings; + } + + @Override + public void parseAndRegister(Object json, URI origin) { + Map jsonMap = asMap(json, "Expected a map of container settings and values"); + jsonMap.forEach((k, v) -> containerSettings.put(k, v.toString())); + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleEnvironmentParser.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleEnvironmentParser.java new file mode 100644 index 000000000000..8fc01e653794 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundleEnvironmentParser.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.configuration; + +import java.net.URI; +import java.util.Map; + +import com.oracle.svm.driver.launcher.json.BundleJSONParserException; + +public class BundleEnvironmentParser extends BundleConfigurationParser { + private static final String environmentKeyField = "key"; + private static final String environmentValueField = "val"; + private final Map environment; + + public BundleEnvironmentParser(Map environment) { + environment.clear(); + this.environment = environment; + } + + @Override + public void parseAndRegister(Object json, URI origin) { + for (var rawEntry : asList(json, "Expected a list of environment variable objects")) { + var entry = asMap(rawEntry, "Expected a environment variable object"); + Object envVarKeyString = entry.get(environmentKeyField); + if (envVarKeyString == null) { + throw new BundleJSONParserException("Expected " + environmentKeyField + "-field in environment variable object"); + } + Object envVarValueString = entry.get(environmentValueField); + if (envVarValueString == null) { + throw new BundleJSONParserException("Expected " + environmentValueField + "-field in environment variable object"); + } + environment.put(envVarKeyString.toString(), envVarValueString.toString()); + } + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundlePathMapParser.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundlePathMapParser.java new file mode 100644 index 000000000000..c8c40c8d0013 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/configuration/BundlePathMapParser.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.configuration; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Map; + +import com.oracle.svm.driver.launcher.json.BundleJSONParserException; + +public class BundlePathMapParser extends BundleConfigurationParser { + + private static final String substitutionMapSrcField = "src"; + private static final String substitutionMapDstField = "dst"; + + private final Map pathMap; + + public BundlePathMapParser(Map pathMap) { + this.pathMap = pathMap; + } + + @Override + public void parseAndRegister(Object json, URI origin) { + for (var rawEntry : asList(json, "Expected a list of path substitution objects")) { + var entry = asMap(rawEntry, "Expected a substitution object"); + Object srcPathString = entry.get(substitutionMapSrcField); + if (srcPathString == null) { + throw new BundleJSONParserException("Expected " + substitutionMapSrcField + "-field in substitution object"); + } + Object dstPathString = entry.get(substitutionMapDstField); + if (dstPathString == null) { + throw new BundleJSONParserException("Expected " + substitutionMapDstField + "-field in substitution object"); + } + pathMap.put(Path.of(srcPathString.toString()), Path.of(dstPathString.toString())); + } + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/json/BundleJSONParser.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/json/BundleJSONParser.java new file mode 100644 index 000000000000..44818e7c0ddf --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/json/BundleJSONParser.java @@ -0,0 +1,458 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.json; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BundleJSONParser { + + private final String source; + private final int length; + private int pos = 0; + + private static final int EOF = -1; + + private static final String TRUE = "true"; + private static final String FALSE = "false"; + private static final String NULL = "null"; + + private static final int STATE_EMPTY = 0; + private static final int STATE_ELEMENT_PARSED = 1; + private static final int STATE_COMMA_PARSED = 2; + + public BundleJSONParser(String source) { + this.source = source; + this.length = source.length(); + } + + public BundleJSONParser(Reader source) throws IOException { + this(readFully(source)); + } + + /** + * Public parse method. Parse a string into a JSON object. + * + * @return the parsed JSON Object + */ + public Object parse() { + final Object value = parseLiteral(); + skipWhiteSpace(); + if (pos < length) { + throw expectedError(pos, "eof", toString(peek())); + } + return value; + } + + private Object parseLiteral() { + skipWhiteSpace(); + + final int c = peek(); + if (c == EOF) { + throw expectedError(pos, "json literal", "eof"); + } + return switch (c) { + case '{' -> parseObject(); + case '[' -> parseArray(); + case '"' -> parseString(); + case 'f' -> parseKeyword(FALSE, Boolean.FALSE); + case 't' -> parseKeyword(TRUE, Boolean.TRUE); + case 'n' -> parseKeyword(NULL, null); + default -> { + if (isDigit(c) || c == '-') { + yield parseNumber(); + } else if (c == '.') { + throw numberError(pos); + } else { + throw expectedError(pos, "json literal", toString(c)); + } + } + }; + } + + private Object parseObject() { + Map result = new HashMap<>(); + int state = STATE_EMPTY; + + assert peek() == '{'; + pos++; + + while (pos < length) { + skipWhiteSpace(); + final int c = peek(); + + switch (c) { + case '"' -> { + if (state == STATE_ELEMENT_PARSED) { + throw expectedError(pos, ", or }", toString(c)); + } + final String id = parseString(); + expectColon(); + final Object value = parseLiteral(); + result.put(id, value); + state = STATE_ELEMENT_PARSED; + } + case ',' -> { + if (state != STATE_ELEMENT_PARSED) { + throw error("Trailing comma is not allowed in JSON", pos); + } + state = STATE_COMMA_PARSED; + pos++; + } + case '}' -> { + if (state == STATE_COMMA_PARSED) { + throw error("Trailing comma is not allowed in JSON", pos); + } + pos++; + return result; + } + default -> throw expectedError(pos, ", or }", toString(c)); + } + } + throw expectedError(pos, ", or }", "eof"); + } + + private void expectColon() { + skipWhiteSpace(); + final int n = next(); + if (n != ':') { + throw expectedError(pos - 1, ":", toString(n)); + } + } + + private Object parseArray() { + List result = new ArrayList<>(); + int state = STATE_EMPTY; + + assert peek() == '['; + pos++; + + while (pos < length) { + skipWhiteSpace(); + final int c = peek(); + + switch (c) { + case ',' -> { + if (state != STATE_ELEMENT_PARSED) { + throw error("Trailing comma is not allowed in JSON", pos); + } + state = STATE_COMMA_PARSED; + pos++; + } + case ']' -> { + if (state == STATE_COMMA_PARSED) { + throw error("Trailing comma is not allowed in JSON", pos); + } + pos++; + return result; + } + default -> { + if (state == STATE_ELEMENT_PARSED) { + throw expectedError(pos, ", or ]", toString(c)); + } + result.add(parseLiteral()); + state = STATE_ELEMENT_PARSED; + } + } + } + + throw expectedError(pos, ", or ]", "eof"); + } + + private String parseString() { + // String buffer is only instantiated if string contains escape sequences. + int start = ++pos; + StringBuilder sb = null; + + while (pos < length) { + final int c = next(); + if (c <= 0x1f) { + // Characters < 0x1f are not allowed in JSON strings. + throw syntaxError(pos, "String contains control character"); + + } else if (c == '\\') { + if (sb == null) { + sb = new StringBuilder(pos - start + 16); + } + sb.append(source, start, pos - 1); + sb.append(parseEscapeSequence()); + start = pos; + + } else if (c == '"') { + if (sb != null) { + sb.append(source, start, pos - 1); + return sb.toString(); + } + return source.substring(start, pos - 1); + } + } + + throw error("Missing close quote", pos); + } + + private char parseEscapeSequence() { + final int c = next(); + return switch (c) { + case '"' -> '"'; + case '\\' -> '\\'; + case '/' -> '/'; + case 'b' -> '\b'; + case 'f' -> '\f'; + case 'n' -> '\n'; + case 'r' -> '\r'; + case 't' -> '\t'; + case 'u' -> parseUnicodeEscape(); + default -> throw error("Invalid escape character", pos - 1); + }; + } + + private char parseUnicodeEscape() { + return (char) (parseHexDigit() << 12 | parseHexDigit() << 8 | parseHexDigit() << 4 | parseHexDigit()); + } + + private int parseHexDigit() { + final int c = next(); + if (c >= '0' && c <= '9') { + return c - '0'; + } else if (c >= 'A' && c <= 'F') { + return c + 10 - 'A'; + } else if (c >= 'a' && c <= 'f') { + return c + 10 - 'a'; + } + throw error("Invalid hex digit", pos - 1); + } + + private static boolean isDigit(final int c) { + return c >= '0' && c <= '9'; + } + + private void skipDigits() { + while (pos < length) { + final int c = peek(); + if (!isDigit(c)) { + break; + } + pos++; + } + } + + private Number parseNumber() { + boolean isFloating = false; + final int start = pos; + int c = next(); + + if (c == '-') { + c = next(); + } + if (!isDigit(c)) { + throw numberError(start); + } + // no more digits allowed after 0 + if (c != '0') { + skipDigits(); + } + + // fraction + if (peek() == '.') { + isFloating = true; + pos++; + if (!isDigit(next())) { + throw numberError(pos - 1); + } + skipDigits(); + } + + // exponent + c = peek(); + if (c == 'e' || c == 'E') { + pos++; + c = next(); + if (c == '-' || c == '+') { + c = next(); + } + if (!isDigit(c)) { + throw numberError(pos - 1); + } + skipDigits(); + } + + String literalValue = source.substring(start, pos); + if (isFloating) { + return Double.parseDouble(literalValue); + } else { + final long l = Long.parseLong(literalValue); + if ((int) l == l) { + return (int) l; + } else { + return l; + } + } + } + + private Object parseKeyword(final String keyword, final Object value) { + if (!source.regionMatches(pos, keyword, 0, keyword.length())) { + throw expectedError(pos, "json literal", "ident"); + } + pos += keyword.length(); + return value; + } + + private int peek() { + if (pos >= length) { + return -1; + } + return source.charAt(pos); + } + + private int next() { + final int next = peek(); + pos++; + return next; + } + + private void skipWhiteSpace() { + while (pos < length) { + switch (peek()) { + case '\t', '\r', '\n', ' ' -> pos++; + default -> { + return; + } + } + } + } + + private static String toString(final int c) { + return c == EOF ? "eof" : String.valueOf((char) c); + } + + private BundleJSONParserException error(final String message, final int start) { + final int lineNum = getLine(start); + final int columnNum = getColumn(start); + final String formatted = format(message, lineNum, columnNum); + return new BundleJSONParserException(formatted); + } + + /** + * Return line number of character position. + * + *

+ * This method can be expensive for large sources as it iterates through all characters up to + * {@code position}. + *

+ * + * @param position Position of character in source content. + * @return Line number. + */ + private int getLine(final int position) { + final CharSequence d = source; + // Line count starts at 1. + int line = 1; + + for (int i = 0; i < position; i++) { + final char ch = d.charAt(i); + // Works for both \n and \r\n. + if (ch == '\n') { + line++; + } + } + + return line; + } + + /** + * Return column number of character position. + * + * @param position Position of character in source content. + * @return Column number. + */ + private int getColumn(final int position) { + return position - findBOLN(position); + } + + /** + * Find the beginning of the line containing position. + * + * @param position Index to offending token. + * @return Index of first character of line. + */ + private int findBOLN(final int position) { + final CharSequence d = source; + for (int i = position - 1; i > 0; i--) { + final char ch = d.charAt(i); + + if (ch == '\n' || ch == '\r') { + return i + 1; + } + } + + return 0; + } + + /** + * Format an error message to include source and line information. + * + * @param message Error message string. + * @param line Source line number. + * @param column Source column number. + * @return formatted string + */ + private static String format(final String message, final int line, final int column) { + return "line " + line + " column " + column + " " + message; + } + + private BundleJSONParserException numberError(final int start) { + return error("Invalid JSON number format", start); + } + + private BundleJSONParserException expectedError(final int start, final String expected, final String found) { + return error("Expected " + expected + " but found " + found, start); + } + + private BundleJSONParserException syntaxError(final int start, final String reason) { + return error("Invalid JSON: " + reason, start); + } + + /** + * Utility function to read all contents of a {@link Reader}, because the JSON parser does not + * support streaming yet. + */ + private static String readFully(final Reader reader) throws IOException { + final char[] arr = new char[1024]; + final StringBuilder sb = new StringBuilder(); + + try (reader) { + int numChars; + while ((numChars = reader.read(arr, 0, arr.length)) > 0) { + sb.append(arr, 0, numChars); + } + } + + return sb.toString(); + } +} diff --git a/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/json/BundleJSONParserException.java b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/json/BundleJSONParserException.java new file mode 100644 index 000000000000..a1d00816b176 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver.launcher/src/com/oracle/svm/driver/launcher/json/BundleJSONParserException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.driver.launcher.json; + +@SuppressWarnings("serial") +public final class BundleJSONParserException extends RuntimeException { + public BundleJSONParserException(final String msg) { + super(msg); + } +} diff --git a/substratevm/src/com.oracle.svm.driver/resources/HelpExtra.txt b/substratevm/src/com.oracle.svm.driver/resources/HelpExtra.txt index 016955b6da0a..ce64c22d3a91 100644 --- a/substratevm/src/com.oracle.svm.driver/resources/HelpExtra.txt +++ b/substratevm/src/com.oracle.svm.driver/resources/HelpExtra.txt @@ -21,14 +21,20 @@ Non-standard options help: --diagnostics-mode Enables logging of image-build information to a diagnostics folder. --dry-run output the command line that would be used for building - --bundle-create[=new-bundle.nib] + --bundle-create[=new-bundle.nib][,dry-run][,container[=][,dockerfile=]] in addition to image building, create a Native Image bundle file (*.nib file) that allows rebuilding of that image again at a later point. If a bundle-file gets passed, the bundle will be created with the given name. Otherwise, the bundle-file name is derived from the image name. - Note both bundle options can be combined with --dry-run to only perform - the bundle operations without any actual image building. - --bundle-apply=some-bundle.nib + Note both bundle options can be extended with ",dry-run" and ",container" + * 'dry-run': only perform the bundle operations without any actual image building. + * 'container': sets up a container image for image building and performs image building + from inside that container. Requires podman or rootless docker to be installed. + If available, 'podman' is preferred and rootless 'docker' is the fallback. Specifying + one or the other as '=' forces the use of a specific tool. + * 'dockerfile=': Use a user provided 'Dockerfile' instead of the default based on + Oracle Linux 8 base images for GraalVM (see https://github.com/graalvm/container) + --bundle-apply=some-bundle.nib[,dry-run][,container[=][,dockerfile=]] an image will be built from the given bundle file with the exact same arguments and files that have been passed to native-image originally to create the bundle. Note that if an extra --bundle-create gets passed diff --git a/substratevm/src/com.oracle.svm.driver/resources/container-default/Dockerfile b/substratevm/src/com.oracle.svm.driver/resources/container-default/Dockerfile new file mode 100644 index 000000000000..7c15c77f6174 --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver/resources/container-default/Dockerfile @@ -0,0 +1,37 @@ +# Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + +ARG BASE_IMAGE=container-registry.oracle.com/os/oraclelinux:8-slim + +FROM ${BASE_IMAGE} as base + +RUN microdnf update -y oraclelinux-release-el8 \ + && microdnf --enablerepo ol8_codeready_builder install bzip2-devel ed gcc gcc-c++ gcc-gfortran gzip file fontconfig less libcurl-devel make openssl openssl-devel readline-devel tar glibc-langpack-en \ + vi which xz-devel zlib-devel findutils glibc-static libstdc++ libstdc++-devel libstdc++-static zlib-static \ + && microdnf clean all +RUN fc-cache -f -v + +ENV LANG=en_US.UTF-8 \ + JAVA_HOME=/graalvm + +WORKDIR / \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.driver/resources/container-default/Dockerfile_muslib_extension b/substratevm/src/com.oracle.svm.driver/resources/container-default/Dockerfile_muslib_extension new file mode 100644 index 000000000000..6259862460aa --- /dev/null +++ b/substratevm/src/com.oracle.svm.driver/resources/container-default/Dockerfile_muslib_extension @@ -0,0 +1,53 @@ +# Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. + +FROM base as muslib + +ARG TEMP_REGION="" +ARG MUSL_LOCATION=http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz +ARG ZLIB_LOCATION=https://zlib.net/fossils/zlib-1.2.11.tar.gz + +ENV TOOLCHAIN_DIR=/usr/local/musl \ + CC=$TOOLCHAIN_DIR/bin/gcc + +RUN echo "$TEMP_REGION" > /etc/dnf/vars/ociregion \ + && rm -rf /etc/yum.repos.d/ol8_graalvm_community.repo \ + && mkdir -p $TOOLCHAIN_DIR \ + && microdnf install -y wget tar gzip make \ + && wget $MUSL_LOCATION && tar -xvf x86_64-linux-musl-native.tgz -C $TOOLCHAIN_DIR --strip-components=1 \ + && wget $ZLIB_LOCATION && tar -xvf zlib-1.2.11.tar.gz \ + && cd zlib-1.2.11 \ + && ./configure --prefix=$TOOLCHAIN_DIR --static \ + && make && make install + + +FROM base as final + +COPY --from=muslib /usr/local/musl /usr/local/musl + +RUN echo "" > /etc/dnf/vars/ociregion + +ENV TOOLCHAIN_DIR=/usr/local/musl \ + CC=$TOOLCHAIN_DIR/bin/gcc + +ENV PATH=$TOOLCHAIN_DIR/bin:$PATH \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/APIOptionHandler.java b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/APIOptionHandler.java index 1c59e5804fee..193c66879229 100644 --- a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/APIOptionHandler.java +++ b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/APIOptionHandler.java @@ -76,7 +76,7 @@ class APIOptionHandler extends NativeImage.OptionHandler { private static final String LEAVE_UNLOCK_SCOPE = SubstrateOptionsParser.commandArgument(SubstrateOptions.UnlockExperimentalVMOptions, "-"); record OptionInfo(String[] variants, char[] valueSeparator, String builderOption, String defaultValue, String helpText, boolean defaultFinal, String deprecationWarning, - List> valueTransformers, APIOptionGroup group, boolean extra) { + List> valueTransformers, APIOptionGroup group, boolean extra, boolean launcherOption) { boolean isDeprecated() { return deprecationWarning.length() > 0; } @@ -259,7 +259,7 @@ private static void extractOption(String optionPrefix, OptionDescriptor optionDe boolean defaultFinal = booleanOption || hasFixedValue; apiOptions.put(apiOptionName, new APIOptionHandler.OptionInfo(apiAnnotation.name(), apiAnnotation.valueSeparator(), builderOption, defaultValue, helpText, - defaultFinal, apiAnnotation.deprecated(), valueTransformers, group, apiAnnotation.extra())); + defaultFinal, apiAnnotation.deprecated(), valueTransformers, group, apiAnnotation.extra(), apiAnnotation.launcherOption())); } if (optionDescriptor.getStability() == OptionStability.STABLE) { @@ -328,6 +328,7 @@ boolean consume(ArgumentQueue args) { String translateOption(ArgumentQueue argQueue) { OptionInfo option = null; + boolean whitespaceSeparated = false; String[] optionNameAndOptionValue = null; OptionOrigin argumentOrigin = OptionOrigin.from(argQueue.argumentOrigin); found: for (OptionInfo optionInfo : apiOptions.values()) { @@ -353,6 +354,7 @@ String translateOption(ArgumentQueue argQueue) { } option = optionInfo; optionNameAndOptionValue = new String[]{headArg, optionValue}; + whitespaceSeparated = true; break found; } else { boolean withSeparator = valueSeparator != APIOption.NO_SEPARATOR; @@ -390,6 +392,14 @@ String translateOption(ArgumentQueue argQueue) { builderOption += transformed.toString(); } + if (nativeImage.useBundle() && option.launcherOption) { + if (whitespaceSeparated) { + nativeImage.bundleSupport.bundleLauncherArgs.addAll(List.of(optionNameAndOptionValue)); + } else { + nativeImage.bundleSupport.bundleLauncherArgs.add(argQueue.peek()); + } + } + return builderOption; } return null; diff --git a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/BundleSupport.java b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/BundleSupport.java index 0097fc926885..5eb2678dec0c 100644 --- a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/BundleSupport.java +++ b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/BundleSupport.java @@ -26,13 +26,15 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; -import java.net.URI; import java.nio.file.CopyOption; +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.StandardCopyOption; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -61,14 +63,16 @@ import java.util.jar.Manifest; import java.util.stream.Stream; -import org.graalvm.util.json.JSONParserException; - import com.oracle.svm.core.OS; import com.oracle.svm.core.SubstrateUtil; -import com.oracle.svm.core.configure.ConfigurationParser; import com.oracle.svm.core.option.BundleMember; import com.oracle.svm.core.util.json.JsonPrinter; import com.oracle.svm.core.util.json.JsonWriter; +import com.oracle.svm.driver.launcher.BundleLauncher; +import com.oracle.svm.driver.launcher.ContainerSupport; +import com.oracle.svm.driver.launcher.configuration.BundleArgsParser; +import com.oracle.svm.driver.launcher.configuration.BundleEnvironmentParser; +import com.oracle.svm.driver.launcher.configuration.BundlePathMapParser; import com.oracle.svm.util.ClassUtil; import com.oracle.svm.util.LogUtils; import com.oracle.svm.util.StringUtil; @@ -78,7 +82,7 @@ final class BundleSupport { final NativeImage nativeImage; final Path rootDir; - + final Path inputDir; final Path stageDir; final Path classPathDir; final Path modulePathDir; @@ -93,6 +97,7 @@ final class BundleSupport { private final boolean forceBuilderOnClasspath; private final List nativeImageArgs; private List updatedNativeImageArgs; + final ArrayList bundleLauncherArgs = new ArrayList<>(); boolean loadBundle; boolean writeBundle; @@ -110,8 +115,21 @@ final class BundleSupport { private final BundleProperties bundleProperties; static final String BUNDLE_OPTION = "--bundle"; + private static final String DRY_RUN_OPTION = "dry-run"; + private static final String CONTAINER_OPTION = "container"; + private static final String DOCKERFILE_OPTION = "dockerfile"; static final String BUNDLE_FILE_EXTENSION = ".nib"; + ContainerSupport containerSupport; + boolean useContainer; + + private static final String DEFAULT_DOCKERFILE = getDockerfile("Dockerfile"); + private static final String DEFAULT_DOCKERFILE_MUSLIB = getDockerfile("Dockerfile_muslib_extension"); + + private static String getDockerfile(String name) { + return NativeImage.getResource("/container-default/" + name); + } + enum BundleOptionVariants { create(), apply(); @@ -123,11 +141,12 @@ String optionName() { static BundleSupport create(NativeImage nativeImage, String bundleArg, NativeImage.ArgumentQueue args) { try { - String variant = bundleArg.substring(BUNDLE_OPTION.length() + 1); String bundleFilename = null; - String[] variantParts = SubstrateUtil.split(variant, "=", 2); + String[] options = SubstrateUtil.split(bundleArg.substring(BUNDLE_OPTION.length() + 1), ","); + + String[] variantParts = SubstrateUtil.split(options[0], "=", 2); + String variant = variantParts[0]; if (variantParts.length == 2) { - variant = variantParts[0]; bundleFilename = variantParts[1]; } String applyOptionName = BundleOptionVariants.apply.optionName(); @@ -174,6 +193,30 @@ static BundleSupport create(NativeImage nativeImage, String bundleArg, NativeIma default: throw new IllegalArgumentException(); } + + Arrays.stream(options) + .skip(1) + .forEach(bundleSupport::parseExtendedOption); + + if (!bundleSupport.useContainer && bundleSupport.bundleProperties.requireContainerBuild()) { + if (!OS.LINUX.isCurrent()) { + LogUtils.warning(BUNDLE_INFO_MESSAGE_PREFIX + "Bundle was built in a container, but container builds are only supported for Linux."); + } else { + bundleSupport.useContainer = true; + bundleSupport.containerSupport = new ContainerSupport(bundleSupport.stageDir, NativeImage::showError, LogUtils::warning, nativeImage::showMessage); + } + } + + if (bundleSupport.useContainer) { + if (!OS.LINUX.isCurrent()) { + nativeImage.showMessage(BUNDLE_INFO_MESSAGE_PREFIX + "Skipping containerized build, only supported for Linux."); + bundleSupport.useContainer = false; + } else if (nativeImage.isDryRun()) { + nativeImage.showMessage(BUNDLE_INFO_MESSAGE_PREFIX + "Skipping container creation for native-image bundle with dry-run option."); + bundleSupport.useContainer = false; + } + } + return bundleSupport; } catch (StringIndexOutOfBoundsException | IllegalArgumentException e) { @@ -182,6 +225,65 @@ static BundleSupport create(NativeImage nativeImage, String bundleArg, NativeIma } } + void createDockerfile(Path dockerfile) { + nativeImage.showVerboseMessage(nativeImage.isVerbose(), BUNDLE_INFO_MESSAGE_PREFIX + "Creating default Dockerfile for native-image bundle."); + String dockerfileText = DEFAULT_DOCKERFILE; + if (nativeImage.staticExecutable && nativeImage.libC.equals("musl")) { + dockerfileText += System.lineSeparator() + DEFAULT_DOCKERFILE_MUSLIB; + } + try { + Files.writeString(dockerfile, dockerfileText); + dockerfile.toFile().deleteOnExit(); + } catch (IOException e) { + throw NativeImage.showError("Failed to create default Dockerfile " + dockerfile); + } + } + + private void parseExtendedOption(String option) { + String optionKey; + String optionValue; + + String[] optionParts = SubstrateUtil.split(option, "=", 2); + if (optionParts.length == 2) { + optionKey = optionParts[0]; + optionValue = optionParts[1]; + } else { + optionKey = option; + optionValue = null; + } + + switch (optionKey) { + case DRY_RUN_OPTION -> nativeImage.setDryRun(true); + case CONTAINER_OPTION -> { + if (containerSupport != null) { + throw NativeImage.showError(String.format("native-image bundle allows option %s to be specified only once.", optionKey)); + } + containerSupport = new ContainerSupport(stageDir, NativeImage::showError, LogUtils::warning, nativeImage::showMessage); + useContainer = true; + if (optionValue != null) { + if (!ContainerSupport.SUPPORTED_TOOLS.contains(optionValue)) { + throw NativeImage.showError(String.format("Container Tool '%s' is not supported, please use one of the following tools: %s", optionValue, ContainerSupport.SUPPORTED_TOOLS)); + } + containerSupport.tool = optionValue; + } + } + case DOCKERFILE_OPTION -> { + if (containerSupport == null) { + throw NativeImage.showError(String.format("native-image bundle option %s is only allowed to be used after option %s.", optionKey, CONTAINER_OPTION)); + } + if (optionValue != null) { + containerSupport.dockerfile = Path.of(optionValue); + if (!Files.isReadable(containerSupport.dockerfile)) { + throw NativeImage.showError(String.format("Dockerfile '%s' is not readable", containerSupport.dockerfile.toAbsolutePath())); + } + } else { + throw NativeImage.showError(String.format("native-image option %s requires a dockerfile argument. E.g. %s=path/to/Dockerfile.", optionKey, optionKey)); + } + } + default -> throw NativeImage.showError(String.format("Unknown option %s. Use --help-extra for usage instructions.", optionKey)); + } + } + private BundleSupport(NativeImage nativeImage) { Objects.requireNonNull(nativeImage); this.nativeImage = nativeImage; @@ -193,7 +295,7 @@ private BundleSupport(NativeImage nativeImage) { bundleProperties = new BundleProperties(); bundleProperties.properties.put(BundleProperties.PROPERTY_KEY_IMAGE_BUILD_ID, UUID.randomUUID().toString()); - Path inputDir = rootDir.resolve("input"); + inputDir = rootDir.resolve("input"); stageDir = Files.createDirectories(inputDir.resolve("stage")); auxiliaryDir = Files.createDirectories(inputDir.resolve("auxiliary")); Path classesDir = inputDir.resolve("classes"); @@ -262,7 +364,7 @@ private BundleSupport(NativeImage nativeImage, String bundleFilenameArg) { nativeImage.config.modulePathBuild = !forceBuilderOnClasspath; try { - Path inputDir = rootDir.resolve("input"); + inputDir = rootDir.resolve("input"); stageDir = Files.createDirectories(inputDir.resolve("stage")); auxiliaryDir = Files.createDirectories(inputDir.resolve("auxiliary")); Path classesDir = inputDir.resolve("classes"); @@ -276,20 +378,20 @@ private BundleSupport(NativeImage nativeImage, String bundleFilenameArg) { Path pathCanonicalizationsFile = stageDir.resolve("path_canonicalizations.json"); try (Reader reader = Files.newBufferedReader(pathCanonicalizationsFile)) { - new PathMapParser(pathCanonicalizations).parseAndRegister(reader); + new BundlePathMapParser(pathCanonicalizations).parseAndRegister(reader); } catch (IOException e) { throw NativeImage.showError("Failed to read bundle-file " + pathCanonicalizationsFile, e); } Path pathSubstitutionsFile = stageDir.resolve("path_substitutions.json"); try (Reader reader = Files.newBufferedReader(pathSubstitutionsFile)) { - new PathMapParser(pathSubstitutions).parseAndRegister(reader); + new BundlePathMapParser(pathSubstitutions).parseAndRegister(reader); } catch (IOException e) { throw NativeImage.showError("Failed to read bundle-file " + pathSubstitutionsFile, e); } Path environmentFile = stageDir.resolve("environment.json"); if (Files.isReadable(environmentFile)) { try (Reader reader = Files.newBufferedReader(environmentFile)) { - new EnvironmentParser(nativeImage.imageBuilderEnvironment).parseAndRegister(reader); + new BundleEnvironmentParser(nativeImage.imageBuilderEnvironment).parseAndRegister(reader); } catch (IOException e) { throw NativeImage.showError("Failed to read bundle-file " + environmentFile, e); } @@ -298,7 +400,7 @@ private BundleSupport(NativeImage nativeImage, String bundleFilenameArg) { Path buildArgsFile = stageDir.resolve("build.json"); try (Reader reader = Files.newBufferedReader(buildArgsFile)) { List buildArgsFromFile = new ArrayList<>(); - new BuildArgsParser(buildArgsFromFile).parseAndRegister(reader); + new BundleArgsParser(buildArgsFromFile).parseAndRegister(reader); nativeImageArgs = Collections.unmodifiableList(buildArgsFromFile); } catch (IOException e) { throw NativeImage.showError("Failed to read bundle-file " + buildArgsFile, e); @@ -593,6 +695,27 @@ private Path writeBundle() { nativeImage.deleteAllFiles(metaInfDir); } + Path bundleLauncherFile = Paths.get("/").resolve(BundleLauncher.class.getName().replace(".", "/") + ".class"); + try (FileSystem fs = FileSystems.newFileSystem(BundleSupport.class.getResource(bundleLauncherFile.toString()).toURI(), new HashMap<>()); + Stream walk = Files.walk(fs.getPath(bundleLauncherFile.getParent().toString()))) { + walk.filter(Predicate.not(Files::isDirectory)) + .map(Path::toString) + .forEach(sourcePath -> { + Path target = rootDir.resolve(Paths.get("/").relativize(Paths.get(sourcePath))); + try (InputStream source = BundleSupport.class.getResourceAsStream(sourcePath)) { + Path bundleFileParent = target.getParent(); + if (bundleFileParent != null) { + Files.createDirectories(bundleFileParent); + } + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw NativeImage.showError("Failed to write bundle-file " + target, e); + } + }); + } catch (Exception e) { + throw NativeImage.showError("Failed to read bundle launcher resources '" + bundleLauncherFile.getParent() + "'", e); + } + Path pathCanonicalizationsFile = stageDir.resolve("path_canonicalizations.json"); try (JsonWriter writer = new JsonWriter(pathCanonicalizationsFile)) { /* Printing as list with defined sort-order ensures useful diffs are possible */ @@ -615,11 +738,47 @@ private Path writeBundle() { throw NativeImage.showError("Failed to write bundle-file " + environmentFile, e); } + if (containerSupport != null) { + Map containerInfo = new HashMap<>(); + if (containerSupport.image != null) { + containerInfo.put(ContainerSupport.IMAGE_JSON_KEY, containerSupport.image); + } + if (containerSupport.tool != null) { + containerInfo.put(ContainerSupport.TOOL_JSON_KEY, containerSupport.tool); + } + if (containerSupport.toolVersion != null) { + containerInfo.put(ContainerSupport.TOOL_VERSION_JSON_KEY, containerSupport.toolVersion); + } + + if (!containerInfo.isEmpty()) { + Path containerFile = stageDir.resolve("container.json"); + try (JsonWriter writer = new JsonWriter(containerFile)) { + writer.print(containerInfo); + } catch (IOException e) { + throw NativeImage.showError("Failed to write bundle-file " + containerFile, e); + } + } + } + + Path dockerfilePath = stageDir.resolve("Dockerfile"); + try { + if (containerSupport == null || !Files.exists(containerSupport.dockerfile)) { + // if no Dockerfile was created yet create a new default Dockerfile + if (!Files.exists(dockerfilePath)) { + createDockerfile(dockerfilePath); + } + } else if (!dockerfilePath.equals(containerSupport.dockerfile)) { + Files.copy(containerSupport.dockerfile, dockerfilePath, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw NativeImage.showError("Failed to write bundle-file " + dockerfilePath, e); + } + Path buildArgsFile = stageDir.resolve("build.json"); + ArrayList bundleArgs = new ArrayList<>(updatedNativeImageArgs != null ? updatedNativeImageArgs : nativeImageArgs); try (JsonWriter writer = new JsonWriter(buildArgsFile)) { List equalsNonBundleOptions = List.of(CmdLineOptionHandler.VERBOSE_OPTION, CmdLineOptionHandler.DRY_RUN_OPTION); List startsWithNonBundleOptions = List.of(BUNDLE_OPTION, DefaultOptionHandler.ADD_ENV_VAR_OPTION, nativeImage.oHPath); - ArrayList bundleArgs = new ArrayList<>(updatedNativeImageArgs != null ? updatedNativeImageArgs : nativeImageArgs); ListIterator bundleArgsIterator = bundleArgs.listIterator(); while (bundleArgsIterator.hasNext()) { String arg = bundleArgsIterator.next(); @@ -639,6 +798,31 @@ private Path writeBundle() { throw NativeImage.showError("Failed to write bundle-file " + buildArgsFile, e); } + // skip run.json for shared library bundles + if (nativeImage.buildExecutable) { + Path runArgsFile = stageDir.resolve("run.json"); + try (JsonWriter writer = new JsonWriter(runArgsFile)) { + List runArgs = new ArrayList<>(bundleLauncherArgs); + boolean hasMainClassModule = nativeImage.mainClassModule != null && !nativeImage.mainClassModule.isEmpty(); + boolean hasMainClass = nativeImage.mainClass != null && !nativeImage.mainClass.isEmpty(); + if (hasMainClassModule) { + runArgs.add("-m"); + StringBuilder mainModule = new StringBuilder(nativeImage.mainClassModule); + if (hasMainClass) { + mainModule.append("/").append(nativeImage.mainClass); + } + runArgs.add(mainModule.toString()); + } else { + runArgs.add(nativeImage.mainClass); + } + + /* Printing as list with defined sort-order ensures useful diffs are possible */ + JsonPrinter.printCollection(writer, runArgs, null, BundleSupport::printBuildArg); + } catch (IOException e) { + throw NativeImage.showError("Failed to write bundle-file " + runArgsFile, e); + } + } + bundleProperties.write(); Path bundleFilePath = bundlePath.resolve(bundleName + BUNDLE_FILE_EXTENSION); @@ -668,7 +852,7 @@ private static Manifest createManifest() { Manifest mf = new Manifest(); Attributes attributes = mf.getMainAttributes(); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); - /* If we add run-bundle-as-java-application a launcher mainclass would be added here */ + attributes.put(Attributes.Name.MAIN_CLASS, BundleLauncher.class.getName()); return mf; } @@ -697,76 +881,6 @@ private static void printEnvironmentVariable(Map.Entry entry, Js w.append('}'); } - private static final class PathMapParser extends ConfigurationParser { - - private final Map pathMap; - - private PathMapParser(Map pathMap) { - super(true); - this.pathMap = pathMap; - } - - @Override - public void parseAndRegister(Object json, URI origin) { - for (var rawEntry : asList(json, "Expected a list of path substitution objects")) { - var entry = asMap(rawEntry, "Expected a substitution object"); - Object srcPathString = entry.get(substitutionMapSrcField); - if (srcPathString == null) { - throw new JSONParserException("Expected " + substitutionMapSrcField + "-field in substitution object"); - } - Object dstPathString = entry.get(substitutionMapDstField); - if (dstPathString == null) { - throw new JSONParserException("Expected " + substitutionMapDstField + "-field in substitution object"); - } - pathMap.put(Path.of(srcPathString.toString()), Path.of(dstPathString.toString())); - } - } - } - - private static final class EnvironmentParser extends ConfigurationParser { - - private final Map environment; - - private EnvironmentParser(Map environment) { - super(true); - environment.clear(); - this.environment = environment; - } - - @Override - public void parseAndRegister(Object json, URI origin) { - for (var rawEntry : asList(json, "Expected a list of environment variable objects")) { - var entry = asMap(rawEntry, "Expected a environment variable object"); - Object envVarKeyString = entry.get(environmentKeyField); - if (envVarKeyString == null) { - throw new JSONParserException("Expected " + environmentKeyField + "-field in environment variable object"); - } - Object envVarValueString = entry.get(environmentValueField); - if (envVarValueString == null) { - throw new JSONParserException("Expected " + environmentValueField + "-field in environment variable object"); - } - environment.put(envVarKeyString.toString(), envVarValueString.toString()); - } - } - } - - private static final class BuildArgsParser extends ConfigurationParser { - - private final List args; - - private BuildArgsParser(List args) { - super(true); - this.args = args; - } - - @Override - public void parseAndRegister(Object json, URI origin) { - for (var arg : asList(json, "Expected a list of arguments")) { - args.add(arg.toString()); - } - } - } - private static final Path bundlePropertiesFileName = Path.of("META-INF/nibundle.properties"); private final class BundleProperties { @@ -849,6 +963,11 @@ private boolean forceBuilderOnClasspath() { return Boolean.parseBoolean(properties.getOrDefault(PROPERTY_KEY_BUILDER_ON_CLASSPATH, Boolean.FALSE.toString())); } + private boolean requireContainerBuild() { + assert !properties.isEmpty() : "Needs to be called after loadAndVerify()"; + return Boolean.parseBoolean(properties.getOrDefault(PROPERTY_KEY_BUILT_WITH_CONTAINER, Boolean.FALSE.toString())); + } + private void write() { properties.put(PROPERTY_KEY_BUNDLE_FILE_VERSION_MAJOR, String.valueOf(BUNDLE_FILE_FORMAT_VERSION_MAJOR)); properties.put(PROPERTY_KEY_BUNDLE_FILE_VERSION_MINOR, String.valueOf(BUNDLE_FILE_FORMAT_VERSION_MINOR)); @@ -857,7 +976,7 @@ private void write() { boolean imageBuilt = !nativeImage.isDryRun(); properties.put(PROPERTY_KEY_IMAGE_BUILT, String.valueOf(imageBuilt)); if (imageBuilt) { - properties.put(PROPERTY_KEY_BUILT_WITH_CONTAINER, String.valueOf(false)); + properties.put(PROPERTY_KEY_BUILT_WITH_CONTAINER, String.valueOf(useContainer)); } properties.put(PROPERTY_KEY_NATIVE_IMAGE_PLATFORM, NativeImage.platform); properties.put(PROPERTY_KEY_NATIVE_IMAGE_VENDOR, System.getProperty("java.vm.vendor")); diff --git a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java index f8781b866b0e..3ab15e83a87b 100644 --- a/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java +++ b/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java @@ -91,6 +91,7 @@ import com.oracle.svm.core.util.VMError; import com.oracle.svm.driver.MacroOption.EnabledOption; import com.oracle.svm.driver.MacroOption.Registry; +import com.oracle.svm.driver.launcher.ContainerSupport; import com.oracle.svm.driver.metainf.MetaInfFileType; import com.oracle.svm.driver.metainf.NativeImageMetaInfResourceProcessor; import com.oracle.svm.driver.metainf.NativeImageMetaInfWalker; @@ -248,6 +249,8 @@ private static String oR(OptionKey option) { final String oHClass = oH(SubstrateOptions.Class); final String oHName = oH(SubstrateOptions.Name); final String oHPath = oH(SubstrateOptions.Path); + final String oHUseLibC = oH(SubstrateOptions.UseLibC); + final String oHEnableStaticExecutable = oHEnabled(SubstrateOptions.StaticExecutable); final String oHEnableSharedLibraryFlagPrefix = oHEnabled + SubstrateOptions.SharedLibrary.getName(); final String oHEnableBuildOutputColorful = oHEnabledByDriver(SubstrateOptions.BuildOutputColorful); final String oHEnableBuildOutputProgress = oHEnabledByDriver(SubstrateOptions.BuildOutputProgress); @@ -1094,7 +1097,9 @@ private int completeImageBuild() { imageBuilderJavaArgs.addAll(getAgentArguments()); mainClass = getHostedOptionFinalArgumentValue(imageBuilderArgs, oHClass); - boolean buildExecutable = imageBuilderArgs.stream().noneMatch(arg -> arg.startsWith(oHEnableSharedLibraryFlagPrefix)); + buildExecutable = imageBuilderArgs.stream().noneMatch(arg -> arg.startsWith(oHEnableSharedLibraryFlagPrefix)); + staticExecutable = imageBuilderArgs.stream().anyMatch(arg -> arg.contains(oHEnableStaticExecutable)); + libC = getHostedOptionFinalArgumentValue(imageBuilderArgs, oHUseLibC); boolean listModules = imageBuilderArgs.stream().anyMatch(arg -> arg.contains(oH + "+" + "ListModules")); printFlags |= imageBuilderArgs.stream().anyMatch(arg -> arg.matches("-H:MicroArchitecture(@[^=]*)?=list")); @@ -1115,7 +1120,7 @@ private int completeImageBuild() { if (!jarOptionMode) { /* Main-class from customImageBuilderArgs counts as explicitMainClass */ boolean explicitMainClass = getHostedOptionFinalArgumentValue(imageBuilderArgs, oHClass) != null; - String mainClassModule = getHostedOptionFinalArgumentValue(imageBuilderArgs, oHModule); + mainClassModule = getHostedOptionFinalArgumentValue(imageBuilderArgs, oHModule); boolean hasMainClassModule = mainClassModule != null && !mainClassModule.isEmpty(); boolean hasMainClass = mainClass != null && !mainClass.isEmpty(); @@ -1409,7 +1414,11 @@ private void addTargetArguments() { } } + boolean buildExecutable; + boolean staticExecutable; + String libC; String mainClass; + String mainClassModule; String imageName; Path imagePath; @@ -1427,7 +1436,7 @@ protected static List createImageBuilderArgs(List imageArgs, Lis return result; } - protected static String createVMInvocationArgumentFile(List arguments) { + protected Path createVMInvocationArgumentFile(List arguments) { try { Path argsFile = Files.createTempFile("vminvocation", ".args"); StringJoiner joiner = new StringJoiner("\n"); @@ -1448,19 +1457,19 @@ protected static String createVMInvocationArgumentFile(List arguments) { String joinedOptions = joiner.toString(); Files.write(argsFile, joinedOptions.getBytes()); argsFile.toFile().deleteOnExit(); - return "@" + argsFile; + return argsFile; } catch (IOException e) { throw showError(e.getMessage()); } } - protected static String createImageBuilderArgumentFile(List imageBuilderArguments) { + protected Path createImageBuilderArgumentFile(List imageBuilderArguments) { try { Path argsFile = Files.createTempFile("native-image", ".args"); String joinedOptions = String.join("\0", imageBuilderArguments); Files.write(argsFile, joinedOptions.getBytes()); argsFile.toFile().deleteOnExit(); - return NativeImageGeneratorRunner.IMAGE_BUILDER_ARG_FILE_OPTION + argsFile.toString(); + return argsFile; } catch (IOException e) { throw showError(e.getMessage()); } @@ -1489,8 +1498,10 @@ protected int buildImage(List javaArgs, LinkedHashSet cp, LinkedHa BiFunction substituteAuxiliaryPath = useBundle() ? bundleSupport::substituteAuxiliaryPath : (a, b) -> a; Function imageArgsTransformer = rawArg -> apiOptionHandler.transformBuilderArgument(rawArg, substituteAuxiliaryPath); List finalImageArgs = imageArgs.stream().map(imageArgsTransformer).collect(Collectors.toList()); + Function substituteClassPath = useBundle() ? bundleSupport::substituteClassPath : Function.identity(); List finalImageClassPath = imagecp.stream().map(substituteClassPath).collect(Collectors.toList()); + Function substituteModulePath = useBundle() ? bundleSupport::substituteModulePath : Function.identity(); List substitutedImageModulePath = imagemp.stream().map(substituteModulePath).toList(); @@ -1554,9 +1565,53 @@ protected int buildImage(List javaArgs, LinkedHashSet cp, LinkedHa /* Construct ProcessBuilder command from final arguments */ List command = new ArrayList<>(); + List completeCommandList = new ArrayList<>(); + + if (useBundle() && bundleSupport.useContainer) { + ContainerSupport.replacePaths(arguments, config.getJavaHome(), bundleSupport.rootDir); + ContainerSupport.replacePaths(finalImageBuilderArgs, config.getJavaHome(), bundleSupport.rootDir); + Path binJava = Paths.get("bin", "java"); + javaExecutable = ContainerSupport.GRAAL_VM_HOME.resolve(binJava).toString(); + } + + Path argFile = createVMInvocationArgumentFile(arguments); + Path builderArgFile = createImageBuilderArgumentFile(finalImageBuilderArgs); + + if (useBundle() && bundleSupport.useContainer) { + if (!Files.exists(bundleSupport.containerSupport.dockerfile)) { + bundleSupport.createDockerfile(bundleSupport.containerSupport.dockerfile); + } + int exitStatusCode = bundleSupport.containerSupport.initializeImage(); + switch (ExitStatus.of(exitStatusCode)) { + case OK -> { + } + case BUILDER_ERROR -> + /* Exit, builder has handled error reporting. */ + throw NativeImage.showError(null, null, exitStatusCode); + case OUT_OF_MEMORY -> { + showOutOfMemoryWarning(); + throw NativeImage.showError(null, null, exitStatusCode); + } + default -> { + String message = String.format("Container build request for '%s' failed with exit status %d", + imageName, exitStatusCode); + throw NativeImage.showError(message, null, exitStatusCode); + } + } + + Map mountMapping = ContainerSupport.mountMappingFor(config.getJavaHome(), bundleSupport.inputDir, bundleSupport.outputDir); + mountMapping.put(argFile, ContainerSupport.TargetPath.readonly(argFile)); + mountMapping.put(builderArgFile, ContainerSupport.TargetPath.readonly(builderArgFile)); + + List containerCommand = bundleSupport.containerSupport.createCommand(imageBuilderEnvironment, mountMapping); + command.addAll(containerCommand); + completeCommandList.addAll(containerCommand); + } + command.add(javaExecutable); - command.add(createVMInvocationArgumentFile(arguments)); - command.add(createImageBuilderArgumentFile(finalImageBuilderArgs)); + command.add("@" + argFile); + command.add(NativeImageGeneratorRunner.IMAGE_BUILDER_ARG_FILE_OPTION + builderArgFile); + ProcessBuilder pb = new ProcessBuilder(); pb.command(command); Map environment = pb.environment(); @@ -1583,8 +1638,7 @@ protected int buildImage(List javaArgs, LinkedHashSet cp, LinkedHa LogUtils.warningDeprecatedEnvironmentVariable(ModuleSupport.ENV_VAR_USE_MODULE_SYSTEM); } - List completeCommandList = new ArrayList<>(); - completeCommandList.addAll(environment.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).sorted().toList()); + completeCommandList.addAll(0, environment.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).sorted().toList()); completeCommandList.add(javaExecutable); completeCommandList.addAll(arguments); completeCommandList.addAll(finalImageBuilderArgs); @@ -2128,7 +2182,7 @@ void setModuleOptionMode(boolean val) { } private void enableModulePathBuild() { - if (config.modulePathBuild == false) { + if (!config.modulePathBuild) { NativeImage.showError("Module options not allowed in this image build. Reason: " + config.imageBuilderModeEnforcer); } config.modulePathBuild = true; @@ -2403,7 +2457,7 @@ private static String safeSubstitution(String source, CharSequence target, CharS return source.replace(target, replacement); } - private static String deletedFileSuffix = ".deleted"; + private static final String deletedFileSuffix = ".deleted"; protected static boolean isDeletedPath(Path toDelete) { return toDelete.getFileName().toString().endsWith(deletedFileSuffix); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/libc/HostedLibCFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/libc/HostedLibCFeature.java index 2b6657e1c974..dee959612abd 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/libc/HostedLibCFeature.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/libc/HostedLibCFeature.java @@ -26,18 +26,13 @@ import java.util.ServiceLoader; -import org.graalvm.collections.UnmodifiableEconomicMap; -import org.graalvm.compiler.options.Option; -import org.graalvm.compiler.options.OptionKey; -import org.graalvm.compiler.options.OptionValues; import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.Platform; +import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.c.libc.LibCBase; import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; -import com.oracle.svm.core.option.APIOption; -import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.util.UserError; @AutomaticallyRegisteredFeature @@ -47,31 +42,9 @@ public boolean isInConfiguration(IsInConfigurationAccess access) { return HostedLibCBase.isPlatformEquivalent(Platform.LINUX.class); } - public static class LibCOptions { - @APIOption(name = "libc")// - @Option(help = "Selects the libc implementation to use. Available implementations: glibc, musl, bionic")// - public static final HostedOptionKey UseLibC = new HostedOptionKey<>(null) { - @Override - public String getValueOrDefault(UnmodifiableEconomicMap, Object> values) { - if (!values.containsKey(this)) { - return Platform.includedIn(Platform.ANDROID.class) - ? "bionic" - : System.getProperty("substratevm.HostLibC", "glibc"); - } - return (String) values.get(this); - } - - @Override - public String getValue(OptionValues values) { - assert checkDescriptorExists(); - return getValueOrDefault(values.getMap()); - } - }; - } - @Override public void afterRegistration(AfterRegistrationAccess access) { - String targetLibC = LibCOptions.UseLibC.getValue(); + String targetLibC = SubstrateOptions.UseLibC.getValue(); ServiceLoader loader = ServiceLoader.load(HostedLibCBase.class); for (HostedLibCBase libc : loader) { if (libc.getName().equals(targetLibC)) {