• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/toolchains:java_runtime.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    local_java_runtime_name = repository_ctx.attr.runtime_name
182
183    repository_ctx.file(
184        "WORKSPACE",
185        "# DO NOT EDIT: automatically generated WORKSPACE file for local_java_repository\n" +
186        "workspace(name = \"{name}\")\n".format(name = local_java_runtime_name),
187    )
188
189    java_bin = java_home.get_child("bin").get_child(_with_os_extension(repository_ctx, "java"))
190
191    if not java_bin.exists:
192        # Java binary does not exist
193        _create_auto_config_error_build_file(
194            repository_ctx,
195            local_java_runtime_name = local_java_runtime_name,
196            java_home = java_home,
197            message = "Cannot find Java binary {java_binary} in {java_home}; " +
198                      "either correct your JAVA_HOME, PATH or specify Java from " +
199                      "remote repository (e.g. --java_runtime_version=remotejdk_11)",
200        )
201        return
202
203    # Detect version
204    version = repository_ctx.attr.version if repository_ctx.attr.version != "" else _detect_java_version(repository_ctx, java_bin)
205    if version == None:
206        # Java version could not be detected
207        _create_auto_config_error_build_file(
208            repository_ctx,
209            local_java_runtime_name = local_java_runtime_name,
210            java_home = java_home,
211            message = "Cannot detect Java version of {java_binary} in {java_home}; " +
212                      "make sure it points to a valid Java executable",
213        )
214        return
215
216    # Prepare BUILD file using "local_java_runtime" macro
217    if repository_ctx.attr.build_file_content and repository_ctx.attr.build_file:
218        fail("build_file and build_file_content are exclusive")
219    if repository_ctx.attr.build_file_content:
220        build_file = repository_ctx.attr.build_file_content
221    elif repository_ctx.attr.build_file:
222        build_file = repository_ctx.read(repository_ctx.path(repository_ctx.attr.build_file))
223    else:
224        build_file = ""
225    build_file = build_file.format(RUNTIME_VERSION = version if version.isdigit() else "0")
226
227    runtime_name = '"jdk"' if build_file else None
228    local_java_runtime_macro = """
229local_java_runtime(
230    name = "%s",
231    runtime_name = %s,
232    java_home = "%s",
233    version = "%s",
234    exec_compatible_with = HOST_CONSTRAINTS,
235)
236""" % (local_java_runtime_name, runtime_name, java_home, version)
237
238    repository_ctx.file(
239        "BUILD.bazel",
240        'load("@rules_java//toolchains:local_java_repository.bzl", "local_java_runtime")\n' +
241        'load("@local_config_platform//:constraints.bzl", "HOST_CONSTRAINTS")\n' +
242        build_file +
243        local_java_runtime_macro,
244    )
245
246    # Symlink all files
247    for file in repository_ctx.path(java_home).readdir():
248        repository_ctx.symlink(file, file.basename)
249
250# Build file template, when JDK could not be detected
251_AUTO_CONFIG_ERROR_BUILD_TPL = '''load("@rules_java//toolchains:fail_rule.bzl", "fail_rule")
252fail_rule(
253   name = "jdk",
254   header = "Auto-Configuration Error:",
255   message = {message},
256)
257config_setting(
258   name = "localjdk_setting",
259   values = {{"java_runtime_version": "{local_jdk}"}},
260   visibility = ["//visibility:private"],
261)
262toolchain(
263   name = "runtime_toolchain_definition",
264   target_settings = [":localjdk_setting"],
265   toolchain_type = "@bazel_tools//tools/jdk:runtime_toolchain_type",
266   toolchain = ":jdk",
267)
268toolchain(
269   name = "bootstrap_runtime_toolchain_definition",
270   target_settings = [":localjdk_setting"],
271   toolchain_type = "@bazel_tools//tools/jdk:bootstrap_runtime_toolchain_type",
272   toolchain = ":jdk",
273)
274'''
275
276def _create_auto_config_error_build_file(repository_ctx, *, local_java_runtime_name, java_home, message):
277    repository_ctx.file(
278        "BUILD.bazel",
279        _AUTO_CONFIG_ERROR_BUILD_TPL.format(
280            local_jdk = local_java_runtime_name,
281            message = repr(message.format(
282                java_binary = _with_os_extension(repository_ctx, "bin/java"),
283                java_home = java_home,
284            )),
285        ),
286        False,
287    )
288
289_local_java_repository_rule = repository_rule(
290    implementation = _local_java_repository_impl,
291    local = True,
292    configure = True,
293    environ = ["JAVA_HOME"],
294    attrs = {
295        "runtime_name": attr.string(),
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, runtime_name = name, java_home = java_home, version = version, build_file = build_file, build_file_content = build_file_content, **kwargs)
336