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