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