xref: /aosp_15_r20/external/bazelbuild-rules_java/toolchains/local_java_repository.bzl (revision abe8e1b943c923005d847f1e3cf6637de4ed1a1f)
1# Copyright 2021 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 importing a local JDK."""
16
17load("//java:defs.bzl", "java_runtime")
18load(":default_java_toolchain.bzl", "default_java_toolchain")
19
20def _detect_java_version(repository_ctx, java_bin):
21    properties_out = repository_ctx.execute([java_bin, "-XshowSettings:properties"]).stderr
22    # This returns an indented list of properties separated with newlines:
23    # "  java.vendor.url.bug = ... \n"
24    # "  java.version = 11.0.8\n"
25    # "  java.version.date = 2020-11-05\"
26
27    strip_properties = [property.strip() for property in properties_out.splitlines()]
28    version_property = [property for property in strip_properties if property.startswith("java.version = ")]
29    if len(version_property) != 1:
30        return None
31
32    version_value = version_property[0][len("java.version = "):]
33    parts = version_value.split(".")
34    major = parts[0]
35    if len(parts) == 1:
36        return major
37    elif major == "1":  # handles versions below 1.8
38        minor = parts[1]
39        return minor
40    return major
41
42def local_java_runtime(name, java_home, version, runtime_name = None, visibility = ["//visibility:public"], exec_compatible_with = [], target_compatible_with = []):
43    """Defines a java_runtime target together with Java runtime and compile toolchain definitions.
44
45    Java runtime toolchain is constrained by flag --java_runtime_version having
46    value set to either name or version argument.
47
48    Java compile toolchains are created for --java_language_version flags values
49    between 8 and version (inclusive). Java compile toolchains use the same
50    (local) JDK for compilation. This requires a different configuration for JDK8
51    than the newer versions.
52
53    Args:
54      name: name of the target.
55      java_home: Path to the JDK.
56      version: Version of the JDK.
57      runtime_name: name of java_runtime target if it already exists.
58      visibility: Visibility that will be applied to the java runtime target
59      exec_compatible_with: A list of constraint values that must be
60                            satisfied by the exec platform for the Java compile
61                            toolchain to be selected. They must be satisfied by
62                            the target platform for the Java runtime toolchain
63                            to be selected.
64      target_compatible_with: A list of constraint values that must be
65                              satisfied by the target platform for the Java
66                              compile toolchain to be selected.
67    """
68
69    if runtime_name == None:
70        runtime_name = name
71        java_runtime(
72            name = runtime_name,
73            java_home = java_home,
74            visibility = visibility,
75            version = int(version) if version.isdigit() else 0,
76        )
77
78    native.config_setting(
79        name = name + "_name_setting",
80        values = {"java_runtime_version": name},
81        visibility = ["//visibility:private"],
82    )
83    native.config_setting(
84        name = name + "_version_setting",
85        values = {"java_runtime_version": version},
86        visibility = ["//visibility:private"],
87    )
88    native.config_setting(
89        name = name + "_name_version_setting",
90        values = {"java_runtime_version": name + "_" + version},
91        visibility = ["//visibility:private"],
92    )
93    native.alias(
94        name = name + "_settings_alias",
95        actual = select({
96            name + "_name_setting": name + "_name_setting",
97            name + "_version_setting": name + "_version_setting",
98            "//conditions:default": name + "_name_version_setting",
99        }),
100        visibility = ["//visibility:private"],
101    )
102    native.toolchain(
103        name = "runtime_toolchain_definition",
104        # A JDK can be used as a runtime *for* the platforms it can be used to compile *on*.
105        target_compatible_with = exec_compatible_with,
106        target_settings = [":%s_settings_alias" % name],
107        toolchain_type = Label("@bazel_tools//tools/jdk:runtime_toolchain_type"),
108        toolchain = runtime_name,
109    )
110    native.toolchain(
111        name = "bootstrap_runtime_toolchain_definition",
112        target_settings = [":%s_settings_alias" % name],
113        toolchain_type = Label("@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type"),
114        toolchain = runtime_name,
115    )
116
117    if type(version) == type("") and version.isdigit() and int(version) > 8:
118        for version in range(8, int(version) + 1):
119            default_java_toolchain(
120                name = name + "_toolchain_java" + str(version),
121                source_version = str(version),
122                target_version = str(version),
123                java_runtime = runtime_name,
124                exec_compatible_with = exec_compatible_with,
125                target_compatible_with = target_compatible_with,
126            )
127
128    # else version is not recognized and no compilation toolchains are predefined
129
130def _is_macos(repository_ctx):
131    return repository_ctx.os.name.lower().find("mac os x") != -1
132
133def _is_windows(repository_ctx):
134    return repository_ctx.os.name.lower().find("windows") != -1
135
136def _with_os_extension(repository_ctx, binary):
137    return binary + (".exe" if _is_windows(repository_ctx) else "")
138
139def _determine_java_home(repository_ctx):
140    """Determine the java home path.
141
142    If the `java_home` attribute is specified, then use the given path,
143    otherwise, try to detect the java home path on the system.
144
145    Args:
146      repository_ctx: repository context
147    """
148    java_home = repository_ctx.attr.java_home
149    if java_home:
150        java_home_path = repository_ctx.path(java_home)
151        if not java_home_path.exists:
152            fail('The path indicated by the "java_home" attribute "%s" (absolute: "%s") does not exist.' % (java_home, str(java_home_path)))
153        return java_home_path
154    if "JAVA_HOME" in repository_ctx.os.environ:
155        return repository_ctx.path(repository_ctx.os.environ["JAVA_HOME"])
156
157    if _is_macos(repository_ctx):
158        # Replicate GetSystemJavabase() in src/main/cpp/blaze_util_darwin.cc
159        result = repository_ctx.execute(["/usr/libexec/java_home", "-v", "1.11+"])
160        if result.return_code == 0:
161            return repository_ctx.path(result.stdout.strip())
162    else:
163        # Calculate java home by locating the javac binary
164        # javac should exists at ${JAVA_HOME}/bin/javac
165        # Replicate GetSystemJavabase() in src/main/cpp/blaze_util_linux.cc
166        # This logic should also work on Windows.
167        javac_path = repository_ctx.which(_with_os_extension(repository_ctx, "javac"))
168        if javac_path:
169            return javac_path.realpath.dirname.dirname
170    return repository_ctx.path("./nosystemjdk")
171
172def _local_java_repository_impl(repository_ctx):
173    """Repository rule local_java_repository implementation.
174
175    Args:
176      repository_ctx: repository context
177    """
178
179    java_home = _determine_java_home(repository_ctx)
180
181    # When Bzlmod is enabled, the Java runtime name should be the last segment of the repository name.
182    local_java_runtime_name = repository_ctx.name.split("~")[-1]
183
184    repository_ctx.file(
185        "WORKSPACE",
186        "# DO NOT EDIT: automatically generated WORKSPACE file for local_java_repository\n" +
187        "workspace(name = \"{name}\")\n".format(name = local_java_runtime_name),
188    )
189
190    java_bin = java_home.get_child("bin").get_child(_with_os_extension(repository_ctx, "java"))
191
192    if not java_bin.exists:
193        # Java binary does not exist
194        _create_auto_config_error_build_file(
195            repository_ctx,
196            local_java_runtime_name = local_java_runtime_name,
197            java_home = java_home,
198            message = "Cannot find Java binary {java_binary} in {java_home}; " +
199                      "either correct your JAVA_HOME, PATH or specify Java from " +
200                      "remote repository (e.g. --java_runtime_version=remotejdk_11)",
201        )
202        return
203
204    # Detect version
205    version = repository_ctx.attr.version if repository_ctx.attr.version != "" else _detect_java_version(repository_ctx, java_bin)
206    if version == None:
207        # Java version could not be detected
208        _create_auto_config_error_build_file(
209            repository_ctx,
210            local_java_runtime_name = local_java_runtime_name,
211            java_home = java_home,
212            message = "Cannot detect Java version of {java_binary} in {java_home}; " +
213                      "make sure it points to a valid Java executable",
214        )
215        return
216
217    # Prepare BUILD file using "local_java_runtime" macro
218    if repository_ctx.attr.build_file_content and repository_ctx.attr.build_file:
219        fail("build_file and build_file_content are exclusive")
220    if repository_ctx.attr.build_file_content:
221        build_file = repository_ctx.attr.build_file_content
222    elif repository_ctx.attr.build_file:
223        build_file = repository_ctx.read(repository_ctx.path(repository_ctx.attr.build_file))
224    else:
225        build_file = ""
226    build_file = build_file.format(RUNTIME_VERSION = version if version.isdigit() else "0")
227
228    runtime_name = '"jdk"' if build_file else None
229    local_java_runtime_macro = """
230local_java_runtime(
231    name = "%s",
232    runtime_name = %s,
233    java_home = "%s",
234    version = "%s",
235    exec_compatible_with = HOST_CONSTRAINTS,
236)
237""" % (local_java_runtime_name, runtime_name, java_home, version)
238
239    repository_ctx.file(
240        "BUILD.bazel",
241        'load("@rules_java//toolchains:local_java_repository.bzl", "local_java_runtime")\n' +
242        'load("@local_config_platform//:constraints.bzl", "HOST_CONSTRAINTS")\n' +
243        build_file +
244        local_java_runtime_macro,
245    )
246
247    # Symlink all files
248    for file in repository_ctx.path(java_home).readdir():
249        repository_ctx.symlink(file, file.basename)
250
251# Build file template, when JDK could not be detected
252_AUTO_CONFIG_ERROR_BUILD_TPL = '''load("@rules_java//toolchains:fail_rule.bzl", "fail_rule")
253fail_rule(
254   name = "jdk",
255   header = "Auto-Configuration Error:",
256   message = {message},
257)
258config_setting(
259   name = "localjdk_setting",
260   values = {{"java_runtime_version": "{local_jdk}"}},
261   visibility = ["//visibility:private"],
262)
263toolchain(
264   name = "runtime_toolchain_definition",
265   target_settings = [":localjdk_setting"],
266   toolchain_type = "@bazel_tools//tools/jdk:runtime_toolchain_type",
267   toolchain = ":jdk",
268)
269toolchain(
270   name = "bootstrap_runtime_toolchain_definition",
271   target_settings = [":localjdk_setting"],
272   toolchain_type = "@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type",
273   toolchain = ":jdk",
274)
275'''
276
277def _create_auto_config_error_build_file(repository_ctx, *, local_java_runtime_name, java_home, message):
278    repository_ctx.file(
279        "BUILD.bazel",
280        _AUTO_CONFIG_ERROR_BUILD_TPL.format(
281            local_jdk = local_java_runtime_name,
282            message = repr(message.format(
283                java_binary = _with_os_extension(repository_ctx, "bin/java"),
284                java_home = java_home,
285            )),
286        ),
287        False,
288    )
289
290_local_java_repository_rule = repository_rule(
291    implementation = _local_java_repository_impl,
292    local = True,
293    configure = True,
294    environ = ["JAVA_HOME"],
295    attrs = {
296        "build_file": attr.label(),
297        "build_file_content": attr.string(),
298        "java_home": attr.string(),
299        "version": attr.string(),
300    },
301)
302
303def local_java_repository(name, java_home = "", version = "", build_file = None, build_file_content = None, **kwargs):
304    """Defines runtime and compile toolchains for a local JDK.
305
306    Register the toolchains defined by this macro as follows (where `<name>` is the value of the
307    `name` parameter):
308    * Runtime toolchains only (recommended)
309      ```
310      register_toolchains("@<name>//:runtime_toolchain_definition")
311      register_toolchains("@<name>//:bootstrap_runtime_toolchain_definition")
312      ```
313    * Runtime and compilation toolchains:
314      ```
315      register_toolchains("@<name>//:all")
316      ```
317
318    Toolchain resolution is constrained with --java_runtime_version flag
319    having value of the "name" or "version" parameter.
320
321    Java compile toolchains are created for --java_language_version flags values
322    between 8 and version (inclusive). Java compile toolchains use the same
323    (local) JDK for compilation.
324
325    If there is no JDK "virtual" targets are created, which fail only when actually needed.
326
327    Args:
328      name: A unique name for this rule.
329      java_home: Location of the JDK imported.
330      build_file: optionally BUILD file template
331      build_file_content: optional BUILD file template as a string
332      version: optionally java version
333      **kwargs: additional arguments for repository rule
334    """
335    _local_java_repository_rule(name = name, java_home = java_home, version = version, build_file = build_file, build_file_content = build_file_content, **kwargs)
336