diff --git a/.circleci/config.yml b/.circleci/config.yml index 047f61578..156f0c1df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ version: 2.1 jobs: check: - docker: [{ image: 'cimg/openjdk:11.0.10-node' }] + docker: [{ image: 'cimg/openjdk:17.0.1-node' }] resource_class: large environment: CIRCLE_TEST_REPORTS: /home/circleci/junit @@ -58,7 +58,7 @@ jobs: - store_artifacts: { path: ~/artifacts } trial-publish: - docker: [{ image: 'cimg/openjdk:11.0.10-node' }] + docker: [{ image: 'cimg/openjdk:17.0.1-node' }] resource_class: medium environment: CIRCLE_TEST_REPORTS: /home/circleci/junit @@ -104,7 +104,7 @@ jobs: - store_artifacts: { path: ~/artifacts } publish: - docker: [{ image: 'cimg/openjdk:11.0.10-node' }] + docker: [{ image: 'cimg/openjdk:17.0.1-node' }] resource_class: medium environment: CIRCLE_TEST_REPORTS: /home/circleci/junit diff --git a/.circleci/template.sh b/.circleci/template.sh index e80105b40..927020ff1 100644 --- a/.circleci/template.sh +++ b/.circleci/template.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash export CIRCLECI_TEMPLATE=java-library-oss -export JDK=11 +export JDK=17 diff --git a/README.md b/README.md index e1dd72637..1bea470fa 100644 --- a/README.md +++ b/README.md @@ -416,40 +416,6 @@ This plugin adds the `-Aimmutables.gradle.incremental` compiler arg to the compi For more details, see the Immutables incremental compilation [tracking issue](https://github.com/immutables/immutables/issues/804). -## com.palantir.baseline-enable-preview-flag (off by default) - -As described in [JEP 12](https://openjdk.java.net/jeps/12), Java allows you to use shiny new syntax features if you add -the `--enable-preview` flag. However, gradle requires you to add it in multiple places. This plugin can be applied to -within an allprojects block and it will automatically ugprade any project which is already using the latest -sourceCompatibility by adding the necessary `--enable-preview` flags to all of the following task types. - -_Note, this plugin should be used with **caution** because preview features may change or be removed, and it -is undesirable to deeply couple a repo to a particular Java version as it makes upgrading to a new major Java version harder._ - -```gradle -// root build.gradle -allprojects { - apply plugin: 'com.palantir.baseline-enable-preview-flag' -} -``` - -```gradle -// shorthand for the below: -tasks.withType(JavaCompile) { - options.compilerArgs += "--enable-preview" -} -tasks.withType(Test) { - jvmArgs += "--enable-preview" -} -tasks.withType(JavaExec) { - jvmArgs += "--enable-preview" -} -``` - -If you've explicitly specified a lower sourceCompatibility (e.g. for a published API jar), then this plugin is a no-op. -In fact, Java will actually error if you try to switch on the `--enable-preview` flag to get cutting edge syntax -features but set `sourceCompatibility` (or `--release`) to an older Java version. - ## com.palantir.baseline-java-versions This plugin allows consistent configuration of JDK versions via [Gradle Toolchains](https://docs.gradle.org/current/userguide/toolchains.html). @@ -482,3 +448,36 @@ javaVersion { ``` The optionally configurable fields of the `javaVersion` extension are `target`, for setting the target version used for compilation and `runtime`, for setting the runtime version used for testing and distributions. + +### Opting in to `--enable-preview` flag + +As described in [JEP 12](https://openjdk.java.net/jeps/12), Java allows you to use incubating syntax features if you add the `--enable-preview` flag. Gradle requires you to add it in many places (including on JavaCompile, Javadoc tasks, as well as in production and on execution tasks like Test, JavaExec). The baseline-java-versions plugin provides a shorthand way of enabling this: + +```gradle +// root build.gradle +apply plugin: 'com.palantir.baseline-java-versions' +javaVersions { + libraryTarget = 11 + distributionTarget = '17_PREVIEW' + runtime = '17_PREVIEW' +} + +// shorthand for configuring all the tasks individually, e.g. +tasks.withType(JavaCompile) { + options.compilerArgs += "--enable-preview" +} +tasks.withType(Test) { + jvmArgs += "--enable-preview" +} +tasks.withType(JavaExec) { + jvmArgs += "--enable-preview" +} +``` + +In the example above, the `Baseline-Enable-Preview: 17` attribute will be embedded in the resultant Jar's `META-INF/MANIFEST.MF` file. To see for yourself, run: + +``` +$ unzip -p /path/to/your-project-1.2.3.jar META-INF/MANIFEST.MF +``` + +_Note, this plugin should be used with **caution** because preview features may change or be removed, which might make upgrading to a new Java version harder._ diff --git a/changelog/@unreleased/pr-2322.v2.yml b/changelog/@unreleased/pr-2322.v2.yml new file mode 100644 index 000000000..25b9660be --- /dev/null +++ b/changelog/@unreleased/pr-2322.v2.yml @@ -0,0 +1,7 @@ +type: feature +feature: + description: Users of the `com.palantir.baseline-java-versions` plugin can now set + `javaVersions { distributionTarget = '17_PREVIEW' }` to opt-in to Java's `--enable-preview` + flag at compile time. + links: + - https://github.com/palantir/gradle-baseline/pull/2322 diff --git a/gradle-baseline-java/build.gradle b/gradle-baseline-java/build.gradle index 37e73e4a8..be937fbfc 100644 --- a/gradle-baseline-java/build.gradle +++ b/gradle-baseline-java/build.gradle @@ -111,11 +111,6 @@ gradlePlugin { displayName = 'Palantir Baseline Release Compatibility Plugin' implementationClass = 'com.palantir.baseline.plugins.BaselineReleaseCompatibility' } - baselineEnablePreviewFlag { - id = 'com.palantir.baseline-enable-preview-flag' - displayName = 'Palantir Baseline --enable-preview Flag Plugin' - implementationClass = 'com.palantir.baseline.plugins.BaselineEnablePreviewFlag' - } baselinePreferProjectModules { id = 'com.palantir.baseline-prefer-project-modules' displayName = 'Palantir Baseline Prefer Project Modules Plugin' diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/extensions/BaselineModuleJvmArgsExtension.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/extensions/BaselineModuleJvmArgsExtension.java index 828bbfe9d..d29e345b4 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/extensions/BaselineModuleJvmArgsExtension.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/extensions/BaselineModuleJvmArgsExtension.java @@ -17,9 +17,13 @@ package com.palantir.baseline.extensions; import com.google.common.collect.ImmutableSet; +import java.util.Optional; +import java.util.Set; import javax.inject.Inject; import org.gradle.api.Project; +import org.gradle.api.provider.Provider; import org.gradle.api.provider.SetProperty; +import org.gradle.jvm.toolchain.JavaLanguageVersion; /** * Extension to configure {@code --add-exports [VALUE]=ALL-UNNAMED} for the current module. @@ -28,11 +32,13 @@ public class BaselineModuleJvmArgsExtension { private final SetProperty exports; private final SetProperty opens; + private final SetProperty enablePreview; // stores a singleton version or the empty set @Inject public BaselineModuleJvmArgsExtension(Project project) { exports = project.getObjects().setProperty(String.class); opens = project.getObjects().setProperty(String.class); + enablePreview = project.getObjects().setProperty(JavaLanguageVersion.class); } /** @@ -71,6 +77,14 @@ public final void setOpens(String... input) { opens.set(immutableDeduplicatedCopy); } + public final void setEnablePreview(Provider> provider) { + enablePreview.set(provider.map(maybeValue -> maybeValue.map(Set::of).orElseGet(Set::of))); + } + + public final Provider> getEnablePreview() { + return enablePreview; + } + private static void validateModulePackagePair(String moduleAndPackage) { if (moduleAndPackage.contains("=")) { throw new IllegalArgumentException(String.format( diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineEnablePreviewFlag.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineEnablePreviewFlag.java deleted file mode 100644 index 9e690a0b1..000000000 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineEnablePreviewFlag.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * (c) Copyright 2020 Palantir Technologies Inc. All rights reserved. - * - * 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 - * - * http://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 com.palantir.baseline.plugins; - -import java.util.Collections; -import java.util.List; -import org.gradle.api.JavaVersion; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.plugins.JavaPluginConvention; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.JavaExec; -import org.gradle.api.tasks.compile.JavaCompile; -import org.gradle.api.tasks.javadoc.Javadoc; -import org.gradle.api.tasks.testing.Test; -import org.gradle.external.javadoc.CoreJavadocOptions; -import org.gradle.process.CommandLineArgumentProvider; - -public final class BaselineEnablePreviewFlag implements Plugin { - - private static final String FLAG = "--enable-preview"; - - @Override - public void apply(Project project) { - // The idea behind registering a single 'extra property' is that other plugins (like - // sls-packaging) can easily detect this and also also add the --enable-preview jvm arg - Provider enablePreview = project.provider(() -> { - JavaVersion jvmExecutingGradle = JavaVersion.current(); - JavaPluginConvention javaConvention = project.getConvention().findPlugin(JavaPluginConvention.class); - if (javaConvention == null) { - return false; - } - - return javaConvention.getSourceCompatibility() == jvmExecutingGradle; - }); - - project.getExtensions().getExtraProperties().set("enablePreview", enablePreview); - - project.getPlugins().withId("java", _unused -> { - project.getTasks().withType(JavaCompile.class).configureEach(t -> { - List args = t.getOptions().getCompilerArgumentProviders(); - args.add(new MaybeEnablePreview(enablePreview)); // mutation is gross, but it's the gradle convention - }); - project.getTasks().withType(Test.class).configureEach(t -> { - t.getJvmArgumentProviders().add(new MaybeEnablePreview(enablePreview)); - }); - project.getTasks().withType(JavaExec.class).configureEach(t -> { - t.getJvmArgumentProviders().add(new MaybeEnablePreview(enablePreview)); - }); - - // sadly we have to use afterEvaluate because the Javadoc task doesn't support passing in providers - project.afterEvaluate(_unused2 -> { - if (enablePreview.get()) { - JavaVersion sourceCompat = project.getConvention() - .getPlugin(JavaPluginConvention.class) - .getSourceCompatibility(); - project.getTasks().withType(Javadoc.class).configureEach(t -> { - CoreJavadocOptions options = (CoreJavadocOptions) t.getOptions(); - - // Yes truly javadoc wants a single leading dash, other javac wants a double leading dash. - // We also have to use these manual string options because they don't have first-class methods - // yet (e.g. https://github.com/gradle/gradle/issues/12898) - options.addBooleanOption("-enable-preview", true); - options.addStringOption("source", sourceCompat.getMajorVersion()); - }); - } - }); - }); - } - - private static class MaybeEnablePreview implements CommandLineArgumentProvider { - private final Provider shouldEnable; - - MaybeEnablePreview(Provider shouldEnable) { - this.shouldEnable = shouldEnable; - } - - @Override - public Iterable asArguments() { - return shouldEnable.get() ? Collections.singletonList(FLAG) : Collections.emptyList(); - } - } -} diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineIdea.groovy b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineIdea.groovy index 46b8441ee..40821e54e 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineIdea.groovy +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineIdea.groovy @@ -20,13 +20,10 @@ import com.google.common.collect.ImmutableMap import com.palantir.baseline.IntellijSupport import com.palantir.baseline.plugins.javaversions.BaselineJavaVersionExtension import com.palantir.baseline.plugins.javaversions.BaselineJavaVersionsExtension +import com.palantir.baseline.plugins.javaversions.ChosenJavaVersion import com.palantir.baseline.util.GitUtils import groovy.transform.CompileStatic import groovy.xml.XmlUtil -import org.gradle.api.JavaVersion -import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel - import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -39,6 +36,7 @@ import org.gradle.api.file.FileTreeElement import org.gradle.api.plugins.quality.CheckstyleExtension import org.gradle.api.specs.Spec import org.gradle.api.tasks.util.PatternFilterable +import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.plugins.ide.idea.GenerateIdeaModule import org.gradle.plugins.ide.idea.GenerateIdeaProject import org.gradle.plugins.ide.idea.GenerateIdeaWorkspace @@ -210,7 +208,7 @@ class BaselineIdea extends AbstractBaselinePlugin { bytecodeTargetLevel.attributes().put("target", defaultBytecodeVersion.toString()) project.allprojects.forEach({ project -> BaselineJavaVersionExtension version = project.getExtensions().findByType(BaselineJavaVersionExtension.class) - if (version != null && version.target().get().asInt() != defaultBytecodeVersion.asInt()) { + if (version != null && version.target().get().javaLanguageVersion().asInt() != defaultBytecodeVersion.asInt()) { bytecodeTargetLevel.appendNode("module", ImmutableMap.of( "name", project.getName(), "target", version.target().get().toString())) @@ -220,22 +218,21 @@ class BaselineIdea extends AbstractBaselinePlugin { private void updateProjectRootManager(Node node, BaselineJavaVersionsExtension versions) { Node projectRootManager = node.component.find { it.'@name' == 'ProjectRootManager' } - int featureRelease = versions.distributionTarget().get().asInt() - JavaVersion javaVersion = JavaVersion.toVersion(featureRelease) + ChosenJavaVersion chosenJavaVersion = versions.distributionTarget().get() + int featureRelease = chosenJavaVersion.javaLanguageVersion().asInt() projectRootManager.attributes().put("project-jdk-name", featureRelease) - projectRootManager.attributes().put("languageLevel", new IdeaLanguageLevel(javaVersion).getLevel()) + projectRootManager.attributes().put("languageLevel", chosenJavaVersion.asIdeaLanguageLevel()) } private static void updateModuleLanguageVersion(IdeaModel ideaModel, Project currentProject) { ideaModel.module.iml.withXml { XmlProvider provider -> // Extension must be checked lazily within the transformer - BaselineJavaVersionExtension version = currentProject.extensions.findByType(BaselineJavaVersionExtension.class) - if (version != null) { - int featureRelease = version.target().get().asInt() - JavaVersion javaVersion = JavaVersion.toVersion(featureRelease) + BaselineJavaVersionExtension versionExtension = currentProject.extensions.findByType(BaselineJavaVersionExtension.class) + if (versionExtension != null) { + ChosenJavaVersion chosenJavaVersion = versionExtension.target().get() Node node = provider.asNode() Node newModuleRootManager = node.component.find { it.'@name' == 'NewModuleRootManager' } - newModuleRootManager.attributes().put("LANGUAGE_LEVEL", new IdeaLanguageLevel(javaVersion).getLevel()) + newModuleRootManager.attributes().put("LANGUAGE_LEVEL", chosenJavaVersion.asIdeaLanguageLevel()) } } } diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineModuleJvmArgs.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineModuleJvmArgs.java index baddde1ce..c183e0f57 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineModuleJvmArgs.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/BaselineModuleJvmArgs.java @@ -22,12 +22,14 @@ import com.google.common.collect.ImmutableMap; import com.palantir.baseline.extensions.BaselineModuleJvmArgsExtension; import com.palantir.baseline.plugins.javaversions.BaselineJavaVersion; +import com.palantir.baseline.plugins.javaversions.BaselineJavaVersionsExtension; import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.jar.JarFile; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.gradle.api.Action; @@ -37,7 +39,7 @@ import org.gradle.api.UnknownTaskException; import org.gradle.api.file.FileCollection; import org.gradle.api.java.archives.Manifest; -import org.gradle.api.provider.SetProperty; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -60,6 +62,7 @@ public final class BaselineModuleJvmArgs implements Plugin { private static final String EXTENSION_NAME = "moduleJvmArgs"; + private static final String ENABLE_PREVIEW_ATTRIBUTE = "Baseline-Enable-Preview"; private static final String ADD_EXPORTS_ATTRIBUTE = "Add-Exports"; private static final String ADD_OPENS_ATTRIBUTE = "Add-Opens"; @@ -220,6 +223,17 @@ public Iterable asArguments() { } }); + // Derive this plugin's `enablePreview` property from BaselineJavaVersion's extension + project.getPlugins().withType(BaselineJavaVersion.class, _unused -> { + BaselineJavaVersionsExtension javaVersionsExtension = + project.getExtensions().getByType(BaselineJavaVersionsExtension.class); + extension.setEnablePreview(javaVersionsExtension.runtime().map(chosenJavaVersion -> { + return chosenJavaVersion.enablePreview() + ? Optional.of(chosenJavaVersion.javaLanguageVersion()) + : Optional.empty(); + })); + }); + project.getTasks().withType(Jar.class).configureEach(new Action() { @Override public void execute(Jar jar) { @@ -231,6 +245,13 @@ public void execute(Task task) { public void execute(Manifest manifest) { addManifestAttribute(jar, manifest, ADD_EXPORTS_ATTRIBUTE, extension.exports()); addManifestAttribute(jar, manifest, ADD_OPENS_ATTRIBUTE, extension.opens()); + addManifestAttribute( + jar, + manifest, + ENABLE_PREVIEW_ATTRIBUTE, + extension.getEnablePreview().map(maybeVersion -> maybeVersion.stream() + .map(v -> Integer.toString(v.asInt())) + .collect(Collectors.toSet()))); } }); } @@ -241,7 +262,7 @@ public void execute(Manifest manifest) { } private static void addManifestAttribute( - Jar jarTask, Manifest manifest, String attributeName, SetProperty valueProperty) { + Jar jarTask, Manifest manifest, String attributeName, Provider> valueProperty) { Project project = jarTask.getProject(); // Only locally defined values are applied to jars Set values = valueProperty.get(); diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersion.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersion.java index 8b0b35a03..d76eba704 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersion.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersion.java @@ -16,6 +16,7 @@ package com.palantir.baseline.plugins.javaversions; +import java.util.Collections; import javax.inject.Inject; import org.gradle.api.Action; import org.gradle.api.DefaultTask; @@ -37,8 +38,9 @@ import org.gradle.api.tasks.scala.ScalaCompile; import org.gradle.api.tasks.scala.ScalaDoc; import org.gradle.api.tasks.testing.Test; -import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.external.javadoc.CoreJavadocOptions; import org.gradle.jvm.toolchain.JavaToolchainSpec; +import org.gradle.process.CommandLineArgumentProvider; public final class BaselineJavaVersion implements Plugin { @@ -58,7 +60,9 @@ public void apply(Project project) { javaPluginExtension.toolchain(new Action() { @Override public void execute(JavaToolchainSpec javaToolchainSpec) { - javaToolchainSpec.getLanguageVersion().set(extension.runtime()); + javaToolchainSpec + .getLanguageVersion() + .set(extension.runtime().map(ChosenJavaVersion::javaLanguageVersion)); } }); @@ -69,7 +73,7 @@ public void execute(JavaToolchainSpec javaToolchainSpec) { configureCompilationTasks(project, extension.target(), javaToolchains.forVersion(extension.target())); // Execution tasks (using the runtime version) - configureExecutionTasks(project, javaToolchains.forVersion(extension.runtime())); + configureExecutionTasks(project, extension.runtime(), javaToolchains.forVersion(extension.runtime())); // Validation TaskProvider checkJavaVersions = project.getTasks() @@ -85,20 +89,23 @@ public void execute(CheckJavaVersionsTask task) { } private static void configureCompilationTasks( - Project project, - Provider targetVersionProvider, - Provider javaToolchain) { + Project project, Property target, Provider javaToolchain) { project.getTasks().withType(JavaCompile.class).configureEach(new Action() { @Override - public void execute(JavaCompile javaCompile) { - javaCompile.getJavaCompiler().set(javaToolchain.flatMap(BaselineJavaToolchain::javaCompiler)); + public void execute(JavaCompile javaCompileTask) { + javaCompileTask.getJavaCompiler().set(javaToolchain.flatMap(BaselineJavaToolchain::javaCompiler)); + javaCompileTask + .getOptions() + .getCompilerArgumentProviders() + .add(new EnablePreviewArgumentProvider(target)); + // Set sourceCompatibility to opt out of '-release', allowing opens/exports to be used. - javaCompile.doFirst(new Action() { + javaCompileTask.doFirst(new Action() { @Override public void execute(Task task) { ((JavaCompile) task) .setSourceCompatibility( - targetVersionProvider.get().toString()); + target.get().javaLanguageVersion().toString()); } }); } @@ -106,22 +113,39 @@ public void execute(Task task) { project.getTasks().withType(Javadoc.class).configureEach(new Action() { @Override - public void execute(Javadoc javadoc) { - javadoc.getJavadocTool().set(javaToolchain.flatMap(BaselineJavaToolchain::javadocTool)); + public void execute(Javadoc javadocTask) { + javadocTask.getJavadocTool().set(javaToolchain.flatMap(BaselineJavaToolchain::javadocTool)); + + // javadocTask doesn't allow us to add a CommandLineArgumentProvider, so we do it just in time + javadocTask.doFirst(new Action() { + @Override + public void execute(Task task) { + CoreJavadocOptions options = (CoreJavadocOptions) ((Javadoc) task).getOptions(); + if (target.get().enablePreview()) { + // yes, javadoc truly takes a single-dash where everyone else takes a double dash + options.addBooleanOption("-enable-preview", true); + } + } + }); } }); project.getTasks().withType(GroovyCompile.class).configureEach(new Action() { @Override - public void execute(GroovyCompile groovyCompile) { - groovyCompile.getJavaLauncher().set(javaToolchain.flatMap(BaselineJavaToolchain::javaLauncher)); + public void execute(GroovyCompile groovyCompileTask) { + groovyCompileTask.getJavaLauncher().set(javaToolchain.flatMap(BaselineJavaToolchain::javaLauncher)); + groovyCompileTask + .getOptions() + .getCompilerArgumentProviders() + .add(new EnablePreviewArgumentProvider(target)); + // Set sourceCompatibility to opt out of '-release', allowing opens/exports to be used. - groovyCompile.doFirst(new Action() { + groovyCompileTask.doFirst(new Action() { @Override public void execute(Task task) { ((GroovyCompile) task) .setSourceCompatibility( - targetVersionProvider.get().toString()); + target.get().javaLanguageVersion().toString()); } }); } @@ -129,15 +153,20 @@ public void execute(Task task) { project.getTasks().withType(ScalaCompile.class).configureEach(new Action() { @Override - public void execute(ScalaCompile scalaCompile) { - scalaCompile.getJavaLauncher().set(javaToolchain.flatMap(BaselineJavaToolchain::javaLauncher)); + public void execute(ScalaCompile scalaCompileTask) { + scalaCompileTask.getJavaLauncher().set(javaToolchain.flatMap(BaselineJavaToolchain::javaLauncher)); + scalaCompileTask + .getOptions() + .getCompilerArgumentProviders() + .add(new EnablePreviewArgumentProvider(target)); + // Set sourceCompatibility to opt out of '-release', allowing opens/exports to be used. - scalaCompile.doFirst(new Action() { + scalaCompileTask.doFirst(new Action() { @Override public void execute(Task task) { ((ScalaCompile) task) .setSourceCompatibility( - targetVersionProvider.get().toString()); + target.get().javaLanguageVersion().toString()); } }); } @@ -151,11 +180,13 @@ public void execute(ScalaDoc scalaDoc) { }); } - private static void configureExecutionTasks(Project project, Provider javaToolchain) { + private static void configureExecutionTasks( + Project project, Provider runtime, Provider javaToolchain) { project.getTasks().withType(JavaExec.class).configureEach(new Action() { @Override public void execute(JavaExec javaExec) { javaExec.getJavaLauncher().set(javaToolchain.flatMap(BaselineJavaToolchain::javaLauncher)); + javaExec.getJvmArgumentProviders().add(new EnablePreviewArgumentProvider(runtime)); } }); @@ -163,6 +194,7 @@ public void execute(JavaExec javaExec) { @Override public void execute(Test test) { test.getJavaLauncher().set(javaToolchain.flatMap(BaselineJavaToolchain::javaLauncher)); + test.getJvmArgumentProviders().add(new EnablePreviewArgumentProvider(runtime)); } }); } @@ -171,39 +203,49 @@ public void execute(Test test) { @SuppressWarnings("checkstyle:DesignForExtension") public static class CheckJavaVersionsTask extends DefaultTask { - private final Property targetVersion; - private final Property runtimeVersion; + private final Property targetVersion; + private final Property runtimeVersion; @Inject public CheckJavaVersionsTask() { setGroup("Verification"); setDescription("Ensures configured java versions are compatible: " + "The runtime version must be greater than or equal to the target version."); - targetVersion = getProject().getObjects().property(JavaLanguageVersion.class); - runtimeVersion = getProject().getObjects().property(JavaLanguageVersion.class); + targetVersion = getProject().getObjects().property(ChosenJavaVersion.class); + runtimeVersion = getProject().getObjects().property(ChosenJavaVersion.class); } @Input - public Property getTargetVersion() { + public Property getTargetVersion() { return targetVersion; } @Input - public Property getRuntimeVersion() { + public Property getRuntimeVersion() { return runtimeVersion; } @TaskAction public final void checkJavaVersions() { - JavaLanguageVersion target = getTargetVersion().get(); - JavaLanguageVersion runtime = getRuntimeVersion().get(); + ChosenJavaVersion target = getTargetVersion().get(); + ChosenJavaVersion runtime = getRuntimeVersion().get(); getLogger() .debug( "BaselineJavaVersion configured project {} with target version {} and runtime version {}", getProject(), target, runtime); - if (target.asInt() > runtime.asInt()) { + + if (target.enablePreview() && !target.equals(runtime)) { + throw new GradleException(String.format( + "Runtime Java version (%s) must be exactly the same as the compilation target (%s), because " + + "--enable-preview is enabled. Otherwise Java will fail to start. See " + + "https://openjdk.org/jeps/12.", + runtime, target)); + } + + if (target.javaLanguageVersion().asInt() + > runtime.javaLanguageVersion().asInt()) { throw new GradleException(String.format( "The requested compilation target Java version (%s) must not " + "exceed the requested runtime Java version (%s)", @@ -211,4 +253,20 @@ public final void checkJavaVersions() { } } } + + private static class EnablePreviewArgumentProvider implements CommandLineArgumentProvider { + + public static final String FLAG = "--enable-preview"; + + private final Provider provider; + + private EnablePreviewArgumentProvider(Provider provider) { + this.provider = provider; + } + + @Override + public Iterable asArguments() { + return provider.get().enablePreview() ? Collections.singletonList(FLAG) : Collections.emptyList(); + } + } } diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionExtension.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionExtension.java index 162b17cfd..a93a53d2d 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionExtension.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionExtension.java @@ -19,7 +19,6 @@ import javax.inject.Inject; import org.gradle.api.Project; import org.gradle.api.provider.Property; -import org.gradle.jvm.toolchain.JavaLanguageVersion; /** * Extension named {@code javaVersion} used to set the @@ -27,36 +26,44 @@ */ public class BaselineJavaVersionExtension { - private final Property target; - private final Property runtime; + private final Property target; + private final Property runtime; + private final Property overrideLibraryAutoDetection; @Inject public BaselineJavaVersionExtension(Project project) { - target = project.getObjects().property(JavaLanguageVersion.class); - runtime = project.getObjects().property(JavaLanguageVersion.class); + target = project.getObjects().property(ChosenJavaVersion.class); + runtime = project.getObjects().property(ChosenJavaVersion.class); overrideLibraryAutoDetection = project.getObjects().property(Boolean.class); + target.finalizeValueOnRead(); runtime.finalizeValueOnRead(); overrideLibraryAutoDetection.finalizeValueOnRead(); } - /** Target {@link JavaLanguageVersion} for compilation. */ - public final Property target() { + /** + * Target {@link ChosenJavaVersion} for compilation. + * + * Also determines whether the `--enable-preview` flag should be used for compilation, producing bytecode with a + * minor version of '65535'. Unlike normal bytecode, this bytecode cannot be run by a higher version of Java that + * it was compiled by. + */ + public final Property target() { return target; } public final void setTarget(int value) { - target.set(JavaLanguageVersion.of(value)); + target.set(ChosenJavaVersion.of(value)); } - /** Runtime {@link JavaLanguageVersion} for testing and distributions. */ - public final Property runtime() { + /** Runtime {@link ChosenJavaVersion} for testing and distributions. */ + public final Property runtime() { return runtime; } public final void setRuntime(int value) { - runtime.set(JavaLanguageVersion.of(value)); + runtime.set(ChosenJavaVersion.of(value)); } /** diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersions.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersions.java index a46edd039..e80811942 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersions.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersions.java @@ -24,6 +24,7 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.publish.Publication; import org.gradle.api.publish.PublishingExtension; import org.gradle.api.publish.ivy.IvyPublication; @@ -59,11 +60,12 @@ public void apply(Project project) { proj.getPluginManager().apply(BaselineJavaVersion.class); BaselineJavaVersionExtension projectVersions = proj.getExtensions().getByType(BaselineJavaVersionExtension.class); - projectVersions - .target() - .convention(proj.provider(() -> isLibrary(proj, projectVersions) - ? rootExtension.libraryTarget().get() - : rootExtension.distributionTarget().get())); + + Provider suggestedTarget = proj.provider(() -> isLibrary(proj, projectVersions) + ? ChosenJavaVersion.of(rootExtension.libraryTarget().get()) + : rootExtension.distributionTarget().get()); + + projectVersions.target().convention(suggestedTarget); projectVersions.runtime().convention(rootExtension.runtime()); })); } diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionsExtension.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionsExtension.java index 9b0cbbb3f..db984f943 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionsExtension.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/BaselineJavaVersionsExtension.java @@ -31,20 +31,22 @@ */ public class BaselineJavaVersionsExtension { private final Property libraryTarget; - private final Property distributionTarget; - private final Property runtime; + private final Property distributionTarget; + private final Property runtime; private final LazilyConfiguredMapping, Project> jdks = new LazilyConfiguredMapping<>(AtomicReference::new); @Inject public BaselineJavaVersionsExtension(Project project) { this.libraryTarget = project.getObjects().property(JavaLanguageVersion.class); - this.distributionTarget = project.getObjects().property(JavaLanguageVersion.class); - this.runtime = project.getObjects().property(JavaLanguageVersion.class); + this.distributionTarget = project.getObjects().property(ChosenJavaVersion.class); + this.runtime = project.getObjects().property(ChosenJavaVersion.class); + // distribution defaults to the library value - distributionTarget.convention(libraryTarget); + distributionTarget.convention(libraryTarget.map(ChosenJavaVersion::of)); // runtime defaults to the distribution value runtime.convention(distributionTarget); + libraryTarget.finalizeValueOnRead(); distributionTarget.finalizeValueOnRead(); runtime.finalizeValueOnRead(); @@ -60,24 +62,34 @@ public final void setLibraryTarget(int value) { } /** - * Target {@link JavaLanguageVersion} for compilation of code used within distributions, + * Target {@link ChosenJavaVersion} for compilation of code used within distributions, * but not published externally. */ - public final Property distributionTarget() { + public final Property distributionTarget() { return distributionTarget; } public final void setDistributionTarget(int value) { - distributionTarget.set(JavaLanguageVersion.of(value)); + distributionTarget.set(ChosenJavaVersion.of(value)); + } + + /** Accepts inputs such as '17_PREVIEW'. */ + public final void setDistributionTarget(String value) { + distributionTarget.set(ChosenJavaVersion.fromString(value)); } - /** Runtime {@link JavaLanguageVersion} for testing and packaging distributions. */ - public final Property runtime() { + /** Runtime {@link ChosenJavaVersion} for testing and packaging distributions. */ + public final Property runtime() { return runtime; } public final void setRuntime(int value) { - runtime.set(JavaLanguageVersion.of(value)); + runtime.set(ChosenJavaVersion.of(value)); + } + + /** Accepts inputs such as '17_PREVIEW'. */ + public final void setRuntime(String value) { + runtime.set(ChosenJavaVersion.fromString(value)); } public final Optional jdkMetadataFor( diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/ChosenJavaVersion.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/ChosenJavaVersion.java new file mode 100644 index 000000000..7b7795f33 --- /dev/null +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/ChosenJavaVersion.java @@ -0,0 +1,84 @@ +/* + * (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.baseline.plugins.javaversions; + +import java.io.Serializable; +import java.util.Objects; +import org.gradle.jvm.toolchain.JavaLanguageVersion; + +/** + * Augments {@link JavaLanguageVersion} with whether --enable-preview should be used or not. Useful for both + * compile time and runtime. + */ +public final class ChosenJavaVersion implements Serializable { + + private final JavaLanguageVersion javaLanguageVersion; + private final boolean enablePreview; + + public ChosenJavaVersion(JavaLanguageVersion javaLanguageVersion, boolean enablePreview) { + this.javaLanguageVersion = javaLanguageVersion; + this.enablePreview = enablePreview; + } + + /** Accepts inputs like '17_PREVIEW' or '17'. */ + public static ChosenJavaVersion fromString(String string) { + return new ChosenJavaVersion( + JavaLanguageVersion.of(string.replaceAll("_PREVIEW", "")), string.endsWith("_PREVIEW")); + } + + public static ChosenJavaVersion of(int number) { + return new ChosenJavaVersion(JavaLanguageVersion.of(number), false); + } + + public static ChosenJavaVersion of(JavaLanguageVersion version) { + return new ChosenJavaVersion(version, false); + } + + public JavaLanguageVersion javaLanguageVersion() { + return javaLanguageVersion; + } + + public boolean enablePreview() { + return enablePreview; + } + + public String asIdeaLanguageLevel() { + return "JDK_" + javaLanguageVersion.toString() + (enablePreview ? "_PREVIEW" : ""); + } + + @Override + public String toString() { + return javaLanguageVersion.toString() + (enablePreview ? "_PREVIEW" : ""); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ChosenJavaVersion that = (ChosenJavaVersion) other; + return enablePreview == that.enablePreview && javaLanguageVersion.equals(that.javaLanguageVersion); + } + + @Override + public int hashCode() { + return Objects.hash(javaLanguageVersion, enablePreview); + } +} diff --git a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/JavaToolchains.java b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/JavaToolchains.java index 4afcf46cc..9bec70510 100644 --- a/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/JavaToolchains.java +++ b/gradle-baseline-java/src/main/groovy/com/palantir/baseline/plugins/javaversions/JavaToolchains.java @@ -19,7 +19,6 @@ import org.gradle.api.Project; import org.gradle.api.provider.Provider; import org.gradle.jvm.toolchain.JavaInstallationMetadata; -import org.gradle.jvm.toolchain.JavaLanguageVersion; import org.gradle.jvm.toolchain.JavaToolchainService; public final class JavaToolchains { @@ -31,23 +30,23 @@ public JavaToolchains(Project project, BaselineJavaVersionsExtension baselineJav this.baselineJavaVersionsExtension = baselineJavaVersionsExtension; } - public Provider forVersion(Provider javaLanguageVersionProvider) { - return javaLanguageVersionProvider.map(javaLanguageVersion -> { + public Provider forVersion(Provider javaLanguageVersionProvider) { + return javaLanguageVersionProvider.map(chosenJavaVersion -> { Provider configuredJdkMetadata = project.provider(() -> baselineJavaVersionsExtension - .jdkMetadataFor(javaLanguageVersion, project) + .jdkMetadataFor(chosenJavaVersion.javaLanguageVersion(), project) .orElseGet(() -> project.getExtensions() .getByType(JavaToolchainService.class) .launcherFor(javaToolchainSpec -> javaToolchainSpec .getLanguageVersion() - .set(javaLanguageVersion)) + .set(chosenJavaVersion.javaLanguageVersion())) .get() .getMetadata())); return new ConfiguredJavaToolchain( project.getObjects(), - project.provider( - () -> new JavaInstallationMetadataWrapper(javaLanguageVersion, configuredJdkMetadata))); + project.provider(() -> new JavaInstallationMetadataWrapper( + chosenJavaVersion.javaLanguageVersion(), configuredJdkMetadata))); }); } } diff --git a/gradle-baseline-java/src/main/resources/META-INF/gradle-plugins/com.palantir.baseline-enable-preview-flag.properties b/gradle-baseline-java/src/main/resources/META-INF/gradle-plugins/com.palantir.baseline-enable-preview-flag.properties deleted file mode 100644 index 6ea86cf75..000000000 --- a/gradle-baseline-java/src/main/resources/META-INF/gradle-plugins/com.palantir.baseline-enable-preview-flag.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=com.palantir.baseline.plugins.BaselineEnablePreviewFlag diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineJavaVersionIntegrationTest.groovy b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineJavaVersionIntegrationTest.groovy index 3b21868fe..da8ce451f 100644 --- a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineJavaVersionIntegrationTest.groovy +++ b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineJavaVersionIntegrationTest.groovy @@ -16,17 +16,23 @@ package com.palantir.baseline -import nebula.test.IntegrationSpec -import nebula.test.functional.ExecutionResult - import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import nebula.test.IntegrationSpec +import nebula.test.functional.ExecutionResult +import org.assertj.core.api.Assumptions +/** + * This test exercises both the root-plugin {@code BaselineJavaVersions} AND the subproject + * specific plugin, {@code BaselineJavaVersion}. + */ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { private static final int JAVA_8_BYTECODE = 52 private static final int JAVA_11_BYTECODE = 55 private static final int JAVA_17_BYTECODE = 61 + private static final int ENABLE_PREVIEW_BYTECODE = 65535 + private static final int NOT_ENABLE_PREVIEW_BYTECODE = 0 def standardBuildFile = ''' buildscript { @@ -72,6 +78,21 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { } '''.stripIndent(true) + def java17PreviewCode = ''' + public class Main { + sealed interface MyUnion { + record Foo(int number) implements MyUnion {} + } + + public static void main(String[] args) { + MyUnion myUnion = new MyUnion.Foo(1234); + switch (myUnion) { + case MyUnion.Foo foo -> System.out.println("Java 17 pattern matching switch: " + foo.number); + } + } + } + ''' + def setup() { // Fork needed or build fails on circleci with "SystemInfo is not supported on this operating system." // Comment out locally in order to get debugging to work @@ -107,7 +128,23 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: runTasksSuccessfully('compileJava') - getBytecodeVersion(compiledClass) == JAVA_17_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_17_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) + } + + def 'java 17 preview compilation works'() { + when: + buildFile << ''' + javaVersions { + libraryTarget = 11 + distributionTarget = '17_PREVIEW' + } + '''.stripIndent(true) + file('src/main/java/Main.java') << java17PreviewCode + File compiledClass = new File(projectDir, "build/classes/java/main/Main.class") + + then: + runTasksSuccessfully('compileJava', '-i') + assertBytecodeVersion(compiledClass, JAVA_17_BYTECODE, ENABLE_PREVIEW_BYTECODE) } def 'library target is used when no artifacts are published but project is overridden as a library'() { @@ -126,7 +163,7 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: runTasksSuccessfully('compileJava') - getBytecodeVersion(compiledClass) == JAVA_11_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_11_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'library target is used when nebula maven publishing plugin is applied'() { @@ -143,7 +180,7 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: runTasksSuccessfully('compileJava') - getBytecodeVersion(compiledClass) == JAVA_11_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_11_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'library target is used when the palantir shadowjar plugin is applied'() { @@ -162,7 +199,7 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: runTasksSuccessfully('--write-locks') runTasksSuccessfully('compileJava') - getBytecodeVersion(compiledClass) == JAVA_11_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_11_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'java 11 compilation succeeds targeting java 11'() { @@ -177,7 +214,7 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: runTasksSuccessfully('compileJava') - getBytecodeVersion(compiledClass) == JAVA_11_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_11_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'java 11 execution succeeds on java 11'() { @@ -193,7 +230,7 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: ExecutionResult result = runTasksSuccessfully('run') result.standardOutput.contains 'jdk11 features on runtime 11' - getBytecodeVersion(compiledClass) == JAVA_11_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_11_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'java 11 execution succeeds on java 17'() { @@ -210,10 +247,13 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: ExecutionResult result = runTasksSuccessfully('run') result.standardOutput.contains 'jdk11 features on runtime 17' - getBytecodeVersion(compiledClass) == JAVA_11_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_11_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'java 8 execution succeeds on java 8'() { + Assumptions.assumeThat(System.getProperty("os.arch")).describedAs( + "On an M1 mac, this test will fail to download https://api.adoptopenjdk.net/v3/binary/latest/8/ga/mac/aarch64/jdk/hotspot/normal/adoptopenjdk") + .isNotEqualTo("aarch64"); when: buildFile << ''' javaVersions { @@ -228,6 +268,10 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { } def 'java 8 execution succeeds on java 11'() { + Assumptions.assumeThat(System.getProperty("os.arch")).describedAs( + "On an M1 mac, this test will fail to download https://api.adoptopenjdk.net/v3/binary/latest/8/ga/mac/aarch64/jdk/hotspot/normal/adoptopenjdk") + .isNotEqualTo("aarch64"); + when: buildFile << ''' javaVersions { @@ -241,7 +285,7 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { then: ExecutionResult result = runTasksSuccessfully('run') result.standardOutput.contains 'jdk8 features on runtime 11' - getBytecodeVersion(compiledClass) == JAVA_8_BYTECODE + assertBytecodeVersion(compiledClass, JAVA_8_BYTECODE, NOT_ENABLE_PREVIEW_BYTECODE) } def 'JavaPluginConvention.getTargetCompatibility() produces the runtime java version'() { @@ -279,6 +323,34 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { result.standardError.contains 'The requested compilation target' } + def 'verification should fail when --enable-preview is on, but versions differ'() { + when: + buildFile << ''' + javaVersions { + distributionTarget = '11_PREVIEW' + runtime = '15_PREVIEW' + } + '''.stripIndent(true) + + then: + ExecutionResult result = runTasksWithFailure('checkJavaVersions') + result.standardError.contains 'Runtime Java version (15_PREVIEW) must be exactly the same as the compilation target (11_PREVIEW)' + } + + def 'verification should fail when runtime does not use --enable-preview but compilation does'() { + when: + buildFile << ''' + javaVersions { + distributionTarget = '17_PREVIEW' + runtime = '17' + } + '''.stripIndent(true) + + then: + ExecutionResult result = runTasksWithFailure('checkJavaVersions') + result.standardError.contains 'Runtime Java version (17) must be exactly the same as the compilation target (17_PREVIEW)' + } + def 'verification should succeed when target and runtime versions match'() { when: buildFile << ''' @@ -293,6 +365,9 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { } def 'can configure a jdk path to be used'() { + Assumptions.assumeThat(System.getenv("CI")).describedAs( + "This test deletes a directory locally, you don't want to run it on your mac").isNotNull(); + Path newJavaHome = Files.createSymbolicLink( projectDir.toPath().resolve("jdk"), Paths.get(System.getProperty("java.home"))) @@ -339,16 +414,19 @@ class BaselineJavaVersionIntegrationTest extends IntegrationSpec { private static final int BYTECODE_IDENTIFIER = (int) 0xCAFEBABE // See http://illegalargumentexception.blogspot.com/2009/07/java-finding-class-versions.html - private static int getBytecodeVersion(File file) { + private static void assertBytecodeVersion(File file, int expectedMajorBytecodeVersion, + int expectedMinorBytecodeVersion) { try (InputStream stream = new FileInputStream(file) DataInputStream dis = new DataInputStream(stream)) { int magic = dis.readInt() if (magic != BYTECODE_IDENTIFIER) { throw new IllegalArgumentException("File " + file + " does not appear to be java bytecode") } - // skip the minor version - dis.readShort() - return 0xFFFF & dis.readShort() + int minorBytecodeVersion = 0xFFFF & dis.readShort() + int majorBytecodeVersion = 0xFFFF & dis.readShort() + + majorBytecodeVersion == expectedMajorBytecodeVersion + minorBytecodeVersion == expectedMinorBytecodeVersion } } } diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineModuleJvmArgsIntegrationTest.groovy b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineModuleJvmArgsIntegrationTest.groovy index 32936f85e..1bdcce7a5 100644 --- a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineModuleJvmArgsIntegrationTest.groovy +++ b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/BaselineModuleJvmArgsIntegrationTest.groovy @@ -278,6 +278,33 @@ class BaselineModuleJvmArgsIntegrationTest extends IntegrationSpec { .collect(MoreCollectors.onlyElement()) String manifestValue = jarFile.getManifest().getMainAttributes().getValue('Add-Exports') manifestValue == 'java.management/sun.management' + + !jarFile.getManifest().getMainAttributes().containsKey('Baseline-Enable-Preview') + } + + def 'Adds Baseline-Enable-Preview attribute to jar manifest'() { + when: + buildFile << ''' + javaVersions { + runtime = '11_PREVIEW' + } + '''.stripIndent(true) + writeJavaSourceFile(''' + package com; + public class Example { + public static void main(String[] args) { + } + } + '''.stripIndent(true)) + + then: + runTasksSuccessfully('jar') + JarFile jarFile = Arrays.stream(directory("build/libs").listFiles()) + .filter(file -> file.name.endsWith(".jar")) + .map(JarFile::new) + .collect(MoreCollectors.onlyElement()) + String manifestValue = jarFile.getManifest().getMainAttributes().getValue('Baseline-Enable-Preview') + manifestValue == '11' } def 'Executes with externally defined exports'() { diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/plugins/BaselineEnablePreviewFlagTest.groovy b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/plugins/BaselineEnablePreviewFlagTest.groovy deleted file mode 100644 index 243a28df2..000000000 --- a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/plugins/BaselineEnablePreviewFlagTest.groovy +++ /dev/null @@ -1,193 +0,0 @@ -/* - * (c) Copyright 2020 Palantir Technologies Inc. All rights reserved. - * - * 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 - * - * http://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 com.palantir.baseline.plugins - - -import nebula.test.IntegrationSpec -import nebula.test.functional.ExecutionResult -import org.gradle.api.JavaVersion -import spock.lang.IgnoreIf - -@IgnoreIf({JavaVersion.current() < JavaVersion.VERSION_14}) -// this class uses records, which were introduced in java 14 -class BaselineEnablePreviewFlagTest extends IntegrationSpec { - - def setupSingleProject(File dir) { - new File(dir, "build.gradle") << ''' - apply plugin: 'java-library' - apply plugin: 'com.palantir.baseline-enable-preview-flag' - apply plugin: 'application' - - application { - mainClass = 'foo.Foo' - } - '''.stripIndent() - - writeSourceFileContainingRecord(dir); - } - - private writeSourceFileContainingRecord(File dir) { - writeJavaSourceFile(''' - package foo; - public class Foo { - /** Hello this is some javadoc. */ - public record Coordinate(int x, int y) {} - - public static void main(String... args) { - System.out.println("Hello, world: " + new Coordinate(1, 2)); - } - } - '''.stripIndent(), dir) - } - - def 'compiles'() { - when: - setupSingleProject(projectDir) - buildFile << ''' - tasks.classes.doLast { - println "COMPILED:" + new File(sourceSets.main.java.classesDirectory.get().getAsFile(), "foo").list() - } - ''' - ExecutionResult executionResult = runTasks('classes', '-is') - - then: - executionResult.getStandardOutput().contains('Foo$Coordinate.class') - executionResult.getStandardOutput().contains('Foo.class') - } - - def 'javadoc'() { - when: - setupSingleProject(projectDir) - - then: - ExecutionResult executionResult = runTasks('javadoc', '-is') - assert executionResult.getSuccess() ?: executionResult.getStandardOutput() - } - - def 'runs'() { - when: - setupSingleProject(projectDir) - ExecutionResult executionResult = runTasks('run', '-is') - - then: - executionResult.getStandardOutput().contains("Hello, world: Coordinate[x=1, y=2]") - } - - def 'testing works'() { - when: - setupSingleProject(projectDir) - buildFile << ''' - repositories { mavenCentral() } - dependencies { - testImplementation 'junit:junit:4.13.1' - } - ''' - - file('src/test/java/foo/FooTest.java') << ''' - package foo; - public final class FooTest { - @org.junit.Ignore("silly junit4 thinks this 'class' actually contains tests") - record Whatever(int x, int y) {} - - @org.junit.Test - public void whatever() { - Foo.main(); - System.out.println("Hello, world: " + new Whatever(1, 2)); - } - } - '''.stripIndent() - ExecutionResult executionResult = runTasks('test', '-is') - - then: - executionResult.getStandardOutput().contains("Hello, world: Coordinate[x=1, y=2]") - executionResult.getStandardOutput().contains("Hello, world: Whatever[x=1, y=2]") - } - - def 'multiproject'() { - when: - buildFile << ''' - subprojects { - apply plugin: 'java-library' - apply plugin: 'com.palantir.baseline-enable-preview-flag' - } - ''' - - File java14Dir = addSubproject("my-java-14", ''' - apply plugin: 'application' - - application { - mainClass = 'foo.Foo' - } - dependencies { - implementation project(':my-java-14-preview') - } - ''') - writeJavaSourceFile(''' - package bar; - public class Bar { - public static void main(String... args) { - foo.Foo.main(); - } - } - ''', java14Dir); - - File java14PreviewDir = addSubproject("my-java-14-preview") - writeSourceFileContainingRecord(java14PreviewDir) - - - then: - ExecutionResult executionResult = runTasks('run', '-is') - executionResult.getStandardOutput().contains 'Hello, world: Coordinate[x=1, y=2]' - } - - def 'does not always apply'() { - when: - File java8Dir = addSubproject('my-java-8-api', """ - sourceCompatibility = 8 - - tasks.withType(JavaCompile) { - doFirst { - println "Args for for \$name are \$options.allCompilerArgs" - } - } - """.stripIndent()) - - buildFile << ''' - subprojects { - apply plugin: 'java-library' - apply plugin: 'com.palantir.baseline-enable-preview-flag' - } - ''' - - writeJavaSourceFile(''' - package foo; - public class FooSecond { - /** Hello this is some javadoc. */ - public static void main(String... args) { - System.out.println("Hello, world"); - } - } - '''.stripIndent(), java8Dir) - - ExecutionResult result = runTasks('my-java-8-api:classes', '-is') - - then: - result.getStandardOutput().contains("Args for for compileJava are []") - !result.getStandardOutput().contains("--enable-preview") - } -} - diff --git a/gradle-baseline-java/src/test/groovy/com/palantir/baseline/plugins/javaversions/ChosenJavaVersionTest.java b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/plugins/javaversions/ChosenJavaVersionTest.java new file mode 100644 index 000000000..7ac0523a0 --- /dev/null +++ b/gradle-baseline-java/src/test/groovy/com/palantir/baseline/plugins/javaversions/ChosenJavaVersionTest.java @@ -0,0 +1,67 @@ +/* + * (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. + * + * 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 + * + * http://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 com.palantir.baseline.plugins.javaversions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.junit.jupiter.api.Test; + +class ChosenJavaVersionTest { + + @Test + void deserialization_produces_sensible_toString() { + assertThat(ChosenJavaVersion.fromString("8")).hasToString("8"); + + assertThat(ChosenJavaVersion.fromString("11_PREVIEW")).hasToString("11_PREVIEW"); + assertThat(ChosenJavaVersion.fromString("11_PREVIEW").enablePreview()).isTrue(); + assertThat(ChosenJavaVersion.fromString("11_PREVIEW").javaLanguageVersion()) + .isEqualTo(JavaLanguageVersion.of(11)); + + assertThat(ChosenJavaVersion.fromString("17_PREVIEW")).hasToString("17_PREVIEW"); + assertThat(ChosenJavaVersion.fromString("33")).hasToString("33"); + + assertThatThrownBy(() -> ChosenJavaVersion.fromString("1.5")).isInstanceOf(NumberFormatException.class); + } + + @Test + void idea_language_level() { + assertThat(ChosenJavaVersion.of(11).asIdeaLanguageLevel()).isEqualTo("JDK_11"); + assertThat(ChosenJavaVersion.fromString("17_PREVIEW").asIdeaLanguageLevel()) + .isEqualTo("JDK_17_PREVIEW"); + } + + @Test + void java_serialization() throws IOException, ClassNotFoundException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream objectOut = new ObjectOutputStream(baos)) { + objectOut.writeObject(ChosenJavaVersion.fromString("11_PREVIEW")); + } + try (ObjectInputStream objectIn = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + Object deserialized = objectIn.readObject(); + assertThat(deserialized) + .describedAs("Necessary for gradle caching / up-to-dateness checking") + .isEqualTo(ChosenJavaVersion.fromString("11_PREVIEW")); + } + } +}