Add `incompatible_language_version_bootclasspath` flag Copybara Import from https://github.com/bazelbuild/rules_java/pull/182 BEGIN_PUBLIC Add `incompatible_language_version_bootclasspath` flag (#182) With `--@rules_java//java:incompatible_language_version_bootclasspath`, the bootclasspath used for Java compilation is now determined based on the numeric version specified in `--java_language_version` and the type specified in `--java_runtime_version` rather than just using the target runtime. For example, with `--java_language_version=8` and `--java_runtime_version=remotejdk_21`, the bootclasspath would be extracted from `remotejdk_8`. For unversioned runtime versions such as `local_jdk`, the behavior doesn't change. If a matching runtime is not available, analysis fails with a customized error message explaining the various options to the user. Work towards https://github.com/bazelbuild/bazel/discussions/21769 Closes #182 END_PUBLIC COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/rules_java/pull/182 from fmeum:java-language-version de275069b6c98919264f2bfce791dde56eaefcde PiperOrigin-RevId: 703396804 Change-Id: Ieb1def97a5eee59763336eb61deaf584f341a1d4
diff --git a/test/analysis/bootclasspath_tests.bzl b/test/analysis/bootclasspath_tests.bzl index c862d83..5eb436a 100644 --- a/test/analysis/bootclasspath_tests.bzl +++ b/test/analysis/bootclasspath_tests.bzl
@@ -2,6 +2,7 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") load("@rules_testing//lib:truth.bzl", "subjects") +load("//java/common:java_common.bzl", "java_common") def _test_utf_8_environment(name): analysis_test( @@ -16,10 +17,61 @@ env_subject.keys().contains("LC_CTYPE") env_subject.get("LC_CTYPE", factory = subjects.str).contains("UTF-8") +def _test_incompatible_language_version_bootclasspath_disabled(name): + analysis_test( + name = name, + impl = _test_incompatible_language_version_bootclasspath_disabled_impl, + target = Label("//toolchains:platformclasspath"), + config_settings = { + "//command_line_option:java_language_version": "11", + "//command_line_option:java_runtime_version": "remotejdk_17", + str(Label("//toolchains:incompatible_language_version_bootclasspath")): False, + }, + ) + +def _test_incompatible_language_version_bootclasspath_disabled_impl(env, target): + system_path = target[java_common.BootClassPathInfo]._system_path + env.expect.that_str(system_path).contains("remotejdk17_") + +def _test_incompatible_language_version_bootclasspath_enabled_versioned(name): + analysis_test( + name = name, + impl = _test_incompatible_language_version_bootclasspath_enabled_versioned_impl, + target = Label("//toolchains:platformclasspath"), + config_settings = { + "//command_line_option:java_language_version": "11", + "//command_line_option:java_runtime_version": "remotejdk_17", + str(Label("//toolchains:incompatible_language_version_bootclasspath")): True, + }, + ) + +def _test_incompatible_language_version_bootclasspath_enabled_versioned_impl(env, target): + system_path = target[java_common.BootClassPathInfo]._system_path + env.expect.that_str(system_path).contains("remotejdk11_") + +def _test_incompatible_language_version_bootclasspath_enabled_unversioned(name): + analysis_test( + name = name, + impl = _test_incompatible_language_version_bootclasspath_enabled_unversioned_impl, + target = Label("//toolchains:platformclasspath"), + config_settings = { + "//command_line_option:java_language_version": "11", + "//command_line_option:java_runtime_version": "local_jdk", + str(Label("//toolchains:incompatible_language_version_bootclasspath")): True, + }, + ) + +def _test_incompatible_language_version_bootclasspath_enabled_unversioned_impl(env, target): + system_path = target[java_common.BootClassPathInfo]._system_path + env.expect.that_str(system_path).contains("local_jdk") + def bootclasspath_tests(name): test_suite( name = name, tests = [ _test_utf_8_environment, + _test_incompatible_language_version_bootclasspath_disabled, + _test_incompatible_language_version_bootclasspath_enabled_versioned, + _test_incompatible_language_version_bootclasspath_enabled_unversioned, ], )
diff --git a/toolchains/BUILD b/toolchains/BUILD index e7f4d84..99c4456 100644 --- a/toolchains/BUILD +++ b/toolchains/BUILD
@@ -1,10 +1,15 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_setting") load("@rules_cc//cc:cc_library.bzl", "cc_library") load( + ":bootclasspath.bzl", + "bootclasspath", + "language_version_bootstrap_runtime", +) +load( ":default_java_toolchain.bzl", "DEFAULT_TOOLCHAIN_CONFIGURATION", "PREBUILT_TOOLCHAIN_CONFIGURATION", - "bootclasspath", "default_java_toolchain", "java_runtime_files", ) @@ -31,6 +36,15 @@ srcs = glob(["*.bzl"]), ) +# If enabled, the bootclasspath for Java compilation will be extracted from a Java runtime matching +# the version specified with `--java_language_version` rather than the runtime specified with +# `--java_runtime_version`. +bool_flag( + name = "incompatible_language_version_bootclasspath", + build_setting_default = False, + visibility = ["//visibility:private"], +) + # A single binary distribution of a JDK (e.g., OpenJDK 17 for Windows arm64) provides three # different types of toolchains from the perspective of Bazel: @@ -283,15 +297,46 @@ }), ) +string_setting( + name = "java_language_version", + build_setting_default = "", + visibility = ["//visibility:private"], +) + +string_setting( + name = "java_runtime_version", + build_setting_default = "", + visibility = ["//visibility:private"], +) + +language_version_bootstrap_runtime( + name = "language_version_bootstrap_runtime", + java_language_version = ":java_language_version", + java_runtime_version = ":java_runtime_version", + visibility = ["//visibility:private"], +) + utf8_environment( name = "utf8_environment", visibility = ["//visibility:private"], ) +config_setting( + name = "incompatible_language_version_bootclasspath_enabled", + flag_values = { + ":incompatible_language_version_bootclasspath": "True", + }, + visibility = ["//visibility:private"], +) + bootclasspath( name = "platformclasspath", src = "DumpPlatformClassPath.java", java_runtime_alias = ":current_java_runtime", + language_version_bootstrap_runtime = select({ + ":incompatible_language_version_bootclasspath_enabled": ":language_version_bootstrap_runtime", + "//conditions:default": None, + }), ) default_java_toolchain(
diff --git a/toolchains/bootclasspath.bzl b/toolchains/bootclasspath.bzl new file mode 100644 index 0000000..b6f57e0 --- /dev/null +++ b/toolchains/bootclasspath.bzl
@@ -0,0 +1,246 @@ +# Copyright 2024 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. + +"""Rules for extracting a platform classpath from Java runtimes.""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("//java/common:java_common.bzl", "java_common") +load(":utf8_environment.bzl", "Utf8EnvironmentInfo") + +visibility("private") + +# TODO: This provider and is only necessary since --java_{language,runtime}_version +# are not available directly to Starlark. +_JavaVersionsInfo = provider( + "Exposes the --java_{language,runtime}_version value as extracted from a transition to a dependant.", + fields = { + "java_language_version": "The value of --java_language_version", + "java_runtime_version": "The value of --java_runtime_version", + }, +) + +def _language_version_bootstrap_runtime(ctx): + providers = [ + _JavaVersionsInfo( + java_language_version = ctx.attr.java_language_version[BuildSettingInfo].value, + java_runtime_version = ctx.attr.java_runtime_version[BuildSettingInfo].value, + ), + ] + + bootstrap_runtime = ctx.toolchains["@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type"] + if bootstrap_runtime: + providers.append(bootstrap_runtime.java_runtime) + + return providers + +language_version_bootstrap_runtime = rule( + implementation = _language_version_bootstrap_runtime, + attrs = { + "java_language_version": attr.label( + providers = [BuildSettingInfo], + ), + "java_runtime_version": attr.label( + providers = [BuildSettingInfo], + ), + }, + toolchains = [ + config_common.toolchain_type("@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type", mandatory = False), + ], +) + +def _get_bootstrap_runtime_version(*, java_language_version, java_runtime_version): + """Returns the runtime version to use for bootstrapping the given language version. + + If the runtime version is not versioned, e.g. "local_jdk", it is used as is. + Otherwise, the language version replaces the numeric part of the runtime version, e.g., + "remotejdk_17" becomes "remotejdk_8". + """ + prefix, separator, version = java_runtime_version.rpartition("_") + if version and version.isdigit(): + new_version = java_language_version + else: + # The runtime version is not versioned, e.g. "local_jdk". Use it as is. + new_version = version + + return prefix + separator + new_version + +def _bootclasspath_transition_impl(settings, _): + java_language_version = settings["//command_line_option:java_language_version"] + java_runtime_version = settings["//command_line_option:java_runtime_version"] + + return { + "//command_line_option:java_runtime_version": _get_bootstrap_runtime_version( + java_language_version = java_language_version, + java_runtime_version = java_runtime_version, + ), + "//toolchains:java_language_version": java_language_version, + "//toolchains:java_runtime_version": java_runtime_version, + } + +_bootclasspath_transition = transition( + implementation = _bootclasspath_transition_impl, + inputs = [ + "//command_line_option:java_language_version", + "//command_line_option:java_runtime_version", + ], + outputs = [ + "//command_line_option:java_runtime_version", + "//toolchains:java_language_version", + "//toolchains:java_runtime_version", + ], +) + +_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type") + +# Opt the Java bootstrap actions into path mapping: +# https://github.com/bazelbuild/bazel/commit/a239ea84832f18ee8706682145e9595e71b39680 +_SUPPORTS_PATH_MAPPING = {"supports-path-mapping": "1"} + +def _java_home(java_executable): + return java_executable.dirname[:-len("/bin")] + +def _bootclasspath_impl(ctx): + exec_javabase = ctx.attr.java_runtime_alias[java_common.JavaRuntimeInfo] + env = ctx.attr._utf8_environment[Utf8EnvironmentInfo].environment + + class_dir = ctx.actions.declare_directory("%s_classes" % ctx.label.name) + + args = ctx.actions.args() + args.add("-source") + args.add("8") + args.add("-target") + args.add("8") + args.add("-Xlint:-options") + args.add("-J-XX:-UsePerfData") + args.add("-d") + args.add_all([class_dir], expand_directories = False) + args.add(ctx.file.src) + + ctx.actions.run( + executable = "%s/bin/javac" % exec_javabase.java_home, + mnemonic = "JavaToolchainCompileClasses", + inputs = [ctx.file.src] + ctx.files.java_runtime_alias, + outputs = [class_dir], + arguments = [args], + env = env, + execution_requirements = _SUPPORTS_PATH_MAPPING, + ) + + bootclasspath = ctx.outputs.output_jar + + args = ctx.actions.args() + args.add("-XX:+IgnoreUnrecognizedVMOptions") + args.add("-XX:-UsePerfData") + args.add("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED") + args.add("--add-exports=jdk.compiler/com.sun.tools.javac.platform=ALL-UNNAMED") + args.add("--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED") + args.add_all("-cp", [class_dir], expand_directories = False) + args.add("DumpPlatformClassPath") + args.add(bootclasspath) + + if ctx.attr.language_version_bootstrap_runtime: + # The attribute is subject to a split transition. + language_version_bootstrap_runtime = ctx.attr.language_version_bootstrap_runtime[0] + if java_common.JavaRuntimeInfo in language_version_bootstrap_runtime: + any_javabase = language_version_bootstrap_runtime[java_common.JavaRuntimeInfo] + else: + java_versions_info = language_version_bootstrap_runtime[_JavaVersionsInfo] + bootstrap_runtime_version = _get_bootstrap_runtime_version( + java_language_version = java_versions_info.java_language_version, + java_runtime_version = java_versions_info.java_runtime_version, + ) + is_exec = "-exec" in ctx.bin_dir.path + tool_prefix = "tool_" if is_exec else "" + fail(""" +No Java runtime found to extract the bootclasspath from for --{tool_prefix}java_language_version={language_version} and --{tool_prefix}java_runtime_version={runtime_version}. +You can: + + * register a Java runtime with name "{bootstrap_runtime_version}" to provide the bootclasspath or + * set --java_language_version to the Java version of an available runtime. + +Rerun with --toolchain_resolution_debug='@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type' to see more details about toolchain resolution. +""".format( + language_version = java_versions_info.java_language_version, + runtime_version = java_versions_info.java_runtime_version, + bootstrap_runtime_version = bootstrap_runtime_version, + tool_prefix = tool_prefix, + )) + else: + any_javabase = ctx.toolchains[_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE].java_runtime + any_javabase_files = any_javabase.files.to_list() + + # If possible, add the Java executable to the command line as a File so that it can be path + # mapped. + java_executable = [f for f in any_javabase_files if f.path == any_javabase.java_executable_exec_path] + if len(java_executable) == 1: + args.add_all(java_executable, map_each = _java_home) + else: + args.add(any_javabase.java_home) + + system_files = ("release", "modules", "jrt-fs.jar") + system = [f for f in any_javabase_files if f.basename in system_files] + if len(system) != len(system_files): + system = None + + inputs = depset([class_dir] + ctx.files.java_runtime_alias, transitive = [any_javabase.files]) + ctx.actions.run( + executable = str(exec_javabase.java_executable_exec_path), + mnemonic = "JavaToolchainCompileBootClasspath", + inputs = inputs, + outputs = [bootclasspath], + arguments = [args], + env = env, + execution_requirements = _SUPPORTS_PATH_MAPPING, + ) + return [ + DefaultInfo(files = depset([bootclasspath])), + java_common.BootClassPathInfo( + bootclasspath = [bootclasspath], + system = system, + ), + OutputGroupInfo(jar = [bootclasspath]), + ] + +_bootclasspath = rule( + implementation = _bootclasspath_impl, + attrs = { + "java_runtime_alias": attr.label( + cfg = "exec", + providers = [java_common.JavaRuntimeInfo], + ), + "language_version_bootstrap_runtime": attr.label( + cfg = _bootclasspath_transition, + ), + "output_jar": attr.output(mandatory = True), + "src": attr.label( + cfg = "exec", + allow_single_file = True, + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + "_utf8_environment": attr.label( + default = ":utf8_environment", + cfg = "exec", + ), + }, + toolchains = [_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE], +) + +def bootclasspath(name, **kwargs): + _bootclasspath( + name = name, + output_jar = name + ".jar", + **kwargs + )
diff --git a/toolchains/default_java_toolchain.bzl b/toolchains/default_java_toolchain.bzl index 5aaa06b..4ec8961 100644 --- a/toolchains/default_java_toolchain.bzl +++ b/toolchains/default_java_toolchain.bzl
@@ -14,9 +14,8 @@ """Rules for defining default_java_toolchain""" -load("//java/common:java_common.bzl", "java_common") load("//java/toolchains:java_toolchain.bzl", "java_toolchain") -load(":utf8_environment.bzl", "Utf8EnvironmentInfo") +load(":bootclasspath.bzl", _bootclasspath = "bootclasspath") # JVM options, without patching java.compiler and jdk.compiler modules. BASE_JDK9_JVM_OPTS = [ @@ -213,112 +212,4 @@ tags = ["manual"], ) -_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type") - -# Opt the Java bootstrap actions into path mapping: -# https://github.com/bazelbuild/bazel/commit/a239ea84832f18ee8706682145e9595e71b39680 -_SUPPORTS_PATH_MAPPING = {"supports-path-mapping": "1"} - -def _java_home(java_executable): - return java_executable.dirname[:-len("/bin")] - -def _bootclasspath_impl(ctx): - exec_javabase = ctx.attr.java_runtime_alias[java_common.JavaRuntimeInfo] - env = ctx.attr._utf8_environment[Utf8EnvironmentInfo].environment - - class_dir = ctx.actions.declare_directory("%s_classes" % ctx.label.name) - - args = ctx.actions.args() - args.add("-source") - args.add("8") - args.add("-target") - args.add("8") - args.add("-Xlint:-options") - args.add("-J-XX:-UsePerfData") - args.add("-d") - args.add_all([class_dir], expand_directories = False) - args.add(ctx.file.src) - - ctx.actions.run( - executable = "%s/bin/javac" % exec_javabase.java_home, - mnemonic = "JavaToolchainCompileClasses", - inputs = [ctx.file.src] + ctx.files.java_runtime_alias, - outputs = [class_dir], - arguments = [args], - env = env, - execution_requirements = _SUPPORTS_PATH_MAPPING, - ) - - bootclasspath = ctx.outputs.output_jar - - args = ctx.actions.args() - args.add("-XX:+IgnoreUnrecognizedVMOptions") - args.add("-XX:-UsePerfData") - args.add("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED") - args.add("--add-exports=jdk.compiler/com.sun.tools.javac.platform=ALL-UNNAMED") - args.add("--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED") - args.add_all("-cp", [class_dir], expand_directories = False) - args.add("DumpPlatformClassPath") - args.add(bootclasspath) - - any_javabase = ctx.toolchains[_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE].java_runtime - any_javabase_files = any_javabase.files.to_list() - - # If possible, add the Java executable to the command line as a File so that it can be path - # mapped. - java_executable = [f for f in any_javabase_files if f.path == any_javabase.java_executable_exec_path] - if len(java_executable) == 1: - args.add_all(java_executable, map_each = _java_home) - else: - args.add(any_javabase.java_home) - - system_files = ("release", "modules", "jrt-fs.jar") - system = [f for f in any_javabase_files if f.basename in system_files] - if len(system) != len(system_files): - system = None - - inputs = depset([class_dir] + ctx.files.java_runtime_alias, transitive = [any_javabase.files]) - ctx.actions.run( - executable = str(exec_javabase.java_executable_exec_path), - mnemonic = "JavaToolchainCompileBootClasspath", - inputs = inputs, - outputs = [bootclasspath], - arguments = [args], - env = env, - execution_requirements = _SUPPORTS_PATH_MAPPING, - ) - return [ - DefaultInfo(files = depset([bootclasspath])), - java_common.BootClassPathInfo( - bootclasspath = [bootclasspath], - system = system, - ), - OutputGroupInfo(jar = [bootclasspath]), - ] - -_bootclasspath = rule( - implementation = _bootclasspath_impl, - attrs = { - "java_runtime_alias": attr.label( - cfg = "exec", - providers = [java_common.JavaRuntimeInfo], - ), - "output_jar": attr.output(mandatory = True), - "src": attr.label( - cfg = "exec", - allow_single_file = True, - ), - "_utf8_environment": attr.label( - default = ":utf8_environment", - cfg = "exec", - ), - }, - toolchains = [_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE], -) - -def bootclasspath(name, **kwargs): - _bootclasspath( - name = name, - output_jar = name + ".jar", - **kwargs - ) +bootclasspath = _bootclasspath