From a8d553218c5e888c83a953cd653397520512053d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 12 Mar 2021 16:13:50 +0100 Subject: [PATCH] Introduce Gradle Toolchain support in build Prior to this commit, the Spring Framework build would rely on setting a custom Java HOME for building all sources and tests with that JDK. This approach is not flexible enough, since we would be testing the source compatibility against a recent JDK, but not a common case experienced by the community: compiling and running application code with a recent JDK and the official, JDK8-based Framework artifacts. This method is also limiting our choice of JDKs to the ones currently supported by Gradle itself. This commit introduces the support of Gradle JVM Toolchains in the Spring Framework build. We can now select a specific JDK for compiling the main SourceSets (Java, Groovy and Kotlin) and another one for compiling and running the test SourceSets: `./gradlew check -PmainToolChain=8 -PtestToolchain=15` Gradle will automatically find the JDKs present on the host or download one automcatically. You can find out about the ones installed on your host using: `./gradlew -q javaToolchains` Finally, this commit also refactors the CI infrastructure to: * only have a single CI image (with all the supported JDKs) * use this new feature to compile with JDK8 but test it against JDK11 and JDK15. Closes gh-25787 --- build.gradle | 4 +- buildSrc/README.md | 10 +- .../compile/CompilerConventionsPlugin.java | 25 +--- ci/images/ci-image-jdk11/Dockerfile | 8 -- ci/images/ci-image-jdk15/Dockerfile | 8 -- ci/images/ci-image/Dockerfile | 5 +- ci/images/setup.sh | 17 ++- ci/pipeline.yml | 34 ++--- ci/scripts/check-project.sh | 3 +- ci/tasks/check-project.yml | 2 + gradle/custom-java-home.gradle | 80 ----------- gradle/spring-module.gradle | 1 + gradle/toolchains.gradle | 127 ++++++++++++++++++ settings.gradle | 8 +- spring-beans/spring-beans.gradle | 2 - 15 files changed, 168 insertions(+), 166 deletions(-) delete mode 100644 ci/images/ci-image-jdk11/Dockerfile delete mode 100644 ci/images/ci-image-jdk15/Dockerfile delete mode 100644 gradle/custom-java-home.gradle create mode 100644 gradle/toolchains.gradle diff --git a/build.gradle b/build.gradle index e7671f6d01..b1dfde00bd 100644 --- a/build.gradle +++ b/build.gradle @@ -310,14 +310,13 @@ configure([rootProject] + javaProjects) { project -> apply plugin: "java-test-fixtures" apply plugin: "checkstyle" apply plugin: 'org.springframework.build.compile' - apply from: "${rootDir}/gradle/custom-java-home.gradle" + apply from: "${rootDir}/gradle/toolchains.gradle" apply from: "${rootDir}/gradle/ide.gradle" pluginManager.withPlugin("kotlin") { apply plugin: "org.jetbrains.dokka" compileKotlin { kotlinOptions { - jvmTarget = "1.8" languageVersion = "1.3" apiVersion = "1.3" freeCompilerArgs = ["-Xjsr305=strict"] @@ -326,7 +325,6 @@ configure([rootProject] + javaProjects) { project -> } compileTestKotlin { kotlinOptions { - jvmTarget = "1.8" freeCompilerArgs = ["-Xjsr305=strict"] } } diff --git a/buildSrc/README.md b/buildSrc/README.md index f48339e6d6..ada387cf71 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -8,16 +8,10 @@ They are declared in the `build.gradle` file in this folder. ### Compiler conventions The `org.springframework.build.compile` plugin applies the Java compiler conventions to the build. -By default, the build compiles sources with Java `1.8` source and target compatibility. -You can test a different source compatibility version on the CLI with a project property like: - -``` -./gradlew test -PjavaSourceVersion=11 -``` ## Build Plugins -## Optional dependencies +### Optional dependencies The `org.springframework.build.optional-dependencies` plugin creates a new `optional` Gradle configuration - it adds the dependencies to the project's compile and runtime classpath @@ -25,7 +19,7 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. -## API Diff +### API Diff This plugin uses the [Gradle JApiCmp](https://github.com/melix/japicmp-gradle-plugin) plugin to generate API Diff reports for each Spring Framework module. This plugin is applied once on the root diff --git a/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java index db51666f74..5284df28f1 100644 --- a/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/compile/CompilerConventionsPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -20,31 +20,21 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.gradle.api.JavaVersion; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.compile.JavaCompile; /** * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. - *

One can override the default Java source compatibility version - * with a dedicated property on the CLI: {@code "./gradlew test -PjavaSourceVersion=11"}. * * @author Brian Clozel * @author Sam Brannen */ public class CompilerConventionsPlugin implements Plugin { - /** - * The project property that can be used to switch the Java source - * compatibility version for building source and test classes. - */ - public static final String JAVA_SOURCE_VERSION_PROPERTY = "javaSourceVersion"; - - public static final JavaVersion DEFAULT_COMPILER_VERSION = JavaVersion.VERSION_1_8; - private static final List COMPILER_ARGS; private static final List TEST_COMPILER_ARGS; @@ -69,7 +59,7 @@ public class CompilerConventionsPlugin implements Plugin { @Override public void apply(Project project) { - project.getPlugins().withType(JavaPlugin.class, javaPlugin -> applyJavaCompileConventions(project)); + project.getPlugins().withType(JavaLibraryPlugin.class, javaPlugin -> applyJavaCompileConventions(project)); } /** @@ -79,15 +69,6 @@ public class CompilerConventionsPlugin implements Plugin { */ private void applyJavaCompileConventions(Project project) { JavaPluginConvention java = project.getConvention().getPlugin(JavaPluginConvention.class); - if (project.hasProperty(JAVA_SOURCE_VERSION_PROPERTY)) { - JavaVersion javaSourceVersion = JavaVersion.toVersion(project.property(JAVA_SOURCE_VERSION_PROPERTY)); - java.setSourceCompatibility(javaSourceVersion); - } - else { - java.setSourceCompatibility(DEFAULT_COMPILER_VERSION); - } - java.setTargetCompatibility(DEFAULT_COMPILER_VERSION); - project.getTasks().withType(JavaCompile.class) .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME)) .forEach(compileTask -> { diff --git a/ci/images/ci-image-jdk11/Dockerfile b/ci/images/ci-image-jdk11/Dockerfile deleted file mode 100644 index 6de48e0f1c..0000000000 --- a/ci/images/ci-image-jdk11/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:focal-20210119 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java11 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/ci-image-jdk15/Dockerfile b/ci/images/ci-image-jdk15/Dockerfile deleted file mode 100644 index 71b47fbe07..0000000000 --- a/ci/images/ci-image-jdk15/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:focal-20210119 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java15 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index 2952402431..1ce41f37a9 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -4,5 +4,8 @@ ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh RUN ./setup.sh java8 -ENV JAVA_HOME /opt/openjdk +ENV JAVA_HOME /opt/openjdk/java8 +ENV JDK11 /opt/openjdk/java11 +ENV JDK15 /opt/openjdk/java15 + ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/setup.sh b/ci/images/setup.sh index 87c3fbc2cc..1fca196502 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -19,13 +19,20 @@ curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/ ########################################################### # JAVA ########################################################### -JDK_URL=$( ./get-jdk-url.sh $1 ) mkdir -p /opt/openjdk -cd /opt/openjdk -curl -L ${JDK_URL} | tar zx --strip-components=1 -test -f /opt/openjdk/bin/java -test -f /opt/openjdk/bin/javac +pushd /opt/openjdk > /dev/null +for jdk in java8 java11 java15 +do + JDK_URL=$( /get-jdk-url.sh $jdk ) + mkdir $jdk + pushd $jdk > /dev/null + curl -L ${JDK_URL} | tar zx --strip-components=1 + test -f bin/java + test -f bin/javac + popd > /dev/null +done +popd ########################################################### # GRADLE ENTERPRISE diff --git a/ci/pipeline.yml b/ci/pipeline.yml index e93bed5235..0de0158368 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -85,18 +85,6 @@ resources: source: <<: *docker-resource-source repository: ((docker-hub-organization))/spring-framework-ci -- name: ci-image-jdk11 - type: docker-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci-jdk11 -- name: ci-image-jdk15 - type: docker-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci-jdk15 - name: artifactory-repo type: artifactory-resource icon: package-variant @@ -161,14 +149,6 @@ jobs: params: build: ci-images-git-repo/ci/images dockerfile: ci-images-git-repo/ci/images/ci-image/Dockerfile - - put: ci-image-jdk11 - params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/ci-image-jdk11/Dockerfile - - put: ci-image-jdk15 - params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/ci-image-jdk15/Dockerfile - name: build serial: true public: true @@ -227,7 +207,7 @@ jobs: serial: true public: true plan: - - get: ci-image-jdk11 + - get: ci-image - get: git-repo - get: every-morning trigger: true @@ -235,8 +215,11 @@ jobs: params: { state: "pending", commit: "git-repo" } - do: - task: check-project - image: ci-image-jdk11 + image: ci-image file: git-repo/ci/tasks/check-project.yml + params: + MAIN_TOOLCHAIN: 8 + TEST_TOOLCHAIN: 11 <<: *build-project-task-params on_failure: do: @@ -251,7 +234,7 @@ jobs: serial: true public: true plan: - - get: ci-image-jdk15 + - get: ci-image - get: git-repo - get: every-morning trigger: true @@ -259,8 +242,11 @@ jobs: params: { state: "pending", commit: "git-repo" } - do: - task: check-project - image: ci-image-jdk15 + image: ci-image file: git-repo/ci/tasks/check-project.yml + params: + MAIN_TOOLCHAIN: 8 + TEST_TOOLCHAIN: 15 <<: *build-project-task-params on_failure: do: diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh index 94c4e8df65..f2bf454e35 100755 --- a/ci/scripts/check-project.sh +++ b/ci/scripts/check-project.sh @@ -4,5 +4,6 @@ set -e source $(dirname $0)/common.sh pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Dorg.gradle.java.installations.fromEnv=JDK11,JDK15 \ + -PmainToolchain=$MAIN_TOOLCHAIN -PtestToolchain=$TEST_TOOLCHAIN --no-daemon --max-workers=4 check popd > /dev/null diff --git a/ci/tasks/check-project.yml b/ci/tasks/check-project.yml index ea6d6ddb94..bea1185231 100644 --- a/ci/tasks/check-project.yml +++ b/ci/tasks/check-project.yml @@ -10,6 +10,8 @@ caches: params: BRANCH: CI: true + MAIN_TOOLCHAIN: + TEST_TOOLCHAIN: GRADLE_ENTERPRISE_ACCESS_KEY: GRADLE_ENTERPRISE_CACHE_USERNAME: GRADLE_ENTERPRISE_CACHE_PASSWORD: diff --git a/gradle/custom-java-home.gradle b/gradle/custom-java-home.gradle deleted file mode 100644 index 54d1de1eb8..0000000000 --- a/gradle/custom-java-home.gradle +++ /dev/null @@ -1,80 +0,0 @@ -// ----------------------------------------------------------------------------- -// -// This script adds support for the following two JVM system properties -// that control the build for alternative JDKs (i.e., a JDK other than -// the one used to launch the Gradle process). -// -// - customJavaHome: absolute path to the alternate JDK installation to -// use to compile Java code and execute tests. This system property -// is also used in spring-oxm.gradle to determine whether JiBX is -// supported. -// -// - customJavaSourceVersion: Java version supplied to the `--release` -// command line flag to control the Java source and target -// compatibility version. Supported versions include 9 or higher. -// Do not set this system property if Java 8 should be used. -// -// Examples: -// -// ./gradlew -DcustomJavaHome=/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home test -// -// ./gradlew --no-build-cache -DcustomJavaHome=/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home test -// -// ./gradlew -DcustomJavaHome=/Library/Java/JavaVirtualMachines/jdk-14.jdk/Contents/Home -DcustomJavaSourceVersion=14 test -// -// -// Credits: inspired by work from Marc Philipp and Stephane Nicoll -// -// ----------------------------------------------------------------------------- - -import org.gradle.internal.os.OperatingSystem -// import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile - -def customJavaHome = System.getProperty("customJavaHome") - -if (customJavaHome) { - def customJavaHomeDir = new File(customJavaHome) - def customJavaSourceVersion = System.getProperty("customJavaSourceVersion") - - tasks.withType(JavaCompile) { - logger.info("Java home for " + it.name + " task in " + project.name + ": " + customJavaHomeDir) - options.forkOptions.javaHome = customJavaHomeDir - inputs.property("customJavaHome", customJavaHome) - if (customJavaSourceVersion) { - options.compilerArgs += [ "--release", customJavaSourceVersion] - inputs.property("customJavaSourceVersion", customJavaSourceVersion) - } - } - - tasks.withType(GroovyCompile) { - logger.info("Java home for " + it.name + " task in " + project.name + ": " + customJavaHomeDir) - options.forkOptions.javaHome = customJavaHomeDir - inputs.property("customJavaHome", customJavaHome) - if (customJavaSourceVersion) { - options.compilerArgs += [ "--release", customJavaSourceVersion] - inputs.property("customJavaSourceVersion", customJavaSourceVersion) - } - } - - /* - tasks.withType(KotlinJvmCompile) { - logger.info("Java home for " + it.name + " task in " + project.name + ": " + customJavaHome) - kotlinOptions.jdkHome = customJavaHomeDir - inputs.property("customJavaHome", customJavaHome) - } - */ - - tasks.withType(Test) { - def javaExecutable = customJavaHome + "/bin/java" - if (OperatingSystem.current().isWindows()) { - javaExecutable += ".exe" - } - logger.info("Java executable for " + it.name + " task in " + project.name + ": " + javaExecutable) - executable = javaExecutable - inputs.property("customJavaHome", customJavaHome) - if (customJavaSourceVersion) { - inputs.property("customJavaSourceVersion", customJavaSourceVersion) - } - } - -} diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index 51b18007f8..49efaeae84 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -1,3 +1,4 @@ +apply plugin: 'java-library' apply plugin: 'org.springframework.build.compile' apply plugin: 'org.springframework.build.optional-dependencies' // Uncomment the following for Shadow support in the jmhJar block. diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle new file mode 100644 index 0000000000..2ae10a0581 --- /dev/null +++ b/gradle/toolchains.gradle @@ -0,0 +1,127 @@ +/** + * Apply the JVM Toolchain conventions + * See https://docs.gradle.org/current/userguide/toolchains.html + * + * One can choose the toolchain to use for compiling the MAIN sources and/or compiling + * and running the TEST sources. These options apply to Java, Kotlin and Groovy sources + * when available. + * {@code "./gradlew check -PmainToolchain=8 -PtestToolchain=11"} will use: + *

+ * + * Gradle will automatically detect JDK distributions in well-known locations. + * The following command will list the detected JDKs on the host. + * {@code + * $ ./gradlew -q javaToolchains + * } + * + * We can also configure ENV variables and let Gradle know about them: + * {@code + * $ echo JDK11 + * /opt/openjdk/java11 + * $ echo JDK15 + * /opt/openjdk/java15 + * $ ./gradlew -Dorg.gradle.java.installations.fromEnv=JDK11,JDK15 check + * } + * + * @author Brian Clozel + */ +def mainToolchain = 'mainToolchain' +def testToolchain = 'testToolchain' + +plugins.withType(JavaPlugin) { + // Configure the Java Toolchain if the 'mainToolchain' property is defined + if (project.hasProperty(mainToolchain)) { + def mainLanguageVersion = JavaLanguageVersion.of(project.property(mainToolchain).toString()) + java { + toolchain { + languageVersion = mainLanguageVersion + } + } + } + else { + // Fallback to JDK8 + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + } + } + // Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined + if (project.hasProperty(testToolchain)) { + def testLanguageVersion = JavaLanguageVersion.of(project.property(testToolchain).toString()); + tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = testLanguageVersion + } + } + tasks.withType(Test).configureEach{ + javaLauncher = javaToolchains.launcherFor { + languageVersion = testLanguageVersion + } + } + } +} + +plugins.withType(GroovyPlugin) { + // Fallback to JDK8 + if (!project.hasProperty(mainToolchain)) { + compileGroovy { + sourceCompatibility = JavaVersion.VERSION_1_8 + } + } +} + +// Configure the Kotlin compiler if the 'mainToolchain' property is defined +pluginManager.withPlugin("kotlin") { + if (project.hasProperty(mainToolchain)) { + def mainLanguageVersion = JavaLanguageVersion.of(project.property(mainToolchain).toString()); + def compiler = javaToolchains.compilerFor { + languageVersion = mainLanguageVersion + } + // See https://kotlinlang.org/docs/gradle.html#attributes-specific-for-jvm + def javaVersion = mainLanguageVersion.toString() == '8' ? '1.8' : mainLanguageVersion.toString() + compileKotlin { + kotlinOptions { + jvmTarget = javaVersion + jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath + } + } + // Compile the test classes with the same version, 'testToolchain' will override if defined + compileTestKotlin { + kotlinOptions { + jvmTarget = javaVersion + jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath + } + } + } + else { + compileKotlin { + kotlinOptions { + jvmTarget = '1.8' + } + } + } + + if (project.hasProperty(testToolchain)) { + def testLanguageVersion = JavaLanguageVersion.of(project.property(testToolchain).toString()); + def compiler = javaToolchains.compilerFor { + languageVersion = testLanguageVersion + } + // See https://kotlinlang.org/docs/gradle.html#attributes-specific-for-jvm + def javaVersion = testLanguageVersion.toString() == '8' ? '1.8' : testLanguageVersion.toString() + compileTestKotlin { + kotlinOptions { + jvmTarget = javaVersion + jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath + } + } + } + else { + compileTestKotlin { + kotlinOptions { + jvmTarget = '1.8' + } + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index be0672db50..d852c01eb9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,11 +45,11 @@ rootProject.children.each {project -> settings.gradle.projectsLoaded { gradleEnterprise { buildScan { - if (settings.gradle.rootProject.hasProperty('customJavaHome')) { - value("Custom JAVA_HOME", settings.gradle.rootProject.getProperty('customJavaHome')) + if (settings.gradle.rootProject.hasProperty('mainToolchain')) { + value("Main toolchain", 'JDK' + settings.gradle.rootProject.getProperty('mainToolchain')) } - if (settings.gradle.rootProject.hasProperty('customJavaSourceVersion')) { - value("Custom Java Source Version", settings.gradle.rootProject.getProperty('customJavaSourceVersion')) + if (settings.gradle.rootProject.hasProperty('testToolchain')) { + value("Test toolchain", 'JDK' + settings.gradle.rootProject.getProperty('testToolchain')) } File buildDir = settings.gradle.rootProject.getBuildDir() buildDir.mkdirs() diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle index 73e9942f8e..db894e8b83 100644 --- a/spring-beans/spring-beans.gradle +++ b/spring-beans/spring-beans.gradle @@ -23,8 +23,6 @@ sourceSets { } compileGroovy { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 options.compilerArgs += "-Werror" }