• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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:defs.bzl", _py_binary = "py_binary", _py_test = "py_test")
21load("//python/config_settings/private:py_args.bzl", "py_args")
22
23def _transition_python_version_impl(_, attr):
24    return {"//python/config_settings:python_version": str(attr.python_version)}
25
26_transition_python_version = transition(
27    implementation = _transition_python_version_impl,
28    inputs = [],
29    outputs = ["//python/config_settings:python_version"],
30)
31
32def _transition_py_impl(ctx):
33    target = ctx.attr.target
34    windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]
35    target_is_windows = ctx.target_platform_has_constraint(windows_constraint)
36    executable = ctx.actions.declare_file(ctx.attr.name + (".exe" if target_is_windows else ""))
37    ctx.actions.symlink(
38        is_executable = True,
39        output = executable,
40        target_file = target[DefaultInfo].files_to_run.executable,
41    )
42    zipfile_symlink = None
43    if target_is_windows:
44        # Under Windows, the expected "<name>.zip" does not exist, so we have to
45        # create the symlink ourselves to achieve the same behaviour as in macOS
46        # and Linux.
47        zipfile = None
48        expected_target_path = target[DefaultInfo].files_to_run.executable.short_path[:-4] + ".zip"
49        for file in target[DefaultInfo].default_runfiles.files.to_list():
50            if file.short_path == expected_target_path:
51                zipfile = file
52        zipfile_symlink = ctx.actions.declare_file(ctx.attr.name + ".zip")
53        ctx.actions.symlink(
54            is_executable = True,
55            output = zipfile_symlink,
56            target_file = zipfile,
57        )
58    env = {}
59    for k, v in ctx.attr.env.items():
60        env[k] = ctx.expand_location(v)
61
62    providers = [
63        DefaultInfo(
64            executable = executable,
65            files = depset([zipfile_symlink] if zipfile_symlink else [], transitive = [target[DefaultInfo].files]),
66            runfiles = ctx.runfiles([zipfile_symlink] if zipfile_symlink else []).merge(target[DefaultInfo].default_runfiles),
67        ),
68        target[PyInfo],
69        target[PyRuntimeInfo],
70        # Ensure that the binary we're wrapping is included in code coverage.
71        coverage_common.instrumented_files_info(
72            ctx,
73            dependency_attributes = ["target"],
74        ),
75        target[OutputGroupInfo],
76        # TODO(f0rmiga): testing.TestEnvironment is deprecated in favour of RunEnvironmentInfo but
77        # RunEnvironmentInfo is not exposed in Bazel < 5.3.
78        # https://github.com/bazelbuild/rules_python/issues/901
79        # https://github.com/bazelbuild/bazel/commit/dbdfa07e92f99497be9c14265611ad2920161483
80        testing.TestEnvironment(env),
81    ]
82    return providers
83
84_COMMON_ATTRS = {
85    "deps": attr.label_list(
86        mandatory = False,
87    ),
88    "env": attr.string_dict(
89        mandatory = False,
90    ),
91    "python_version": attr.string(
92        mandatory = True,
93    ),
94    "srcs": attr.label_list(
95        allow_files = True,
96        mandatory = False,
97    ),
98    "target": attr.label(
99        executable = True,
100        cfg = "target",
101        mandatory = True,
102        providers = [PyInfo],
103    ),
104    # "tools" is a hack here. It should be "data" but "data" is not included by default in the
105    # location expansion in the same way it is in the native Python rules. The difference on how
106    # the Bazel deals with those special attributes differ on the LocationExpander, e.g.:
107    # https://github.com/bazelbuild/bazel/blob/ce611646/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L415-L429
108    #
109    # Since the default LocationExpander used by ctx.expand_location is not the same as the native
110    # rules (it doesn't set "allowDataAttributeEntriesInLabel"), we use "tools" temporarily while a
111    # proper fix in Bazel happens.
112    #
113    # A fix for this was proposed in https://github.com/bazelbuild/bazel/pull/16381.
114    "tools": attr.label_list(
115        allow_files = True,
116        mandatory = False,
117    ),
118    # Required to Opt-in to the transitions feature.
119    "_allowlist_function_transition": attr.label(
120        default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
121    ),
122    "_windows_constraint": attr.label(
123        default = "@platforms//os:windows",
124    ),
125}
126
127_transition_py_binary = rule(
128    _transition_py_impl,
129    attrs = _COMMON_ATTRS,
130    cfg = _transition_python_version,
131    executable = True,
132)
133
134_transition_py_test = rule(
135    _transition_py_impl,
136    attrs = _COMMON_ATTRS,
137    cfg = _transition_python_version,
138    test = True,
139)
140
141def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs):
142    pyargs = py_args(name, kwargs)
143    args = pyargs["args"]
144    data = pyargs["data"]
145    env = pyargs["env"]
146    srcs = pyargs["srcs"]
147    deps = pyargs["deps"]
148    main = pyargs["main"]
149
150    # Attributes common to all build rules.
151    # https://bazel.build/reference/be/common-definitions#common-attributes
152    compatible_with = kwargs.pop("compatible_with", None)
153    deprecation = kwargs.pop("deprecation", None)
154    distribs = kwargs.pop("distribs", None)
155    exec_compatible_with = kwargs.pop("exec_compatible_with", None)
156    exec_properties = kwargs.pop("exec_properties", None)
157    features = kwargs.pop("features", None)
158    restricted_to = kwargs.pop("restricted_to", None)
159    tags = kwargs.pop("tags", None)
160    target_compatible_with = kwargs.pop("target_compatible_with", None)
161    testonly = kwargs.pop("testonly", None)
162    toolchains = kwargs.pop("toolchains", None)
163    visibility = kwargs.pop("visibility", None)
164
165    common_attrs = {
166        "compatible_with": compatible_with,
167        "deprecation": deprecation,
168        "distribs": distribs,
169        "exec_compatible_with": exec_compatible_with,
170        "exec_properties": exec_properties,
171        "features": features,
172        "restricted_to": restricted_to,
173        "target_compatible_with": target_compatible_with,
174        "testonly": testonly,
175        "toolchains": toolchains,
176    }
177
178    # Test-specific extra attributes.
179    if "env_inherit" in kwargs:
180        common_attrs["env_inherit"] = kwargs.pop("env_inherit")
181    if "size" in kwargs:
182        common_attrs["size"] = kwargs.pop("size")
183    if "timeout" in kwargs:
184        common_attrs["timeout"] = kwargs.pop("timeout")
185    if "flaky" in kwargs:
186        common_attrs["flaky"] = kwargs.pop("flaky")
187    if "shard_count" in kwargs:
188        common_attrs["shard_count"] = kwargs.pop("shard_count")
189    if "local" in kwargs:
190        common_attrs["local"] = kwargs.pop("local")
191
192    # Binary-specific extra attributes.
193    if "output_licenses" in kwargs:
194        common_attrs["output_licenses"] = kwargs.pop("output_licenses")
195
196    rule_impl(
197        name = "_" + name,
198        args = args,
199        data = data,
200        deps = deps,
201        env = env,
202        srcs = srcs,
203        main = main,
204        tags = ["manual"] + (tags if tags else []),
205        visibility = ["//visibility:private"],
206        **dicts.add(common_attrs, kwargs)
207    )
208
209    return transition_rule(
210        name = name,
211        args = args,
212        deps = deps,
213        env = env,
214        python_version = python_version,
215        srcs = srcs,
216        tags = tags,
217        target = ":_" + name,
218        tools = data,
219        visibility = visibility,
220        **common_attrs
221    )
222
223def py_binary(name, python_version, **kwargs):
224    return _py_rule(_py_binary, _transition_py_binary, name, python_version, **kwargs)
225
226def py_test(name, python_version, **kwargs):
227    return _py_rule(_py_test, _transition_py_test, name, python_version, **kwargs)
228