Skip to content

Commit 449303e

Browse files
fmeumrules_java Copybara
authored andcommitted
Add incompatible_language_version_bootclasspath flag
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
1 parent 4cb426e commit 449303e

4 files changed

Lines changed: 346 additions & 112 deletions

File tree

test/analysis/bootclasspath_tests.bzl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
44
load("@rules_testing//lib:truth.bzl", "subjects")
5+
load("//java/common:java_common.bzl", "java_common")
56

67
def _test_utf_8_environment(name):
78
analysis_test(
@@ -16,10 +17,61 @@ def _test_utf_8_environment_impl(env, target):
1617
env_subject.keys().contains("LC_CTYPE")
1718
env_subject.get("LC_CTYPE", factory = subjects.str).contains("UTF-8")
1819

20+
def _test_incompatible_language_version_bootclasspath_disabled(name):
21+
analysis_test(
22+
name = name,
23+
impl = _test_incompatible_language_version_bootclasspath_disabled_impl,
24+
target = Label("//toolchains:platformclasspath"),
25+
config_settings = {
26+
"//command_line_option:java_language_version": "11",
27+
"//command_line_option:java_runtime_version": "remotejdk_17",
28+
str(Label("//toolchains:incompatible_language_version_bootclasspath")): False,
29+
},
30+
)
31+
32+
def _test_incompatible_language_version_bootclasspath_disabled_impl(env, target):
33+
system_path = target[java_common.BootClassPathInfo]._system_path
34+
env.expect.that_str(system_path).contains("remotejdk17_")
35+
36+
def _test_incompatible_language_version_bootclasspath_enabled_versioned(name):
37+
analysis_test(
38+
name = name,
39+
impl = _test_incompatible_language_version_bootclasspath_enabled_versioned_impl,
40+
target = Label("//toolchains:platformclasspath"),
41+
config_settings = {
42+
"//command_line_option:java_language_version": "11",
43+
"//command_line_option:java_runtime_version": "remotejdk_17",
44+
str(Label("//toolchains:incompatible_language_version_bootclasspath")): True,
45+
},
46+
)
47+
48+
def _test_incompatible_language_version_bootclasspath_enabled_versioned_impl(env, target):
49+
system_path = target[java_common.BootClassPathInfo]._system_path
50+
env.expect.that_str(system_path).contains("remotejdk11_")
51+
52+
def _test_incompatible_language_version_bootclasspath_enabled_unversioned(name):
53+
analysis_test(
54+
name = name,
55+
impl = _test_incompatible_language_version_bootclasspath_enabled_unversioned_impl,
56+
target = Label("//toolchains:platformclasspath"),
57+
config_settings = {
58+
"//command_line_option:java_language_version": "11",
59+
"//command_line_option:java_runtime_version": "local_jdk",
60+
str(Label("//toolchains:incompatible_language_version_bootclasspath")): True,
61+
},
62+
)
63+
64+
def _test_incompatible_language_version_bootclasspath_enabled_unversioned_impl(env, target):
65+
system_path = target[java_common.BootClassPathInfo]._system_path
66+
env.expect.that_str(system_path).contains("local_jdk")
67+
1968
def bootclasspath_tests(name):
2069
test_suite(
2170
name = name,
2271
tests = [
2372
_test_utf_8_environment,
73+
_test_incompatible_language_version_bootclasspath_disabled,
74+
_test_incompatible_language_version_bootclasspath_enabled_versioned,
75+
_test_incompatible_language_version_bootclasspath_enabled_unversioned,
2476
],
2577
)

toolchains/BUILD

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
2+
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_setting")
23
load("@rules_cc//cc:cc_library.bzl", "cc_library")
4+
load(
5+
":bootclasspath.bzl",
6+
"bootclasspath",
7+
"language_version_bootstrap_runtime",
8+
)
39
load(
410
":default_java_toolchain.bzl",
511
"DEFAULT_TOOLCHAIN_CONFIGURATION",
612
"PREBUILT_TOOLCHAIN_CONFIGURATION",
7-
"bootclasspath",
813
"default_java_toolchain",
914
"java_runtime_files",
1015
)
@@ -31,6 +36,15 @@ filegroup(
3136
srcs = glob(["*.bzl"]),
3237
)
3338

39+
# If enabled, the bootclasspath for Java compilation will be extracted from a Java runtime matching
40+
# the version specified with `--java_language_version` rather than the runtime specified with
41+
# `--java_runtime_version`.
42+
bool_flag(
43+
name = "incompatible_language_version_bootclasspath",
44+
build_setting_default = False,
45+
visibility = ["//visibility:private"],
46+
)
47+
3448
# A single binary distribution of a JDK (e.g., OpenJDK 17 for Windows arm64) provides three
3549
# different types of toolchains from the perspective of Bazel:
3650

@@ -283,15 +297,46 @@ alias(
283297
}),
284298
)
285299

300+
string_setting(
301+
name = "java_language_version",
302+
build_setting_default = "",
303+
visibility = ["//visibility:private"],
304+
)
305+
306+
string_setting(
307+
name = "java_runtime_version",
308+
build_setting_default = "",
309+
visibility = ["//visibility:private"],
310+
)
311+
312+
language_version_bootstrap_runtime(
313+
name = "language_version_bootstrap_runtime",
314+
java_language_version = ":java_language_version",
315+
java_runtime_version = ":java_runtime_version",
316+
visibility = ["//visibility:private"],
317+
)
318+
286319
utf8_environment(
287320
name = "utf8_environment",
288321
visibility = ["//visibility:private"],
289322
)
290323

324+
config_setting(
325+
name = "incompatible_language_version_bootclasspath_enabled",
326+
flag_values = {
327+
":incompatible_language_version_bootclasspath": "True",
328+
},
329+
visibility = ["//visibility:private"],
330+
)
331+
291332
bootclasspath(
292333
name = "platformclasspath",
293334
src = "DumpPlatformClassPath.java",
294335
java_runtime_alias = ":current_java_runtime",
336+
language_version_bootstrap_runtime = select({
337+
":incompatible_language_version_bootclasspath_enabled": ":language_version_bootstrap_runtime",
338+
"//conditions:default": None,
339+
}),
295340
)
296341

297342
default_java_toolchain(

toolchains/bootclasspath.bzl

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Rules for extracting a platform classpath from Java runtimes."""
16+
17+
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
18+
load("//java/common:java_common.bzl", "java_common")
19+
load(":utf8_environment.bzl", "Utf8EnvironmentInfo")
20+
21+
visibility("private")
22+
23+
# TODO: This provider and is only necessary since --java_{language,runtime}_version
24+
# are not available directly to Starlark.
25+
_JavaVersionsInfo = provider(
26+
"Exposes the --java_{language,runtime}_version value as extracted from a transition to a dependant.",
27+
fields = {
28+
"java_language_version": "The value of --java_language_version",
29+
"java_runtime_version": "The value of --java_runtime_version",
30+
},
31+
)
32+
33+
def _language_version_bootstrap_runtime(ctx):
34+
providers = [
35+
_JavaVersionsInfo(
36+
java_language_version = ctx.attr.java_language_version[BuildSettingInfo].value,
37+
java_runtime_version = ctx.attr.java_runtime_version[BuildSettingInfo].value,
38+
),
39+
]
40+
41+
bootstrap_runtime = ctx.toolchains["@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type"]
42+
if bootstrap_runtime:
43+
providers.append(bootstrap_runtime.java_runtime)
44+
45+
return providers
46+
47+
language_version_bootstrap_runtime = rule(
48+
implementation = _language_version_bootstrap_runtime,
49+
attrs = {
50+
"java_language_version": attr.label(
51+
providers = [BuildSettingInfo],
52+
),
53+
"java_runtime_version": attr.label(
54+
providers = [BuildSettingInfo],
55+
),
56+
},
57+
toolchains = [
58+
config_common.toolchain_type("@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type", mandatory = False),
59+
],
60+
)
61+
62+
def _get_bootstrap_runtime_version(*, java_language_version, java_runtime_version):
63+
"""Returns the runtime version to use for bootstrapping the given language version.
64+
65+
If the runtime version is not versioned, e.g. "local_jdk", it is used as is.
66+
Otherwise, the language version replaces the numeric part of the runtime version, e.g.,
67+
"remotejdk_17" becomes "remotejdk_8".
68+
"""
69+
prefix, separator, version = java_runtime_version.rpartition("_")
70+
if version and version.isdigit():
71+
new_version = java_language_version
72+
else:
73+
# The runtime version is not versioned, e.g. "local_jdk". Use it as is.
74+
new_version = version
75+
76+
return prefix + separator + new_version
77+
78+
def _bootclasspath_transition_impl(settings, _):
79+
java_language_version = settings["//command_line_option:java_language_version"]
80+
java_runtime_version = settings["//command_line_option:java_runtime_version"]
81+
82+
return {
83+
"//command_line_option:java_runtime_version": _get_bootstrap_runtime_version(
84+
java_language_version = java_language_version,
85+
java_runtime_version = java_runtime_version,
86+
),
87+
"//toolchains:java_language_version": java_language_version,
88+
"//toolchains:java_runtime_version": java_runtime_version,
89+
}
90+
91+
_bootclasspath_transition = transition(
92+
implementation = _bootclasspath_transition_impl,
93+
inputs = [
94+
"//command_line_option:java_language_version",
95+
"//command_line_option:java_runtime_version",
96+
],
97+
outputs = [
98+
"//command_line_option:java_runtime_version",
99+
"//toolchains:java_language_version",
100+
"//toolchains:java_runtime_version",
101+
],
102+
)
103+
104+
_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type")
105+
106+
# Opt the Java bootstrap actions into path mapping:
107+
# https://github.com/bazelbuild/bazel/commit/a239ea84832f18ee8706682145e9595e71b39680
108+
_SUPPORTS_PATH_MAPPING = {"supports-path-mapping": "1"}
109+
110+
def _java_home(java_executable):
111+
return java_executable.dirname[:-len("/bin")]
112+
113+
def _bootclasspath_impl(ctx):
114+
exec_javabase = ctx.attr.java_runtime_alias[java_common.JavaRuntimeInfo]
115+
env = ctx.attr._utf8_environment[Utf8EnvironmentInfo].environment
116+
117+
class_dir = ctx.actions.declare_directory("%s_classes" % ctx.label.name)
118+
119+
args = ctx.actions.args()
120+
args.add("-source")
121+
args.add("8")
122+
args.add("-target")
123+
args.add("8")
124+
args.add("-Xlint:-options")
125+
args.add("-J-XX:-UsePerfData")
126+
args.add("-d")
127+
args.add_all([class_dir], expand_directories = False)
128+
args.add(ctx.file.src)
129+
130+
ctx.actions.run(
131+
executable = "%s/bin/javac" % exec_javabase.java_home,
132+
mnemonic = "JavaToolchainCompileClasses",
133+
inputs = [ctx.file.src] + ctx.files.java_runtime_alias,
134+
outputs = [class_dir],
135+
arguments = [args],
136+
env = env,
137+
execution_requirements = _SUPPORTS_PATH_MAPPING,
138+
)
139+
140+
bootclasspath = ctx.outputs.output_jar
141+
142+
args = ctx.actions.args()
143+
args.add("-XX:+IgnoreUnrecognizedVMOptions")
144+
args.add("-XX:-UsePerfData")
145+
args.add("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")
146+
args.add("--add-exports=jdk.compiler/com.sun.tools.javac.platform=ALL-UNNAMED")
147+
args.add("--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED")
148+
args.add_all("-cp", [class_dir], expand_directories = False)
149+
args.add("DumpPlatformClassPath")
150+
args.add(bootclasspath)
151+
152+
if ctx.attr.language_version_bootstrap_runtime:
153+
# The attribute is subject to a split transition.
154+
language_version_bootstrap_runtime = ctx.attr.language_version_bootstrap_runtime[0]
155+
if java_common.JavaRuntimeInfo in language_version_bootstrap_runtime:
156+
any_javabase = language_version_bootstrap_runtime[java_common.JavaRuntimeInfo]
157+
else:
158+
java_versions_info = language_version_bootstrap_runtime[_JavaVersionsInfo]
159+
bootstrap_runtime_version = _get_bootstrap_runtime_version(
160+
java_language_version = java_versions_info.java_language_version,
161+
java_runtime_version = java_versions_info.java_runtime_version,
162+
)
163+
is_exec = "-exec" in ctx.bin_dir.path
164+
tool_prefix = "tool_" if is_exec else ""
165+
fail("""
166+
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}.
167+
You can:
168+
169+
* register a Java runtime with name "{bootstrap_runtime_version}" to provide the bootclasspath or
170+
* set --java_language_version to the Java version of an available runtime.
171+
172+
Rerun with --toolchain_resolution_debug='@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type' to see more details about toolchain resolution.
173+
""".format(
174+
language_version = java_versions_info.java_language_version,
175+
runtime_version = java_versions_info.java_runtime_version,
176+
bootstrap_runtime_version = bootstrap_runtime_version,
177+
tool_prefix = tool_prefix,
178+
))
179+
else:
180+
any_javabase = ctx.toolchains[_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE].java_runtime
181+
any_javabase_files = any_javabase.files.to_list()
182+
183+
# If possible, add the Java executable to the command line as a File so that it can be path
184+
# mapped.
185+
java_executable = [f for f in any_javabase_files if f.path == any_javabase.java_executable_exec_path]
186+
if len(java_executable) == 1:
187+
args.add_all(java_executable, map_each = _java_home)
188+
else:
189+
args.add(any_javabase.java_home)
190+
191+
system_files = ("release", "modules", "jrt-fs.jar")
192+
system = [f for f in any_javabase_files if f.basename in system_files]
193+
if len(system) != len(system_files):
194+
system = None
195+
196+
inputs = depset([class_dir] + ctx.files.java_runtime_alias, transitive = [any_javabase.files])
197+
ctx.actions.run(
198+
executable = str(exec_javabase.java_executable_exec_path),
199+
mnemonic = "JavaToolchainCompileBootClasspath",
200+
inputs = inputs,
201+
outputs = [bootclasspath],
202+
arguments = [args],
203+
env = env,
204+
execution_requirements = _SUPPORTS_PATH_MAPPING,
205+
)
206+
return [
207+
DefaultInfo(files = depset([bootclasspath])),
208+
java_common.BootClassPathInfo(
209+
bootclasspath = [bootclasspath],
210+
system = system,
211+
),
212+
OutputGroupInfo(jar = [bootclasspath]),
213+
]
214+
215+
_bootclasspath = rule(
216+
implementation = _bootclasspath_impl,
217+
attrs = {
218+
"java_runtime_alias": attr.label(
219+
cfg = "exec",
220+
providers = [java_common.JavaRuntimeInfo],
221+
),
222+
"language_version_bootstrap_runtime": attr.label(
223+
cfg = _bootclasspath_transition,
224+
),
225+
"output_jar": attr.output(mandatory = True),
226+
"src": attr.label(
227+
cfg = "exec",
228+
allow_single_file = True,
229+
),
230+
"_allowlist_function_transition": attr.label(
231+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
232+
),
233+
"_utf8_environment": attr.label(
234+
default = ":utf8_environment",
235+
cfg = "exec",
236+
),
237+
},
238+
toolchains = [_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE],
239+
)
240+
241+
def bootclasspath(name, **kwargs):
242+
_bootclasspath(
243+
name = name,
244+
output_jar = name + ".jar",
245+
**kwargs
246+
)

0 commit comments

Comments
 (0)