Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradle 8.0. silently dropped support for custom compilers in JavaCompile #23990

Closed
TheMrMilchmann opened this issue Feb 21, 2023 · 8 comments · Fixed by #24014
Closed

Gradle 8.0. silently dropped support for custom compilers in JavaCompile #23990

TheMrMilchmann opened this issue Feb 21, 2023 · 8 comments · Fixed by #24014
Assignees
Labels
a:regression This used to work in:java-plugins java-library, java, java-base, java-platform, java-test-fixtures
Milestone

Comments

@TheMrMilchmann
Copy link
Contributor

TheMrMilchmann commented Feb 21, 2023

In some cases, it may be desirable to use a custom Java compiler (e.g., ECJ) instead of javac (ref). Since Gradle does not currently have first-class support for this, I accomplished this by configuring the JavaCompile tasks manually. Specifically, in Gradle 7.x (including 7.6), first I set the javaCompiler to null. Second, I configure the forkOptions.executable to point at a java (not javac!) executable. Finally, the ECJ jar and its entrypoint are injected into the commandline arguments.

This works fine as can be seen when inspecting the debug output of a project that was configured accordingly. Importantly, there is no warning about this being unsupported or subject for removal.

Expected Behavior

The configuration should work fine on Gradle 8. , just like it did on 7.6.

Current Behavior

On Gradle 8, however, this is broken. First, setting the javaCompiler to null straight up does not work anymore. An ISE is thrown because Gradle attempts to access the value unconditionally. Additionally, Gradle introduced a check that validates that the forkOptions.executable and the javaCompiler properties of JavaCompile tasks point to the same file. Thus, even without setting the javaCompiler to null, the build fails.

As it stands right now, I'm also questioning the purpose of forkOptions.executable as the value is forced match the value of the javaCompiler property. However, it is not deprecated which leads me to believe that something is incorrect here.

Steps to Reproduce

An MCVE is available at https://github.com/TheMrMilchmann/MCVE/tree/gradle/18632-ecj-7.6-repro

Running gradlew compileJava on this project (using Gradle 7.6) works. The debug output can be inspected to validate the compiler invocation. When using Gradle 8. , the behavior described above can be observed.

@eskatos eskatos added this to the 8.0.2 (unconfirmed) milestone Feb 22, 2023
@eskatos eskatos added in:java-plugins java-library, java, java-base, java-platform, java-test-fixtures has:reproducer and removed to-triage labels Feb 22, 2023
@eskatos
Copy link
Member

eskatos commented Feb 22, 2023

Thank you for providing a valid reproducer.

The issue is in the backlog of the relevant team and is prioritized by them.

@alllex
Copy link
Member

alllex commented Feb 24, 2023

Hey! We are looking into fixing this here. In the meantime, here are some comments on the issue.

First, setting the javaCompiler to null straight up does not work anymore.

This is intentional, because javaCompiler is not an optional property anymore. However, setting it to null should not be required, as the compiler property gets inferred from the forkOptions.javaHome or forkOptions.executable if those are configured. The fork options paths take priority even if the project level java { toolchain { ... } } is configured.

the purpose of forkOptions.executable as the value is forced match the value of the javaCompiler property. However, it is not deprecated which leads me to believe that something is incorrect here.

For the majority of cases, if a build configures javaCompiler on the compilation task directly, there is no reason to configure the executables as well. Therefore, having both is most likely an error, as one will be ignored. If a build does end up having both configured, they should either agree on the value or the conflict should be resolved by the user. Normally that would be removing the usage of forkOptions and relying directly on toolchains.

The reason it is not deprecated is that most builds that do use the executable (without migrating to toolchains) simply point it to a javac in the existing JDK installation, which is the main use case and is supported.

@Vampire
Copy link
Contributor

Vampire commented Feb 24, 2023

First, setting the javaCompiler to null straight up does not work anymore.

This is intentional, because javaCompiler is not an optional property anymore.

But shouldn't setting to null then have been deprecated before?
As far as I understood it gave no deprecation warning in 7.6, yet breaks with 8.0 which is uncommon for Gradle.

@TheMrMilchmann
Copy link
Contributor Author

After putting some more thought into it and running into a somewhat related issue, I agree with @Vampire here. I've filed #24055 to track this separately.

@alllex
Copy link
Member

alllex commented Mar 1, 2023

@TheMrMilchmann we merged the fix for this into the Gradle 8.0.x line and published a snapshot.
Could you please verify that it works for you now?

You can use the wrapper with 8.0.2-20230301020537 0000 version. Here is a command to update the wrapper:

gradle wrapper --gradle-version=8.0.2-20230301020537 0000

@TheMrMilchmann
Copy link
Contributor Author

I can confirm that this does seem to work mostly. However, I have one remaining issue/question: In an attempt to prevent potential ordering issues, I have deferred the retrieval of the tool and, thus, the configuration of forkOptions.executable to the execution phase (via doFirst). This looks roughly as follows:

val javaLauncher = provider {
    if (java.toolchain.languageVersion.orNull?.canCompileOrRun(REQUIRED_JAVA_VERSION) == true) {
        javaToolchains.launcherFor(java.toolchain).orNull ?: error("Could not get launcher for toolchain: ${java.toolchain}")
    } else {
        javaToolchains.launcherFor {
            languageVersion.set(JavaLanguageVersion.of(PREFERRED_JAVA_VERSION))
        }.orNull ?: error("Could not provision launcher for Java $PREFERRED_JAVA_VERSION")
    }
}

inputs.property("javaLauncher", javaLauncher.map { it.metadata.languageVersion.asInt() })

/* See https://docs.gradle.org/7.4.2/userguide/validation_problems.html#implementation_unknown */
@Suppress("ObjectLiteralToLambda")
doFirst(object : Action<Task> {
    override fun execute(t: Task) {
        options.forkOptions.executable = javaLauncher.get().executablePath.asFile.absolutePath
    }
})

This, too, works fine on Gradle 7.6.1 but fails on 8 (including 8.0.2-20230301020537 0000) in some cases. For example, if a project toolchain is available but incompatible). Since this is a somewhat hacky approach I'm wondering if it should be supported. However, I tend to believe it should until forkOptions.executable becomes or is replaced by a property.

I've updated the MCVE in TheMrMilchmann/MCVE@e195bf3 to reproduce this.

TheMrMilchmann added a commit to TheMrMilchmann/gradle-ecj that referenced this issue Mar 2, 2023
This is incomplete and does not fully work yet. We might have to get rid of the lazy configuration of `forkOptions.executable`. See gradle/gradle#23990 (comment)

See #9
@wolfs
Copy link
Member

wolfs commented Mar 2, 2023

How about you set the toolchain you want to have on the JavaCompile task directly instead of adding it in a "sneaky" way via a new input property?

The following setup allows doing that. The only downside it has is that it selects the executable by path manipulation. Though I think it is much cleaner. The input tracking for the Java version works out of the box.

val currentJavaToolchain = provider {
    java.toolchain
}

val selectedCompiler = currentJavaToolchain.map { toolchain ->
    val currentCompiler = project.javaToolchains.compilerFor(toolchain).get()
    if (currentCompiler.metadata.languageVersion.canCompileOrRun(REQUIRED_JAVA_VERSION)) {
        currentCompiler
    } else {
        project.javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(PREFERRED_JAVA_VERSION)) }.get()
    }
}

withType<JavaCompile>().configureEach {
    javaCompiler.set(selectedCompiler)

    /* ECJ does not support generating JNI headers. Make sure the property is not used. */
    options.headerOutputDirectory.set(provider { null })

    options.isFork = true
    options.forkOptions.jvmArgumentProviders.add(CommandLineArgumentProvider {
        mutableListOf("-cp", ecj.asPath, "org.eclipse.jdt.internal.compiler.batch.Main")
    })
    @Suppress("ObjectLiteralToLambda")
    doFirst(object : Action<Task> {
        override fun execute(t: Task) {
            val javacExecutable = javaCompiler.get().executablePath.asFile
            val javaExecutableName = javacExecutable.name.replace("javac", "java")
            val javaExecutablePath = javacExecutable.parentFile.resolve(javaExecutableName).absolutePath
            options.forkOptions.executable = javaExecutablePath
        }
    })

}

@TheMrMilchmann
Copy link
Contributor Author

TheMrMilchmann commented Mar 2, 2023

Thanks for the input, @wolfs! I've considered doing something similar before but it didn't cross my mind that manipulating the path like that is possible now. Given that this is possible, I think we can consider this issue to be fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a:regression This used to work in:java-plugins java-library, java, java-base, java-platform, java-test-fixtures
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants