1# Copyright 2024 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"""Macro to generate all of the targets present in a {obj}`whl_library`.""" 16 17load("@bazel_skylib//rules:copy_file.bzl", "copy_file") 18load("//python:py_binary.bzl", "py_binary") 19load("//python:py_library.bzl", "py_library") 20load("//python/private:glob_excludes.bzl", "glob_excludes") 21load("//python/private:normalize_name.bzl", "normalize_name") 22load( 23 ":labels.bzl", 24 "DATA_LABEL", 25 "DIST_INFO_LABEL", 26 "PY_LIBRARY_IMPL_LABEL", 27 "PY_LIBRARY_PUBLIC_LABEL", 28 "WHEEL_ENTRY_POINT_PREFIX", 29 "WHEEL_FILE_IMPL_LABEL", 30 "WHEEL_FILE_PUBLIC_LABEL", 31) 32 33def whl_library_targets( 34 *, 35 name, 36 dep_template, 37 data_exclude = [], 38 srcs_exclude = [], 39 tags = [], 40 filegroups = { 41 DIST_INFO_LABEL: ["site-packages/*.dist-info/**"], 42 DATA_LABEL: ["data/**"], 43 }, 44 dependencies = [], 45 dependencies_by_platform = {}, 46 group_deps = [], 47 group_name = "", 48 data = [], 49 copy_files = {}, 50 copy_executables = {}, 51 entry_points = {}, 52 native = native, 53 rules = struct( 54 copy_file = copy_file, 55 py_binary = py_binary, 56 py_library = py_library, 57 )): 58 """Create all of the whl_library targets. 59 60 Args: 61 name: {type}`str` The file to match for including it into the `whl` 62 filegroup. This may be also parsed to generate extra metadata. 63 dep_template: {type}`str` The dep_template to use for dependency 64 interpolation. 65 tags: {type}`list[str]` The tags set on the `py_library`. 66 dependencies: {type}`list[str]` A list of dependencies. 67 dependencies_by_platform: {type}`dict[str, list[str]]` A list of 68 dependencies by platform key. 69 filegroups: {type}`dict[str, list[str]]` A dictionary of the target 70 names and the glob matches. 71 group_name: {type}`str` name of the dependency group (if any) which 72 contains this library. If set, this library will behave as a shim 73 to group implementation rules which will provide simultaneously 74 installed dependencies which would otherwise form a cycle. 75 group_deps: {type}`list[str]` names of fellow members of the group (if 76 any). These will be excluded from generated deps lists so as to avoid 77 direct cycles. These dependencies will be provided at runtime by the 78 group rules which wrap this library and its fellows together. 79 copy_executables: {type}`dict[str, str]` The mapping between src and 80 dest locations for the targets. 81 copy_files: {type}`dict[str, str]` The mapping between src and 82 dest locations for the targets. 83 data_exclude: {type}`list[str]` The globs for data attribute exclusion 84 in `py_library`. 85 srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion 86 in `py_library`. 87 data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`. 88 entry_points: {type}`dict[str, str]` The mapping between the script 89 name and the python file to use. DEPRECATED. 90 native: {type}`native` The native struct for overriding in tests. 91 rules: {type}`struct` A struct with references to rules for creating targets. 92 """ 93 _ = name # buildifier: @unused 94 95 dependencies = sorted([normalize_name(d) for d in dependencies]) 96 dependencies_by_platform = { 97 platform: sorted([normalize_name(d) for d in deps]) 98 for platform, deps in dependencies_by_platform.items() 99 } 100 tags = sorted(tags) 101 data = [] + data 102 103 for filegroup_name, glob in filegroups.items(): 104 native.filegroup( 105 name = filegroup_name, 106 srcs = native.glob(glob, allow_empty = True), 107 visibility = ["//visibility:public"], 108 ) 109 110 for src, dest in copy_files.items(): 111 rules.copy_file( 112 name = dest + ".copy", 113 src = src, 114 out = dest, 115 visibility = ["//visibility:public"], 116 ) 117 data.append(dest) 118 for src, dest in copy_executables.items(): 119 rules.copy_file( 120 name = dest + ".copy", 121 src = src, 122 out = dest, 123 is_executable = True, 124 visibility = ["//visibility:public"], 125 ) 126 data.append(dest) 127 128 _config_settings( 129 dependencies_by_platform.keys(), 130 native = native, 131 visibility = ["//visibility:private"], 132 ) 133 134 # TODO @aignas 2024-10-25: remove the entry_point generation once 135 # `py_console_script_binary` is the only way to use entry points. 136 for entry_point, entry_point_script_name in entry_points.items(): 137 rules.py_binary( 138 name = "{}_{}".format(WHEEL_ENTRY_POINT_PREFIX, entry_point), 139 # Ensure that this works on Windows as well - script may have Windows path separators. 140 srcs = [entry_point_script_name.replace("\\", "/")], 141 # This makes this directory a top-level in the python import 142 # search path for anything that depends on this. 143 imports = ["."], 144 deps = [":" + PY_LIBRARY_PUBLIC_LABEL], 145 visibility = ["//visibility:public"], 146 ) 147 148 # Ensure this list is normalized 149 # Note: mapping used as set 150 group_deps = { 151 normalize_name(d): True 152 for d in group_deps 153 } 154 155 dependencies = [ 156 d 157 for d in dependencies 158 if d not in group_deps 159 ] 160 dependencies_by_platform = { 161 p: deps 162 for p, deps in dependencies_by_platform.items() 163 for deps in [[d for d in deps if d not in group_deps]] 164 if deps 165 } 166 167 # If this library is a member of a group, its public label aliases need to 168 # point to the group implementation rule not the implementation rules. We 169 # also need to mark the implementation rules as visible to the group 170 # implementation. 171 if group_name and "//:" in dep_template: 172 # This is the legacy behaviour where the group library is outside the hub repo 173 label_tmpl = dep_template.format( 174 name = "_groups", 175 target = normalize_name(group_name) + "_{}", 176 ) 177 impl_vis = [dep_template.format( 178 name = "_groups", 179 target = "__pkg__", 180 )] 181 182 native.alias( 183 name = PY_LIBRARY_PUBLIC_LABEL, 184 actual = label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL), 185 visibility = ["//visibility:public"], 186 ) 187 native.alias( 188 name = WHEEL_FILE_PUBLIC_LABEL, 189 actual = label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL), 190 visibility = ["//visibility:public"], 191 ) 192 py_library_label = PY_LIBRARY_IMPL_LABEL 193 whl_file_label = WHEEL_FILE_IMPL_LABEL 194 195 elif group_name: 196 py_library_label = PY_LIBRARY_PUBLIC_LABEL 197 whl_file_label = WHEEL_FILE_PUBLIC_LABEL 198 impl_vis = [dep_template.format(name = "", target = "__subpackages__")] 199 200 else: 201 py_library_label = PY_LIBRARY_PUBLIC_LABEL 202 whl_file_label = WHEEL_FILE_PUBLIC_LABEL 203 impl_vis = ["//visibility:public"] 204 205 if hasattr(native, "filegroup"): 206 native.filegroup( 207 name = whl_file_label, 208 srcs = [name], 209 data = _deps( 210 deps = dependencies, 211 deps_by_platform = dependencies_by_platform, 212 tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), 213 # NOTE @aignas 2024-10-28: Actually, `select` is not part of 214 # `native`, but in order to support bazel 6.4 in unit tests, I 215 # have to somehow pass the `select` implementation in the unit 216 # tests and I chose this to be routed through the `native` 217 # struct. So, tests` will be successful in `getattr` and the 218 # real code will use the fallback provided here. 219 select = getattr(native, "select", select), 220 ), 221 visibility = impl_vis, 222 ) 223 224 if hasattr(rules, "py_library"): 225 _data_exclude = [ 226 "**/*.py", 227 "**/*.pyc", 228 "**/*.pyc.*", # During pyc creation, temp files named *.pyc.NNNN are created 229 # RECORD is known to contain sha256 checksums of files which might include the checksums 230 # of generated files produced when wheels are installed. The file is ignored to avoid 231 # Bazel caching issues. 232 "**/*.dist-info/RECORD", 233 ] + glob_excludes.version_dependent_exclusions() 234 for item in data_exclude: 235 if item not in _data_exclude: 236 _data_exclude.append(item) 237 238 rules.py_library( 239 name = py_library_label, 240 srcs = native.glob( 241 ["site-packages/**/*.py"], 242 exclude = srcs_exclude, 243 # Empty sources are allowed to support wheels that don't have any 244 # pure-Python code, e.g. pymssql, which is written in Cython. 245 allow_empty = True, 246 ), 247 data = data + native.glob( 248 ["site-packages/**/*"], 249 exclude = _data_exclude, 250 ), 251 # This makes this directory a top-level in the python import 252 # search path for anything that depends on this. 253 imports = ["site-packages"], 254 deps = _deps( 255 deps = dependencies, 256 deps_by_platform = dependencies_by_platform, 257 tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), 258 select = getattr(native, "select", select), 259 ), 260 tags = tags, 261 visibility = impl_vis, 262 ) 263 264def _config_settings(dependencies_by_platform, native = native, **kwargs): 265 """Generate config settings for the targets. 266 267 Args: 268 dependencies_by_platform: {type}`list[str]` platform keys, can be 269 one of the following formats: 270 * `//conditions:default` 271 * `@platforms//os:{value}` 272 * `@platforms//cpu:{value}` 273 * `@//python/config_settings:is_python_3.{minor_version}` 274 * `{os}_{cpu}` 275 * `cp3{minor_version}_{os}_{cpu}` 276 native: {type}`native` The native struct for overriding in tests. 277 **kwargs: Extra kwargs to pass to the rule. 278 """ 279 for p in dependencies_by_platform: 280 if p.startswith("@") or p.endswith("default"): 281 continue 282 283 abi, _, tail = p.partition("_") 284 if not abi.startswith("cp"): 285 tail = p 286 abi = "" 287 288 os, _, arch = tail.partition("_") 289 os = "" if os == "anyos" else os 290 arch = "" if arch == "anyarch" else arch 291 292 _kwargs = dict(kwargs) 293 if arch: 294 _kwargs.setdefault("constraint_values", []).append("@platforms//cpu:{}".format(arch)) 295 if os: 296 _kwargs.setdefault("constraint_values", []).append("@platforms//os:{}".format(os)) 297 298 if abi: 299 _kwargs["flag_values"] = { 300 "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}".format( 301 minor_version = abi[len("cp3"):], 302 ), 303 } 304 305 native.config_setting( 306 name = "is_{name}".format( 307 name = p.replace("cp3", "python_3."), 308 ), 309 **_kwargs 310 ) 311 312def _plat_label(plat): 313 if plat.endswith("default"): 314 return plat 315 elif plat.startswith("@//"): 316 return Label(plat.strip("@")) 317 elif plat.startswith("@"): 318 return plat 319 else: 320 return ":is_" + plat.replace("cp3", "python_3.") 321 322def _deps(deps, deps_by_platform, tmpl, select = select): 323 deps = [tmpl.format(d) for d in sorted(deps)] 324 325 if not deps_by_platform: 326 return deps 327 328 deps_by_platform = { 329 _plat_label(p): [ 330 tmpl.format(d) 331 for d in sorted(deps) 332 ] 333 for p, deps in sorted(deps_by_platform.items()) 334 } 335 336 # Add the default, which means that we will be just using the dependencies in 337 # `deps` for platforms that are not handled in a special way by the packages 338 deps_by_platform.setdefault("//conditions:default", []) 339 340 if not deps: 341 return select(deps_by_platform) 342 else: 343 return deps + select(deps_by_platform) 344