diff --git a/build-logic/common-plugins/src/main/kotlin/org.graalvm.build.java.gradle.kts b/build-logic/common-plugins/src/main/kotlin/org.graalvm.build.java.gradle.kts index 7d09f21f0..990e6fe4a 100644 --- a/build-logic/common-plugins/src/main/kotlin/org.graalvm.build.java.gradle.kts +++ b/build-logic/common-plugins/src/main/kotlin/org.graalvm.build.java.gradle.kts @@ -64,12 +64,16 @@ repositories { group = "org.graalvm.buildtools" extensions.findByType()?.also { catalogs -> - val versionFromCatalog = catalogs.named("libs") + if (catalogs.find("libs").isPresent) { + val versionFromCatalog = catalogs.named("libs") .findVersion("nativeBuildTools") - if (versionFromCatalog.isPresent()) { - version = versionFromCatalog.get().requiredVersion + if (versionFromCatalog.isPresent()) { + version = versionFromCatalog.get().requiredVersion + } else { + throw GradleException("Version catalog doesn't define project version 'nativeBuildTools'") + } } else { - throw GradleException("Version catalog doesn't define project version 'nativeBuildTools'") + version = "undefined" } } diff --git a/common/utils/src/main/java/org/graalvm/buildtools/utils/JarMetadata.java b/common/utils/src/main/java/org/graalvm/buildtools/utils/JarMetadata.java new file mode 100644 index 000000000..042b8a8be --- /dev/null +++ b/common/utils/src/main/java/org/graalvm/buildtools/utils/JarMetadata.java @@ -0,0 +1,47 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.graalvm.buildtools.utils; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +public class JarMetadata { + private final List packageList; + + public JarMetadata(List packageList) { + this.packageList = packageList; + } + + public List getPackageList() { + return packageList; + } + + public static JarMetadata readFrom(Path propertiesFile) { + Properties props = new Properties(); + try (InputStream is = Files.newInputStream(propertiesFile)) { + props.load(is); + } catch (Exception e) { + throw new RuntimeException("Unable to read metadata from properties file " + propertiesFile, e); + } + String packages = (String) props.get("packages"); + List packageList = packages == null ? List.of() : Arrays.asList(packages.split(",")); + return new JarMetadata(packageList); + } +} diff --git a/common/utils/src/main/java/org/graalvm/buildtools/utils/JarScanner.java b/common/utils/src/main/java/org/graalvm/buildtools/utils/JarScanner.java new file mode 100644 index 000000000..2288d84cf --- /dev/null +++ b/common/utils/src/main/java/org/graalvm/buildtools/utils/JarScanner.java @@ -0,0 +1,68 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.graalvm.buildtools.utils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +/** + * Performs scanning of a JAR file and extracts some metadata in the + * form of a properties file. For now this type only extracts the list + * of packages from a jar. + */ +public class JarScanner { + /** + * Scans a jar and creates a properties file with metadata about the jar contents. + * @param inputJar the input jar + * @param outputFile the output file + * @throws IOException + */ + public static void scanJar(Path inputJar, Path outputFile) throws IOException { + try (Writer fileWriter = Files.newBufferedWriter(outputFile); PrintWriter writer = new PrintWriter(fileWriter)) { + Set packageList = new TreeSet<>(); + try (FileSystem jarFileSystem = FileSystems.newFileSystem(inputJar, null)) { + Path root = jarFileSystem.getPath("/"); + try (Stream files = Files.walk(root)) { + files.forEach(path -> { + if (path.toString().endsWith(".class") && !path.toString().contains("META-INF")) { + Path relativePath = root.relativize(path); + String className = relativePath.toString() + .replace('/', '.') + .replace('\\', '.') + .replaceAll("[.]class$", ""); + var lastDot = className.lastIndexOf("."); + if (lastDot > 0) { + var packageName = className.substring(0, lastDot); + packageList.add(packageName); + } + } + }); + } + } + writer.println("packages=" + String.join(",", packageList)); + } catch (IOException ex) { + throw new RuntimeException("Unable to write JAR analysis", ex); + } + } +} diff --git a/docs/src/docs/asciidoc/changelog.adoc b/docs/src/docs/asciidoc/changelog.adoc index 7adb93a9b..acdc9d8c4 100644 --- a/docs/src/docs/asciidoc/changelog.adoc +++ b/docs/src/docs/asciidoc/changelog.adoc @@ -1,6 +1,12 @@ [[changelog]] == Changelog +== Release 0.10.7 + +=== Gradle plugin + +- Added experimental support for layered images + == Release 0.10.6 === Gradle plugin diff --git a/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy index c630abc4f..1b2e2ae92 100644 --- a/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy +++ b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy @@ -45,7 +45,7 @@ import org.graalvm.buildtools.gradle.fixtures.AbstractFunctionalTest class JUnitFunctionalTests extends AbstractFunctionalTest { def "test if JUint support works with various annotations, reflection and resources"() { - + debug=true given: withSample("junit-tests") diff --git a/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/LayeredApplicationFunctionalTest.groovy b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/LayeredApplicationFunctionalTest.groovy new file mode 100644 index 000000000..f9920e0f2 --- /dev/null +++ b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/LayeredApplicationFunctionalTest.groovy @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.buildtools.gradle + +import org.graalvm.buildtools.gradle.fixtures.AbstractFunctionalTest +import org.graalvm.buildtools.gradle.fixtures.GraalVMSupport +import org.graalvm.buildtools.utils.NativeImageUtils +import spock.lang.Requires +import spock.util.concurrent.PollingConditions + +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.charset.StandardCharsets + +@Requires( + { NativeImageUtils.getMajorJDKVersion(GraalVMSupport.getGraalVMHomeVersionString()) >= 25 } +) +class LayeredApplicationFunctionalTest extends AbstractFunctionalTest { + def "can build a native image using layers"() { + def nativeApp = getExecutableFile("build/native/nativeCompile/layered-java-application") + + given: + withSample("layered-java-application") + + when: + run 'nativeLibdependenciesCompile' + + then: + tasks { + succeeded ':nativeLibdependenciesCompile' + } + outputContains "'-H:LayerCreate' (origin(s): command line)" + + when: + run 'nativeRun', '-Pmessage="Hello, layered images!"' + + then: + tasks { + upToDate ':nativeLibdependenciesCompile' + succeeded ':nativeCompile' + } + nativeApp.exists() + + and: + outputContains "- '-H:LayerUse' (origin(s): command line)" + outputContains "Hello, layered images!" + + when: "Updating the application without changing the dependencies" + file("src/main/java/org/graalvm/demo/Application.java").text = """ +package org.graalvm.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Application { + private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + LOGGER.info("App started with args {}", String.join(", ", args)); + } + +} + +""" + run 'nativeRun', '-Pmessage="Hello, layered images!"' + + then: + tasks { + // Base layer is not rebuilt + upToDate ':nativeLibdependenciesCompile' + // Application layer is recompiled + succeeded ':nativeCompile' + } + + outputContains "- '-H:LayerUse' (origin(s): command line)" + outputContains "Hello, layered images!" + } + + def "can build a layered Micronaut application"() { + given: + withSample("layered-mn-application") + + when: + run 'nativeCompile' + + then: + tasks { + succeeded ':nativeLibdependenciesCompile', ':nativeCompile' + } + + when: + def builder = new ProcessBuilder() + .directory(testDirectory.toFile()) + .inheritIO() + .command("build/native/nativeCompile/layered-mn-app${IS_WINDOWS?".exe":""}") + def env = builder.environment() + env["LD_LIBRARY_PATH"] = testDirectory.resolve("build/native/nativeLibdependenciesCompile").toString() + def process = builder.start() + def client = HttpClient.newHttpClient() + def request = HttpRequest.newBuilder() + .GET() + .uri(new URI("http://localhost:8080/")) + .build() + def conditions = new PollingConditions() + + then: + conditions.within(10) { + def response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).body() + response == "Hello, layered images!" + } + + cleanup: + process.destroy() + } +} diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java index e0647eab9..d33192b30 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -55,16 +55,17 @@ import org.graalvm.buildtools.gradle.internal.GraalVMLogger; import org.graalvm.buildtools.gradle.internal.GraalVMReachabilityMetadataService; import org.graalvm.buildtools.gradle.internal.GradleUtils; -import org.graalvm.buildtools.gradle.internal.NativeConfigurations; import org.graalvm.buildtools.gradle.internal.agent.AgentConfigurationFactory; import org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask; import org.graalvm.buildtools.gradle.tasks.CollectReachabilityMetadata; import org.graalvm.buildtools.gradle.tasks.GenerateResourcesConfigFile; import org.graalvm.buildtools.gradle.tasks.MetadataCopyTask; import org.graalvm.buildtools.gradle.tasks.NativeRunTask; +import org.graalvm.buildtools.gradle.tasks.UseLayerOptions; import org.graalvm.buildtools.gradle.tasks.actions.CleanupAgentFilesAction; import org.graalvm.buildtools.gradle.tasks.actions.MergeAgentFilesAction; import org.graalvm.buildtools.utils.JUnitUtils; +import org.graalvm.buildtools.gradle.tasks.scanner.JarAnalyzerTransform; import org.graalvm.buildtools.utils.SharedConstants; import org.graalvm.reachability.DirectoryConfiguration; import org.gradle.api.Action; @@ -159,12 +160,11 @@ public class NativeImagePlugin implements Plugin { public static final String NATIVE_MAIN_EXTENSION = "main"; public static final String NATIVE_TEST_EXTENSION = "test"; - public static final String DEPRECATED_NATIVE_BUILD_EXTENSION = "nativeBuild"; - public static final String DEPRECATED_NATIVE_TEST_EXTENSION = "nativeTest"; public static final String DEPRECATED_NATIVE_BUILD_TASK = "nativeBuild"; public static final String DEPRECATED_NATIVE_TEST_BUILD_TASK = "nativeTestBuild"; public static final String CONFIG_REPO_LOGLEVEL = "org.graalvm.internal.gradle.configrepo.logging"; + public static final Attribute JAR_ANALYSIS_ATTRIBUTE = Attribute.of("jar-analysis", Boolean.class); private static final String JUNIT_PLATFORM_LISTENERS_UID_TRACKING_ENABLED = "junit.platform.listeners.uid.tracking.enabled"; private static final String JUNIT_PLATFORM_LISTENERS_UID_TRACKING_OUTPUT_DIR = "junit.platform.listeners.uid.tracking.output.dir"; @@ -197,7 +197,7 @@ public void apply(@Nonnull Project project) { DefaultGraalVmExtension graalExtension = (DefaultGraalVmExtension) registerGraalVMExtension(project); graalExtension.getUseArgFile().convention(IS_WINDOWS); project.getPlugins() - .withType(JavaPlugin.class, javaPlugin -> configureJavaProject(project, nativeImageServiceProvider, graalExtension)); + .withType(JavaPlugin.class, javaPlugin -> configureJavaProject(project, nativeImageServiceProvider, graalExtension)); project.getPlugins().withType(JavaLibraryPlugin.class, javaLibraryPlugin -> graalExtension.getAgent().getDefaultMode().convention("conditional")); @@ -235,12 +235,16 @@ private static boolean isTaskInstrumentableByAgent(Task task) { } private static String deriveTaskName(String name, String prefix, String suffix) { - if ("main".equals(name)) { + if (NATIVE_MAIN_EXTENSION.equals(name)) { return prefix + suffix; } return prefix + capitalize(name) + suffix; } + public static String compileTaskNameForBinary(String name) { + return deriveTaskName(name, "native", "Compile"); + } + private void configureJavaProject(Project project, Provider nativeImageServiceProvider, DefaultGraalVmExtension graalExtension) { logger.log("===================="); logger.log("Initializing project: " + project.getName()); @@ -250,13 +254,21 @@ private void configureJavaProject(Project project, Provider NativeImageOptions mainOptions = createMainOptions(graalExtension, project); project.getPlugins().withId("application", p -> mainOptions.getMainClass().convention( - Objects.requireNonNull(project.getExtensions().findByType(JavaApplication.class)).getMainClass() + Objects.requireNonNull(project.getExtensions().findByType(JavaApplication.class)).getMainClass() )); project.getPlugins().withId("java-library", p -> mainOptions.getSharedLibrary().convention(true)); registerServiceProvider(project, nativeImageServiceProvider); + // Configure artifact transform which allows extracting the package list from jars + project.getDependencies().getAttributesSchema().attribute(JAR_ANALYSIS_ATTRIBUTE); + project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(JAR_ANALYSIS_ATTRIBUTE, false); + project.getDependencies().registerTransform(JarAnalyzerTransform.class, t -> { + t.getFrom().attribute(JAR_ANALYSIS_ATTRIBUTE, false); + t.getTo().attribute(JAR_ANALYSIS_ATTRIBUTE, true); + }); + // Register Native Image tasks TaskContainer tasks = project.getTasks(); JavaPluginExtension javaConvention = project.getExtensions().getByType(JavaPluginExtension.class); @@ -290,14 +302,14 @@ private void configureJavaProject(Project project, Provider GraalVMReachabilityMetadataRepositoryExtension metadataRepositoryExtension = reachabilityExtensionOn(graalExtension); TaskCollection reachabilityMetadataCopyTasks = project.getTasks() - .withType(CollectReachabilityMetadata.class); + .withType(CollectReachabilityMetadata.class); reachabilityMetadataCopyTasks.configureEach(task -> { Provider reachabilityMetadataService = graalVMReachabilityMetadataService( - project, metadataRepositoryExtension); + project, metadataRepositoryExtension); task.getMetadataService().set(reachabilityMetadataService); task.usesService(reachabilityMetadataService); task.getUri().convention(task.getVersion().map(serializableTransformerOf(this::getReachabilityMetadataRepositoryUrlForVersion)) - .orElse(metadataRepositoryExtension.getUri())); + .orElse(metadataRepositoryExtension.getUri())); task.getExcludedModules().convention(metadataRepositoryExtension.getExcludedModules()); task.getModuleToConfigVersion().convention(metadataRepositoryExtension.getModuleToConfigVersion()); task.getInto().convention(project.getLayout().getBuildDirectory().dir("native-reachability-metadata")); @@ -311,18 +323,19 @@ private void configureAutomaticTaskCreation(Project project, graalExtension.getBinaries().configureEach(options -> { String binaryName = options.getName(); String compileTaskName = deriveTaskName(binaryName, "native", "Compile"); - if ("main".equals(binaryName)) { + if (NATIVE_MAIN_EXTENSION.equals(binaryName)) { compileTaskName = NATIVE_COMPILE_TASK_NAME; } TaskProvider imageBuilder = tasks.register(compileTaskName, - BuildNativeImageTask.class, builder -> { - builder.setDescription("Compiles a native image for the " + options.getName() + " binary"); - builder.setGroup(LifecycleBasePlugin.BUILD_GROUP); - builder.getOptions().convention(options); - builder.getUseArgFile().convention(graalExtension.getUseArgFile()); - }); + BuildNativeImageTask.class, builder -> { + builder.setDescription("Compiles a native image for the " + options.getName() + " binary"); + builder.setGroup(LifecycleBasePlugin.BUILD_GROUP); + builder.getOptions().convention(options); + builder.getUseArgFile().convention(graalExtension.getUseArgFile()); + }); String runTaskName = deriveTaskName(binaryName, "native", "Run"); - if ("main".equals(binaryName)) { + var providers = project.getProviders(); + if (NATIVE_MAIN_EXTENSION.equals(binaryName)) { runTaskName = NativeRunTask.TASK_NAME; } else if (binaryName.toLowerCase(Locale.US).endsWith("test")) { runTaskName = "native" + capitalize(binaryName); @@ -332,26 +345,46 @@ private void configureAutomaticTaskCreation(Project project, task.setDescription("Executes the " + options.getName() + " native binary"); task.getImage().convention(imageBuilder.flatMap(BuildNativeImageTask::getOutputFile)); task.getRuntimeArgs().convention(options.getRuntimeArgs()); + var useLayers = options.getLayers() + .stream() + .filter(layer -> layer instanceof UseLayerOptions) + .collect(Collectors.toList()); + if (!useLayers.isEmpty()) { + var libsDirs = project.getObjects().fileCollection(); + libsDirs.from(useLayers + .stream() + .map(l -> l.getLayerName().flatMap(layerName -> tasks.named(compileTaskNameForBinary(layerName), BuildNativeImageTask.class)) + .map(t -> t.getOutputDirectory().getAsFile()) + ) + .collect(Collectors.toList())); + if (IS_WINDOWS) { + var pathVariable = providers.environmentVariable("PATH"); + task.getEnvironment().put("PATH", pathVariable.map(path -> path + ";" + libsDirs.getAsPath()).orElse(providers.provider(libsDirs::getAsPath))); + } else { + var ldLibraryPath = providers.environmentVariable("LD_LIBRARY_PATH"); + task.getEnvironment().put("LD_LIBRARY_PATH", ldLibraryPath.map(path -> path + ":" + libsDirs.getAsPath()).orElse(providers.provider(libsDirs::getAsPath))); + } + } task.getInternalRuntimeArgs().convention( - imageBuilder.zip(options.getPgoInstrument(), serializableBiFunctionOf((builder, pgo) -> { - if (Boolean.TRUE.equals(pgo)) { - File outputDir = builder.getOutputDirectory().get().getAsFile(); - return Collections.singletonList("-XX:ProfilesDumpFile=" + new File(outputDir, "default.iprof").getAbsolutePath()); - } - return Collections.emptyList(); - }) - )); + imageBuilder.zip(options.getPgoInstrument(), serializableBiFunctionOf((builder, pgo) -> { + if (Boolean.TRUE.equals(pgo)) { + File outputDir = builder.getOutputDirectory().get().getAsFile(); + return Collections.singletonList("-XX:ProfilesDumpFile=" + new File(outputDir, "default.iprof").getAbsolutePath()); + } + return Collections.emptyList(); + }) + )); }); configureClasspathJarFor(tasks, options, imageBuilder); SourceSet sourceSet = "test".equals(binaryName) ? sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME) : sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); TaskProvider generateResourcesConfig = registerResourcesConfigTask( - graalExtension.getGeneratedResourcesDirectory(), - options, - tasks, - transitiveProjectArtifacts(project, sourceSet.getRuntimeClasspathConfigurationName()), - deriveTaskName(binaryName, "generate", "ResourcesConfigFile")); + graalExtension.getGeneratedResourcesDirectory(), + options, + tasks, + transitiveProjectArtifacts(project, sourceSet.getRuntimeClasspathConfigurationName()), + deriveTaskName(binaryName, "generate", "ResourcesConfigFile")); options.getConfigurationFileDirectories().from(generateResourcesConfig.map(serializableTransformerOf(t -> - t.getOutputFile().map(serializableTransformerOf(f -> f.getAsFile().getParentFile())) + t.getOutputFile().map(serializableTransformerOf(f -> f.getAsFile().getParentFile())) ))); configureJvmReachabilityConfigurationDirectories(project, graalExtension, options, sourceSet); configureJvmReachabilityExcludeConfigArgs(project, graalExtension, options, sourceSet); @@ -363,10 +396,10 @@ private void configureJvmReachabilityConfigurationDirectories(Project project, NativeImageOptions options, SourceSet sourceSet) { options.getConfigurationFileDirectories().from( - graalVMReachabilityQueryForConfigDirectories(project, - graalExtension, - sourceSet, - configuration -> true) + graalVMReachabilityQueryForConfigDirectories(project, + graalExtension, + sourceSet, + configuration -> true) ); } @@ -375,67 +408,67 @@ private static File getConfigurationDirectory(DirectoryConfiguration configurati } private Provider> graalVMReachabilityQueryForConfigDirectories(Project project, GraalVMExtension graalExtension, - SourceSet sourceSet, Predicate filter) { + SourceSet sourceSet, Predicate filter) { GraalVMReachabilityMetadataRepositoryExtension extension = reachabilityExtensionOn(graalExtension); Provider metadataServiceProvider = graalVMReachabilityMetadataService(project, extension); Provider rootComponent = project.getConfigurations() - .getByName(sourceSet.getRuntimeClasspathConfigurationName()) - .getIncoming() - .getResolutionResult() - .getRootComponent(); + .getByName(sourceSet.getRuntimeClasspathConfigurationName()) + .getIncoming() + .getResolutionResult() + .getRootComponent(); SetProperty excludedModulesProperty = extension.getExcludedModules(); MapProperty moduleToConfigVersion = extension.getModuleToConfigVersion(); Property uri = extension.getUri(); ProviderFactory providers = project.getProviders(); return extension.getEnabled().flatMap(serializableTransformerOf(enabled -> { - if (Boolean.TRUE.equals(enabled) && uri.isPresent()) { - Set excludedModules = excludedModulesProperty.getOrElse(Collections.emptySet()); - Map forcedVersions = moduleToConfigVersion.getOrElse(Collections.emptyMap()); - return metadataServiceProvider.map(serializableTransformerOf(service -> { - Set components = findAllComponentsFrom(rootComponent.get()); - return components.stream().flatMap(serializableFunctionOf(component -> { - ModuleVersionIdentifier moduleVersion = component.getModuleVersion(); - Set configurations = service.findConfigurationsFor(excludedModules, forcedVersions, moduleVersion); - return configurations.stream() - .filter(filter) - .map(NativeImagePlugin::getConfigurationDirectory); - })).collect(Collectors.toList()); - })); - } - return providers.provider(Collections::emptyList); - })); + if (Boolean.TRUE.equals(enabled) && uri.isPresent()) { + Set excludedModules = excludedModulesProperty.getOrElse(Collections.emptySet()); + Map forcedVersions = moduleToConfigVersion.getOrElse(Collections.emptyMap()); + return metadataServiceProvider.map(serializableTransformerOf(service -> { + Set components = findAllComponentsFrom(rootComponent.get()); + return components.stream().flatMap(serializableFunctionOf(component -> { + ModuleVersionIdentifier moduleVersion = component.getModuleVersion(); + Set configurations = service.findConfigurationsFor(excludedModules, forcedVersions, moduleVersion); + return configurations.stream() + .filter(filter) + .map(NativeImagePlugin::getConfigurationDirectory); + })).collect(Collectors.toList()); + })); + } + return providers.provider(Collections::emptyList); + })); } private Provider>> graalVMReachabilityQueryForExcludeList(Project project, GraalVMExtension graalExtension, - SourceSet sourceSet, Predicate filter) { + SourceSet sourceSet, Predicate filter) { GraalVMReachabilityMetadataRepositoryExtension extension = reachabilityExtensionOn(graalExtension); Provider metadataServiceProvider = graalVMReachabilityMetadataService(project, extension); Provider rootComponent = project.getConfigurations() - .getByName(sourceSet.getRuntimeClasspathConfigurationName()) - .getIncoming() - .getResolutionResult() - .getRootComponent(); + .getByName(sourceSet.getRuntimeClasspathConfigurationName()) + .getIncoming() + .getResolutionResult() + .getRootComponent(); SetProperty excludedModulesProperty = extension.getExcludedModules(); MapProperty moduleToConfigVersion = extension.getModuleToConfigVersion(); Property uri = extension.getUri(); ProviderFactory providers = project.getProviders(); return extension.getEnabled().flatMap(serializableTransformerOf(enabled -> { - if (Boolean.TRUE.equals(enabled) && uri.isPresent()) { - Set excludedModules = excludedModulesProperty.getOrElse(Collections.emptySet()); - Map forcedVersions = moduleToConfigVersion.getOrElse(Collections.emptyMap()); - return metadataServiceProvider.map(serializableTransformerOf(service -> { - Set components = findAllComponentsFrom(rootComponent.get()); - return components.stream().flatMap(serializableFunctionOf(component -> { - ModuleVersionIdentifier moduleVersion = component.getModuleVersion(); - Set configurations = service.findConfigurationsFor(excludedModules, forcedVersions, moduleVersion); - return configurations.stream() - .filter(filter) - .map(e -> getExclusionConfig(moduleVersion)); - })).collect(Collectors.toMap(ExcludeEntry::getGav, ExcludeEntry::getExcludes)); - })); - } - return providers.provider(Collections::emptyMap); - })); + if (Boolean.TRUE.equals(enabled) && uri.isPresent()) { + Set excludedModules = excludedModulesProperty.getOrElse(Collections.emptySet()); + Map forcedVersions = moduleToConfigVersion.getOrElse(Collections.emptyMap()); + return metadataServiceProvider.map(serializableTransformerOf(service -> { + Set components = findAllComponentsFrom(rootComponent.get()); + return components.stream().flatMap(serializableFunctionOf(component -> { + ModuleVersionIdentifier moduleVersion = component.getModuleVersion(); + Set configurations = service.findConfigurationsFor(excludedModules, forcedVersions, moduleVersion); + return configurations.stream() + .filter(filter) + .map(e -> getExclusionConfig(moduleVersion)); + })).collect(Collectors.toMap(ExcludeEntry::getGav, ExcludeEntry::getExcludes)); + })); + } + return providers.provider(Collections::emptyMap); + })); } private static Set findAllComponentsFrom(ResolvedComponentResult resolvedComponentResult) { @@ -458,21 +491,21 @@ private static void findAllComponentsFrom(ResolvedComponentResult resolvedCompon private Provider graalVMReachabilityMetadataService(Project project, GraalVMReachabilityMetadataRepositoryExtension repositoryExtension) { return project.getGradle() - .getSharedServices() - .registerIfAbsent("nativeConfigurationService", GraalVMReachabilityMetadataService.class, spec -> { - LogLevel logLevel = determineLogLevel(); - spec.getMaxParallelUsages().set(1); - spec.getParameters().getLogLevel().set(logLevel); - spec.getParameters().getUri().set(repositoryExtension.getUri().map(serializableTransformerOf(configuredUri -> computeMetadataRepositoryUri(project, repositoryExtension, m -> logFallbackToDefaultUri(m, logger))))); - spec.getParameters().getCacheDir().set( - new File(project.getGradle().getGradleUserHomeDir(), "native-build-tools/repositories")); - spec.getParameters().getBackoffMaxRetries().convention( - GradleUtils.intProperty(project.getProviders(), "exponential.backoff.max.retries", 3) - ); - spec.getParameters().getInitialBackoffMillis().convention( - GradleUtils.intProperty(project.getProviders(), "exponential.backoff.initial.delay", 100) - ); - }); + .getSharedServices() + .registerIfAbsent("nativeConfigurationService", GraalVMReachabilityMetadataService.class, spec -> { + LogLevel logLevel = determineLogLevel(); + spec.getMaxParallelUsages().set(1); + spec.getParameters().getLogLevel().set(logLevel); + spec.getParameters().getUri().set(repositoryExtension.getUri().map(serializableTransformerOf(configuredUri -> computeMetadataRepositoryUri(project, repositoryExtension, m -> logFallbackToDefaultUri(m, logger))))); + spec.getParameters().getCacheDir().set( + new File(project.getGradle().getGradleUserHomeDir(), "native-build-tools/repositories")); + spec.getParameters().getBackoffMaxRetries().convention( + GradleUtils.intProperty(project.getProviders(), "exponential.backoff.max.retries", 3) + ); + spec.getParameters().getInitialBackoffMillis().convention( + GradleUtils.intProperty(project.getProviders(), "exponential.backoff.initial.delay", 100) + ); + }); } private static void logFallbackToDefaultUri(URI defaultUri, GraalVMLogger logger) { @@ -499,9 +532,9 @@ static URI computeMetadataRepositoryUri(Project project, Dependency e = project.getDependencies().create(REPOSITORY_COORDINATES); configuration.getDependencies().add(e); Set files = configuration.getIncoming() - .artifactView(view -> view.setLenient(true)) - .getFiles() - .getFiles(); + .artifactView(view -> view.setLenient(true)) + .getFiles() + .getFiles(); if (files.size() == 1) { return files.iterator().next().toURI(); } else { @@ -513,10 +546,10 @@ static URI computeMetadataRepositoryUri(Project project, private void configureJvmReachabilityExcludeConfigArgs(Project project, GraalVMExtension graalExtension, NativeImageOptions options, SourceSet sourceSet) { options.getExcludeConfig().putAll( - graalVMReachabilityQueryForExcludeList(project, - graalExtension, - sourceSet, - DirectoryConfiguration::isOverride) + graalVMReachabilityQueryForExcludeList(project, + graalExtension, + sourceSet, + DirectoryConfiguration::isOverride) ); GraalVMReachabilityMetadataRepositoryExtension repositoryExtension = reachabilityExtensionOn(graalExtension); graalVMReachabilityMetadataService(project, repositoryExtension); @@ -545,16 +578,16 @@ private void configureClasspathJarFor(TaskContainer tasks, NativeImageOptions op TaskProvider classpathJar = tasks.register(baseName + "ClasspathJar", Jar.class, jar -> { jar.setDescription("Builds a pathing jar for the " + options.getName() + " native binary"); jar.from( - options.getClasspath() - .getElements() - .map(elems -> elems.stream() - .map(e -> { - if (isJar(e)) { - return getArchiveOperations().zipTree(e); - } - return e; - }) - .collect(Collectors.toList())) + options.getClasspath() + .getElements() + .map(elems -> elems.stream() + .map(e -> { + if (isJar(e)) { + return getArchiveOperations().zipTree(e); + } + return e; + }) + .collect(Collectors.toList())) ); jar.setDuplicatesStrategy(DuplicatesStrategy.WARN); jar.getArchiveBaseName().set(baseName.toLowerCase(Locale.ENGLISH) + "-classpath"); @@ -572,19 +605,19 @@ private static boolean isJar(FileSystemLocation location) { private GraalVMExtension registerGraalVMExtension(Project project) { NamedDomainObjectContainer nativeImages = project.getObjects() - .domainObjectContainer(NativeImageOptions.class, name -> - project.getObjects().newInstance(BaseNativeImageOptions.class, - name, - project.getObjects(), - project.getProviders(), - project.getExtensions().findByType(JavaToolchainService.class), - project.getName()) - ); + .domainObjectContainer(NativeImageOptions.class, name -> + project.getObjects().newInstance(BaseNativeImageOptions.class, + name, + project.getObjects(), + project.getProviders(), + project.getExtensions().findByType(JavaToolchainService.class), + project.getName()) + ); GraalVMExtension graalvmNative = project.getExtensions().create(GraalVMExtension.class, "graalvmNative", - DefaultGraalVmExtension.class, nativeImages, this, project); + DefaultGraalVmExtension.class, nativeImages, this, project); graalvmNative.getGeneratedResourcesDirectory().set(project.getLayout() - .getBuildDirectory() - .dir("native/generated/")); + .getBuildDirectory() + .dir("native/generated/")); configureNativeConfigurationRepo((ExtensionAware) graalvmNative); return graalvmNative; } @@ -623,7 +656,7 @@ private TaskProvider registerResourcesConfigTask(Pr public void registerTestBinary(Project project, DefaultGraalVmExtension graalExtension, DefaultTestBinaryConfig config) { - NativeImageOptions mainOptions = graalExtension.getBinaries().getByName("main"); + NativeImageOptions mainOptions = graalExtension.getBinaries().getByName(NATIVE_MAIN_EXTENSION); String name = config.getName(); boolean isPrimaryTest = "test".equals(name); TaskContainer tasks = project.getTasks(); @@ -683,23 +716,23 @@ public void registerTestBinary(Project project, */ private static Provider agentProperty(Project project, AgentOptions options) { return project.getProviders() - .gradleProperty(AGENT_PROPERTY) - .map(serializableTransformerOf(v -> { - if (!v.isEmpty()) { - return v; - } - return options.getDefaultMode().get(); - })) - .orElse(options.getEnabled().map(serializableTransformerOf(enabled -> enabled ? options.getDefaultMode().get() : "disabled"))); + .gradleProperty(AGENT_PROPERTY) + .map(serializableTransformerOf(v -> { + if (!v.isEmpty()) { + return v; + } + return options.getDefaultMode().get(); + })) + .orElse(options.getEnabled().map(serializableTransformerOf(enabled -> enabled ? options.getDefaultMode().get() : "disabled"))); } private static void registerServiceProvider(Project project, Provider nativeImageServiceProvider) { project.getTasks() - .withType(BuildNativeImageTask.class) - .configureEach(task -> { - task.usesService(nativeImageServiceProvider); - task.getService().set(nativeImageServiceProvider); - }); + .withType(BuildNativeImageTask.class) + .configureEach(task -> { + task.usesService(nativeImageServiceProvider); + task.getService().set(nativeImageServiceProvider); + }); } private static String capitalize(String name) { @@ -709,17 +742,33 @@ private static String capitalize(String name) { return name; } + private static String prefixFor(String binaryName) { + return NATIVE_MAIN_EXTENSION.equals(binaryName) ? "" : capitalize(binaryName); + } + + private static String configurationNameFor(String binaryName, String kind) { + var prefix = prefixFor(binaryName); + return "nativeImage" + prefix + capitalize(kind); + } + + private static String imageClasspathConfigurationNameFor(String binaryName) { + return configurationNameFor(binaryName, "classpath"); + } + + private static String compileOnlyClasspathConfigurationNameFor(String binaryName) { + return configurationNameFor(binaryName, "compileOnly"); + } + @SuppressWarnings("unchecked") - private static NativeConfigurations createNativeConfigurations(Project project, String binaryName, String baseClasspathConfigurationName) { + private static void createNativeConfigurations(Project project, String binaryName, String baseClasspathConfigurationName) { ConfigurationContainer configurations = project.getConfigurations(); - String prefix = "main".equals(binaryName) ? "" : capitalize(binaryName); Configuration baseClasspath = configurations.getByName(baseClasspathConfigurationName); ProviderFactory providers = project.getProviders(); - Configuration compileOnly = configurations.create("nativeImage" + prefix + "CompileOnly", c -> { + Configuration compileOnly = configurations.create(compileOnlyClasspathConfigurationNameFor(binaryName), c -> { c.setCanBeResolved(false); c.setCanBeConsumed(false); }); - Configuration classpath = configurations.create("nativeImage" + prefix + "Classpath", c -> { + configurations.create(imageClasspathConfigurationNameFor(binaryName), c -> { c.setCanBeConsumed(false); c.setCanBeResolved(true); c.extendsFrom(compileOnly); @@ -733,34 +782,35 @@ private static NativeConfigurations createNativeConfigurations(Project project, }); }); compileOnly.getDependencies().add(project.getDependencies().create(project)); - return new NativeConfigurations(compileOnly, classpath); } - private static NativeImageOptions createMainOptions(GraalVMExtension graalExtension, Project project) { + private NativeImageOptions createMainOptions(GraalVMExtension graalExtension, Project project) { NativeImageOptions buildExtension = graalExtension.getBinaries().create(NATIVE_MAIN_EXTENSION); - NativeConfigurations configs = createNativeConfigurations( - project, - "main", - JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME + createNativeConfigurations( + project, + NATIVE_MAIN_EXTENSION, + JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME ); - setupExtensionConfigExcludes(buildExtension, configs); - buildExtension.getClasspath().from(configs.getImageClasspathConfiguration()); + var configurations = project.getConfigurations(); + setupExtensionConfigExcludes(buildExtension, configurations); + buildExtension.getClasspath().from(configurations.getByName(imageClasspathConfigurationNameFor(NATIVE_MAIN_EXTENSION))); return buildExtension; } - private static NativeImageOptions createTestOptions(GraalVMExtension graalExtension, - String binaryName, - Project project, - NativeImageOptions mainExtension, - SourceSet sourceSet) { + private NativeImageOptions createTestOptions(GraalVMExtension graalExtension, + String binaryName, + Project project, + NativeImageOptions mainExtension, + SourceSet sourceSet) { NativeImageOptions testExtension = graalExtension.getBinaries().create(binaryName); - NativeConfigurations configs = createNativeConfigurations( - project, - binaryName, - JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME + createNativeConfigurations( + project, + binaryName, + JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME ); - setupExtensionConfigExcludes(testExtension, configs); + var configurations = project.getConfigurations(); + setupExtensionConfigExcludes(testExtension, configurations); testExtension.getMainClass().set("org.graalvm.junit.platform.NativeImageJUnitLauncher"); testExtension.getMainClass().finalizeValue(); @@ -772,7 +822,7 @@ private static NativeImageOptions createTestOptions(GraalVMExtension graalExtens testExtension.buildArgs("--features=org.graalvm.junit.platform.JUnitPlatformFeature"); ConfigurableFileCollection classpath = testExtension.getClasspath(); - classpath.from(configs.getImageClasspathConfiguration()); + classpath.from(configurations.getByName(imageClasspathConfigurationNameFor(binaryName))); classpath.from(sourceSet.getOutput().getClassesDirs()); classpath.from(sourceSet.getOutput().getResourcesDir()); @@ -790,8 +840,9 @@ private static void addExcludeConfigArg(List args, Path jarPath, List { + var imageClasspathConfig = configurations.getByName(imageClasspathConfigurationNameFor(options.getName())); List args = new ArrayList<>(); excludedConfig.forEach((entry, listOfResourcePatterns) -> { if (entry instanceof File) { @@ -799,23 +850,23 @@ private static void setupExtensionConfigExcludes(NativeImageOptions options, Nat } else if (entry instanceof String) { String dependency = (String) entry; // Resolve jar for this dependency. - configs.getImageClasspathConfiguration().getIncoming().artifactView(view -> { - view.setLenient(true); - view.componentFilter(id -> { - if (id instanceof ModuleComponentIdentifier) { - ModuleComponentIdentifier mid = (ModuleComponentIdentifier) id; - String gav = String.format("%s:%s:%s", - mid.getGroup(), - mid.getModule(), - mid.getVersion() - ); - return gav.startsWith(dependency); - } - return false; - }); - }).getFiles().getFiles().stream() - .map(File::toPath) - .forEach(jarPath -> addExcludeConfigArg(args, jarPath, listOfResourcePatterns)); + imageClasspathConfig.getIncoming().artifactView(view -> { + view.setLenient(true); + view.componentFilter(id -> { + if (id instanceof ModuleComponentIdentifier) { + ModuleComponentIdentifier mid = (ModuleComponentIdentifier) id; + String gav = String.format("%s:%s:%s", + mid.getGroup(), + mid.getModule(), + mid.getVersion() + ); + return gav.startsWith(dependency); + } + return false; + }); + }).getFiles().getFiles().stream() + .map(File::toPath) + .forEach(jarPath -> addExcludeConfigArg(args, jarPath, listOfResourcePatterns)); } else { throw new UnsupportedOperationException("Expected File or GAV coordinate for excludeConfig option, got " + entry.getClass()); } @@ -826,11 +877,11 @@ private static void setupExtensionConfigExcludes(NativeImageOptions options, Nat private static List agentSessionDirectories(Directory outputDirectory) { return Arrays.stream(Objects.requireNonNull( - outputDirectory.getAsFile() - .listFiles(file -> file.isDirectory() && file.getName().startsWith("session-")) - ) - ).map(File::getAbsolutePath) - .collect(Collectors.toList()); + outputDirectory.getAsFile() + .listFiles(file -> file.isDirectory() && file.getName().startsWith("session-")) + ) + ).map(File::getAbsolutePath) + .collect(Collectors.toList()); } private void configureAgent(Project project, @@ -866,15 +917,15 @@ public void execute(@Nonnull Task task) { javaForkOptions.getJvmArgumentProviders().add(cliProvider); taskToInstrument.doLast(new MergeAgentFilesAction( - isMergingEnabled, - agentModeProvider, - project.provider(() -> false), - project.getObjects(), - graalvmHomeProvider(project.getProviders()), - mergeInputDirs, - mergeOutputDirs, - graalExtension.getToolchainDetection(), - execOperations)); + isMergingEnabled, + agentModeProvider, + project.provider(() -> false), + project.getObjects(), + graalvmHomeProvider(project.getProviders()), + mergeInputDirs, + mergeOutputDirs, + graalExtension.getToolchainDetection(), + execOperations)); taskToInstrument.doLast(new CleanupAgentFilesAction(mergeInputDirs, fileOperations)); } @@ -936,4 +987,5 @@ public List getExcludes() { return excludes; } } + } diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageCompileOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageCompileOptions.java index abbaa93bf..6d0d08bd7 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageCompileOptions.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageCompileOptions.java @@ -41,11 +41,18 @@ package org.graalvm.buildtools.gradle.dsl; import org.graalvm.buildtools.gradle.dsl.agent.DeprecatedAgentOptions; +import org.graalvm.buildtools.gradle.tasks.CreateLayerOptions; +import org.graalvm.buildtools.gradle.tasks.LayerOptions; +import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; @@ -56,6 +63,8 @@ import org.gradle.api.tasks.PathSensitivity; import org.gradle.jvm.toolchain.JavaLauncher; +import java.io.File; +import java.util.ArrayList; import java.util.List; /** @@ -232,4 +241,35 @@ public interface NativeImageCompileOptions { @PathSensitive(PathSensitivity.NONE) @Optional DirectoryProperty getPgoProfilesDirectory(); + + @Nested + DomainObjectSet getLayers(); + + void layers(Action> spec); + + void useLayer(String name); + + void createLayer(Action spec); + + default Provider> externalDependenciesOf(Provider configurationProvider) { + return configurationProvider.flatMap(this::externalDependenciesOf); + } + + default Provider> externalDependenciesOf(Configuration configuration) { + return configuration.getIncoming() + .artifactView(view -> { + view.setLenient(false); + view.componentFilter(id -> id instanceof ModuleComponentIdentifier); + }) + .getArtifacts() + .getResolvedArtifacts() + .map(artifacts -> { + var files = new ArrayList(); + // not using .stream() intentionally because of https://github.com/gradle/gradle/issues/33152 + for (var artifact : artifacts) { + files.add(artifact.getFile()); + } + return files; + }); + } } diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/BaseNativeImageOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/BaseNativeImageOptions.java index 3832d4953..742c7357e 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/BaseNativeImageOptions.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/BaseNativeImageOptions.java @@ -44,8 +44,13 @@ import org.graalvm.buildtools.gradle.dsl.NativeImageOptions; import org.graalvm.buildtools.gradle.dsl.NativeResourcesOptions; import org.graalvm.buildtools.gradle.dsl.agent.DeprecatedAgentOptions; +import org.graalvm.buildtools.gradle.tasks.BuildNativeImageTask; +import org.graalvm.buildtools.gradle.tasks.CreateLayerOptions; +import org.graalvm.buildtools.gradle.tasks.LayerOptions; +import org.graalvm.buildtools.gradle.tasks.UseLayerOptions; import org.graalvm.buildtools.utils.SharedConstants; import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.ProjectLayout; @@ -73,6 +78,8 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import static org.graalvm.buildtools.gradle.NativeImagePlugin.compileTaskNameForBinary; + /** * Class that declares native image options. @@ -82,8 +89,11 @@ @SuppressWarnings({"unused", "UnusedReturnValue"}) public abstract class BaseNativeImageOptions implements NativeImageOptions { private static final GraalVMLogger LOGGER = GraalVMLogger.of(Logging.getLogger(BaseNativeImageOptions.class)); + private final DomainObjectSet layers; private final String name; + private final transient TaskContainer tasks; + private final ObjectFactory objects; @Override @Internal @@ -270,6 +280,9 @@ public BaseNativeImageOptions(String name, DirectoryProperty pgoProfileDir = objectFactory.directoryProperty(); pgoProfileDir.convention(layout.getProjectDirectory().dir("src/pgo-profiles/" + name)); getPgoProfilesDirectory().convention(pgoProfileDir.map(d -> d.getAsFile().exists() ? d : null)); + this.layers = objectFactory.domainObjectSet(LayerOptions.class); + this.tasks = tasks; + this.objects = objectFactory; } private static Provider property(ProviderFactory providers, String name) { @@ -404,4 +417,35 @@ public BaseNativeImageOptions runtimeArgs(Iterable arguments) { public void agent(Action spec) { spec.execute(getAgent()); } + + @Override + public DomainObjectSet getLayers() { + return layers; + } + + @Override + public void layers(Action> spec) { + spec.execute(layers); + } + + @Override + public void useLayer(String name) { + var taskName = compileTaskNameForBinary(name); + var layer = objects.newInstance(UseLayerOptions.class); + layer.getLayerName().convention(name); + layer.getLayerFile().convention(tasks.named(taskName, BuildNativeImageTask.class).flatMap(BuildNativeImageTask::getCreatedLayerFile)); + layers(options -> options.add(layer)); + } + + @Override + public void createLayer(Action spec) { + var layer = objects.newInstance(CreateLayerOptions.class); + var binaryName = getName(); + if (!binaryName.startsWith("lib")) { + throw new IllegalArgumentException("Binary name for a layer must start with 'lib'"); + } + layer.getLayerName().convention(binaryName); + spec.execute(layer); + layers(options -> options.add(layer)); + } } diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/DelegatingCompileOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/DelegatingCompileOptions.java index c49755846..f8ad69a89 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/DelegatingCompileOptions.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/DelegatingCompileOptions.java @@ -43,6 +43,10 @@ import org.graalvm.buildtools.gradle.dsl.NativeImageCompileOptions; import org.graalvm.buildtools.gradle.dsl.NativeResourcesOptions; import org.graalvm.buildtools.gradle.dsl.agent.DeprecatedAgentOptions; +import org.graalvm.buildtools.gradle.tasks.CreateLayerOptions; +import org.graalvm.buildtools.gradle.tasks.LayerOptions; +import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.provider.ListProperty; @@ -171,4 +175,24 @@ public Property getPgoInstrument() { public DirectoryProperty getPgoProfilesDirectory() { return options.getPgoProfilesDirectory(); } + + @Override + public DomainObjectSet getLayers() { + return options.getLayers(); + } + + @Override + public void layers(Action> spec) { + options.layers(spec); + } + + @Override + public void useLayer(String name) { + options.useLayer(name); + } + + @Override + public void createLayer(Action spec) { + options.createLayer(spec); + } } diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeImageCommandLineProvider.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeImageCommandLineProvider.java index d7e226788..09e130945 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeImageCommandLineProvider.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeImageCommandLineProvider.java @@ -42,8 +42,12 @@ package org.graalvm.buildtools.gradle.internal; import org.graalvm.buildtools.gradle.dsl.NativeImageOptions; +import org.graalvm.buildtools.gradle.tasks.CreateLayerOptions; +import org.graalvm.buildtools.gradle.tasks.LayerOptions; +import org.graalvm.buildtools.gradle.tasks.UseLayerOptions; import org.graalvm.buildtools.utils.NativeImageUtils; import org.gradle.api.Transformer; +import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileSystemLocation; import org.gradle.api.file.FileTree; import org.gradle.api.file.RegularFile; @@ -52,6 +56,8 @@ import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.Nested; import org.gradle.process.CommandLineArgumentProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.nio.file.Path; @@ -63,6 +69,8 @@ import java.util.stream.Collectors; public class NativeImageCommandLineProvider implements CommandLineArgumentProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(NativeImageCommandLineProvider.class); + private static final Transformer NEGATE = b -> !b; private final Provider options; @@ -116,10 +124,68 @@ public Provider getClasspathJar() { public List asArguments() { NativeImageOptions options = getOptions().get(); List cliArgs = new ArrayList<>(20); + boolean hasLayers = !options.getLayers().isEmpty(); + String layerCreateName = null; + ConfigurableFileCollection jarsClasspath = null; + if (hasLayers) { + LOGGER.warn("Experimental support for layered images enabled. DSL may change at any time."); + cliArgs.add("-H:+UnlockExperimentalVMOptions"); + var layers = options.getLayers(); + var arg = new StringBuilder(); + for (LayerOptions layer : layers) { + if (arg.length() > 0) { + arg.append(" "); + } + if (layer instanceof CreateLayerOptions) { + var create = (CreateLayerOptions) layer; + layerCreateName = layer.getLayerName().get(); + arg.append("-H:LayerCreate="); + arg.append(layerCreateName).append(".nil"); + var modules = create.getModules().get(); + jarsClasspath = create.getJars(); + boolean hasModules = !modules.isEmpty(); + boolean hasPackage = create.getPackages().isPresent() && !create.getPackages().get().isEmpty(); + var createLayerJars = jarsClasspath.getFiles(); + boolean hasJars = !createLayerJars.isEmpty(); + if (hasModules || hasPackage || hasJars) { + var packages = create.getPackages().get(); + arg.append(","); + if (hasModules) { + arg.append(modules.stream().map(m -> "module=" + m).collect(Collectors.joining(","))); + } + if (hasPackage) { + if (hasModules) { + arg.append(","); + } + arg.append(packages.stream().map(p -> "package=" + p).collect(Collectors.joining(","))); + } + if (hasJars) { + if (hasModules || hasPackage) { + arg.append(","); + } + arg.append(jarsClasspath.getFiles().stream().map(p -> "path=" + p).collect(Collectors.joining(","))); + } + } + } else { + var layerUse = (UseLayerOptions) layer; + arg.append("-H:LayerUse="); + arg.append(layerUse.getLayerFile().getAsFile().get().getAbsolutePath()); + } + } + cliArgs.add(arg.toString()); + } cliArgs.addAll(options.getExcludeConfigArgs().get()); - cliArgs.add("-cp"); - String classpathString = buildClasspathString(options); - cliArgs.add(classpathString); + String classpathString = buildClasspathString(options).trim(); + if (!classpathString.isEmpty()) { + cliArgs.add("-cp"); + cliArgs.add(classpathString); + } else if (jarsClasspath != null && !jarsClasspath.isEmpty()) { + // This is a shortcut in case of the creation of a layer using the "jars" mode (e.g, not package, nor modules) + // in which case the classpath must replicate the jars so we want to avoid that the user has to configure + // things twice + cliArgs.add("-cp"); + cliArgs.add(jarsClasspath.getAsPath()); + } appendBooleanOption(cliArgs, options.getDebug(), "-g"); appendBooleanOption(cliArgs, options.getFallback().map(NEGATE), "--no-fallback"); appendBooleanOption(cliArgs, options.getVerbose(), "--verbose"); @@ -131,12 +197,14 @@ public List asArguments() { appendBooleanOption(cliArgs, options.getPgoInstrument(), "--pgo-instrument"); String targetOutputPath = getExecutableName().get(); + if (layerCreateName != null) { + targetOutputPath = layerCreateName; + } if (getOutputDirectory().isPresent()) { targetOutputPath = getOutputDirectory().get() + File.separator + targetOutputPath; } cliArgs.add("-o"); cliArgs.add(targetOutputPath); - options.getSystemProperties().get().forEach((n, v) -> { if (v != null) { cliArgs.add("-D" + n + "=\"" + v + "\""); @@ -146,12 +214,12 @@ public List asArguments() { options.getJvmArgs().get().forEach(jvmArg -> cliArgs.add("-J" + jvmArg)); String configFiles = options.getConfigurationFileDirectories() - .getElements() - .get() - .stream() - .map(FileSystemLocation::getAsFile) - .map(File::getAbsolutePath) - .collect(Collectors.joining(",")); + .getElements() + .get() + .stream() + .map(FileSystemLocation::getAsFile) + .map(File::getAbsolutePath) + .collect(Collectors.joining(",")); if (!configFiles.isEmpty()) { cliArgs.add("-H:ConfigurationFileDirectories=" + configFiles); } @@ -166,8 +234,8 @@ public List asArguments() { List actualCliArgs; if (useArgFile.getOrElse(true)) { - Path argFileDir = Paths.get(workingDirectory.get()); - actualCliArgs = new ArrayList<>(NativeImageUtils.convertToArgsFile(cliArgs, argFileDir, argFileDir)); + Path argFileDir = Paths.get(System.getProperty("java.io.tmpdir")); + actualCliArgs = new ArrayList<>(NativeImageUtils.convertToArgsFile(cliArgs, argFileDir, Paths.get(workingDirectory.get()))); } else { actualCliArgs = cliArgs; } diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/BuildNativeImageTask.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/BuildNativeImageTask.java index badd386bd..836869432 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/BuildNativeImageTask.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/BuildNativeImageTask.java @@ -51,6 +51,7 @@ import org.gradle.api.DefaultTask; import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileSystemOperations; import org.gradle.api.file.RegularFile; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.model.ObjectFactory; @@ -203,6 +204,15 @@ protected Provider getGraalVMHome() { return graalvmHomeProvider; } + @Internal + public Provider getCreatedLayerFile() { + return getOptions().zip(getOutputDirectory(), (options, dir) -> dir.file(options.getLayers().stream() + .filter(CreateLayerOptions.class::isInstance) + .map(cl -> cl.getLayerName().get() + ".nil") + .findFirst() + .orElseThrow())); + } + @Internal public Provider getExecutableShortName() { return getOptions().flatMap(options -> @@ -263,7 +273,8 @@ private List buildActualCommandLineArgs(int majorJDKVersion) { getClasspathJar(), getUseArgFile(), getProviders().provider(() -> majorJDKVersion), - getProviders().provider(() -> useColors)).asArguments(); + getProviders().provider(() -> useColors)) + .asArguments(); } // This property provides access to the service instance @@ -272,6 +283,9 @@ private List buildActualCommandLineArgs(int majorJDKVersion) { @Internal public abstract Property getService(); + @Inject + protected abstract FileSystemOperations getFileSystemOperations(); + @TaskAction public void exec() { NativeImageOptions options = getOptions().get(); @@ -298,6 +312,7 @@ public void exec() { } String executable = executablePath.getAbsolutePath(); File outputDir = getOutputDirectory().getAsFile().get(); + getFileSystemOperations().delete(d -> d.delete(outputDir)); if (outputDir.isDirectory() || outputDir.mkdirs()) { getExecOperations().exec(spec -> { MapProperty environmentVariables = options.getEnvironmentVariables(); diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/CreateLayerOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/CreateLayerOptions.java new file mode 100644 index 000000000..58214cde7 --- /dev/null +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/CreateLayerOptions.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.buildtools.gradle.tasks; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +/** + * Layer creation options. + */ +public interface CreateLayerOptions extends LayerOptions { + /** + * The list of packages to include in the layer. + * @return the package list (can be empty) + */ + @Input + @Optional + ListProperty getPackages(); + + /** + * If set, all classes in the supplied jars will be included + * in the generated layer. + * @return the classpath to include + */ + @Classpath + @Optional + ConfigurableFileCollection getJars(); + + /** + * Module names to include in the package. For a base layer, + * this should typically include "java.base". + * @return the list of modules to include in the layer + */ + @Input + ListProperty getModules(); +} diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/LayerOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/LayerOptions.java new file mode 100644 index 000000000..4e4573c61 --- /dev/null +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/LayerOptions.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.buildtools.gradle.tasks; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; + +/** + * Base interface for common options of layer use and layer create. + */ +public interface LayerOptions { + /** + * The name of the layer. + * @return the name property + */ + @Input + Property getLayerName(); +} diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/UseLayerOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/UseLayerOptions.java new file mode 100644 index 000000000..891d28545 --- /dev/null +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/UseLayerOptions.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.buildtools.gradle.tasks; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; + +/** + * Configures a layer for use. + */ +public interface UseLayerOptions extends LayerOptions { + /** + * The path to the layer to use. + * @return the path property. + */ + @InputFile + @PathSensitive(PathSensitivity.NAME_ONLY) + RegularFileProperty getLayerFile(); +} diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/scanner/JarAnalyzerTransform.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/scanner/JarAnalyzerTransform.java new file mode 100644 index 000000000..5621ae258 --- /dev/null +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/tasks/scanner/JarAnalyzerTransform.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.buildtools.gradle.tasks.scanner; + +import org.graalvm.buildtools.utils.JarScanner; +import org.gradle.api.artifacts.transform.InputArtifact; +import org.gradle.api.artifacts.transform.TransformAction; +import org.gradle.api.artifacts.transform.TransformOutputs; +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.provider.Provider; + +import java.io.File; +import java.io.IOException; + +public abstract class JarAnalyzerTransform implements TransformAction { + + @InputArtifact + public abstract Provider getInputArtifact(); + + @Override + public void transform(TransformOutputs outputs) { + File inputFile = getInputArtifact().get().getAsFile(); + File outputFile = outputs.file(inputFile.getName().replace(".jar", ".properties")); + try { + JarScanner.scanJar(inputFile.toPath(), outputFile.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/samples/layered-java-application/build.gradle b/samples/layered-java-application/build.gradle new file mode 100644 index 000000000..d30d6b5f4 --- /dev/null +++ b/samples/layered-java-application/build.gradle @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +plugins { + id 'application' + id 'org.graalvm.buildtools.native' +} + +repositories { + mavenCentral() +} + +application { + mainClass.set('org.graalvm.demo.Application') +} + +def junitVersion = providers.gradleProperty('junit.jupiter.version') + .get() + +dependencies { + implementation("org.slf4j:slf4j-api:2.0.17") + runtimeOnly("ch.qos.logback:logback-classic:1.5.18") + testImplementation(platform("org.junit:junit-bom:${junitVersion}")) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +test { + useJUnitPlatform() +} + +tasks.named("nativeRun") { + runtimeArgs.add(providers.gradleProperty("message").orElse("default message")) +} + +graalvmNative { + binaries { + libdependencies { + createLayer { + modules = ["java.base"] + jars.from(externalDependenciesOf(configurations.runtimeClasspath)) + } + } + main { + useLayer("libdependencies") + } + } +} diff --git a/samples/layered-java-application/gradle.properties b/samples/layered-java-application/gradle.properties new file mode 100644 index 000000000..8579077e5 --- /dev/null +++ b/samples/layered-java-application/gradle.properties @@ -0,0 +1,3 @@ +native.gradle.plugin.version = 0.10.7-SNAPSHOT +junit.jupiter.version = 5.10.0 +junit.platform.version = 1.10.0 diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeConfigurations.java b/samples/layered-java-application/settings.gradle similarity index 73% rename from native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeConfigurations.java rename to samples/layered-java-application/settings.gradle index c611980fb..85a362f2e 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/internal/NativeConfigurations.java +++ b/samples/layered-java-application/settings.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -38,25 +38,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package org.graalvm.buildtools.gradle.internal; -import org.gradle.api.artifacts.Configuration; - -public class NativeConfigurations { - private final Configuration imageCompileOnly; - private final Configuration imageClasspath; - - public NativeConfigurations(Configuration imageCompileOnly, Configuration imageClasspath) { - this.imageCompileOnly = imageCompileOnly; - this.imageClasspath = imageClasspath; - } - - public Configuration getImageCompileOnlyConfiguration() { - return imageCompileOnly; +pluginManagement { + plugins { + id 'org.graalvm.buildtools.native' version getProperty('native.gradle.plugin.version') } - - public Configuration getImageClasspathConfiguration() { - return imageClasspath; - } - } + +rootProject.name = 'layered-java-application' diff --git a/samples/layered-java-application/src/main/java/org/graalvm/demo/Application.java b/samples/layered-java-application/src/main/java/org/graalvm/demo/Application.java new file mode 100644 index 000000000..b7da59096 --- /dev/null +++ b/samples/layered-java-application/src/main/java/org/graalvm/demo/Application.java @@ -0,0 +1,14 @@ +package org.graalvm.demo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Arrays; + +public class Application { + private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + LOGGER.info("App started with args {}", Arrays.asList(args)); + } + +} diff --git a/samples/layered-mn-application/build.gradle b/samples/layered-mn-application/build.gradle new file mode 100644 index 000000000..6fe898ac7 --- /dev/null +++ b/samples/layered-mn-application/build.gradle @@ -0,0 +1,72 @@ +plugins { + id("io.micronaut.minimal.application") + id("org.graalvm.buildtools.native") +} + +version = "0.1" +group = "layered.mn.app" + +repositories { + mavenCentral() +} + +dependencies { + annotationProcessor("io.micronaut:micronaut-http-validation") + annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + implementation("io.micronaut.serde:micronaut-serde-jackson") + compileOnly("io.micronaut:micronaut-http-client") + runtimeOnly("ch.qos.logback:logback-classic") + testImplementation("io.micronaut:micronaut-http-client") +} + +application { + mainClass = "layered.mn.app.Application" +} + +graalvmNative.toolchainDetection = false + +micronaut { + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("layered.mn.app.*") + } +} + +graalvmNative { + binaries { + all { + verbose = true + buildArgs( + "--initialize-at-run-time=io.netty.channel.unix.Errors", + "--initialize-at-run-time=io.netty.channel.unix.IovArray", + "--initialize-at-run-time=io.netty.channel.unix.Limits", + "--initialize-at-run-time=io.netty.buffer", + "--initialize-at-run-time=io.netty.channel", + "--initialize-at-run-time=io.netty.handler", + "--initialize-at-run-time=io.netty.resolver", + "--initialize-at-run-time=io.netty.util", + "--initialize-at-run-time=io.netty.bootstrap", + "-H:+UseSharedLayerGraphs", + "--initialize-at-build-time=io.micronaut.http.server.netty.discovery.\$NettyServiceDiscovery\$ApplicationEventListener\$onStop2\$Intercepted\$Definition\$Exec", + "--initialize-at-build-time=io.micronaut.http.server.netty.discovery.\$NettyServiceDiscovery\$ApplicationEventListener\$onStart1\$Intercepted\$Definition\$Exec" + ) + } + create("libdependencies") { + var externalJars = externalDependenciesOf(configurations.runtimeClasspath) + createLayer { + modules = ["java.base"] + jars.from(externalJars) + } + buildArgs( + "-H:ApplicationLayerOnlySingletons=io.micronaut.core.io.service.ServiceScanner\$StaticServiceDefinitions", + "-H:ApplicationLayerInitializedClasses=io.micronaut.core.io.service.MicronautMetaServiceLoaderUtils", + "-H:ApplicationLayerInitializedClasses=io.micronaut.inject.annotation.AnnotationMetadataSupport" + ) + } + named("main") { + useLayer("libdependencies") + } + } +} diff --git a/samples/layered-mn-application/gradle.properties b/samples/layered-mn-application/gradle.properties new file mode 100644 index 000000000..ac7cb1293 --- /dev/null +++ b/samples/layered-mn-application/gradle.properties @@ -0,0 +1,2 @@ +micronautVersion=4.8.1 +native.gradle.plugin.version = 0.10.7-SNAPSHOT diff --git a/samples/layered-mn-application/settings.gradle b/samples/layered-mn-application/settings.gradle new file mode 100644 index 000000000..f9216f7a3 --- /dev/null +++ b/samples/layered-mn-application/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } + plugins { + id("io.micronaut.minimal.application") version "4.5.3" + id("io.micronaut.graalvm") version "4.5.3" + id("org.graalvm.buildtools.native") version(getProperty("native.gradle.plugin.version")) + } +} + +rootProject.name = "layered-mn-app" diff --git a/samples/layered-mn-application/src/main/java/layered/mn/app/Application.java b/samples/layered-mn-application/src/main/java/layered/mn/app/Application.java new file mode 100644 index 000000000..57aad663a --- /dev/null +++ b/samples/layered-mn-application/src/main/java/layered/mn/app/Application.java @@ -0,0 +1,10 @@ +package layered.mn.app; + +import io.micronaut.runtime.Micronaut; + +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class, args); + } +} \ No newline at end of file diff --git a/samples/layered-mn-application/src/main/java/layered/mn/app/HelloController.java b/samples/layered-mn-application/src/main/java/layered/mn/app/HelloController.java new file mode 100644 index 000000000..c567484e6 --- /dev/null +++ b/samples/layered-mn-application/src/main/java/layered/mn/app/HelloController.java @@ -0,0 +1,43 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package layered.mn.app; + +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; + +@Controller +public class HelloController { + @Get("/") + @Produces(MediaType.TEXT_PLAIN) + public String sayHello() { + return sayHello("layered images"); + } + + @Get("/{name}") + @Produces(MediaType.TEXT_PLAIN) + public String sayHello(String name) { + return "Hello, "+ name + "!"; + } + + @Get("/bye/{name}") + @Produces(MediaType.TEXT_PLAIN) + public String sayBye(String name) { + return "Bye, "+ name + "!"; + } + +} diff --git a/samples/layered-mn-application/src/main/resources/application.properties b/samples/layered-mn-application/src/main/resources/application.properties new file mode 100644 index 000000000..ef0e3e26f --- /dev/null +++ b/samples/layered-mn-application/src/main/resources/application.properties @@ -0,0 +1,2 @@ +#Mon Mar 10 16:06:16 CET 2025 +micronaut.application.name=layered-mn-app diff --git a/samples/layered-mn-application/src/main/resources/logback.xml b/samples/layered-mn-application/src/main/resources/logback.xml new file mode 100644 index 000000000..2d77bdab5 --- /dev/null +++ b/samples/layered-mn-application/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/samples/layered-mn-application/src/test/java/layered/mn/app/LayeredMnAppTest.java b/samples/layered-mn-application/src/test/java/layered/mn/app/LayeredMnAppTest.java new file mode 100644 index 000000000..7ff0dbb50 --- /dev/null +++ b/samples/layered-mn-application/src/test/java/layered/mn/app/LayeredMnAppTest.java @@ -0,0 +1,21 @@ +package layered.mn.app; + +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +import jakarta.inject.Inject; + +@MicronautTest +class LayeredMnAppTest { + + @Inject + EmbeddedApplication application; + + @Test + void testItWorks() { + Assertions.assertTrue(application.isRunning()); + } + +}