xref: /aosp_15_r20/external/bazelbuild-rules_python/python/config_settings/transition.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1# Copyright 2023 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"""The transition module contains the rule definitions to wrap py_binary and py_test and transition
16them to the desired target platform.
17"""
18
19load("@bazel_skylib//lib:dicts.bzl", "dicts")
20load("//python:py_binary.bzl", _py_binary = "py_binary")
21load("//python:py_info.bzl", "PyInfo")
22load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
23load("//python:py_test.bzl", _py_test = "py_test")
24load("//python/config_settings/private:py_args.bzl", "py_args")
25load("//python/private:reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
26
27def _transition_python_version_impl(_, attr):
28    return {"//python/config_settings:python_version": str(attr.python_version)}
29
30_transition_python_version = transition(
31    implementation = _transition_python_version_impl,
32    inputs = [],
33    outputs = ["//python/config_settings:python_version"],
34)
35
36def _transition_py_impl(ctx):
37    target = ctx.attr.target
38    windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]
39    target_is_windows = ctx.target_platform_has_constraint(windows_constraint)
40    executable = ctx.actions.declare_file(ctx.attr.name + (".exe" if target_is_windows else ""))
41    ctx.actions.symlink(
42        is_executable = True,
43        output = executable,
44        target_file = target[DefaultInfo].files_to_run.executable,
45    )
46    default_outputs = []
47    if target_is_windows:
48        # NOTE: Bazel 6 + host=linux + target=windows results in the .exe extension missing
49        inner_bootstrap_path = _strip_suffix(target[DefaultInfo].files_to_run.executable.short_path, ".exe")
50        inner_bootstrap = None
51        inner_zip_file_path = inner_bootstrap_path + ".zip"
52        inner_zip_file = None
53        for file in target[DefaultInfo].files.to_list():
54            if file.short_path == inner_bootstrap_path:
55                inner_bootstrap = file
56            elif file.short_path == inner_zip_file_path:
57                inner_zip_file = file
58
59        # TODO: Use `fragments.py.build_python_zip` once Bazel 6 support is dropped.
60        # Which file the Windows .exe looks for depends on the --build_python_zip file.
61        # Bazel 7+ has APIs to know the effective value of that flag, but not Bazel 6.
62        # To work around this, we treat the existence of a .zip in the default outputs
63        # to mean --build_python_zip=true.
64        if inner_zip_file:
65            suffix = ".zip"
66            underlying_launched_file = inner_zip_file
67        else:
68            suffix = ""
69            underlying_launched_file = inner_bootstrap
70
71        if underlying_launched_file:
72            launched_file_symlink = ctx.actions.declare_file(ctx.attr.name + suffix)
73            ctx.actions.symlink(
74                is_executable = True,
75                output = launched_file_symlink,
76                target_file = underlying_launched_file,
77            )
78            default_outputs.append(launched_file_symlink)
79
80    env = {}
81    for k, v in ctx.attr.env.items():
82        env[k] = ctx.expand_location(v)
83
84    providers = [
85        DefaultInfo(
86            executable = executable,
87            files = depset(default_outputs, transitive = [target[DefaultInfo].files]),
88            runfiles = ctx.runfiles(default_outputs).merge(target[DefaultInfo].default_runfiles),
89        ),
90        # Ensure that the binary we're wrapping is included in code coverage.
91        coverage_common.instrumented_files_info(
92            ctx,
93            dependency_attributes = ["target"],
94        ),
95        target[OutputGroupInfo],
96        # TODO(f0rmiga): testing.TestEnvironment is deprecated in favour of RunEnvironmentInfo but
97        # RunEnvironmentInfo is not exposed in Bazel < 5.3.
98        # https://github.com/bazelbuild/rules_python/issues/901
99        # https://github.com/bazelbuild/bazel/commit/dbdfa07e92f99497be9c14265611ad2920161483
100        testing.TestEnvironment(env),
101    ]
102    if PyInfo in target:
103        providers.append(target[PyInfo])
104    if BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
105        providers.append(target[BuiltinPyInfo])
106
107    if PyRuntimeInfo in target:
108        providers.append(target[PyRuntimeInfo])
109    if BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
110        providers.append(target[BuiltinPyRuntimeInfo])
111    return providers
112
113_COMMON_ATTRS = {
114    "deps": attr.label_list(
115        mandatory = False,
116    ),
117    "env": attr.string_dict(
118        mandatory = False,
119    ),
120    "python_version": attr.string(
121        mandatory = True,
122    ),
123    "srcs": attr.label_list(
124        allow_files = True,
125        mandatory = False,
126    ),
127    "target": attr.label(
128        executable = True,
129        cfg = "target",
130        mandatory = True,
131        providers = [PyInfo],
132    ),
133    # "tools" is a hack here. It should be "data" but "data" is not included by default in the
134    # location expansion in the same way it is in the native Python rules. The difference on how
135    # the Bazel deals with those special attributes differ on the LocationExpander, e.g.:
136    # https://github.com/bazelbuild/bazel/blob/ce611646/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L415-L429
137    #
138    # Since the default LocationExpander used by ctx.expand_location is not the same as the native
139    # rules (it doesn't set "allowDataAttributeEntriesInLabel"), we use "tools" temporarily while a
140    # proper fix in Bazel happens.
141    #
142    # A fix for this was proposed in https://github.com/bazelbuild/bazel/pull/16381.
143    "tools": attr.label_list(
144        allow_files = True,
145        mandatory = False,
146    ),
147    # Required to Opt-in to the transitions feature.
148    "_allowlist_function_transition": attr.label(
149        default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
150    ),
151    "_windows_constraint": attr.label(
152        default = "@platforms//os:windows",
153    ),
154}
155
156_PY_TEST_ATTRS = {
157    # Magic attribute to help C++ coverage work. There's no
158    # docs about this; see TestActionBuilder.java
159    "_collect_cc_coverage": attr.label(
160        default = "@bazel_tools//tools/test:collect_cc_coverage",
161        executable = True,
162        cfg = "exec",
163    ),
164    # Magic attribute to make coverage work. There's no
165    # docs about this; see TestActionBuilder.java
166    "_lcov_merger": attr.label(
167        default = configuration_field(fragment = "coverage", name = "output_generator"),
168        executable = True,
169        cfg = "exec",
170    ),
171}
172
173_transition_py_binary = rule(
174    _transition_py_impl,
175    attrs = _COMMON_ATTRS | _PY_TEST_ATTRS,
176    cfg = _transition_python_version,
177    executable = True,
178    fragments = ["py"],
179)
180
181_transition_py_test = rule(
182    _transition_py_impl,
183    attrs = _COMMON_ATTRS | _PY_TEST_ATTRS,
184    cfg = _transition_python_version,
185    test = True,
186    fragments = ["py"],
187)
188
189def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs):
190    pyargs = py_args(name, kwargs)
191    args = pyargs["args"]
192    data = pyargs["data"]
193    env = pyargs["env"]
194    srcs = pyargs["srcs"]
195    deps = pyargs["deps"]
196    main = pyargs["main"]
197
198    # Attributes common to all build rules.
199    # https://bazel.build/reference/be/common-definitions#common-attributes
200    compatible_with = kwargs.pop("compatible_with", None)
201    deprecation = kwargs.pop("deprecation", None)
202    exec_compatible_with = kwargs.pop("exec_compatible_with", None)
203    exec_properties = kwargs.pop("exec_properties", None)
204    features = kwargs.pop("features", None)
205    restricted_to = kwargs.pop("restricted_to", None)
206    tags = kwargs.pop("tags", None)
207    target_compatible_with = kwargs.pop("target_compatible_with", None)
208    testonly = kwargs.pop("testonly", None)
209    toolchains = kwargs.pop("toolchains", None)
210    visibility = kwargs.pop("visibility", None)
211
212    common_attrs = {
213        "compatible_with": compatible_with,
214        "deprecation": deprecation,
215        "exec_compatible_with": exec_compatible_with,
216        "exec_properties": exec_properties,
217        "features": features,
218        "restricted_to": restricted_to,
219        "target_compatible_with": target_compatible_with,
220        "testonly": testonly,
221        "toolchains": toolchains,
222    }
223
224    # Test-specific extra attributes.
225    if "env_inherit" in kwargs:
226        common_attrs["env_inherit"] = kwargs.pop("env_inherit")
227    if "size" in kwargs:
228        common_attrs["size"] = kwargs.pop("size")
229    if "timeout" in kwargs:
230        common_attrs["timeout"] = kwargs.pop("timeout")
231    if "flaky" in kwargs:
232        common_attrs["flaky"] = kwargs.pop("flaky")
233    if "shard_count" in kwargs:
234        common_attrs["shard_count"] = kwargs.pop("shard_count")
235    if "local" in kwargs:
236        common_attrs["local"] = kwargs.pop("local")
237
238    # Binary-specific extra attributes.
239    if "output_licenses" in kwargs:
240        common_attrs["output_licenses"] = kwargs.pop("output_licenses")
241
242    rule_impl(
243        name = "_" + name,
244        args = args,
245        data = data,
246        deps = deps,
247        env = env,
248        srcs = srcs,
249        main = main,
250        tags = ["manual"] + (tags if tags else []),
251        visibility = ["//visibility:private"],
252        **dicts.add(common_attrs, kwargs)
253    )
254
255    return transition_rule(
256        name = name,
257        args = args,
258        deps = deps,
259        env = env,
260        python_version = python_version,
261        srcs = srcs,
262        tags = tags,
263        target = ":_" + name,
264        tools = data,
265        visibility = visibility,
266        **common_attrs
267    )
268
269def py_binary(name, python_version, **kwargs):
270    return _py_rule(_py_binary, _transition_py_binary, name, python_version, **kwargs)
271
272def py_test(name, python_version, **kwargs):
273    return _py_rule(_py_test, _transition_py_test, name, python_version, **kwargs)
274
275def _strip_suffix(s, suffix):
276    if s.endswith(suffix):
277        return s[:-len(suffix)]
278    else:
279        return s
280