| # Copyright 2023 The Bazel Authors. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| "Python toolchain module extensions for use with bzlmod." | |
| load("@bazel_features//:features.bzl", "bazel_features") | |
| load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URLS", "PLATFORMS") | |
| load(":auth.bzl", "AUTH_ATTRS") | |
| load(":full_version.bzl", "full_version") | |
| load(":pbs_manifest.bzl", "parse_runtime_manifest") | |
| load(":platform_info.bzl", "platform_info") | |
| load(":pyproject_utils.bzl", "read_pyproject", "version_from_requires_python") | |
| load(":python_register_toolchains.bzl", "python_register_toolchains") | |
| load(":pythons_hub.bzl", "hub_repo") | |
| load(":repo_utils.bzl", "repo_utils") | |
| load( | |
| ":toolchains_repo.bzl", | |
| "host_compatible_python_repo", | |
| "multi_toolchain_aliases", | |
| "sorted_host_platform_names", | |
| "sorted_host_platforms", | |
| ) | |
| load(":version.bzl", "version") | |
| def parse_modules(*, module_ctx, logger = None, _fail = fail): | |
| """Parse the modules and return a struct for registrations. | |
| Args: | |
| module_ctx: {type}`module_ctx` module context. | |
| logger: {type}`repo_utils.logger` A logger to use. | |
| _fail: {type}`function` the failure function, mainly for testing. | |
| Returns: | |
| A struct with the following attributes: | |
| * `toolchains`: {type}`list[ToolchainConfig]` The list of toolchains to | |
| register. The last element is special and is treated as the default | |
| toolchain. | |
| * `config`: Various toolchain config, see `_get_toolchain_config`. | |
| * `debug_info`: {type}`None | dict` extra information to be passed | |
| to the debug repo. | |
| * `platforms`: {type}`dict[str, platform_info]` of the base set of | |
| platforms toolchains should be created for, if possible. | |
| ToolchainConfig struct: | |
| * python_version: str, full python version string | |
| * name: str, the base toolchain name, e.g., "python_3_10", no | |
| platform suffix. | |
| * register_coverage_tool: bool | |
| """ | |
| if module_ctx.getenv("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": | |
| debug_info = { | |
| "toolchains_registered": [], | |
| } | |
| else: | |
| debug_info = None | |
| # The toolchain_info structs to register, in the order to register them in. | |
| # NOTE: The last element is special: it is treated as the default toolchain, | |
| # so there is special handling to ensure the last entry is the correct one. | |
| toolchains = [] | |
| # We store the default toolchain separately to ensure it is the last | |
| # toolchain added to toolchains. | |
| # This is a toolchain_info struct. | |
| default_toolchain = None | |
| # Map of string Major.Minor or Major.Minor.Patch to the toolchain_info struct | |
| global_toolchain_versions = {} | |
| config = _get_toolchain_config(mctx = module_ctx, modules = module_ctx.modules, _fail = _fail) | |
| default_python_version = _compute_default_python_version(module_ctx) | |
| seen_versions = {} | |
| for mod in module_ctx.modules: | |
| module_toolchain_versions = [] | |
| toolchain_attr_structs = _create_toolchain_attr_structs( | |
| mod = mod, | |
| seen_versions = seen_versions, | |
| config = config, | |
| default_python_version = default_python_version, | |
| ) | |
| for toolchain_attr in toolchain_attr_structs: | |
| toolchain_version = toolchain_attr.python_version | |
| toolchain_name = "python_" + toolchain_version.replace(".", "_") | |
| # Duplicate versions within a module indicate a misconfigured module. | |
| if toolchain_version in module_toolchain_versions: | |
| _fail_duplicate_module_toolchain_version(toolchain_version, mod.name) | |
| module_toolchain_versions.append(toolchain_version) | |
| if mod.is_root: | |
| # Only the root module and rules_python are allowed to specify the default | |
| # toolchain for a couple reasons: | |
| # * It prevents submodules from specifying different defaults and only | |
| # one of them winning. | |
| # * rules_python needs to set a soft default in case the root module doesn't, | |
| # e.g. if the root module doesn't use Python itself. | |
| # * The root module is allowed to override the rules_python default. | |
| is_default = default_python_version == toolchain_version | |
| elif mod.name == "rules_python" and not default_toolchain: | |
| # This branch handles when the root module doesn't declare a | |
| # Python toolchain | |
| is_default = default_python_version == toolchain_version | |
| else: | |
| is_default = False | |
| if is_default and default_toolchain != None: | |
| _fail_multiple_default_toolchains_chosen( | |
| first = default_toolchain.name, | |
| second = toolchain_name, | |
| ) | |
| # Ignore version collisions in the global scope because there isn't | |
| # much else that can be done. Modules don't know and can't control | |
| # what other modules do, so the first in the dependency graph wins. | |
| if toolchain_version in global_toolchain_versions: | |
| # If the python version is explicitly provided by the root | |
| # module, they should not be warned for choosing the same | |
| # version that rules_python provides as default. | |
| first = global_toolchain_versions[toolchain_version] | |
| if mod.name != "rules_python" or not first.module.is_root: | |
| # The warning can be enabled by setting the verbosity: | |
| # env RULES_PYTHON_REPO_DEBUG_VERBOSITY=INFO bazel build //... | |
| _warn_duplicate_global_toolchain_version( | |
| toolchain_version, | |
| first = first, | |
| second_toolchain_name = toolchain_name, | |
| second_module_name = mod.name, | |
| logger = logger or repo_utils.logger(module_ctx, "python", mod = mod), | |
| ) | |
| toolchain_info = None | |
| else: | |
| toolchain_info = struct( | |
| python_version = toolchain_attr.python_version, | |
| name = toolchain_name, | |
| register_coverage_tool = toolchain_attr.configure_coverage_tool, | |
| module = struct(name = mod.name, is_root = mod.is_root), | |
| ) | |
| global_toolchain_versions[toolchain_version] = toolchain_info | |
| if debug_info: | |
| debug_info["toolchains_registered"].append({ | |
| "module": {"is_root": mod.is_root, "name": mod.name}, | |
| "name": toolchain_name, | |
| }) | |
| if is_default: | |
| # This toolchain is setting the default, but the actual | |
| # registration was performed previously, by a different module. | |
| if toolchain_info == None: | |
| default_toolchain = global_toolchain_versions[toolchain_version] | |
| # Remove it because later code will add it at the end to | |
| # ensure it is last in the list. | |
| toolchains.remove(default_toolchain) | |
| else: | |
| default_toolchain = toolchain_info | |
| elif toolchain_info: | |
| toolchains.append(toolchain_info) | |
| # A default toolchain is required so that the non-version-specific rules | |
| # are able to match a toolchain. | |
| if default_toolchain == None: | |
| fail("No default Python toolchain configured. Is rules_python missing `python.defaults()`?") | |
| elif default_toolchain.python_version not in global_toolchain_versions: | |
| fail('Default version "{python_version}" selected by module ' + | |
| '"{module_name}", but no toolchain with that version registered'.format( | |
| python_version = default_toolchain.python_version, | |
| module_name = default_toolchain.module.name, | |
| )) | |
| # The last toolchain in the BUILD file is set as the default | |
| # toolchain. We need the default last. | |
| toolchains.append(default_toolchain) | |
| # sort the toolchains so that the toolchain versions that are in the | |
| # `minor_mapping` are coming first. This ensures that `python_version = | |
| # "3.X"` transitions work as expected. | |
| minor_version_toolchains = [] | |
| other_toolchains = [] | |
| minor_mapping = list(config.minor_mapping.values()) | |
| for t in toolchains: | |
| # FIXME @aignas 2025-04-04: How can we unit test that this ordering is | |
| # consistent with what would actually work? | |
| if config.minor_mapping.get(t.python_version, t.python_version) in minor_mapping: | |
| minor_version_toolchains.append(t) | |
| else: | |
| other_toolchains.append(t) | |
| toolchains = minor_version_toolchains + other_toolchains | |
| return struct( | |
| config = config, | |
| debug_info = debug_info, | |
| default_python_version = default_toolchain.python_version, | |
| toolchains = [ | |
| struct( | |
| python_version = t.python_version, | |
| name = t.name, | |
| register_coverage_tool = t.register_coverage_tool, | |
| ) | |
| for t in toolchains | |
| ], | |
| ) | |
| def _python_impl(module_ctx): | |
| py = parse_modules(module_ctx = module_ctx) | |
| # For all other processing (after parsing the modules) let's use a single logger. | |
| logger = repo_utils.logger(module_ctx, "python", mod = module_ctx.modules[0]) | |
| # Host compatible runtime repos | |
| # dict[str version, struct] where struct has: | |
| # * full_python_version: str | |
| # * platform: platform_info struct | |
| # * platform_name: str platform name | |
| # * impl_repo_name: str repo name of the runtime's python_repository() repo | |
| all_host_compatible_impls = {} | |
| # Host compatible repos that still need to be created because, when | |
| # creating the actual runtime repo, there wasn't a host-compatible | |
| # variant defined for it. | |
| # dict[str reponame, struct] where struct has: | |
| # * compatible_version: str, e.g. 3.10 or 3.10.1. The version the host | |
| # repo should be compatible with | |
| # * full_python_version: str, e.g. 3.10.1, the full python version of | |
| # the toolchain that still needs a host repo created. | |
| needed_host_repos = {} | |
| # list of structs; see inline struct call within the loop below. | |
| toolchain_impls = [] | |
| # list[str] of the repo names for host compatible repos | |
| all_host_compatible_repo_names = [] | |
| # Create the underlying python_repository repos that contain the | |
| # python runtimes and their toolchain implementation definitions. | |
| for i, toolchain_info in enumerate(py.toolchains): | |
| is_last = (i + 1) == len(py.toolchains) | |
| # Ensure that we pass the full version here. | |
| full_python_version = full_version( | |
| version = toolchain_info.python_version, | |
| minor_mapping = py.config.minor_mapping, | |
| fail_on_err = False, | |
| ) | |
| if not full_python_version: | |
| logger.info(lambda: ( | |
| "The actual toolchain for python_version '{version}' " + | |
| "has not been registered, but was requested, please configure a toolchain " + | |
| "to be actually downloaded and setup" | |
| ).format( | |
| version = toolchain_info.python_version, | |
| )) | |
| continue | |
| kwargs = { | |
| "python_version": full_python_version, | |
| "register_coverage_tool": toolchain_info.register_coverage_tool, | |
| } | |
| # Allow overrides per python version | |
| kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {})) | |
| kwargs.update(py.config.kwargs.get(full_python_version, {})) | |
| kwargs.update(py.config.default) | |
| register_result = python_register_toolchains( | |
| name = toolchain_info.name, | |
| _internal_bzlmod_toolchain_call = True, | |
| **kwargs | |
| ) | |
| if not register_result.impl_repos: | |
| continue | |
| host_platforms = {} | |
| for repo_name, (platform_name, platform_info) in register_result.impl_repos.items(): | |
| toolchain_impls.append(struct( | |
| # str: The base name to use for the toolchain() target | |
| name = repo_name, | |
| # str: The repo name the toolchain() target points to. | |
| impl_repo_name = repo_name, | |
| # str: platform key in the passed-in platforms dict | |
| platform_name = platform_name, | |
| # struct: platform_info() struct | |
| platform = platform_info, | |
| # str: Major.Minor.Micro python version | |
| full_python_version = full_python_version, | |
| # bool: whether to implicitly add the python version constraint | |
| # to the toolchain's target_settings. | |
| # The last toolchain is the default; it can't have version constraints | |
| set_python_version_constraint = is_last, | |
| )) | |
| if _is_compatible_with_host(module_ctx, platform_info): | |
| host_compat_entry = struct( | |
| full_python_version = full_python_version, | |
| platform = platform_info, | |
| platform_name = platform_name, | |
| impl_repo_name = repo_name, | |
| ) | |
| host_platforms[platform_name] = host_compat_entry | |
| all_host_compatible_impls.setdefault(full_python_version, []).append( | |
| host_compat_entry, | |
| ) | |
| parsed_version = version.parse(full_python_version) | |
| all_host_compatible_impls.setdefault( | |
| "{}.{}".format(*parsed_version.release[0:2]), | |
| [], | |
| ).append(host_compat_entry) | |
| host_repo_name = toolchain_info.name + "_host" | |
| if host_platforms: | |
| all_host_compatible_repo_names.append(host_repo_name) | |
| host_platforms = sorted_host_platforms(host_platforms) | |
| entries = host_platforms.values() | |
| host_compatible_python_repo( | |
| name = host_repo_name, | |
| base_name = host_repo_name, | |
| # NOTE: Order matters. The first found to be compatible is | |
| # (usually) used. | |
| platforms = host_platforms.keys(), | |
| os_names = {str(i): e.platform.os_name for i, e in enumerate(entries)}, | |
| arch_names = {str(i): e.platform.arch for i, e in enumerate(entries)}, | |
| python_versions = {str(i): e.full_python_version for i, e in enumerate(entries)}, | |
| impl_repo_names = {str(i): e.impl_repo_name for i, e in enumerate(entries)}, | |
| ) | |
| else: | |
| needed_host_repos[host_repo_name] = struct( | |
| compatible_version = toolchain_info.python_version, | |
| full_python_version = full_python_version, | |
| ) | |
| if needed_host_repos: | |
| for key, entries in all_host_compatible_impls.items(): | |
| all_host_compatible_impls[key] = sorted( | |
| entries, | |
| reverse = True, | |
| key = lambda e: version.key(version.parse(e.full_python_version)), | |
| ) | |
| for host_repo_name, info in needed_host_repos.items(): | |
| choices = [] | |
| if info.compatible_version not in all_host_compatible_impls: | |
| logger.warn("No host compatible runtime found compatible with version {}".format(info.compatible_version)) | |
| continue | |
| choices = all_host_compatible_impls[info.compatible_version] | |
| platform_keys = [ | |
| # We have to prepend the offset because the same platform | |
| # name might occur across different versions | |
| "{}_{}".format(i, entry.platform_name) | |
| for i, entry in enumerate(choices) | |
| ] | |
| platform_keys = sorted_host_platform_names(platform_keys) | |
| all_host_compatible_repo_names.append(host_repo_name) | |
| host_compatible_python_repo( | |
| name = host_repo_name, | |
| base_name = host_repo_name, | |
| platforms = platform_keys, | |
| impl_repo_names = { | |
| str(i): entry.impl_repo_name | |
| for i, entry in enumerate(choices) | |
| }, | |
| os_names = {str(i): entry.platform.os_name for i, entry in enumerate(choices)}, | |
| arch_names = {str(i): entry.platform.arch for i, entry in enumerate(choices)}, | |
| python_versions = {str(i): entry.full_python_version for i, entry in enumerate(choices)}, | |
| ) | |
| # list[str] The infix to use for the resulting toolchain() `name` arg. | |
| toolchain_names = [] | |
| # dict[str i, str repo]; where repo is the full repo name | |
| # ("python_3_10_unknown-linux-x86_64") for the toolchain | |
| # i corresponds to index `i` in toolchain_names | |
| toolchain_repo_names = {} | |
| # dict[str i, list[str] constraints]; where constraints is a list | |
| # of labels for target_compatible_with | |
| # i corresponds to index `i` in toolchain_names | |
| toolchain_tcw_map = {} | |
| # dict[str i, list[str] settings]; where settings is a list | |
| # of labels for target_settings | |
| # i corresponds to index `i` in toolchain_names | |
| toolchain_ts_map = {} | |
| # dict[str i, str set_constraint]; where set_constraint is the string | |
| # "True" or "False". | |
| # i corresponds to index `i` in toolchain_names | |
| toolchain_set_python_version_constraints = {} | |
| # dict[str i, str python_version]; where python_version is the full | |
| # python version ("3.4.5"). | |
| toolchain_python_versions = {} | |
| # dict[str i, str platform_key]; where platform_key is the key within | |
| # the PLATFORMS global for this toolchain | |
| toolchain_platform_keys = {} | |
| # Extra target_settings to add to every registered toolchain, e.g. for | |
| # gating the default toolchains behind a custom config_setting. | |
| global_add_target_settings = py.config.add_target_settings | |
| # Split the toolchain info into separate objects so they can be passed onto | |
| # the repository rule. | |
| for entry in toolchain_impls: | |
| key = str(len(toolchain_names)) | |
| toolchain_names.append(entry.name) | |
| toolchain_repo_names[key] = entry.impl_repo_name | |
| toolchain_tcw_map[key] = entry.platform.compatible_with | |
| # The target_settings attribute may not be present for users | |
| # patching python/versions.bzl. | |
| toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) + global_add_target_settings | |
| toolchain_platform_keys[key] = entry.platform_name | |
| toolchain_python_versions[key] = entry.full_python_version | |
| # Repo rules can't accept dict[str, bool], so encode them as a string value. | |
| toolchain_set_python_version_constraints[key] = ( | |
| "True" if entry.set_python_version_constraint else "False" | |
| ) | |
| hub_repo( | |
| name = "pythons_hub", | |
| toolchain_names = toolchain_names, | |
| toolchain_repo_names = toolchain_repo_names, | |
| toolchain_target_compatible_with_map = toolchain_tcw_map, | |
| toolchain_target_settings_map = toolchain_ts_map, | |
| toolchain_platform_keys = toolchain_platform_keys, | |
| toolchain_python_versions = toolchain_python_versions, | |
| toolchain_set_python_version_constraints = toolchain_set_python_version_constraints, | |
| host_compatible_repo_names = sorted(all_host_compatible_repo_names), | |
| default_python_version = py.default_python_version, | |
| minor_mapping = py.config.minor_mapping, | |
| python_versions = list(py.config.default["tool_versions"].keys()), | |
| ) | |
| # This is require in order to support multiple version py_test | |
| # and py_binary | |
| multi_toolchain_aliases( | |
| name = "python_versions", | |
| python_versions = { | |
| toolchain.python_version: toolchain.name | |
| for toolchain in py.toolchains | |
| }, | |
| ) | |
| if py.debug_info != None: | |
| _debug_repo( | |
| name = "rules_python_bzlmod_debug", | |
| debug_info = json.encode_indent(py.debug_info), | |
| ) | |
| if bazel_features.external_deps.extension_metadata_has_reproducible: | |
| return module_ctx.extension_metadata(reproducible = True) | |
| else: | |
| return None | |
| def _is_compatible_with_host(mctx, platform_info): | |
| os_name = repo_utils.get_platforms_os_name(mctx) | |
| cpu_name = repo_utils.get_platforms_cpu_name(mctx) | |
| return platform_info.os_name == os_name and platform_info.arch == cpu_name | |
| def _one_or_the_same(first, second, *, onerror = None): | |
| if not first: | |
| return second | |
| if not second or second == first: | |
| return first | |
| if onerror: | |
| return onerror(first, second) | |
| else: | |
| fail("Unique value needed, got both '{}' and '{}', which are different".format( | |
| first, | |
| second, | |
| )) | |
| def _fail_duplicate_module_toolchain_version(version, module): | |
| fail(("Duplicate module toolchain version: module '{module}' attempted " + | |
| "to use version '{version}' multiple times in itself").format( | |
| version = version, | |
| module = module, | |
| )) | |
| def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_name, second_module_name, logger): | |
| if not logger: | |
| return | |
| logger.info(lambda: ( | |
| "Ignoring toolchain '{second_toolchain}' from module '{second_module}': " + | |
| "Toolchain '{first_toolchain}' from module '{first_module}' " + | |
| "already registered Python version {version} and has precedence." | |
| ).format( | |
| first_toolchain = first.name, | |
| first_module = first.module.name, | |
| second_module = second_module_name, | |
| second_toolchain = second_toolchain_name, | |
| version = version, | |
| )) | |
| def _fail_multiple_defaults_python_version(first, second): | |
| fail(("Multiple python_version entries in defaults: " + | |
| "First default was python_version '{first}'. " + | |
| "Second was python_version '{second}'").format( | |
| first = first, | |
| second = second, | |
| )) | |
| def _fail_multiple_defaults_python_version_file(first, second): | |
| fail(("Multiple python_version_file entries in defaults: " + | |
| "First default was python_version_file '{first}'. " + | |
| "Second was python_version_file '{second}'").format( | |
| first = first, | |
| second = second, | |
| )) | |
| def _fail_multiple_defaults_python_version_env(first, second): | |
| fail(("Multiple python_version_env entries in defaults: " + | |
| "First default was python_version_env '{first}'. " + | |
| "Second was python_version_env '{second}'").format( | |
| first = first, | |
| second = second, | |
| )) | |
| def _fail_multiple_default_toolchains_chosen(first, second): | |
| fail(("Multiple default toolchains: only one toolchain " + | |
| "can be chosen as a default. First default " + | |
| "was toolchain '{first}'. Second was '{second}'").format( | |
| first = first, | |
| second = second, | |
| )) | |
| def _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs): | |
| fail(("Multiple default toolchains: only one toolchain " + | |
| "can have is_default=True.\n" + | |
| "Module '{module}' contains {count} toolchains with " + | |
| "is_default=True: {versions}").format( | |
| module = mod.name, | |
| count = len(toolchain_attrs), | |
| versions = ", ".join(sorted([v.python_version for v in toolchain_attrs])), | |
| )) | |
| def _validate_version(version_str, *, _fail = fail): | |
| v = version.parse(version_str, strict = True, _fail = _fail) | |
| if v == None: | |
| # Only reachable in tests | |
| return False | |
| if len(v.release) < 3: | |
| _fail("The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '{}'".format(v.string)) | |
| return False | |
| return True | |
| def _process_single_version_overrides(*, tag, _fail = fail, default): | |
| if not _validate_version(tag.python_version, _fail = _fail): | |
| return | |
| available_versions = default["tool_versions"] | |
| kwargs = default.setdefault("kwargs", {}) | |
| if tag.sha256 or tag.urls: | |
| if not (tag.sha256 and tag.urls): | |
| _fail("Both `sha256` and `urls` overrides need to be provided together") | |
| return | |
| for platform in (tag.sha256 or []): | |
| if platform not in default["platforms"]: | |
| _fail("The platform must be one of {allowed} but got '{got}'".format( | |
| allowed = sorted(default["platforms"]), | |
| got = platform, | |
| )) | |
| return | |
| sha256 = dict(tag.sha256) or available_versions[tag.python_version]["sha256"] | |
| override = { | |
| "sha256": sha256, | |
| "strip_prefix": { | |
| platform: tag.strip_prefix | |
| for platform in sha256 | |
| }, | |
| "url": { | |
| platform: list(tag.urls) | |
| for platform in tag.sha256 | |
| } or available_versions[tag.python_version]["url"], | |
| } | |
| if tag.patches: | |
| override["patch_strip"] = { | |
| platform: tag.patch_strip | |
| for platform in sha256 | |
| } | |
| override["patches"] = { | |
| platform: list(tag.patches) | |
| for platform in sha256 | |
| } | |
| available_versions[tag.python_version] = {k: v for k, v in override.items() if v} | |
| if tag.distutils_content: | |
| kwargs.setdefault(tag.python_version, {})["distutils_content"] = tag.distutils_content | |
| if tag.distutils: | |
| kwargs.setdefault(tag.python_version, {})["distutils"] = tag.distutils | |
| def _process_single_version_platform_overrides(*, tag, _fail = fail, default): | |
| if not _validate_version(tag.python_version, _fail = _fail): | |
| return | |
| available_versions = default["tool_versions"] | |
| if tag.python_version not in available_versions: | |
| if not tag.urls or not tag.sha256 or not tag.strip_prefix: | |
| _fail("When introducing a new python_version '{}', 'sha256', 'strip_prefix' and 'urls' must be specified".format(tag.python_version)) | |
| return | |
| available_versions[tag.python_version] = {} | |
| if tag.coverage_tool: | |
| available_versions[tag.python_version].setdefault("coverage_tool", {})[tag.platform] = tag.coverage_tool | |
| if tag.patch_strip: | |
| available_versions[tag.python_version].setdefault("patch_strip", {})[tag.platform] = tag.patch_strip | |
| if tag.patches: | |
| available_versions[tag.python_version].setdefault("patches", {})[tag.platform] = list(tag.patches) | |
| if tag.sha256: | |
| available_versions[tag.python_version].setdefault("sha256", {})[tag.platform] = tag.sha256 | |
| if tag.strip_prefix: | |
| available_versions[tag.python_version].setdefault("strip_prefix", {})[tag.platform] = tag.strip_prefix | |
| if tag.urls: | |
| available_versions[tag.python_version].setdefault("url", {})[tag.platform] = tag.urls | |
| # If platform is customized, or doesn't exist, (re)define one. | |
| if ((tag.target_compatible_with or tag.target_settings or tag.os_name or tag.arch) or | |
| tag.platform not in default["platforms"]): | |
| os_name = tag.os_name | |
| arch = tag.arch | |
| if not tag.target_compatible_with: | |
| target_compatible_with = [] | |
| if os_name: | |
| target_compatible_with.append("@platforms//os:{}".format( | |
| repo_utils.get_platforms_os_name(os_name), | |
| )) | |
| if arch: | |
| target_compatible_with.append("@platforms//cpu:{}".format( | |
| repo_utils.get_platforms_cpu_name(arch), | |
| )) | |
| else: | |
| target_compatible_with = tag.target_compatible_with | |
| # For lack of a better option, give a bogus value. It only affects | |
| # if the runtime is considered host-compatible. | |
| if not os_name: | |
| os_name = "UNKNOWN_CUSTOM_OS" | |
| if not arch: | |
| arch = "UNKNOWN_CUSTOM_ARCH" | |
| # Move the override earlier in the ordering -- the platform key ordering | |
| # becomes the toolchain ordering within the version. This allows the | |
| # override to have a superset of constraints from a regular runtimes | |
| # (e.g. same platform, but with a custom flag required). | |
| override_first = { | |
| tag.platform: platform_info( | |
| compatible_with = target_compatible_with, | |
| target_settings = tag.target_settings, | |
| os_name = os_name, | |
| arch = arch, | |
| ), | |
| } | |
| for key, value in default["platforms"].items(): | |
| # Don't replace our override with the old value | |
| if key in override_first: | |
| continue | |
| override_first[key] = value | |
| default["platforms"] = override_first | |
| def _process_global_overrides(*, tag, default, _fail = fail): | |
| if tag.available_python_versions: | |
| available_versions = default["tool_versions"] | |
| all_versions = dict(available_versions) | |
| available_versions.clear() | |
| for v in tag.available_python_versions: | |
| if v not in all_versions: | |
| _fail("unknown version '{}', known versions are: {}".format( | |
| v, | |
| sorted(all_versions), | |
| )) | |
| return | |
| available_versions[v] = all_versions[v] | |
| if tag.minor_mapping: | |
| for minor_version, full_version in tag.minor_mapping.items(): | |
| parsed = version.parse(minor_version, strict = True, _fail = _fail) | |
| if len(parsed.release) > 2 or parsed.pre or parsed.post or parsed.dev or parsed.local: | |
| fail("Expected the key to be of `X.Y` format but got `{}`".format(parsed.string)) | |
| # Ensure that the version is valid | |
| version.parse(full_version, strict = True, _fail = _fail) | |
| default["minor_mapping"] = tag.minor_mapping | |
| if tag.add_target_settings: | |
| default["add_target_settings"] = list(tag.add_target_settings) | |
| forwarded_attrs = sorted(AUTH_ATTRS) + [ | |
| "base_urls", | |
| "register_all_versions", | |
| ] | |
| for key in forwarded_attrs: | |
| if getattr(tag, key, None): | |
| default[key] = getattr(tag, key) | |
| def _override_defaults(*overrides, modules, _fail = fail, default): | |
| mod = modules[0] if modules else None | |
| if not mod or not mod.is_root: | |
| return | |
| overriden_keys = [] | |
| for override in overrides: | |
| for tag in getattr(mod.tags, override.name): | |
| key = override.key(tag) | |
| if key not in overriden_keys: | |
| overriden_keys.append(key) | |
| elif key: | |
| _fail("Only a single 'python.{}' can be present for '{}'".format(override.name, key)) | |
| return | |
| else: | |
| _fail("Only a single 'python.{}' can be present".format(override.name)) | |
| return | |
| override.fn(tag = tag, _fail = _fail, default = default) | |
| def _manifest_entry_sort_key(entry): | |
| flavor_rank = {"full": 3, "install_only": 1, "install_only_stripped": 2}.get(entry.archive_flavor, 4) | |
| microarch = entry.microarch | |
| if not microarch: | |
| microarch_rank = 0 | |
| elif microarch.startswith("v") and microarch[1:].isdigit(): | |
| microarch_rank = int(microarch[1:]) | |
| else: | |
| microarch_rank = 999 | |
| return (flavor_rank, microarch_rank) | |
| def _populate_from_pbs_manifest( | |
| *, | |
| mctx, | |
| add_runtime_manifest_urls = [], | |
| add_runtime_manifest_files = [], | |
| runtime_manifest_sha = "", | |
| base_urls = [], | |
| available_versions, | |
| _fail): | |
| manifest_contents = [] | |
| if add_runtime_manifest_urls: | |
| manifest_path = mctx.path("runtime_manifest") | |
| result = mctx.download( | |
| url = add_runtime_manifest_urls, | |
| output = manifest_path, | |
| sha256 = runtime_manifest_sha, | |
| ) | |
| if not result.success: | |
| _fail("Failed to download manifest from {}: {}".format(add_runtime_manifest_urls, result)) | |
| return | |
| manifest_contents.append(mctx.read(manifest_path)) | |
| for manifest_file in add_runtime_manifest_files: | |
| manifest_contents.append(mctx.read(manifest_file, watch = "yes")) | |
| if not manifest_contents: | |
| return | |
| base_download_urls = [url.rpartition("/")[0] for url in add_runtime_manifest_urls] | |
| if not base_download_urls and base_urls: | |
| base_download_urls = list(base_urls) | |
| entries = [] | |
| for content in manifest_contents: | |
| entries.extend(parse_runtime_manifest(content)) | |
| # We don't model archive_flavor via flags yet, so have to pick one. | |
| # Preference is given to install_only because its smaller | |
| entries = sorted( | |
| entries, | |
| key = _manifest_entry_sort_key, | |
| ) | |
| for entry in entries: | |
| location = entry.location | |
| sha256 = entry.sha256 | |
| py_version = entry.python_version | |
| # Fallback to matching against PLATFORMS keys as before to ensure compatibility | |
| # with rules_python expected platform keys. | |
| matched_platform = "{}-{}-{}".format(entry.arch, entry.vendor, entry.os) | |
| if entry.libc: | |
| matched_platform += "-" + entry.libc | |
| if entry.freethreaded: | |
| matched_platform += "-freethreaded" | |
| if matched_platform not in PLATFORMS: | |
| continue | |
| if entry.archive_flavor not in ["install_only", "install_only_stripped", "full"]: | |
| continue | |
| v_dict = available_versions.setdefault(py_version, {}) | |
| if matched_platform in v_dict.get("sha256", {}): | |
| continue | |
| if "://" in location: | |
| urls = [location] | |
| else: | |
| urls = ["{}/{}".format(b_url, location) for b_url in base_download_urls] | |
| strip_prefix = "python/install" if entry.archive_flavor == "full" else "python" | |
| v_dict.setdefault("sha256", {})[matched_platform] = sha256 | |
| v_dict.setdefault("url", {})[matched_platform] = urls | |
| v_dict.setdefault("strip_prefix", {})[matched_platform] = strip_prefix | |
| def _get_toolchain_config(*, mctx, modules, _fail = fail): | |
| """Computes the configs for toolchains. | |
| Args: | |
| mctx: The module context. | |
| modules: The modules from module_ctx | |
| _fail: Function to call for failing; only used for testing. | |
| Returns: | |
| A struct with the following: | |
| * `kwargs`: {type}`dict[str, dict[str, object]` custom kwargs to pass to | |
| `python_register_toolchains`, keyed by python version. | |
| The first key is either a Major.Minor or Major.Minor.Patch | |
| string. | |
| * `minor_mapping`: {type}`dict[str, str]` the mapping of Major.Minor | |
| to Major.Minor.Patch. | |
| * `default`: {type}`dict[str, object]` of kwargs passed along to | |
| `python_register_toolchains`. These keys take final precedence. | |
| * `register_all_versions`: {type}`bool` whether all known versions | |
| should be registered. | |
| """ | |
| # Items that can be overridden | |
| available_versions = {} | |
| _populate_from_pbs_manifest( | |
| mctx = mctx, | |
| add_runtime_manifest_files = [Label("//python/private:runtimes_manifest.txt")], | |
| base_urls = DEFAULT_RELEASE_BASE_URLS, | |
| available_versions = available_versions, | |
| _fail = _fail, | |
| ) | |
| # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module | |
| root_module = modules[0] if modules else None | |
| if root_module and root_module.is_root: | |
| for tag in root_module.tags.override: | |
| if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files: | |
| _populate_from_pbs_manifest( | |
| mctx = mctx, | |
| add_runtime_manifest_urls = tag.add_runtime_manifest_urls, | |
| add_runtime_manifest_files = tag.add_runtime_manifest_files, | |
| runtime_manifest_sha = tag.runtime_manifest_sha, | |
| base_urls = tag.base_urls, | |
| available_versions = available_versions, | |
| _fail = _fail, | |
| ) | |
| # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module | |
| root_module = modules[0] if modules else None | |
| if root_module and root_module.is_root: | |
| for tag in root_module.tags.override: | |
| if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files: | |
| _populate_from_pbs_manifest( | |
| mctx = mctx, | |
| add_runtime_manifest_urls = tag.add_runtime_manifest_urls, | |
| add_runtime_manifest_files = tag.add_runtime_manifest_files, | |
| runtime_manifest_sha = tag.runtime_manifest_sha, | |
| base_urls = tag.base_urls, | |
| available_versions = available_versions, | |
| _fail = _fail, | |
| ) | |
| default = { | |
| "base_urls": DEFAULT_RELEASE_BASE_URLS, | |
| "platforms": dict(PLATFORMS), # Copy so it's mutable. | |
| "tool_versions": available_versions, | |
| } | |
| _override_defaults( | |
| # First override by single version, because the sha256 will replace | |
| # anything that has been there before. | |
| struct( | |
| name = "single_version_override", | |
| key = lambda t: t.python_version, | |
| fn = _process_single_version_overrides, | |
| ), | |
| # Then override particular platform entries if they need to be overridden. | |
| struct( | |
| name = "single_version_platform_override", | |
| key = lambda t: (t.python_version, t.platform), | |
| fn = _process_single_version_platform_overrides, | |
| ), | |
| # Then finally add global args and remove the unnecessary toolchains. | |
| # This ensures that we can do further validations when removing. | |
| struct( | |
| name = "override", | |
| key = lambda t: None, | |
| fn = _process_global_overrides, | |
| ), | |
| modules = modules, | |
| default = default, | |
| _fail = _fail, | |
| ) | |
| register_all_versions = default.pop("register_all_versions", False) | |
| add_target_settings = default.pop("add_target_settings", []) | |
| kwargs = default.pop("kwargs", {}) | |
| versions = {} | |
| for version_string in available_versions: | |
| v = version.parse(version_string, strict = True) | |
| versions.setdefault( | |
| "{}.{}".format(v.release[0], v.release[1]), | |
| [], | |
| ).append((version.key(v), v.string)) | |
| minor_mapping = { | |
| major_minor: max(subset)[1] | |
| for major_minor, subset in versions.items() | |
| } | |
| # The following ensures that all of the versions will be present in the minor_mapping | |
| minor_mapping_overrides = default.pop("minor_mapping", {}) | |
| for major_minor, full in minor_mapping_overrides.items(): | |
| minor_mapping[major_minor] = full | |
| return struct( | |
| kwargs = kwargs, | |
| minor_mapping = minor_mapping, | |
| default = default, | |
| register_all_versions = register_all_versions, | |
| add_target_settings = add_target_settings, | |
| ) | |
| def _compute_default_python_version(mctx): | |
| default_python_version = None | |
| for mod in mctx.modules: | |
| # Only the root module and rules_python are allowed to specify the default | |
| # toolchain for a couple reasons: | |
| # * It prevents submodules from specifying different defaults and only | |
| # one of them winning. | |
| # * rules_python needs to set a soft default in case the root module doesn't, | |
| # e.g. if the root module doesn't use Python itself. | |
| # * The root module is allowed to override the rules_python default. | |
| if not (mod.is_root or mod.name == "rules_python"): | |
| continue | |
| defaults_attr_structs = _create_defaults_attr_structs(mod = mod) | |
| default_python_version_env = None | |
| default_python_version_file = None | |
| pyproject_toml_label = None | |
| for defaults_attr in defaults_attr_structs: | |
| pyproject_toml_label = _one_or_the_same( | |
| pyproject_toml_label, | |
| defaults_attr.pyproject_toml, | |
| onerror = lambda: fail("Multiple pyproject.toml files specified in defaults"), | |
| ) | |
| default_python_version = _one_or_the_same( | |
| default_python_version, | |
| defaults_attr.python_version, | |
| onerror = _fail_multiple_defaults_python_version, | |
| ) | |
| default_python_version_env = _one_or_the_same( | |
| default_python_version_env, | |
| defaults_attr.python_version_env, | |
| onerror = _fail_multiple_defaults_python_version_env, | |
| ) | |
| default_python_version_file = _one_or_the_same( | |
| default_python_version_file, | |
| defaults_attr.python_version_file, | |
| onerror = _fail_multiple_defaults_python_version_file, | |
| ) | |
| # Priority order: ENV > pyproject_toml > python_version_file > python_version | |
| if default_python_version_file: | |
| default_python_version = _one_or_the_same( | |
| default_python_version, | |
| mctx.read(default_python_version_file, watch = "yes").strip(), | |
| ) | |
| if pyproject_toml_label: | |
| pyproject = read_pyproject(mctx, pyproject_toml_label) | |
| if pyproject.requires_python: | |
| default_python_version = version_from_requires_python(pyproject.requires_python) | |
| if default_python_version_env: | |
| default_python_version = mctx.getenv( | |
| default_python_version_env, | |
| default_python_version, | |
| ) | |
| if default_python_version: | |
| break | |
| # Otherwise, look at legacy python.toolchain() calls for a default | |
| toolchain_attrs = mod.tags.toolchain | |
| # Convenience: if one python.toolchain() call exists, treat it as | |
| # the default. | |
| if len(toolchain_attrs) == 1: | |
| default_python_version = toolchain_attrs[0].python_version | |
| else: | |
| sets_default = [v for v in toolchain_attrs if v.is_default] | |
| if len(sets_default) == 1: | |
| default_python_version = sets_default[0].python_version | |
| elif len(sets_default) > 1: | |
| _fail_multiple_default_toolchains_in_module(mod, toolchain_attrs) | |
| if default_python_version: | |
| break | |
| return default_python_version | |
| def _create_defaults_attr_structs(*, mod): | |
| arg_structs = [] | |
| for tag in mod.tags.defaults: | |
| arg_structs.append(_create_defaults_attr_struct(tag = tag)) | |
| return arg_structs | |
| def _create_defaults_attr_struct(*, tag): | |
| return struct( | |
| python_version = getattr(tag, "python_version", None), | |
| python_version_env = getattr(tag, "python_version_env", None), | |
| python_version_file = getattr(tag, "python_version_file", None), | |
| pyproject_toml = getattr(tag, "pyproject_toml", None), | |
| ) | |
| def _create_toolchain_attr_structs(*, mod, config, seen_versions, default_python_version): | |
| arg_structs = [] | |
| # Auto-register a toolchain for the default version if not already | |
| # registered via an explicit python.toolchain() call. | |
| # This works for any default source: pyproject_toml, python_version_file, | |
| # python_version_env, or python_version. | |
| has_explicit_toolchain = default_python_version and any([ | |
| tag.python_version == default_python_version | |
| for tag in mod.tags.toolchain | |
| ]) | |
| if (default_python_version and | |
| default_python_version not in seen_versions and | |
| mod.is_root and not has_explicit_toolchain): | |
| arg_structs.append(_create_toolchain_attrs_struct( | |
| python_version = default_python_version, | |
| toolchain_tag_count = 1, | |
| )) | |
| seen_versions[default_python_version] = True | |
| for tag in mod.tags.toolchain: | |
| arg_structs.append(_create_toolchain_attrs_struct( | |
| tag = tag, | |
| toolchain_tag_count = len(mod.tags.toolchain), | |
| )) | |
| seen_versions[tag.python_version] = True | |
| if config.register_all_versions: | |
| arg_structs.extend([ | |
| _create_toolchain_attrs_struct(python_version = v) | |
| for v in config.default["tool_versions"].keys() + config.minor_mapping.keys() | |
| if v not in seen_versions | |
| ]) | |
| return arg_structs | |
| def _create_toolchain_attrs_struct( | |
| *, | |
| tag = None, | |
| python_version = None, | |
| toolchain_tag_count = None): | |
| if tag and python_version: | |
| fail("Only one of tag and python version can be specified") | |
| if tag: | |
| # A single toolchain is treated as the default because it's unambiguous. | |
| is_default = tag.is_default or toolchain_tag_count == 1 | |
| else: | |
| is_default = False | |
| return struct( | |
| is_default = is_default, | |
| python_version = python_version if python_version else tag.python_version, | |
| configure_coverage_tool = getattr(tag, "configure_coverage_tool", False), | |
| ) | |
| _defaults = tag_class( | |
| doc = """Tag class to specify the default Python version.""", | |
| attrs = { | |
| "pyproject_toml": attr.label( | |
| mandatory = False, | |
| doc = """\ | |
| Label pointing to pyproject.toml file to read the default Python version from. | |
| When specified, reads the `requires-python` field from pyproject.toml. | |
| The version must be specified as `==X.Y.Z` (exact version with full semver). | |
| :::{versionadded} 2.3.0 | |
| ::: | |
| """, | |
| ), | |
| "python_version": attr.string( | |
| mandatory = False, | |
| doc = """\ | |
| String saying what the default Python version should be. If the string | |
| matches the {attr}`python_version` attribute of a toolchain, this | |
| toolchain is the default version. If this attribute is set, the | |
| {attr}`is_default` attribute of the toolchain is ignored. | |
| :::{versionadded} 1.4.0 | |
| ::: | |
| """, | |
| ), | |
| "python_version_env": attr.string( | |
| mandatory = False, | |
| doc = """\ | |
| Environment variable saying what the default Python version should be. | |
| If the string matches the {attr}`python_version` attribute of a | |
| toolchain, this toolchain is the default version. If this attribute is | |
| set, the {attr}`is_default` attribute of the toolchain is ignored. | |
| :::{versionadded} 1.4.0 | |
| ::: | |
| """, | |
| ), | |
| "python_version_file": attr.label( | |
| mandatory = False, | |
| allow_single_file = True, | |
| doc = """\ | |
| File saying what the default Python version should be. If the contents | |
| of the file match the {attr}`python_version` attribute of a toolchain, | |
| this toolchain is the default version. If this attribute is set, the | |
| {attr}`is_default` attribute of the toolchain is ignored. | |
| :::{versionadded} 1.4.0 | |
| ::: | |
| """, | |
| ), | |
| }, | |
| ) | |
| _toolchain = tag_class( | |
| doc = """Tag class used to register Python toolchains. | |
| Use this tag class to register one or more Python toolchains. This class | |
| is also potentially called by sub modules. The following covers different | |
| business rules and use cases. | |
| :::{topic} Toolchains in the Root Module | |
| This class registers all toolchains in the root module. | |
| ::: | |
| :::{topic} Toolchains in Sub Modules | |
| It will create a toolchain that is in a sub module, if the toolchain | |
| of the same name does not exist in the root module. The extension stops name | |
| clashing between toolchains in the root module and toolchains in sub modules. | |
| You cannot configure more than one toolchain as the default toolchain. | |
| ::: | |
| :::{topic} Toolchain set as the default version | |
| This extension will not create a toolchain that exists in a sub module, | |
| if the sub module toolchain is marked as the default version. If you have | |
| more than one toolchain in your root module, you need to set one of the | |
| toolchains as the default version. If there is only one toolchain it | |
| is set as the default toolchain. | |
| ::: | |
| :::{topic} Toolchain repository name | |
| A toolchain's repository name uses the format `python_{major}_{minor}`, e.g. | |
| `python_3_10`. The `major` and `minor` components are | |
| `major` and `minor` are the Python version from the `python_version` attribute. | |
| If a toolchain is registered in `X.Y.Z`, then similarly the toolchain name will | |
| be `python_{major}_{minor}_{patch}`, e.g. `python_3_10_19`. | |
| ::: | |
| :::{topic} Toolchain detection | |
| The definition of the first toolchain wins, which means that the root module | |
| can override settings for any python toolchain available. This relies on the | |
| documented module traversal from the {obj}`module_ctx.modules`. | |
| ::: | |
| :::{tip} | |
| In order to use a different name than the above, you can use the following `MODULE.bazel` | |
| syntax: | |
| ```starlark | |
| python = use_extension("@rules_python//python/extensions:python.bzl", "python") | |
| python.defaults(python_version = "3.11") | |
| python.toolchain(python_version = "3.11") | |
| use_repo(python, my_python_name = "python_3_11") | |
| ``` | |
| Then the python interpreter will be available as `my_python_name`. | |
| ::: | |
| """, | |
| attrs = { | |
| "configure_coverage_tool": attr.bool( | |
| mandatory = False, | |
| doc = "Whether or not to configure the default coverage tool provided by `rules_python` for the compatible toolchains.", | |
| ), | |
| "ignore_root_user_error": attr.bool( | |
| default = True, | |
| doc = """\ | |
| :::{versionchanged} 1.8.0 | |
| Noop, will be removed in the next major release. | |
| ::: | |
| """, | |
| mandatory = False, | |
| ), | |
| "is_default": attr.bool( | |
| mandatory = False, | |
| doc = """\ | |
| Whether the toolchain is the default version. | |
| :::{versionchanged} 1.4.0 | |
| This setting is ignored if the default version is set using the `defaults` | |
| tag class (encouraged). | |
| ::: | |
| """, | |
| ), | |
| "python_version": attr.string( | |
| mandatory = True, | |
| doc = """\ | |
| The Python version, in `major.minor` or `major.minor.patch` format, e.g | |
| `3.12` (or `3.12.3`), to create a toolchain for. | |
| """, | |
| ), | |
| }, | |
| ) | |
| _override = tag_class( | |
| doc = """Tag class used to override defaults and behaviour of the module extension. | |
| :::{versionadded} 0.36.0 | |
| ::: | |
| """, | |
| attrs = { | |
| "add_runtime_manifest_files": attr.label_list( | |
| mandatory = False, | |
| allow_files = True, | |
| doc = """ | |
| Labels pointing to local python-build-standalone manifest files (e.g., `SHA256SUMS`). | |
| Example: | |
| `//my/custom/manifest:SHA256SUMS` | |
| :::{seealso} | |
| [Manifest file format documentation](https://rules-python.readthedocs.io/en/latest/toolchains.html#manifest-file-format) | |
| ::: | |
| :::{versionadded} 2.1.0 | |
| ::: | |
| """, | |
| ), | |
| "add_runtime_manifest_urls": attr.string_list( | |
| mandatory = False, | |
| doc = """ | |
| URLs pointing to python-build-standalone manifest files (e.g., SHA256SUMS). | |
| Example: | |
| `https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS` | |
| Note that `/latest/` can be used in place of a specific release date (e.g., `20260414`) to automatically use the latest release: | |
| `https://github.com/astral-sh/python-build-standalone/releases/latest/download/SHA256SUMS` | |
| :::{seealso} | |
| [Manifest file format documentation](https://rules-python.readthedocs.io/en/latest/toolchains.html#manifest-file-format) | |
| ::: | |
| :::{versionadded} 2.1.0 | |
| ::: | |
| """, | |
| ), | |
| "add_target_settings": attr.string_list( | |
| mandatory = False, | |
| doc = """\ | |
| A list of `config_setting` labels to add to the `target_settings` of every | |
| toolchain registered by this module extension. This is useful for creating | |
| separate "families" of toolchains gated behind custom build settings. | |
| For example, to ensure the default prebuilt toolchains are only resolved when | |
| a `prebuilt` config setting is active: | |
| ```starlark | |
| python.override( | |
| add_target_settings = ["@@//:python_toolchain_family_prebuilt"], | |
| ) | |
| ``` | |
| These settings are appended to the `target_settings` of all toolchains | |
| registered by the extension, including any that already have settings | |
| from `python.single_version_platform_override`. | |
| :::{versionadded} 2.1.0 | |
| ::: | |
| """, | |
| ), | |
| "available_python_versions": attr.string_list( | |
| mandatory = False, | |
| doc = """\ | |
| The list of available python tool versions to use. Must be in `X.Y.Z` format. | |
| If the unknown version given the processing of the extension will fail - all of | |
| the versions in the list have to be defined with | |
| {obj}`python.single_version_override` or | |
| {obj}`python.single_version_platform_override` before they are used in this | |
| list. | |
| This attribute is usually used in order to ensure that no unexpected transitive | |
| dependencies are introduced. | |
| """, | |
| ), | |
| "base_urls": attr.string_list( | |
| mandatory = False, | |
| doc = "The base URLs to be used when downloading toolchains.", | |
| default = DEFAULT_RELEASE_BASE_URLS, | |
| ), | |
| "ignore_root_user_error": attr.bool( | |
| default = True, | |
| doc = """Deprecated; do not use. This attribute has no effect.""", | |
| mandatory = False, | |
| ), | |
| "minor_mapping": attr.string_dict( | |
| mandatory = False, | |
| doc = """\ | |
| The mapping between `X.Y` to `X.Y.Z` versions to be used when setting up | |
| toolchains. It defaults to the interpreter with the highest available patch | |
| version for each minor version. For example if one registers `3.10.3`, `3.10.4` | |
| and `3.11.4` then the default for the `minor_mapping` dict will be: | |
| ```starlark | |
| { | |
| "3.10": "3.10.4", | |
| "3.11": "3.11.4", | |
| } | |
| ``` | |
| :::{versionchanged} 0.37.0 | |
| The values in this mapping override the default values and do not replace them. | |
| ::: | |
| """, | |
| default = {}, | |
| ), | |
| "register_all_versions": attr.bool(default = False, doc = "Add all versions"), | |
| "runtime_manifest_sha": attr.string( | |
| mandatory = False, | |
| doc = """ | |
| SHA256 hash for the add_runtime_manifest_urls. | |
| :::{versionadded} 2.1.0 | |
| ::: | |
| """, | |
| ), | |
| } | AUTH_ATTRS, | |
| ) | |
| _single_version_override = tag_class( | |
| doc = """Override single python version URLs and patches for all platforms. | |
| :::{note} | |
| This will replace any existing configuration for the given python version. | |
| ::: | |
| :::{tip} | |
| If you would like to modify the configuration for a specific `(version, | |
| platform)`, please use the {obj}`single_version_platform_override` tag | |
| class. | |
| ::: | |
| :::{versionadded} 0.36.0 | |
| ::: | |
| """, | |
| attrs = { | |
| # NOTE @aignas 2024-09-01: all of the attributes except for `version` | |
| # can be part of the `python.toolchain` call. That would make it more | |
| # ergonomic to define new toolchains and to override values for old | |
| # toolchains. The same semantics of the `first one wins` would apply, | |
| # so technically there is no need for any overrides? | |
| # | |
| # Although these attributes would override the code that is used by the | |
| # code in non-root modules, so technically this could be thought as | |
| # being overridden. | |
| # | |
| # rules_go has a single download call: | |
| # https://github.com/bazelbuild/rules_go/blob/master/go/private/extensions.bzl#L38 | |
| # | |
| # However, we need to understand how to accommodate the fact that | |
| # {attr}`single_version_override.version` only allows patch versions. | |
| "distutils": attr.label( | |
| allow_single_file = True, | |
| doc = "A distutils.cfg file to be included in the Python installation. " + | |
| "Either {attr}`distutils` or {attr}`distutils_content` can be specified, but not both.", | |
| mandatory = False, | |
| ), | |
| "distutils_content": attr.string( | |
| doc = "A distutils.cfg file content to be included in the Python installation. " + | |
| "Either {attr}`distutils` or {attr}`distutils_content` can be specified, but not both.", | |
| mandatory = False, | |
| ), | |
| "patch_strip": attr.int( | |
| mandatory = False, | |
| doc = "Same as the --strip argument of Unix patch.", | |
| default = 0, | |
| ), | |
| "patches": attr.label_list( | |
| mandatory = False, | |
| 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.", | |
| ), | |
| "python_version": attr.string( | |
| mandatory = True, | |
| doc = "The python version to override URLs for. Must be in `X.Y.Z` format.", | |
| ), | |
| "sha256": attr.string_dict( | |
| mandatory = False, | |
| doc = "The python platform to sha256 dict. See {attr}`python.single_version_platform_override.platform` for allowed key values.", | |
| ), | |
| "strip_prefix": attr.string( | |
| mandatory = False, | |
| doc = "The 'strip_prefix' for the archive, defaults to 'python'.", | |
| default = "python", | |
| ), | |
| "urls": attr.string_list( | |
| mandatory = False, | |
| doc = "The URL template to fetch releases for this Python version. See {attr}`python.single_version_platform_override.urls` for documentation.", | |
| ), | |
| }, | |
| ) | |
| _single_version_platform_override = tag_class( | |
| doc = """Override single python version for a single existing platform. | |
| If the `(version, platform)` is new, we will add it to the existing versions and will | |
| use the same `url` template. | |
| :::{tip} | |
| If you would like to add or remove platforms to a single python version toolchain | |
| configuration, please use {obj}`single_version_override`. | |
| ::: | |
| :::{versionadded} 0.36.0 | |
| ::: | |
| """, | |
| attrs = { | |
| "arch": attr.string( | |
| doc = """ | |
| The arch (cpu) the runtime is compatible with. | |
| If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. | |
| If set, the `os_name`, `target_compatible_with` and `target_settings` attributes | |
| should also be set. | |
| The values should be one of the values in `@platforms//cpu` | |
| :::{seealso} | |
| Docs for [Registering custom runtimes] | |
| ::: | |
| :::{versionadded} 1.5.0 | |
| ::: | |
| """, | |
| ), | |
| "coverage_tool": attr.label( | |
| doc = """\ | |
| The coverage tool to be used for a particular Python interpreter. This can override | |
| `rules_python` defaults. | |
| """, | |
| ), | |
| "os_name": attr.string( | |
| doc = """ | |
| The host OS the runtime is compatible with. | |
| If not set, then the runtime cannot be used as a `python_X_Y_host` runtime. | |
| If set, the `os_name`, `target_compatible_with` and `target_settings` attributes | |
| should also be set. | |
| The values should be one of the values in `@platforms//os` | |
| :::{seealso} | |
| Docs for [Registering custom runtimes] | |
| ::: | |
| :::{versionadded} 1.5.0 | |
| ::: | |
| """, | |
| ), | |
| "patch_strip": attr.int( | |
| mandatory = False, | |
| doc = "Same as the --strip argument of Unix patch.", | |
| default = 0, | |
| ), | |
| "patches": attr.label_list( | |
| mandatory = False, | |
| 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.", | |
| ), | |
| "platform": attr.string( | |
| mandatory = True, | |
| doc = """ | |
| The platform to override the values for, typically one of:\n | |
| {platforms} | |
| Other values are allowed, in which case, `target_compatible_with`, | |
| `target_settings`, `os_name`, and `arch` should be specified so the toolchain is | |
| only used when appropriate. | |
| :::{{versionchanged}} 1.5.0 | |
| Arbitrary platform strings allowed. | |
| ::: | |
| """.format( | |
| platforms = "\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS])), | |
| ), | |
| ), | |
| "python_version": attr.string( | |
| mandatory = True, | |
| doc = "The python version to override URLs for. Must be in `X.Y.Z` format.", | |
| ), | |
| "sha256": attr.string( | |
| mandatory = False, | |
| doc = "The sha256 for the archive", | |
| ), | |
| "strip_prefix": attr.string( | |
| mandatory = False, | |
| doc = "The 'strip_prefix' for the archive, defaults to 'python'.", | |
| default = "python", | |
| ), | |
| "target_compatible_with": attr.string_list( | |
| doc = """ | |
| The `target_compatible_with` values to use for the toolchain definition. | |
| If not set, then `os_name` and `arch` will be used to populate it. | |
| If set, `target_settings`, `os_name`, and `arch` should also be set. | |
| :::{seealso} | |
| Docs for [Registering custom runtimes] | |
| ::: | |
| :::{versionadded} 1.5.0 | |
| ::: | |
| """, | |
| ), | |
| "target_settings": attr.string_list( | |
| doc = """ | |
| The `target_setings` values to use for the toolchain definition. | |
| If set, `target_compatible_with`, `os_name`, and `arch` should also be set. | |
| :::{seealso} | |
| Docs for [Registering custom runtimes] | |
| ::: | |
| :::{versionadded} 1.5.0 | |
| ::: | |
| """, | |
| ), | |
| "urls": attr.string_list( | |
| mandatory = False, | |
| 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.", | |
| ), | |
| }, | |
| ) | |
| python = module_extension( | |
| doc = """Bzlmod extension that is used to register Python toolchains. | |
| """, | |
| implementation = _python_impl, | |
| tag_classes = { | |
| "defaults": _defaults, | |
| "override": _override, | |
| "single_version_override": _single_version_override, | |
| "single_version_platform_override": _single_version_platform_override, | |
| "toolchain": _toolchain, | |
| }, | |
| environ = ["RULES_PYTHON_BZLMOD_DEBUG"], | |
| ) | |
| _DEBUG_BUILD_CONTENT = """ | |
| package( | |
| default_visibility = ["//visibility:public"], | |
| ) | |
| exports_files(["debug_info.json"]) | |
| """ | |
| def _debug_repo_impl(repo_ctx): | |
| repo_ctx.file("BUILD.bazel", _DEBUG_BUILD_CONTENT) | |
| repo_ctx.file("debug_info.json", repo_ctx.attr.debug_info) | |
| _debug_repo = repository_rule( | |
| implementation = _debug_repo_impl, | |
| attrs = { | |
| "debug_info": attr.string(), | |
| }, | |
| ) |