| 
14 | 14 | 
 
  | 
15 | 15 | """The transition module contains the rule definitions to wrap py_binary and py_test and transition  | 
16 | 16 | them to the desired target platform.  | 
 | 17 | +
  | 
 | 18 | +:::{versionchanged} VERSION_NEXT_PATCH  | 
 | 19 | +The `py_binary` and `py_test` symbols are aliases to the regular rules. Usages  | 
 | 20 | +of them should be changed to load the regular rules directly.  | 
 | 21 | +:::  | 
17 | 22 | """  | 
18 | 23 | 
 
  | 
19 |  | -load("@bazel_skylib//lib:dicts.bzl", "dicts")  | 
20 | 24 | load("//python:py_binary.bzl", _py_binary = "py_binary")  | 
21 |  | -load("//python:py_info.bzl", "PyInfo")  | 
22 |  | -load("//python:py_runtime_info.bzl", "PyRuntimeInfo")  | 
23 | 25 | load("//python:py_test.bzl", _py_test = "py_test")  | 
24 |  | -load("//python/config_settings/private:py_args.bzl", "py_args")  | 
25 |  | -load("//python/private:reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")  | 
26 |  | - | 
27 |  | -def _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 |  | - | 
36 |  | -def _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 != None and 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 != None and 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 |  | - | 
189 |  | -def _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 |  | - | 
269 |  | -def py_binary(name, python_version, **kwargs):  | 
270 |  | -    return _py_rule(_py_binary, _transition_py_binary, name, python_version, **kwargs)  | 
271 |  | - | 
272 |  | -def py_test(name, python_version, **kwargs):  | 
273 |  | -    return _py_rule(_py_test, _transition_py_test, name, python_version, **kwargs)  | 
274 | 26 | 
 
  | 
275 |  | -def _strip_suffix(s, suffix):  | 
276 |  | -    if s.endswith(suffix):  | 
277 |  | -        return s[:-len(suffix)]  | 
278 |  | -    else:  | 
279 |  | -        return s  | 
 | 27 | +py_binary = _py_binary  | 
 | 28 | +py_test = _py_test  | 
0 commit comments