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"Python toolchain module extensions for use with bzlmod." 16 17load("@bazel_features//:features.bzl", "bazel_features") 18load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") 19load(":auth.bzl", "AUTH_ATTRS") 20load(":full_version.bzl", "full_version") 21load(":python_register_toolchains.bzl", "python_register_toolchains") 22load(":pythons_hub.bzl", "hub_repo") 23load(":repo_utils.bzl", "repo_utils") 24load(":semver.bzl", "semver") 25load(":text_util.bzl", "render") 26load(":toolchains_repo.bzl", "multi_toolchain_aliases") 27load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") 28 29# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all 30# targets using any of these toolchains due to the changed repository name. 31_MAX_NUM_TOOLCHAINS = 9999 32_TOOLCHAIN_INDEX_PAD_LENGTH = len(str(_MAX_NUM_TOOLCHAINS)) 33 34def parse_modules(*, module_ctx, _fail = fail): 35 """Parse the modules and return a struct for registrations. 36 37 Args: 38 module_ctx: {type}`module_ctx` module context. 39 _fail: {type}`function` the failure function, mainly for testing. 40 41 Returns: 42 A struct with the following attributes: 43 * `toolchains`: The list of toolchains to register. The last 44 element is special and is treated as the default toolchain. 45 * `defaults`: The default `kwargs` passed to 46 {bzl:obj}`python_register_toolchains`. 47 * `debug_info`: {type}`None | dict` extra information to be passed 48 to the debug repo. 49 """ 50 if module_ctx.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": 51 debug_info = { 52 "toolchains_registered": [], 53 } 54 else: 55 debug_info = None 56 57 # The toolchain_info structs to register, in the order to register them in. 58 # NOTE: The last element is special: it is treated as the default toolchain, 59 # so there is special handling to ensure the last entry is the correct one. 60 toolchains = [] 61 62 # We store the default toolchain separately to ensure it is the last 63 # toolchain added to toolchains. 64 # This is a toolchain_info struct. 65 default_toolchain = None 66 67 # Map of string Major.Minor or Major.Minor.Patch to the toolchain_info struct 68 global_toolchain_versions = {} 69 70 ignore_root_user_error = None 71 72 logger = repo_utils.logger(module_ctx, "python") 73 74 # if the root module does not register any toolchain then the 75 # ignore_root_user_error takes its default value: False 76 if not module_ctx.modules[0].tags.toolchain: 77 ignore_root_user_error = False 78 79 config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail) 80 81 seen_versions = {} 82 for mod in module_ctx.modules: 83 module_toolchain_versions = [] 84 toolchain_attr_structs = _create_toolchain_attr_structs( 85 mod = mod, 86 seen_versions = seen_versions, 87 config = config, 88 ) 89 90 for toolchain_attr in toolchain_attr_structs: 91 toolchain_version = toolchain_attr.python_version 92 toolchain_name = "python_" + toolchain_version.replace(".", "_") 93 94 # Duplicate versions within a module indicate a misconfigured module. 95 if toolchain_version in module_toolchain_versions: 96 _fail_duplicate_module_toolchain_version(toolchain_version, mod.name) 97 module_toolchain_versions.append(toolchain_version) 98 99 if mod.is_root: 100 # Only the root module and rules_python are allowed to specify the default 101 # toolchain for a couple reasons: 102 # * It prevents submodules from specifying different defaults and only 103 # one of them winning. 104 # * rules_python needs to set a soft default in case the root module doesn't, 105 # e.g. if the root module doesn't use Python itself. 106 # * The root module is allowed to override the rules_python default. 107 is_default = toolchain_attr.is_default 108 109 # Also only the root module should be able to decide ignore_root_user_error. 110 # Modules being depended upon don't know the final environment, so they aren't 111 # in the right position to know or decide what the correct setting is. 112 113 # If an inconsistency in the ignore_root_user_error among multiple toolchains is detected, fail. 114 if ignore_root_user_error != None and toolchain_attr.ignore_root_user_error != ignore_root_user_error: 115 fail("Toolchains in the root module must have consistent 'ignore_root_user_error' attributes") 116 117 ignore_root_user_error = toolchain_attr.ignore_root_user_error 118 elif mod.name == "rules_python" and not default_toolchain: 119 # We don't do the len() check because we want the default that rules_python 120 # sets to be clearly visible. 121 is_default = toolchain_attr.is_default 122 else: 123 is_default = False 124 125 if is_default and default_toolchain != None: 126 _fail_multiple_default_toolchains( 127 first = default_toolchain.name, 128 second = toolchain_name, 129 ) 130 131 # Ignore version collisions in the global scope because there isn't 132 # much else that can be done. Modules don't know and can't control 133 # what other modules do, so the first in the dependency graph wins. 134 if toolchain_version in global_toolchain_versions: 135 # If the python version is explicitly provided by the root 136 # module, they should not be warned for choosing the same 137 # version that rules_python provides as default. 138 first = global_toolchain_versions[toolchain_version] 139 if mod.name != "rules_python" or not first.module.is_root: 140 # The warning can be enabled by setting the verbosity: 141 # env RULES_PYTHON_REPO_DEBUG_VERBOSITY=INFO bazel build //... 142 _warn_duplicate_global_toolchain_version( 143 toolchain_version, 144 first = first, 145 second_toolchain_name = toolchain_name, 146 second_module_name = mod.name, 147 logger = logger, 148 ) 149 toolchain_info = None 150 else: 151 toolchain_info = struct( 152 python_version = toolchain_attr.python_version, 153 name = toolchain_name, 154 register_coverage_tool = toolchain_attr.configure_coverage_tool, 155 module = struct(name = mod.name, is_root = mod.is_root), 156 ) 157 global_toolchain_versions[toolchain_version] = toolchain_info 158 if debug_info: 159 debug_info["toolchains_registered"].append({ 160 "ignore_root_user_error": ignore_root_user_error, 161 "module": {"is_root": mod.is_root, "name": mod.name}, 162 "name": toolchain_name, 163 }) 164 165 if is_default: 166 # This toolchain is setting the default, but the actual 167 # registration was performed previously, by a different module. 168 if toolchain_info == None: 169 default_toolchain = global_toolchain_versions[toolchain_version] 170 171 # Remove it because later code will add it at the end to 172 # ensure it is last in the list. 173 toolchains.remove(default_toolchain) 174 else: 175 default_toolchain = toolchain_info 176 elif toolchain_info: 177 toolchains.append(toolchain_info) 178 179 config.default.setdefault("ignore_root_user_error", ignore_root_user_error) 180 181 # A default toolchain is required so that the non-version-specific rules 182 # are able to match a toolchain. 183 if default_toolchain == None: 184 fail("No default Python toolchain configured. Is rules_python missing `is_default=True`?") 185 elif default_toolchain.python_version not in global_toolchain_versions: 186 fail('Default version "{python_version}" selected by module ' + 187 '"{module_name}", but no toolchain with that version registered'.format( 188 python_version = default_toolchain.python_version, 189 module_name = default_toolchain.module.name, 190 )) 191 192 # The last toolchain in the BUILD file is set as the default 193 # toolchain. We need the default last. 194 toolchains.append(default_toolchain) 195 196 if len(toolchains) > _MAX_NUM_TOOLCHAINS: 197 fail("more than {} python versions are not supported".format(_MAX_NUM_TOOLCHAINS)) 198 199 return struct( 200 config = config, 201 debug_info = debug_info, 202 default_python_version = toolchains[-1].python_version, 203 toolchains = [ 204 struct( 205 python_version = t.python_version, 206 name = t.name, 207 register_coverage_tool = t.register_coverage_tool, 208 ) 209 for t in toolchains 210 ], 211 ) 212 213def _python_impl(module_ctx): 214 py = parse_modules(module_ctx = module_ctx) 215 216 for toolchain_info in py.toolchains: 217 # Ensure that we pass the full version here. 218 full_python_version = full_version( 219 version = toolchain_info.python_version, 220 minor_mapping = py.config.minor_mapping, 221 ) 222 kwargs = { 223 "python_version": full_python_version, 224 "register_coverage_tool": toolchain_info.register_coverage_tool, 225 } 226 227 # Allow overrides per python version 228 kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {})) 229 kwargs.update(py.config.kwargs.get(full_python_version, {})) 230 kwargs.update(py.config.default) 231 python_register_toolchains(name = toolchain_info.name, **kwargs) 232 233 # Create the pythons_hub repo for the interpreter meta data and the 234 # the various toolchains. 235 hub_repo( 236 name = "pythons_hub", 237 # Last toolchain is default 238 default_python_version = py.default_python_version, 239 toolchain_prefixes = [ 240 render.toolchain_prefix(index, toolchain.name, _TOOLCHAIN_INDEX_PAD_LENGTH) 241 for index, toolchain in enumerate(py.toolchains) 242 ], 243 toolchain_python_versions = [ 244 full_version(version = t.python_version, minor_mapping = py.config.minor_mapping) 245 for t in py.toolchains 246 ], 247 # The last toolchain is the default; it can't have version constraints 248 # Despite the implication of the arg name, the values are strs, not bools 249 toolchain_set_python_version_constraints = [ 250 "True" if i != len(py.toolchains) - 1 else "False" 251 for i in range(len(py.toolchains)) 252 ], 253 toolchain_user_repository_names = [t.name for t in py.toolchains], 254 ) 255 256 # This is require in order to support multiple version py_test 257 # and py_binary 258 multi_toolchain_aliases( 259 name = "python_versions", 260 python_versions = { 261 toolchain.python_version: toolchain.name 262 for toolchain in py.toolchains 263 }, 264 ) 265 266 if py.debug_info != None: 267 _debug_repo( 268 name = "rules_python_bzlmod_debug", 269 debug_info = json.encode_indent(py.debug_info), 270 ) 271 272 if bazel_features.external_deps.extension_metadata_has_reproducible: 273 return module_ctx.extension_metadata(reproducible = True) 274 else: 275 return None 276 277def _fail_duplicate_module_toolchain_version(version, module): 278 fail(("Duplicate module toolchain version: module '{module}' attempted " + 279 "to use version '{version}' multiple times in itself").format( 280 version = version, 281 module = module, 282 )) 283 284def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_name, second_module_name, logger): 285 if not logger: 286 return 287 288 logger.info(lambda: ( 289 "Ignoring toolchain '{second_toolchain}' from module '{second_module}': " + 290 "Toolchain '{first_toolchain}' from module '{first_module}' " + 291 "already registered Python version {version} and has precedence." 292 ).format( 293 first_toolchain = first.name, 294 first_module = first.module.name, 295 second_module = second_module_name, 296 second_toolchain = second_toolchain_name, 297 version = version, 298 )) 299 300def _fail_multiple_default_toolchains(first, second): 301 fail(("Multiple default toolchains: only one toolchain " + 302 "can have is_default=True. First default " + 303 "was toolchain '{first}'. Second was '{second}'").format( 304 first = first, 305 second = second, 306 )) 307 308def _validate_version(*, version, _fail = fail): 309 parsed = semver(version) 310 if parsed.patch == None or parsed.build or parsed.pre_release: 311 _fail("The 'python_version' attribute needs to specify an 'X.Y.Z' semver-compatible version, got: '{}'".format(version)) 312 return False 313 314 return True 315 316def _process_single_version_overrides(*, tag, _fail = fail, default): 317 if not _validate_version(version = tag.python_version, _fail = _fail): 318 return 319 320 available_versions = default["tool_versions"] 321 kwargs = default.setdefault("kwargs", {}) 322 323 if tag.sha256 or tag.urls: 324 if not (tag.sha256 and tag.urls): 325 _fail("Both `sha256` and `urls` overrides need to be provided together") 326 return 327 328 for platform in (tag.sha256 or []): 329 if platform not in PLATFORMS: 330 _fail("The platform must be one of {allowed} but got '{got}'".format( 331 allowed = sorted(PLATFORMS), 332 got = platform, 333 )) 334 return 335 336 sha256 = dict(tag.sha256) or available_versions[tag.python_version]["sha256"] 337 override = { 338 "sha256": sha256, 339 "strip_prefix": { 340 platform: tag.strip_prefix 341 for platform in sha256 342 }, 343 "url": { 344 platform: list(tag.urls) 345 for platform in tag.sha256 346 } or available_versions[tag.python_version]["url"], 347 } 348 349 if tag.patches: 350 override["patch_strip"] = { 351 platform: tag.patch_strip 352 for platform in sha256 353 } 354 override["patches"] = { 355 platform: list(tag.patches) 356 for platform in sha256 357 } 358 359 available_versions[tag.python_version] = {k: v for k, v in override.items() if v} 360 361 if tag.distutils_content: 362 kwargs.setdefault(tag.python_version, {})["distutils_content"] = tag.distutils_content 363 if tag.distutils: 364 kwargs.setdefault(tag.python_version, {})["distutils"] = tag.distutils 365 366def _process_single_version_platform_overrides(*, tag, _fail = fail, default): 367 if not _validate_version(version = tag.python_version, _fail = _fail): 368 return 369 370 available_versions = default["tool_versions"] 371 372 if tag.python_version not in available_versions: 373 if not tag.urls or not tag.sha256 or not tag.strip_prefix: 374 _fail("When introducing a new python_version '{}', 'sha256', 'strip_prefix' and 'urls' must be specified".format(tag.python_version)) 375 return 376 available_versions[tag.python_version] = {} 377 378 if tag.coverage_tool: 379 available_versions[tag.python_version].setdefault("coverage_tool", {})[tag.platform] = tag.coverage_tool 380 if tag.patch_strip: 381 available_versions[tag.python_version].setdefault("patch_strip", {})[tag.platform] = tag.patch_strip 382 if tag.patches: 383 available_versions[tag.python_version].setdefault("patches", {})[tag.platform] = list(tag.patches) 384 if tag.sha256: 385 available_versions[tag.python_version].setdefault("sha256", {})[tag.platform] = tag.sha256 386 if tag.strip_prefix: 387 available_versions[tag.python_version].setdefault("strip_prefix", {})[tag.platform] = tag.strip_prefix 388 if tag.urls: 389 available_versions[tag.python_version].setdefault("url", {})[tag.platform] = tag.urls 390 391def _process_global_overrides(*, tag, default, _fail = fail): 392 if tag.available_python_versions: 393 available_versions = default["tool_versions"] 394 all_versions = dict(available_versions) 395 available_versions.clear() 396 for v in tag.available_python_versions: 397 if v not in all_versions: 398 _fail("unknown version '{}', known versions are: {}".format( 399 v, 400 sorted(all_versions), 401 )) 402 return 403 404 available_versions[v] = all_versions[v] 405 406 if tag.minor_mapping: 407 for minor_version, full_version in tag.minor_mapping.items(): 408 parsed = semver(minor_version) 409 if parsed.patch != None or parsed.build or parsed.pre_release: 410 fail("Expected the key to be of `X.Y` format but got `{}`".format(minor_version)) 411 parsed = semver(full_version) 412 if parsed.patch == None: 413 fail("Expected the value to at least be of `X.Y.Z` format but got `{}`".format(minor_version)) 414 415 default["minor_mapping"] = tag.minor_mapping 416 417 forwarded_attrs = sorted(AUTH_ATTRS) + [ 418 "ignore_root_user_error", 419 "base_url", 420 "register_all_versions", 421 ] 422 for key in forwarded_attrs: 423 if getattr(tag, key, None): 424 default[key] = getattr(tag, key) 425 426def _override_defaults(*overrides, modules, _fail = fail, default): 427 mod = modules[0] if modules else None 428 if not mod or not mod.is_root: 429 return 430 431 overriden_keys = [] 432 433 for override in overrides: 434 for tag in getattr(mod.tags, override.name): 435 key = override.key(tag) 436 if key not in overriden_keys: 437 overriden_keys.append(key) 438 elif key: 439 _fail("Only a single 'python.{}' can be present for '{}'".format(override.name, key)) 440 return 441 else: 442 _fail("Only a single 'python.{}' can be present".format(override.name)) 443 return 444 445 override.fn(tag = tag, _fail = _fail, default = default) 446 447def _get_toolchain_config(*, modules, _fail = fail): 448 # Items that can be overridden 449 available_versions = { 450 version: { 451 # Use a dicts straight away so that we could do URL overrides for a 452 # single version. 453 "sha256": dict(item["sha256"]), 454 "strip_prefix": { 455 platform: item["strip_prefix"] 456 for platform in item["sha256"] 457 }, 458 "url": { 459 platform: [item["url"]] 460 for platform in item["sha256"] 461 }, 462 } 463 for version, item in TOOL_VERSIONS.items() 464 } 465 default = { 466 "base_url": DEFAULT_RELEASE_BASE_URL, 467 "tool_versions": available_versions, 468 } 469 470 _override_defaults( 471 # First override by single version, because the sha256 will replace 472 # anything that has been there before. 473 struct( 474 name = "single_version_override", 475 key = lambda t: t.python_version, 476 fn = _process_single_version_overrides, 477 ), 478 # Then override particular platform entries if they need to be overridden. 479 struct( 480 name = "single_version_platform_override", 481 key = lambda t: (t.python_version, t.platform), 482 fn = _process_single_version_platform_overrides, 483 ), 484 # Then finally add global args and remove the unnecessary toolchains. 485 # This ensures that we can do further validations when removing. 486 struct( 487 name = "override", 488 key = lambda t: None, 489 fn = _process_global_overrides, 490 ), 491 modules = modules, 492 default = default, 493 _fail = _fail, 494 ) 495 496 minor_mapping = default.pop("minor_mapping", {}) 497 register_all_versions = default.pop("register_all_versions", False) 498 kwargs = default.pop("kwargs", {}) 499 500 if not minor_mapping: 501 versions = {} 502 for version_string in available_versions: 503 v = semver(version_string) 504 versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string)) 505 506 minor_mapping = { 507 major_minor: max(subset)[1] 508 for major_minor, subset in versions.items() 509 } 510 511 return struct( 512 kwargs = kwargs, 513 minor_mapping = minor_mapping, 514 default = default, 515 register_all_versions = register_all_versions, 516 ) 517 518def _create_toolchain_attr_structs(*, mod, config, seen_versions): 519 arg_structs = [] 520 521 for tag in mod.tags.toolchain: 522 arg_structs.append(_create_toolchain_attrs_struct( 523 tag = tag, 524 toolchain_tag_count = len(mod.tags.toolchain), 525 )) 526 527 seen_versions[tag.python_version] = True 528 529 if config.register_all_versions: 530 arg_structs.extend([ 531 _create_toolchain_attrs_struct(python_version = v) 532 for v in config.default["tool_versions"].keys() + config.minor_mapping.keys() 533 if v not in seen_versions 534 ]) 535 536 return arg_structs 537 538def _create_toolchain_attrs_struct(*, tag = None, python_version = None, toolchain_tag_count = None): 539 if tag and python_version: 540 fail("Only one of tag and python version can be specified") 541 if tag: 542 # A single toolchain is treated as the default because it's unambiguous. 543 is_default = tag.is_default or toolchain_tag_count == 1 544 else: 545 is_default = False 546 547 return struct( 548 is_default = is_default, 549 python_version = python_version if python_version else tag.python_version, 550 configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), 551 ignore_root_user_error = getattr(tag, "ignore_root_user_error", False), 552 ) 553 554def _get_bazel_version_specific_kwargs(): 555 kwargs = {} 556 557 if IS_BAZEL_6_4_OR_HIGHER: 558 kwargs["environ"] = ["RULES_PYTHON_BZLMOD_DEBUG"] 559 560 return kwargs 561 562_toolchain = tag_class( 563 doc = """Tag class used to register Python toolchains. 564Use this tag class to register one or more Python toolchains. This class 565is also potentially called by sub modules. The following covers different 566business rules and use cases. 567 568:::{topic} Toolchains in the Root Module 569 570This class registers all toolchains in the root module. 571::: 572 573:::{topic} Toolchains in Sub Modules 574 575It will create a toolchain that is in a sub module, if the toolchain 576of the same name does not exist in the root module. The extension stops name 577clashing between toolchains in the root module and toolchains in sub modules. 578You cannot configure more than one toolchain as the default toolchain. 579::: 580 581:::{topic} Toolchain set as the default version 582 583This extension will not create a toolchain that exists in a sub module, 584if the sub module toolchain is marked as the default version. If you have 585more than one toolchain in your root module, you need to set one of the 586toolchains as the default version. If there is only one toolchain it 587is set as the default toolchain. 588::: 589 590:::{topic} Toolchain repository name 591 592A toolchain's repository name uses the format `python_{major}_{minor}`, e.g. 593`python_3_10`. The `major` and `minor` components are 594`major` and `minor` are the Python version from the `python_version` attribute. 595 596If a toolchain is registered in `X.Y.Z`, then similarly the toolchain name will 597be `python_{major}_{minor}_{patch}`, e.g. `python_3_10_19`. 598::: 599 600:::{topic} Toolchain detection 601The definition of the first toolchain wins, which means that the root module 602can override settings for any python toolchain available. This relies on the 603documented module traversal from the {obj}`module_ctx.modules`. 604::: 605 606:::{tip} 607In order to use a different name than the above, you can use the following `MODULE.bazel` 608syntax: 609```starlark 610python = use_extension("@rules_python//python/extensions:python.bzl", "python") 611python.toolchain( 612 is_default = True, 613 python_version = "3.11", 614) 615 616use_repo(python, my_python_name = "python_3_11") 617``` 618 619Then the python interpreter will be available as `my_python_name`. 620::: 621""", 622 attrs = { 623 "configure_coverage_tool": attr.bool( 624 mandatory = False, 625 doc = "Whether or not to configure the default coverage tool provided by `rules_python` for the compatible toolchains.", 626 ), 627 "ignore_root_user_error": attr.bool( 628 default = False, 629 doc = """\ 630If `False`, the Python runtime installation will be made read only. This improves 631the ability for Bazel to cache it, but prevents the interpreter from creating 632`.pyc` files for the standard library dynamically at runtime as they are loaded. 633 634If `True`, the Python runtime installation is read-write. This allows the 635interpreter to create `.pyc` files for the standard library, but, because they are 636created as needed, it adversely affects Bazel's ability to cache the runtime and 637can result in spurious build failures. 638""", 639 mandatory = False, 640 ), 641 "is_default": attr.bool( 642 mandatory = False, 643 doc = "Whether the toolchain is the default version", 644 ), 645 "python_version": attr.string( 646 mandatory = True, 647 doc = """\ 648The Python version, in `major.minor` or `major.minor.patch` format, e.g 649`3.12` (or `3.12.3`), to create a toolchain for. 650""", 651 ), 652 }, 653) 654 655_override = tag_class( 656 doc = """Tag class used to override defaults and behaviour of the module extension. 657 658:::{versionadded} 0.36.0 659::: 660""", 661 attrs = { 662 "available_python_versions": attr.string_list( 663 mandatory = False, 664 doc = """\ 665The list of available python tool versions to use. Must be in `X.Y.Z` format. 666If the unknown version given the processing of the extension will fail - all of 667the versions in the list have to be defined with 668{obj}`python.single_version_override` or 669{obj}`python.single_version_platform_override` before they are used in this 670list. 671 672This attribute is usually used in order to ensure that no unexpected transitive 673dependencies are introduced. 674""", 675 ), 676 "base_url": attr.string( 677 mandatory = False, 678 doc = "The base URL to be used when downloading toolchains.", 679 default = DEFAULT_RELEASE_BASE_URL, 680 ), 681 "ignore_root_user_error": attr.bool( 682 default = False, 683 doc = """\ 684If `False`, the Python runtime installation will be made read only. This improves 685the ability for Bazel to cache it, but prevents the interpreter from creating 686`.pyc` files for the standard library dynamically at runtime as they are loaded. 687 688If `True`, the Python runtime installation is read-write. This allows the 689interpreter to create `.pyc` files for the standard library, but, because they are 690created as needed, it adversely affects Bazel's ability to cache the runtime and 691can result in spurious build failures. 692""", 693 mandatory = False, 694 ), 695 "minor_mapping": attr.string_dict( 696 mandatory = False, 697 doc = """\ 698The mapping between `X.Y` to `X.Y.Z` versions to be used when setting up 699toolchains. It defaults to the interpreter with the highest available patch 700version for each minor version. For example if one registers `3.10.3`, `3.10.4` 701and `3.11.4` then the default for the `minor_mapping` dict will be: 702```starlark 703{ 704"3.10": "3.10.4", 705"3.11": "3.11.4", 706} 707``` 708""", 709 default = {}, 710 ), 711 "register_all_versions": attr.bool(default = False, doc = "Add all versions"), 712 } | AUTH_ATTRS, 713) 714 715_single_version_override = tag_class( 716 doc = """Override single python version URLs and patches for all platforms. 717 718:::{note} 719This will replace any existing configuration for the given python version. 720::: 721 722:::{tip} 723If you would like to modify the configuration for a specific `(version, 724platform)`, please use the {obj}`single_version_platform_override` tag 725class. 726::: 727 728:::{versionadded} 0.36.0 729::: 730""", 731 attrs = { 732 # NOTE @aignas 2024-09-01: all of the attributes except for `version` 733 # can be part of the `python.toolchain` call. That would make it more 734 # ergonomic to define new toolchains and to override values for old 735 # toolchains. The same semantics of the `first one wins` would apply, 736 # so technically there is no need for any overrides? 737 # 738 # Although these attributes would override the code that is used by the 739 # code in non-root modules, so technically this could be thought as 740 # being overridden. 741 # 742 # rules_go has a single download call: 743 # https://github.com/bazelbuild/rules_go/blob/master/go/private/extensions.bzl#L38 744 # 745 # However, we need to understand how to accommodate the fact that 746 # {attr}`single_version_override.version` only allows patch versions. 747 "distutils": attr.label( 748 allow_single_file = True, 749 doc = "A distutils.cfg file to be included in the Python installation. " + 750 "Either {attr}`distutils` or {attr}`distutils_content` can be specified, but not both.", 751 mandatory = False, 752 ), 753 "distutils_content": attr.string( 754 doc = "A distutils.cfg file content to be included in the Python installation. " + 755 "Either {attr}`distutils` or {attr}`distutils_content` can be specified, but not both.", 756 mandatory = False, 757 ), 758 "patch_strip": attr.int( 759 mandatory = False, 760 doc = "Same as the --strip argument of Unix patch.", 761 default = 0, 762 ), 763 "patches": attr.label_list( 764 mandatory = False, 765 doc = "A list of labels pointing to patch files to apply for the interpreter repository. They are applied in the list order and are applied before any platform-specific patches are applied.", 766 ), 767 "python_version": attr.string( 768 mandatory = True, 769 doc = "The python version to override URLs for. Must be in `X.Y.Z` format.", 770 ), 771 "sha256": attr.string_dict( 772 mandatory = False, 773 doc = "The python platform to sha256 dict. See {attr}`python.single_version_platform_override.platform` for allowed key values.", 774 ), 775 "strip_prefix": attr.string( 776 mandatory = False, 777 doc = "The 'strip_prefix' for the archive, defaults to 'python'.", 778 default = "python", 779 ), 780 "urls": attr.string_list( 781 mandatory = False, 782 doc = "The URL template to fetch releases for this Python version. See {attr}`python.single_version_platform_override.urls` for documentation.", 783 ), 784 }, 785) 786 787_single_version_platform_override = tag_class( 788 doc = """Override single python version for a single existing platform. 789 790If the `(version, platform)` is new, we will add it to the existing versions and will 791use the same `url` template. 792 793:::{tip} 794If you would like to add or remove platforms to a single python version toolchain 795configuration, please use {obj}`single_version_override`. 796::: 797 798:::{versionadded} 0.36.0 799::: 800""", 801 attrs = { 802 "coverage_tool": attr.label( 803 doc = """\ 804The coverage tool to be used for a particular Python interpreter. This can override 805`rules_python` defaults. 806""", 807 ), 808 "patch_strip": attr.int( 809 mandatory = False, 810 doc = "Same as the --strip argument of Unix patch.", 811 default = 0, 812 ), 813 "patches": attr.label_list( 814 mandatory = False, 815 doc = "A list of labels pointing to patch files to apply for the interpreter repository. They are applied in the list order and are applied after the common patches are applied.", 816 ), 817 "platform": attr.string( 818 mandatory = True, 819 values = PLATFORMS.keys(), 820 doc = "The platform to override the values for, must be one of:\n{}.".format("\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS]))), 821 ), 822 "python_version": attr.string( 823 mandatory = True, 824 doc = "The python version to override URLs for. Must be in `X.Y.Z` format.", 825 ), 826 "sha256": attr.string( 827 mandatory = False, 828 doc = "The sha256 for the archive", 829 ), 830 "strip_prefix": attr.string( 831 mandatory = False, 832 doc = "The 'strip_prefix' for the archive, defaults to 'python'.", 833 default = "python", 834 ), 835 "urls": attr.string_list( 836 mandatory = False, 837 doc = "The URL template to fetch releases for this Python version. If the URL template results in a relative fragment, default base URL is going to be used. Occurrences of `{python_version}`, `{platform}` and `{build}` will be interpolated based on the contents in the override and the known {attr}`platform` values.", 838 ), 839 }, 840) 841 842python = module_extension( 843 doc = """Bzlmod extension that is used to register Python toolchains. 844""", 845 implementation = _python_impl, 846 tag_classes = { 847 "override": _override, 848 "single_version_override": _single_version_override, 849 "single_version_platform_override": _single_version_platform_override, 850 "toolchain": _toolchain, 851 }, 852 **_get_bazel_version_specific_kwargs() 853) 854 855_DEBUG_BUILD_CONTENT = """ 856package( 857 default_visibility = ["//visibility:public"], 858) 859exports_files(["debug_info.json"]) 860""" 861 862def _debug_repo_impl(repo_ctx): 863 repo_ctx.file("BUILD.bazel", _DEBUG_BUILD_CONTENT) 864 repo_ctx.file("debug_info.json", repo_ctx.attr.debug_info) 865 866_debug_repo = repository_rule( 867 implementation = _debug_repo_impl, 868 attrs = { 869 "debug_info": attr.string(), 870 }, 871) 872