Emit javac -source/-target from kotlinc jvm_target (#1648)
For mixed-sources targets ensure java sources are compiled for the same jvm_target as the kotlin sources.
Both java and kotlin compiler invocations will produce bytecode of the same version.
Co-authored-by: Eugene Zhuravlev <eugene.zhuravlev@jetbrains.com>
diff --git a/kotlin/internal/jvm/compile.bzl b/kotlin/internal/jvm/compile.bzl
index 9691e49..9d2a32e 100644
--- a/kotlin/internal/jvm/compile.bzl
+++ b/kotlin/internal/jvm/compile.bzl
@@ -1031,6 +1031,13 @@
# annotation processors in `deps` also.
if len(srcs.kt) > 0:
javac_opts.append("-proc:none")
+
+ # Compile the Java half for the same effective jvm_target, the kotlin part is compiled for.
+ kotlinc_options = ctx.attr.kotlinc_opts[KotlincOptions] if ctx.attr.kotlinc_opts else toolchains.kt.kotlinc_options
+ jvm_target = kotlinc_options.jvm_target if (kotlinc_options and kotlinc_options.jvm_target) else toolchains.kt.jvm_target
+ if jvm_target:
+ javac_opts.extend(_utils.javac_jvm_target_flags(jvm_target))
+
java_info = java_common.compile(
ctx,
source_files = srcs.java,
diff --git a/kotlin/internal/utils/utils.bzl b/kotlin/internal/utils/utils.bzl
index eecf791..8d7bd20 100644
--- a/kotlin/internal/utils/utils.bzl
+++ b/kotlin/internal/utils/utils.bzl
@@ -47,8 +47,19 @@
return args
+def _javac_jvm_target_flags(jvm_target):
+ """Derive javac `-source`/`-target` flags from a kotlinc `jvm_target`.
+ """
+ stripped = jvm_target.strip()
+ dot = stripped.rfind(".")
+ target_version = stripped if dot < 0 else stripped[dot + 1:]
+ if not target_version.isdigit():
+ fail("kotlinc jvm_target '{}' is not a valid JVM target version (expected a number like '8', '11', '17', or the legacy '1.8' form)".format(jvm_target))
+ return ["-source", target_version, "-target", target_version]
+
utils = struct(
add_dicts = dicts.add,
init_args = _init_builder_args,
derive_module_name = _derive_module_name,
+ javac_jvm_target_flags = _javac_jvm_target_flags,
)
diff --git a/src/test/data/jvm/jvm_target/BUILD b/src/test/data/jvm/jvm_target/BUILD
new file mode 100644
index 0000000..264efb4
--- /dev/null
+++ b/src/test/data/jvm/jvm_target/BUILD
@@ -0,0 +1,26 @@
+load("//kotlin:core.bzl", "kt_kotlinc_options")
+load("//kotlin:jvm.bzl", "kt_jvm_library")
+load(":java_language_version.bzl", "java_language_version_env")
+
+package(default_visibility = ["//visibility:public"])
+
+kt_kotlinc_options(
+ name = "jvm8_opts",
+ jvm_target = "1.8",
+)
+
+kt_jvm_library(
+ name = "mixed_jvm8",
+ srcs = [
+ "Mixed.kt",
+ "MixedJava.java",
+ ],
+ kotlinc_opts = ":jvm8_opts",
+)
+
+# Pin java_language_version so the test is independent of the repository's global Java config.
+java_language_version_env(
+ name = "mixed_jvm8_java11",
+ java_language_version = "11",
+ target = ":mixed_jvm8",
+)
diff --git a/src/test/data/jvm/jvm_target/Mixed.kt b/src/test/data/jvm/jvm_target/Mixed.kt
new file mode 100644
index 0000000..53cc07c
--- /dev/null
+++ b/src/test/data/jvm/jvm_target/Mixed.kt
@@ -0,0 +1,5 @@
+package mixed
+
+class Mixed {
+ fun greet(): String = MixedJava.greet()
+}
diff --git a/src/test/data/jvm/jvm_target/MixedJava.java b/src/test/data/jvm/jvm_target/MixedJava.java
new file mode 100644
index 0000000..4241739
--- /dev/null
+++ b/src/test/data/jvm/jvm_target/MixedJava.java
@@ -0,0 +1,7 @@
+package mixed;
+
+public final class MixedJava {
+ public static String greet() {
+ return "hi";
+ }
+}
diff --git a/src/test/data/jvm/jvm_target/java_language_version.bzl b/src/test/data/jvm/jvm_target/java_language_version.bzl
new file mode 100644
index 0000000..8bfc2e0
--- /dev/null
+++ b/src/test/data/jvm/jvm_target/java_language_version.bzl
@@ -0,0 +1,31 @@
+"""Build a target with `//command_line_option:java_language_version` pinned to a fixed value."""
+
+def _pin_java_language_version_impl(_settings, attr):
+ return {"//command_line_option:java_language_version": attr.java_language_version}
+
+_pin_java_language_version = transition(
+ implementation = _pin_java_language_version_impl,
+ inputs = [],
+ outputs = ["//command_line_option:java_language_version"],
+)
+
+def _java_language_version_env_impl(ctx):
+ dep = ctx.attr.target
+ if type(dep) == "list":
+ dep = dep[0]
+ return [DefaultInfo(
+ files = dep[DefaultInfo].files,
+ runfiles = dep[DefaultInfo].default_runfiles,
+ )]
+
+java_language_version_env = rule(
+ doc = "Forwards `target`, built with --java_language_version pinned to `java_language_version`.",
+ implementation = _java_language_version_env_impl,
+ attrs = {
+ "java_language_version": attr.string(mandatory = True),
+ "target": attr.label(mandatory = True, cfg = _pin_java_language_version),
+ "_allowlist_function_transition": attr.label(
+ default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+ ),
+ },
+)
diff --git a/src/test/kotlin/io/bazel/kotlin/BUILD b/src/test/kotlin/io/bazel/kotlin/BUILD
index 5ea88e0..e49429e 100644
--- a/src/test/kotlin/io/bazel/kotlin/BUILD
+++ b/src/test/kotlin/io/bazel/kotlin/BUILD
@@ -72,9 +72,16 @@
data = ["//src/test/data/jvm/ksp"],
)
+kt_rules_e2e_test(
+ name = "JavacJvmTargetAssertionTest",
+ srcs = ["JavacJvmTargetAssertionTest.kt"],
+ data = ["//src/test/data/jvm/jvm_target:mixed_jvm8_java11"],
+)
+
test_suite(
name = "assertion_tests",
tests = [
+ "JavacJvmTargetAssertionTest",
"KotlinJvm13Test",
"KotlinJvmAssociatesBasicVisibilityTest",
"KotlinJvmBasicAssertionTest",
diff --git a/src/test/kotlin/io/bazel/kotlin/JavacJvmTargetAssertionTest.kt b/src/test/kotlin/io/bazel/kotlin/JavacJvmTargetAssertionTest.kt
new file mode 100644
index 0000000..1e831f6
--- /dev/null
+++ b/src/test/kotlin/io/bazel/kotlin/JavacJvmTargetAssertionTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 The Bazel Authors. 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 io.bazel.kotlin
+
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class JavacJvmTargetAssertionTest : KotlinAssertionTestCase("src/test/data/jvm/jvm_target") {
+ @Test
+ fun javaHalfIsCompiledForTheKotlincJvmTarget() {
+ jarTestCase(
+ "mixed_jvm8.jar",
+ description = "In a mixed Kotlin/Java target the Java sources must compile to the same " +
+ "bytecode version as the kotlinc jvm_target",
+ ) {
+ // The Java half must follow the kotlinc jvm_target (1.8 -> major 52), not the pinned default.
+ assertEquals(
+ 52,
+ classFileMajorVersion("mixed/MixedJava.class"),
+ "Java half was not compiled for jvm_target=1.8 (it followed java_language_version instead)",
+ )
+ // Both halves must target the same platform.
+ assertEquals(
+ classFileMajorVersion("mixed/Mixed.class"),
+ classFileMajorVersion("mixed/MixedJava.class"),
+ "Kotlin and Java halves produced different bytecode versions",
+ )
+ }
+ }
+}
diff --git a/src/test/kotlin/io/bazel/kotlin/KotlinAssertionTestCase.kt b/src/test/kotlin/io/bazel/kotlin/KotlinAssertionTestCase.kt
index 0715682..a4d7bd9 100644
--- a/src/test/kotlin/io/bazel/kotlin/KotlinAssertionTestCase.kt
+++ b/src/test/kotlin/io/bazel/kotlin/KotlinAssertionTestCase.kt
@@ -87,6 +87,16 @@
}
}
+ /** Class-file major version (JVMS 4.1) of a `.class` entry. */
+ protected fun JarFile.classFileMajorVersion(entry: String): Int {
+ val classEntry = checkNotNull(getJarEntry(entry)) { "no entry $entry in jar $name" }
+ java.io.DataInputStream(getInputStream(classEntry)).use { input ->
+ input.readInt() // 0xCAFEBABE magic
+ input.readUnsignedShort() // minor version
+ return input.readUnsignedShort() // major version
+ }
+ }
+
/**
* Validated the entry is compressed and has the DOS epoch for it's timestamp.
*/
diff --git a/src/test/starlark/internal/jvm/BUILD.bazel b/src/test/starlark/internal/jvm/BUILD.bazel
index e618755..a522231 100644
--- a/src/test/starlark/internal/jvm/BUILD.bazel
+++ b/src/test/starlark/internal/jvm/BUILD.bazel
@@ -1,9 +1,12 @@
load(":export_only_module_name_test.bzl", "export_only_module_name_test_suite")
+load(":javac_jvm_target_test.bzl", "javac_jvm_target_test_suite")
load(":jvm_deps_tests.bzl", "jvm_deps_test_suite")
load(":kt_jvm_binary_env_test.bzl", "kt_jvm_binary_env_test_suite")
export_only_module_name_test_suite(name = "export_only_module_name_tests")
+javac_jvm_target_test_suite(name = "javac_jvm_target_tests")
+
jvm_deps_test_suite(name = "jvm_tests")
kt_jvm_binary_env_test_suite(name = "kt_jvm_binary_env_tests")
diff --git a/src/test/starlark/internal/jvm/javac_jvm_target_test.bzl b/src/test/starlark/internal/jvm/javac_jvm_target_test.bzl
new file mode 100644
index 0000000..c8d5499
--- /dev/null
+++ b/src/test/starlark/internal/jvm/javac_jvm_target_test.bzl
@@ -0,0 +1,26 @@
+"""Unit tests for `utils.javac_jvm_target_flags` (kotlinc jvm_target -> javac -source/-target flags)."""
+
+load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
+load("//kotlin/internal/utils:utils.bzl", "utils")
+
+def _normalization_test_impl(ctx):
+ env = unittest.begin(ctx)
+
+ # '1.8'-style targets normalize to '8'; '1.6' -> '6'.
+ asserts.equals(env, ["-source", "8", "-target", "8"], utils.javac_jvm_target_flags("1.8"))
+ asserts.equals(env, ["-source", "6", "-target", "6"], utils.javac_jvm_target_flags("1.6"))
+
+ # Already-bare numeric targets pass through unchanged.
+ asserts.equals(env, ["-source", "8", "-target", "8"], utils.javac_jvm_target_flags("8"))
+ asserts.equals(env, ["-source", "11", "-target", "11"], utils.javac_jvm_target_flags("11"))
+ asserts.equals(env, ["-source", "17", "-target", "17"], utils.javac_jvm_target_flags("17"))
+
+ return unittest.end(env)
+
+normalization_test = unittest.make(_normalization_test_impl)
+
+def javac_jvm_target_test_suite(name):
+ unittest.suite(
+ name,
+ normalization_test,
+ )