Skip to content

Commit

Permalink
Add incompatible_language_version_bootclasspath flag
Browse files Browse the repository at this point in the history
Copybara Import from #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 bazelbuild/bazel#21769

Closes #182
END_PUBLIC

COPYBARA_INTEGRATE_REVIEW=#182 from fmeum:java-language-version de27506
PiperOrigin-RevId: 703396804
Change-Id: Ieb1def97a5eee59763336eb61deaf584f341a1d4
  • Loading branch information
fmeum authored and rules_java Copybara committed Dec 6, 2024
1 parent 4cb426e commit 449303e
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 112 deletions.
52 changes: 52 additions & 0 deletions test/analysis/bootclasspath_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -16,10 +17,61 @@ def _test_utf_8_environment_impl(env, target):
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,
],
)
47 changes: 46 additions & 1 deletion toolchains/BUILD
Original file line number Diff line number Diff line change
@@ -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",
)
Expand All @@ -31,6 +36,15 @@ filegroup(
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:

Expand Down Expand Up @@ -283,15 +297,46 @@ alias(
}),
)

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(
Expand Down
246 changes: 246 additions & 0 deletions toolchains/bootclasspath.bzl
Original file line number Diff line number Diff line change
@@ -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
)
Loading

0 comments on commit 449303e

Please sign in to comment.