diff --git a/common/utils/src/main/java/org/graalvm/buildtools/utils/NativeImageUtils.java b/common/utils/src/main/java/org/graalvm/buildtools/utils/NativeImageUtils.java index b0b3a485e..e0dae6827 100644 --- a/common/utils/src/main/java/org/graalvm/buildtools/utils/NativeImageUtils.java +++ b/common/utils/src/main/java/org/graalvm/buildtools/utils/NativeImageUtils.java @@ -49,11 +49,18 @@ import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.graalvm.buildtools.utils.SharedConstants.GRAALVM_EXE_EXTENSION; public class NativeImageUtils { + + private static final Pattern requiredVersionPattern = Pattern.compile("^([0-9]+)(?:\\.([0-9]+)?)?(?:\\.([0-9]+)?)?$"); + + private static final Pattern graalvmVersionPattern = Pattern.compile("^GraalVM ([0-9]+)\\.([0-9]+)\\.([0-9]+).*"); + public static void maybeCreateConfigureUtilSymlink(File configureUtilFile, Path nativeImageExecutablePath) { if (!configureUtilFile.exists()) { // possibly the symlink is missing @@ -102,4 +109,44 @@ public static String escapeArg(String arg) { } return arg; } + + /** + * + * @param requiredVersion Required version can be {@code MAJOR}, {@code MAJOR.MINOR} or {@code MAJOR.MINOR.PATCH} + * @param versionToCheck The version to check, as returned by {@code native-image --version} + * @throws IllegalStateException when the version is not correct + */ + public static void checkVersion(String requiredVersion, String versionToCheck) { + Matcher requiredMatcher = requiredVersionPattern.matcher(requiredVersion); + if (!requiredMatcher.matches()) { + throw new IllegalArgumentException("Invalid version " + requiredVersion + ", should be for example \"22\", \"22.3\" or \"22.3.0\"."); + } + Matcher checkedMatcher = graalvmVersionPattern.matcher(versionToCheck); + if (!checkedMatcher.matches()) { + throw new IllegalArgumentException("Version to check '" + versionToCheck + "' can't be parsed."); + } + int requiredMajor = Integer.parseInt(requiredMatcher.group(1)); + int checkedMajor = Integer.parseInt(checkedMatcher.group(1)); + if (checkedMajor < requiredMajor) { + throw new IllegalStateException("GraalVM version " + requiredMajor + " is required but " + checkedMajor + + " has been detected, please upgrade."); + } + if (requiredMatcher.group(2) != null) { + int requiredMinor = Integer.parseInt(requiredMatcher.group(2)); + int checkedMinor = Integer.parseInt(checkedMatcher.group(2)); + if (checkedMinor < requiredMinor) { + throw new IllegalStateException("GraalVM version " + requiredMajor + "." + requiredMinor + + " is required but " + checkedMajor + "." + checkedMinor + " has been detected, please upgrade."); + } + if (requiredMatcher.group(3) != null) { + int requiredPatch = Integer.parseInt(requiredMatcher.group(3)); + int checkedPatch = Integer.parseInt(checkedMatcher.group(3)); + if (checkedPatch < requiredPatch) { + throw new IllegalStateException("GraalVM version " + requiredMajor + "." + requiredMinor + "." + + requiredPatch + " is required but " + checkedMajor + "." + checkedMinor + "." + checkedPatch + + " has been detected, please upgrade."); + } + } + } + } } diff --git a/common/utils/src/test/java/org/graalvm/buildtools/utils/NativeImageUtilsTest.java b/common/utils/src/test/java/org/graalvm/buildtools/utils/NativeImageUtilsTest.java new file mode 100644 index 000000000..59c874200 --- /dev/null +++ b/common/utils/src/test/java/org/graalvm/buildtools/utils/NativeImageUtilsTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.graalvm.buildtools.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class NativeImageUtilsTest { + + @Test + void invalidVersionToCheck() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + NativeImageUtils.checkVersion("22.3", "invalid")); + Assertions.assertThrows(IllegalArgumentException.class, () -> + NativeImageUtils.checkVersion("22.3", "GraalVM")); + } + + @Test + void invalidRequiredVersion() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + NativeImageUtils.checkVersion("invalid", "GraalVM 22.3.0")); + Assertions.assertThrows(IllegalArgumentException.class, () -> + NativeImageUtils.checkVersion("22.3.0-dev", "GraalVM 22.3.0")); + } + + @Test + void checkGraalVMCEVersion() { + NativeImageUtils.checkVersion("22", "GraalVM 22.3.0 Java 17 CE (Java Version 17.0.5+8-jvmci-22.3-b08)"); + NativeImageUtils.checkVersion("22.3", "GraalVM 22.3.0 Java 17 CE (Java Version 17.0.5+8-jvmci-22.3-b08)"); + NativeImageUtils.checkVersion("22.3.0", "GraalVM 22.3.0 Java 17 CE (Java Version 17.0.5+8-jvmci-22.3-b08)"); + } + + @Test + void checkGraalVMCEDevVersion() { + NativeImageUtils.checkVersion("22", "GraalVM 22.3.0-dev Java 17 CE (Java Version 17.0.5+8-LTS)"); + NativeImageUtils.checkVersion("22.3", "GraalVM 22.3.0-dev Java 17 CE (Java Version 17.0.5+8-LTS)"); + NativeImageUtils.checkVersion("22.3.0", "GraalVM 22.3.0-dev Java 17 CE (Java Version 17.0.5+8-LTS)"); + } + + @Test + void checkGraalVMEEVersion() { + NativeImageUtils.checkVersion("22", "GraalVM 22.3.0 Java 17 EE (Java Version 17.0.5+9-LTS-jvmci-22.3-b07)"); + NativeImageUtils.checkVersion("22.3", "GraalVM 22.3.0 Java 17 EE (Java Version 17.0.5+9-LTS-jvmci-22.3-b07)"); + NativeImageUtils.checkVersion("22.3.0", "GraalVM 22.3.0 Java 17 EE (Java Version 17.0.5+9-LTS-jvmci-22.3-b07)"); + } + + @Test + void checkGreaterVersion() { + NativeImageUtils.checkVersion("22", "GraalVM 23.2.1"); + NativeImageUtils.checkVersion("23.1", "GraalVM 23.2.1"); + NativeImageUtils.checkVersion("23.2.0", "GraalVM 23.2.1"); + } + + @Test + void checkLowerVersion() { + Assertions.assertThrows(IllegalStateException.class, () -> + NativeImageUtils.checkVersion("23", "GraalVM 22.2.1") + ); + Assertions.assertThrows(IllegalStateException.class, () -> + NativeImageUtils.checkVersion("22.3", "GraalVM 22.2.1") + ); + Assertions.assertThrows(IllegalStateException.class, () -> + NativeImageUtils.checkVersion("22.2.2", "GraalVM 22.2.1") + ); + } + +} diff --git a/docs/src/docs/asciidoc/maven-plugin.adoc b/docs/src/docs/asciidoc/maven-plugin.adoc index 98aaff1e1..26d19dc9d 100644 --- a/docs/src/docs/asciidoc/maven-plugin.adoc +++ b/docs/src/docs/asciidoc/maven-plugin.adoc @@ -252,6 +252,13 @@ For example, to build a native image named `executable-name` that uses ---- +``:: +If you want to define the minimum GraalVM version, can be `MAJOR`, `MAJOR.MINOR` or `MAJOR.MINOR.PATCH`: +[source,xml] +---- +22.3 +---- + NOTE: Most of the aforementioned properties can also be set from command line as a part of Maven invocation -- for example if you want to temporarily enable verbose mode you can append `-Dverbose` to your Maven invocation. NOTE: If you use GraalVM Enterprise as the `JAVA_HOME` environment, the plugin builds a native image with enterprise features enabled -- for example, an executable will automatically be built with https://medium.com/graalvm/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-for-graalvm-a044cc50b67e[compressed references] and other optimizations enabled. diff --git a/docs/src/docs/snippets/gradle/groovy/build.gradle b/docs/src/docs/snippets/gradle/groovy/build.gradle index 1ef7fc997..137e0c479 100644 --- a/docs/src/docs/snippets/gradle/groovy/build.gradle +++ b/docs/src/docs/snippets/gradle/groovy/build.gradle @@ -121,6 +121,7 @@ graalvmNative { sharedLibrary = false // Determines if image is a shared library, defaults to false if `java-library` plugin isn't included quickBuild = false // Determines if image is being built in quick build mode (alternatively use GRAALVM_QUICK_BUILD environment variable, or add --native-quick-build to the CLI) richOutput = false // Determines if native-image building should be done with rich output + requiredVersion = '22.3' // The minimal GraalVM version, can be `MAJOR`, `MAJOR.MINOR` or `MAJOR.MINOR.PATCH` systemProperties = [name1: 'value1', name2: 'value2'] // Sets the system properties to use for the native image builder configurationFileDirectories.from(file('src/my-config')) // Adds a native image configuration file directory, containing files like reflection configuration diff --git a/docs/src/docs/snippets/gradle/kotlin/build.gradle.kts b/docs/src/docs/snippets/gradle/kotlin/build.gradle.kts index eda5129ea..db7fa6f0c 100644 --- a/docs/src/docs/snippets/gradle/kotlin/build.gradle.kts +++ b/docs/src/docs/snippets/gradle/kotlin/build.gradle.kts @@ -122,6 +122,7 @@ graalvmNative { sharedLibrary.set(false) // Determines if image is a shared library, defaults to false if `java-library` plugin isn't included quickBuild.set(false) // Determines if image is being built in quick build mode (alternatively use GRAALVM_QUICK_BUILD environment variable, or add --native-quick-build to the CLI) richOutput.set(false) // Determines if native-image building should be done with rich output + requiredVersion.set('22.3') // The minimal GraalVM version, can be `MAJOR`, `MAJOR.MINOR` or `MAJOR.MINOR.PATCH` systemProperties.putAll(mapOf("name1" to "value1", "name2" to "value2")) // Sets the system properties to use for the native image builder configurationFileDirectories.from(file("src/my-config")) // Adds a native image configuration file directory, containing files like reflection configuration diff --git a/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/RequiredVersionTest.groovy b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/RequiredVersionTest.groovy new file mode 100644 index 000000000..0c40e9844 --- /dev/null +++ b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/RequiredVersionTest.groovy @@ -0,0 +1,48 @@ +package org.graalvm.buildtools.gradle; + +import org.graalvm.buildtools.gradle.fixtures.AbstractFunctionalTest; + +class RequiredVersionTest extends AbstractFunctionalTest { + + def "can build a native image with a valid required version"() { + def nativeApp = file("build/native/nativeCompile/java-application") + + given: + withSample("java-application") + + buildFile << """ + graalvmNative.binaries.all { + requiredVersion = '22.2' + } + """.stripIndent() + + when: + run 'nativeCompile' + + then: + tasks { + succeeded ':jar', ':nativeCompile' + doesNotContain ':build', ':run' + } + + and: + nativeApp.exists() + } + + def "can't build a native image with an invalid required version"() { + given: + withSample("java-application") + + buildFile << """ + graalvmNative.binaries.all { + requiredVersion = '100' + } + """.stripIndent() + + when: + fails 'nativeCompile' + + then: + errorOutputContains "GraalVM version 100 is required but 22 has been detected, please upgrade." + } +} diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageOptions.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageOptions.java index 0a24f1419..b625bb1ae 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageOptions.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/dsl/NativeImageOptions.java @@ -314,4 +314,13 @@ public interface NativeImageOptions extends Named { @Input ListProperty getExcludeConfigArgs(); + + /** + * Specify the minimal GraalVM version, can be {@code MAJOR}, {@code MAJOR.MINOR} or {@code MAJOR.MINOR.PATCH}. + * + * @return the required version property. + */ + @Input + @Optional + Property getRequiredVersion(); } 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 ca188c35e..bf7010897 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 @@ -41,11 +41,19 @@ package org.graalvm.buildtools.gradle.tasks; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import javax.inject.Inject; + import org.graalvm.buildtools.gradle.NativeImagePlugin; import org.graalvm.buildtools.gradle.dsl.NativeImageOptions; import org.graalvm.buildtools.gradle.internal.GraalVMLogger; import org.graalvm.buildtools.gradle.internal.NativeImageCommandLineProvider; import org.graalvm.buildtools.gradle.internal.NativeImageExecutableLocator; +import org.graalvm.buildtools.utils.NativeImageUtils; import org.gradle.api.DefaultTask; import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; @@ -67,10 +75,7 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; import org.gradle.process.ExecOperations; - -import javax.inject.Inject; -import java.io.File; -import java.util.List; +import org.gradle.process.ExecResult; import static org.graalvm.buildtools.gradle.internal.NativeImageExecutableLocator.graalvmHomeProvider; import static org.graalvm.buildtools.utils.SharedConstants.EXECUTABLE_EXTENSION; @@ -195,6 +200,7 @@ public void exec() { getExecOperations(), logger, diagnostics); + checkRequiredVersionIfNeeded(executablePath, options); for (String diagnostic : diagnostics.getDiagnostics()) { logger.lifecycle(diagnostic); } @@ -220,4 +226,19 @@ public void exec() { } } + private void checkRequiredVersionIfNeeded(File executablePath, NativeImageOptions options) { + if (!options.getRequiredVersion().isPresent()) { + return; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ExecResult execResult = getExecOperations().exec(spec -> { + spec.setStandardOutput(outputStream); + spec.args("--version"); + spec.setExecutable(executablePath.getAbsolutePath()); + }); + execResult.assertNormalExitValue(); + String versionToCheck = new String(outputStream.toByteArray(), StandardCharsets.UTF_8).replace("\n", ""); + NativeImageUtils.checkVersion(options.getRequiredVersion().get(), versionToCheck); + } + } diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/RequireVersionFunctionalTest.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/RequireVersionFunctionalTest.groovy new file mode 100644 index 000000000..142edb4cc --- /dev/null +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/RequireVersionFunctionalTest.groovy @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package org.graalvm.buildtools.maven + +class RequireVersionFunctionalTest extends AbstractGraalVMMavenFunctionalTest { + + def "can build a native image with a valid required version"() { + withSample("java-application") + + when: + mvn '-Pnative', '-DskipTests', 'native:compile' + + then: + buildSucceeded + } + + def "can't build a native image with an invalid required version"() { + withSample("java-application") + + when: + mvn '-Pnative', '-DskipTests', '-DrequiredVersion=100', 'native:compile' + + then: + buildFailed + } + +} diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java index 1497740f6..ed71a1564 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/AbstractNativeImageMojo.java @@ -41,9 +41,13 @@ package org.graalvm.buildtools.maven; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -159,6 +163,9 @@ public abstract class AbstractNativeImageMojo extends AbstractNativeMojo { @Parameter(property = NATIVE_IMAGE_DRY_RUN, defaultValue = "false") protected boolean dryRun; + @Parameter(property = "requiredVersion") + protected String requiredVersion; + @Component protected ToolchainManager toolchainManager; @@ -375,6 +382,7 @@ protected String getClasspath() throws MojoExecutionException { } protected void buildImage() throws MojoExecutionException { + checkRequiredVersionIfNeeded(); Path nativeImageExecutable = Utils.getNativeImage(logger); try { @@ -407,6 +415,31 @@ protected void buildImage() throws MojoExecutionException { } } + protected void checkRequiredVersionIfNeeded() throws MojoExecutionException { + if (requiredVersion == null) { + return; + } + Path nativeImageExecutable = Utils.getNativeImage(logger); + try { + ProcessBuilder processBuilder = new ProcessBuilder(nativeImageExecutable.toString()); + processBuilder.command().add("--version"); + Process versionCheckProcess = processBuilder.start(); + if (versionCheckProcess.waitFor() != 0) { + String commandString = String.join(" ", processBuilder.command()); + throw new MojoExecutionException("Execution of " + commandString + " returned non-zero result"); + } + InputStream inputStream = versionCheckProcess.getInputStream(); + String versionToCheck = new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + NativeImageUtils.checkVersion(requiredVersion, versionToCheck); + + } catch (IOException | InterruptedException e) { + throw new MojoExecutionException("Checking GraalVM version with " + nativeImageExecutable + " failed", e); + } + } + protected void maybeAddGeneratedResourcesConfig(List into) { if (resourcesConfigDirectory.exists() || agentResourceDirectory != null) { File[] dirs = resourcesConfigDirectory.listFiles(); diff --git a/samples/java-application/pom.xml b/samples/java-application/pom.xml index 4a45531bf..28e04a23c 100644 --- a/samples/java-application/pom.xml +++ b/samples/java-application/pom.xml @@ -55,6 +55,7 @@ 0.9.17-SNAPSHOT example-app org.graalvm.demo.Application + 22.2 @@ -88,6 +89,7 @@ false ${imageName} false + ${requiredVersion}