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 plugin does not respect all possible ways to configure java source target compatibility and produces variants with a wrong jvm version #1772

Open
AlexanderBartash opened this issue Sep 24, 2024 · 6 comments
Labels

Comments

@AlexanderBartash
Copy link
Contributor

AlexanderBartash commented Sep 24, 2024

What happened?

Currently no other ways of configuring source compatibility is supported except:

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

while Gradle recommends to use instead e.g.

> When Java code is compiled using a specific toolchain, the actual compilation is carried out by a compiler of the specified Java version. The compiler provides access to the language features and JDK APIs for the requested Java language version.
> In the simplest case, the toolchain can be configured for a project using the java extension. This way, not only compilation benefits from it, but also other tasks such as test and javadoc will also consistently use the same toolchain.

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

or

> Setting the release flag ensures the specified language level is used regardless of which compiler actually performs the compilation. To use this feature, the compiler must support the requested release version. It is possible to specify an earlier release version while compiling with a more recent toolchain.

tasks.compileJava {
    options.release = 7
}

or:

    withType<JavaCompile> {
        sourceCompatibility = "1.7"
        targetCompatibility = "1.7"
    }

which is not good because the "java {" way does not do this:

The release flag provides guarantees similar to toolchains. It validates that the Java sources are not using language features introduced in later Java versions, and also that the code does not access APIs from more recent JDKs. The bytecode produced by the compiler also corresponds to the requested Java version, meaning that the compiled code cannot be executed on older JVMs.

Relevant documentation https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation

This may be useful: https://github.com/gradle/gradle/blob/master/platforms/jvm/plugins-java-base/src/main/java/org/gradle/api/plugins/jvm/internal/DefaultJvmLanguageUtilities.java#L82

This came out of #1713
See https://github.com/AlexanderBartash/intellij-platform-plugin-template/blob/issue-1713/common/build.gradle.kts#L50

Try running "./gradlew :common:outgoingVariants" with and without this "java {" block

//java {
//    sourceCompatibility = JavaVersion.VERSION_1_7
//    targetCompatibility = JavaVersion.VERSION_1_7
//}

The difference:
image

--------------------------------------------------
Variant intellijPlatformComposedJar
--------------------------------------------------
Capabilities
    - org.jetbrains.plugins.template:common:unspecified (default capability)
Attributes
    - org.gradle.category                = library
    - org.gradle.dependency.bundling     = external
    - org.gradle.jvm.environment         = standard-jvm
    - org.gradle.jvm.version             = 7 <<<<<<<<<<<<<<<<<<< with this configuration
    - org.gradle.libraryelements         = composed-jar
    - org.gradle.usage                   = java-runtime
    - org.jetbrains.kotlin.platform.type = jvm
Artifacts
    - build/libs/common.jar (artifactType = jar)
--------------------------------------------------
Variant intellijPlatformComposedJar
--------------------------------------------------
Capabilities
    - org.jetbrains.plugins.template:common:unspecified (default capability)
Attributes
    - org.gradle.category                = library
    - org.gradle.dependency.bundling     = external
    - org.gradle.jvm.environment         = standard-jvm
    - org.gradle.jvm.version             = 17 <<<<<<<<<<<<<<<<<<< without this configuration
    - org.gradle.libraryelements         = composed-jar
    - org.gradle.usage                   = java-runtime
    - org.jetbrains.kotlin.platform.type = jvm
Artifacts
    - build/libs/common.jar (artifactType = jar)

Relevant log output or stack trace

No response

Steps to reproduce

See https://github.com/AlexanderBartash/intellij-platform-plugin-template/blob/issue-1713/common/build.gradle.kts#L50

Gradle IntelliJ Plugin version

2.0.1

Gradle version

8.10.2

Operating System

Linux

Link to build, i.e. failing GitHub Action job

No response

@AlexanderBartash
Copy link
Contributor Author

AlexanderBartash commented Sep 24, 2024

I did a couple of tests to see how different configurations behave.

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.JETBRAINS
    }
}

//tasks.compileJava {
//    options.release = 7
//}

//tasks {
//    withType<JavaCompile> {
//        sourceCompatibility = "1.7"
//        targetCompatibility = "1.7"
//    }
//}

//java {
//    sourceCompatibility = JavaVersion.VERSION_1_7
//    targetCompatibility = JavaVersion.VERSION_1_7
//}

println("java.sourceCompatibility = " + java.sourceCompatibility)
println("java.targetCompatibility = " + java.targetCompatibility)
tasks.withType<JavaCompile> {
    println("withType<JavaCompile> sourceCompatibility = $sourceCompatibility")
    println("withType<JavaCompile> targetCompatibility = $targetCompatibility")
}
tasks.compileJava {
    println("tasks.compileJava options.release = " + options.release.get())
}

Output:

java.sourceCompatibility = 11
java.targetCompatibility = 11
withType<JavaCompile> sourceCompatibility = 11
withType<JavaCompile> targetCompatibility = 11
withType<JavaCompile> sourceCompatibility = 11
withType<JavaCompile> targetCompatibility = 11
Cannot query the value of task ':common:compileJava' property 'options.release' because it has no value available.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.JETBRAINS
    }
}

tasks.compileJava {
    options.release = 7
}

//tasks {
//    withType<JavaCompile> {
//        sourceCompatibility = "1.7"
//        targetCompatibility = "1.7"
//    }
//}

//java {
//    sourceCompatibility = JavaVersion.VERSION_1_7
//    targetCompatibility = JavaVersion.VERSION_1_7
//}

println("java.sourceCompatibility = " + java.sourceCompatibility)
println("java.targetCompatibility = " + java.targetCompatibility)
tasks.withType<JavaCompile> {
    println("withType<JavaCompile> sourceCompatibility = $sourceCompatibility")
    println("withType<JavaCompile> targetCompatibility = $targetCompatibility")
}
tasks.compileJava {
    println("tasks.compileJava options.release = " + options.release.get())
}

Output:

java.sourceCompatibility = 11
java.targetCompatibility = 11
withType<JavaCompile> sourceCompatibility = 1.7
withType<JavaCompile> targetCompatibility = 1.7
withType<JavaCompile> sourceCompatibility = 11
withType<JavaCompile> targetCompatibility = 11
tasks.compileJava options.release = 7
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.JETBRAINS
    }
}

//tasks.compileJava {
//    options.release = 7
//}

tasks {
    withType<JavaCompile> {
        sourceCompatibility = "1.7"
        targetCompatibility = "1.7"
    }
}

//java {
//    sourceCompatibility = JavaVersion.VERSION_1_7
//    targetCompatibility = JavaVersion.VERSION_1_7
//}

println("java.sourceCompatibility = " + java.sourceCompatibility)
println("java.targetCompatibility = " + java.targetCompatibility)
tasks.withType<JavaCompile> {
    println("withType<JavaCompile> sourceCompatibility = $sourceCompatibility")
    println("withType<JavaCompile> targetCompatibility = $targetCompatibility")
}
tasks.compileJava {
    println("tasks.compileJava options.release = " + options.release.get())
}

Output:

java.sourceCompatibility = 11
java.targetCompatibility = 11
withType<JavaCompile> sourceCompatibility = 1.7
withType<JavaCompile> targetCompatibility = 1.7
withType<JavaCompile> sourceCompatibility = 1.7
withType<JavaCompile> targetCompatibility = 1.7
Cannot query the value of task ':common:compileJava' property 'options.release' because it has no value available.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.JETBRAINS
    }
}

//tasks.compileJava {
//    options.release = 7
//}

//tasks {
//    withType<JavaCompile> {
//        sourceCompatibility = "1.7"
//        targetCompatibility = "1.7"
//    }
//}

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

println("java.sourceCompatibility = " + java.sourceCompatibility)
println("java.targetCompatibility = " + java.targetCompatibility)
tasks.withType<JavaCompile> {
    println("withType<JavaCompile> sourceCompatibility = $sourceCompatibility")
    println("withType<JavaCompile> targetCompatibility = $targetCompatibility")
}
tasks.compileJava {
    println("tasks.compileJava options.release = " + options.release.get())
}

Output:

java.sourceCompatibility = 1.7
java.targetCompatibility = 1.7
withType<JavaCompile> sourceCompatibility = 1.7
withType<JavaCompile> targetCompatibility = 1.7
withType<JavaCompile> sourceCompatibility = 1.7
withType<JavaCompile> targetCompatibility = 1.7
Cannot query the value of task ':common:compileJava' property 'options.release' because it has no value available.

@AlexanderBartash
Copy link
Contributor Author

AlexanderBartash commented Sep 24, 2024

According to the above

java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

Changes probably all possible configurations except only this thing remains uninitialized:

tasks.compileJava {
    options.release = 
}

But the problem with this way is that:

The sourceCompatibility and targetCompatibility options correspond to the Java compiler options -source and -target. They are considered a legacy mechanism for targeting a specific Java version. However, these options do not protect against the use of APIs introduced in later Java versions.
https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation

@AlexanderBartash
Copy link
Contributor Author

AlexanderBartash commented Sep 24, 2024

I see the next ways to fix this:

  1. Check all possible ways to configure the thing.
  2. Implemented an IDE inspection to make it impossible not to know about.
  3. Simply mentioned in the docs here https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin.html

@AlexanderBartash
Copy link
Contributor Author

AlexanderBartash commented Sep 24, 2024

This issue is probably relevant only when compiling for ancient JDKs like for 1.7. It may be necessary if the plugin does some customizations to the JPS or running JUnit tests (in that case a class from the plugin may be run directly on JDK from user's project). Because in all other ways I we could simply use this approach to configure JDK & source level:

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
        vendor = JvmVendorSpec.JETBRAINS
    }
}

But the above approach does not seem to be available for JDK less than 11 unless we also change the vendor from JvmVendorSpec.JETBRAINS to whatever else provides old JDKs.

@AlexanderBartash
Copy link
Contributor Author

AlexanderBartash commented Sep 24, 2024

Yep, just tested

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(7)
    }
}

creates a correct variant as well

--------------------------------------------------
Variant intellijPlatformComposedJar
--------------------------------------------------

Capabilities
    - org.jetbrains.plugins.template:common:unspecified (default capability)
Attributes
    - org.gradle.category                = library
    - org.gradle.dependency.bundling     = external
    - org.gradle.jvm.environment         = standard-jvm
    - org.gradle.jvm.version             = 7
    - org.gradle.libraryelements         = composed-jar
    - org.gradle.usage                   = java-runtime
    - org.jetbrains.kotlin.platform.type = jvm
Artifacts
    - build/libs/common.jar (artifactType = jar)

But it works only by a coincidence because this way the source level matches the JDK and we also are not using JBR anymore (because there are not old JBRs), which is not critical but not ideal either.

@AlexanderBartash
Copy link
Contributor Author

AlexanderBartash commented Sep 24, 2024

So the recommendation should be:

  1. Use toolchain of the same version as your desired source level.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(7)
        //vendor = JvmVendorSpec.JETBRAINS
    }
}
  1. If the above not possible, add two duplicating configurations:
// This one will configure source level in all possible places in Gradle (tested above)
// However, these options do not protect against the use of APIs introduced in later Java versions.
// Also this is required to fix https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1772 because Intellij Gradle plugin does not look at any other configuration
java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

// And another config to fix the lack of API control in the above config:
// The release flag provides guarantees similar to toolchains.
// It validates that the Java sources are not using language features introduced in later Java versions, 
// and also that the code does not access APIs from more recent JDKs.
// The bytecode produced by the compiler also corresponds to the requested Java version, 
// meaning that the compiled code cannot be executed on older JVMs.
tasks.compileJava {
    options.release = 7
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant