From 77e3c35c03b72eef0cdf525ed34908aa1e16b62d Mon Sep 17 00:00:00 2001 From: Alex Semin Date: Tue, 28 Feb 2023 12:08:01 +0100 Subject: [PATCH] Support non-javac toolchain tools to be used as a Java compiler (Gradle 8.0.2) --- ...JavaCompileToolchainIntegrationTest.groovy | 86 +++++++++++++++++++ .../AbstractJavaCompileSpecFactory.java | 34 ++++---- .../gradle/api/tasks/compile/JavaCompile.java | 15 ++-- 3 files changed, 112 insertions(+), 23 deletions(-) diff --git a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/JavaCompileToolchainIntegrationTest.groovy b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/JavaCompileToolchainIntegrationTest.groovy index 35ed63dbb288..9d1ab6196686 100644 --- a/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/JavaCompileToolchainIntegrationTest.groovy +++ b/subprojects/language-java/src/integTest/groovy/org/gradle/api/tasks/compile/JavaCompileToolchainIntegrationTest.groovy @@ -469,6 +469,92 @@ class JavaCompileToolchainIntegrationTest extends AbstractIntegrationSpec implem JavaVersion.current() | "[deprecation] foo() in Foo has been deprecated" } + @Issue("https://github.com/gradle/gradle/issues/23990") + def "can compile with a custom compiler executable"() { + def otherJdk = AvailableJavaHomes.getJdk(JavaVersion.current()) + def jdk = AvailableJavaHomes.getDifferentVersion { + def v = it.languageVersion.majorVersion.toInteger() + 11 <= v && v <= 18 // Java versions supported by ECJ releases used in the test + } + + buildFile << """ + plugins { + id("java") + } + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(${otherJdk.javaVersion.majorVersion}) + } + } + + configurations { + ecj { + canBeConsumed = false + canBeResolved = true + } + } + + ${mavenCentralRepository()} + + dependencies { + def changed = providers.gradleProperty("changed").isPresent() + ecj(!changed ? "org.eclipse.jdt:ecj:3.31.0" : "org.eclipse.jdt:ecj:3.32.0") + } + + // Make sure the provider is up-to-date only if the ECJ classpath does not change + class EcjClasspathProvider implements CommandLineArgumentProvider { + @Classpath + final FileCollection ecjClasspath + + EcjClasspathProvider(FileCollection ecjClasspath) { + this.ecjClasspath = ecjClasspath + } + + @Override + List asArguments() { + return ["-cp", ecjClasspath.asPath, "org.eclipse.jdt.internal.compiler.batch.Main"] + } + } + + compileJava { + def customJavaLauncher = javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(${jdk.javaVersion.majorVersion})) + }.get() + + // ECJ does not support generating JNI headers + options.headerOutputDirectory.set(provider { null }) + options.fork = true + options.forkOptions.executable = customJavaLauncher.executablePath.asFile.absolutePath + options.forkOptions.jvmArgumentProviders.add(new EcjClasspathProvider(configurations.ecj)) + } + """ + + when: + withInstallations(jdk, otherJdk).run(":compileJava", "--info") + then: + executedAndNotSkipped(":compileJava") + outputContains("Compiling with toolchain '${jdk.javaHome.absolutePath}'") + outputContains("Compiling with Java command line compiler '${jdk.javaExecutable.absolutePath}'") + classJavaVersion(javaClassFile("Foo.class")) == jdk.javaVersion + + // Test up-to-date checks + when: + withInstallations(jdk, otherJdk).run(":compileJava") + then: + skipped(":compileJava") + + when: + withInstallations(jdk, otherJdk).run(":compileJava", "-Pchanged") + then: + executedAndNotSkipped(":compileJava") + + when: + withInstallations(jdk, otherJdk).run(":compileJava", "-Pchanged") + then: + skipped(":compileJava") + } + private TestFile configureForkOptionsExecutable(Jvm jdk) { buildFile << """ compileJava { diff --git a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/AbstractJavaCompileSpecFactory.java b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/AbstractJavaCompileSpecFactory.java index dc33f66efd4f..90a8a1b0afeb 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/AbstractJavaCompileSpecFactory.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/internal/tasks/compile/AbstractJavaCompileSpecFactory.java @@ -41,17 +41,7 @@ public T create() { } if (compileOptions.isFork()) { - File customJavaHome = compileOptions.getForkOptions().getJavaHome(); - if (customJavaHome != null) { - return getCommandLineSpec(Jvm.forHome(customJavaHome).getJavacExecutable()); - } - - String customExecutable = compileOptions.getForkOptions().getExecutable(); - if (customExecutable != null) { - return getCommandLineSpec(new File(customExecutable)); - } - - return getForkingSpec(Jvm.current().getJavaHome()); + return chooseSpecFromCompileOptions(Jvm.current().getJavaHome()); } return getDefaultSpec(); @@ -64,13 +54,7 @@ private T chooseSpecForToolchain() { } if (compileOptions.isFork()) { - // Presence of the fork options means that the user has explicitly requested a command-line compiler - if (compileOptions.getForkOptions().getJavaHome() != null || compileOptions.getForkOptions().getExecutable() != null) { - // We use the toolchain path because the fork options must agree with the selected toolchain - return getCommandLineSpec(Jvm.forHome(toolchainJavaHome).getJavacExecutable()); - } - - return getForkingSpec(toolchainJavaHome); + return chooseSpecFromCompileOptions(toolchainJavaHome); } if (!toolchain.isCurrentJvm()) { @@ -80,6 +64,20 @@ private T chooseSpecForToolchain() { return getDefaultSpec(); } + private T chooseSpecFromCompileOptions(File fallbackJavaHome) { + File forkJavaHome = compileOptions.getForkOptions().getJavaHome(); + if (forkJavaHome != null) { + return getCommandLineSpec(Jvm.forHome(forkJavaHome).getJavacExecutable()); + } + + String forkExecutable = compileOptions.getForkOptions().getExecutable(); + if (forkExecutable != null) { + return getCommandLineSpec(new File(forkExecutable)); + } + + return getForkingSpec(fallbackJavaHome); + } + abstract protected T getCommandLineSpec(File executable); abstract protected T getForkingSpec(File javaHome); diff --git a/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java b/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java index 8e1da6dc6a84..a99765698c28 100644 --- a/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java +++ b/subprojects/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java @@ -259,7 +259,6 @@ private void validateForkOptionsMatchToolchain() { JavaCompiler javaCompilerTool = getJavaCompiler().get(); File toolchainJavaHome = javaCompilerTool.getMetadata().getInstallationPath().getAsFile(); - File toolchainExecutable = javaCompilerTool.getExecutablePath().getAsFile(); ForkOptions forkOptions = getOptions().getForkOptions(); File customJavaHome = forkOptions.getJavaHome(); @@ -269,10 +268,16 @@ private void validateForkOptionsMatchToolchain() { ); String customExecutablePath = forkOptions.getExecutable(); - checkState( - customExecutablePath == null || new File(customExecutablePath).equals(toolchainExecutable), - "Toolchain from `executable` property on `ForkOptions` does not match toolchain from `javaCompiler` property" - ); + // We do not match the custom executable against the compiler executable from the toolchain (javac), + // because the custom executable can be set to the path of another tool in the toolchain such as a launcher (java). + if (customExecutablePath != null) { + // Relying on the layout of the toolchain distribution: /bin/ + File customExecutableJavaHome = new File(customExecutablePath).getParentFile().getParentFile(); + checkState( + customExecutableJavaHome.equals(toolchainJavaHome), + "Toolchain from `executable` property on `ForkOptions` does not match toolchain from `javaCompiler` property" + ); + } } private boolean isToolchainCompatibleWithJava8() {