diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a6bdf0a96..a113c7411f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,10 @@ END_UNRELEASED_TEMPLATE Set the `RULES_PYTHON_ENABLE_PIPSTAR=1` environment variable to enable it. * (utils) Add a way to run a REPL for any `rules_python` target that returns a `PyInfo` provider. +* (toolchains) Arbitrary python-build-standalone runtimes can be registered + and activated with custom flags. See the [Registering custom runtimes] + docs and {obj}`single_version_platform_override()` API docs for more + information. {#v0-0-0-removed} ### Removed diff --git a/MODULE.bazel b/MODULE.bazel index d3a95350e5..144e130c1b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -125,6 +125,22 @@ dev_python.override( register_all_versions = True, ) +# For testing an arbitrary runtime triggered by a custom flag. +# See //tests/toolchains:custom_platform_toolchain_test +dev_python.single_version_platform_override( + platform = "linux-x86-install-only-stripped", + python_version = "3.13.1", + sha256 = "56817aa976e4886bec1677699c136cb01c1cdfe0495104c0d8ef546541864bbb", + target_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + target_settings = [ + "@@//tests/support:is_custom_runtime_linux-x86-install-only-stripped", + ], + urls = ["https://github.com/astral-sh/python-build-standalone/releases/download/20250115/cpython-3.13.1+20250115-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"], +) + dev_pip = use_extension( "//python/extensions:pip.bzl", "pip", diff --git a/docs/toolchains.md b/docs/toolchains.md index ada887c945..57d43d27f1 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -243,6 +243,73 @@ existing attributes: * Adding additional Python versions via {bzl:obj}`python.single_version_override` or {bzl:obj}`python.single_version_platform_override`. +### Registering custom runtimes + +Because the python-build-standalone project has _thousands_ of prebuilt runtimes +available, rules_python only includes popular runtimes in its built in +configurations. If you want to use a runtime that isn't already known to +rules_python then {obj}`single_version_platform_override()` can be used to do +so. In short, it allows specifying an arbitrary URL and using custom flags +to control when a runtime is used. + +In the example below, we register a particular python-build-standalone runtime +that is activated for Linux x86 builds when the custom flag +`--//:runtime=my-custom-runtime` is set. + +``` +# File: MODULE.bazel +bazel_dep(name = "bazel_skylib", version = "1.7.1.") +bazel_dep(name = "rules_python", version = "1.5.0") +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.single_version_platform_override( + platform = "my-platform", + python_version = "3.13.3", + sha256 = "01d08b9bc8a96698b9d64c2fc26da4ecc4fa9e708ce0a34fb88f11ab7e552cbd", + os_name = "linux", + arch = "x86_64", + target_settings = [ + "@@//:runtime=my-custom-runtime", + ], + urls = ["https://github.com/astral-sh/python-build-standalone/releases/download/20250409/cpython-3.13.3+20250409-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"], +) +# File: //:BUILD.bazel +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +string_flag( + name = "custom_runtime", + build_setting_default = "", +) +config_setting( + name = "is_custom_runtime_linux-x86-install-only-stripped", + flag_values = { + ":custom_runtime": "linux-x86-install-only-stripped", + }, +) +``` + +Notes: +- While any URL and archive can be used, it's assumed their content looks how + a python-build-standalone archive looks. +- A "version aware" toolchain is registered, which means the Python version flag + must also match (e.g. `--@rules_python//python/config_settings:python_version=3.13.3` + must be set -- see `minor_mapping` and `is_default` for controls and docs + about version matching and selection). +- The `target_compatible_with` attribute can be used to entirely specify the + arg of the same name the toolchain uses. +- The labels in `target_settings` must be absolute; `@@` refers to the main repo. +- The `target_settings` are `config_setting` targets, which means you can + customize how matching occurs. + +:::{seealso} +See {obj}`//python/config_settings` for flags rules_python already defines +that can be used with `target_settings`. Some particular ones of note are: +{flag}`--py_linux_libc` and {flag}`--py_freethreaded`, among others. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +Added support for custom platform names, `target_compatible_with`, and +`target_settings` with `single_version_platform_override`. +::: + ### Using defined toolchains from WORKSPACE It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example diff --git a/internal_dev_setup.bzl b/internal_dev_setup.bzl index 62a11ab1d4..c37c59a5da 100644 --- a/internal_dev_setup.bzl +++ b/internal_dev_setup.bzl @@ -42,7 +42,7 @@ def rules_python_internal_setup(): toolchain_platform_keys = {}, toolchain_python_versions = {}, toolchain_set_python_version_constraints = {}, - base_toolchain_repo_names = [], + host_compatible_repo_names = [], ) runtime_env_repo(name = "rules_python_runtime_env_tc_info") diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 867c43478a..58cff5b99d 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -247,6 +247,7 @@ bzl_library( name = "versions_bzl", srcs = ["versions.bzl"], visibility = ["//:__subpackages__"], + deps = ["//python/private:platform_info_bzl"], ) # NOTE: Remember to add bzl_library targets to //tests:bzl_libraries diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index ce22421300..b319919305 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -241,11 +241,17 @@ bzl_library( ], ) +bzl_library( + name = "platform_info_bzl", + srcs = ["platform_info.bzl"], +) + bzl_library( name = "python_bzl", srcs = ["python.bzl"], deps = [ ":full_version_bzl", + ":platform_info_bzl", ":python_register_toolchains_bzl", ":pythons_hub_bzl", ":repo_utils_bzl", diff --git a/python/private/platform_info.bzl b/python/private/platform_info.bzl new file mode 100644 index 0000000000..3f7dc00165 --- /dev/null +++ b/python/private/platform_info.bzl @@ -0,0 +1,34 @@ +"""Helper to define a struct used to define platform metadata.""" + +def platform_info( + *, + compatible_with = [], + flag_values = {}, + target_settings = [], + os_name, + arch): + """Creates a struct of platform metadata. + + This is just a helper to ensure structs are created the same and + the meaning/values are documented. + + Args: + compatible_with: list[str], where the values are string labels. These + are the target_compatible_with values to use with the toolchain + flag_values: dict[str|Label, Any] of config_setting.flag_values + compatible values. DEPRECATED -- use target_settings instead + target_settings: list[str], where the values are string labels. These + are the target_settings values to use with the toolchain. + os_name: str, the os name; must match the name used in `@platfroms//os` + arch: str, the cpu name; must match the name used in `@platforms//cpu` + + Returns: + A struct with attributes and values matching the args. + """ + return struct( + compatible_with = compatible_with, + flag_values = flag_values, + target_settings = target_settings, + os_name = os_name, + arch = arch, + ) diff --git a/python/private/py_repositories.bzl b/python/private/py_repositories.bzl index b5bd93b7c1..10bc06630b 100644 --- a/python/private/py_repositories.bzl +++ b/python/private/py_repositories.bzl @@ -47,7 +47,7 @@ def py_repositories(): toolchain_platform_keys = {}, toolchain_python_versions = {}, toolchain_set_python_version_constraints = {}, - base_toolchain_repo_names = [], + host_compatible_repo_names = [], ) http_archive( name = "bazel_skylib", diff --git a/python/private/python.bzl b/python/private/python.bzl index a7e257601f..8e23668879 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -18,29 +18,44 @@ load("@bazel_features//:features.bzl", "bazel_features") load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") load(":auth.bzl", "AUTH_ATTRS") load(":full_version.bzl", "full_version") +load(":platform_info.bzl", "platform_info") 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_platforms") +load( + ":toolchains_repo.bzl", + "host_compatible_python_repo", + "multi_toolchain_aliases", + "sorted_host_platform_names", + "sorted_host_platforms", +) load(":util.bzl", "IS_BAZEL_6_4_OR_HIGHER") load(":version.bzl", "version") -def parse_modules(*, module_ctx, _fail = fail): +def parse_modules(*, module_ctx, logger, _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`: The list of toolchains to register. The last - element is special and is treated as the default toolchain. + * `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.os.environ.get("RULES_PYTHON_BZLMOD_DEBUG", "0") == "1": debug_info = { @@ -64,8 +79,6 @@ def parse_modules(*, module_ctx, _fail = fail): ignore_root_user_error = None - logger = repo_utils.logger(module_ctx, "python") - # if the root module does not register any toolchain then the # ignore_root_user_error takes its default value: True if not module_ctx.modules[0].tags.toolchain: @@ -265,19 +278,37 @@ def parse_modules(*, module_ctx, _fail = fail): ) def _python_impl(module_ctx): - py = parse_modules(module_ctx = module_ctx) + logger = repo_utils.logger(module_ctx, "python") + py = parse_modules(module_ctx = module_ctx, logger = logger) + + # 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 base names of toolchain repos - base_toolchain_repo_names = [] + # 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) - base_toolchain_repo_names.append(toolchain_info.name) # Ensure that we pass the full version here. full_python_version = full_version( @@ -298,6 +329,8 @@ def _python_impl(module_ctx): _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(): @@ -318,27 +351,81 @@ def _python_impl(module_ctx): set_python_version_constraint = is_last, )) if _is_compatible_with_host(module_ctx, platform_info): - host_platforms[platform_name] = 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)), + ) - host_platforms = sorted_host_platforms(host_platforms) + 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 = toolchain_info.name + "_host", - # NOTE: Order matters. The first found to be compatible is (usually) used. - platforms = host_platforms.keys(), - os_names = { - str(i): platform_info.os_name - for i, platform_info in enumerate(host_platforms.values()) - }, - arch_names = { - str(i): platform_info.arch - for i, platform_info in enumerate(host_platforms.values()) + 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) }, - python_version = full_python_version, + 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 of the base names ("python_3_10") for the toolchain repos - base_toolchain_repo_names = [] - # list[str] The infix to use for the resulting toolchain() `name` arg. toolchain_names = [] @@ -399,7 +486,7 @@ def _python_impl(module_ctx): toolchain_platform_keys = toolchain_platform_keys, toolchain_python_versions = toolchain_python_versions, toolchain_set_python_version_constraints = toolchain_set_python_version_constraints, - base_toolchain_repo_names = [t.name for t in py.toolchains], + 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()), @@ -583,9 +670,56 @@ def _process_single_version_platform_overrides(*, tag, _fail = fail, default): 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"] @@ -664,22 +798,29 @@ def _get_toolchain_config(*, modules, _fail = fail): """ # Items that can be overridden - available_versions = { - version: { - # Use a dicts straight away so that we could do URL overrides for a - # single version. - "sha256": dict(item["sha256"]), - "strip_prefix": { - platform: item["strip_prefix"] - for platform in item["sha256"] - } if type(item["strip_prefix"]) == type("") else item["strip_prefix"], - "url": { - platform: [item["url"]] - for platform in item["sha256"] - } if type(item["url"]) == type("") else item["url"], - } - for version, item in TOOL_VERSIONS.items() - } + available_versions = {} + for py_version, item in TOOL_VERSIONS.items(): + available_versions[py_version] = {} + available_versions[py_version]["sha256"] = dict(item["sha256"]) + platforms = item["sha256"].keys() + + strip_prefix = item["strip_prefix"] + if type(strip_prefix) == type(""): + available_versions[py_version]["strip_prefix"] = { + platform: strip_prefix + for platform in platforms + } + else: + available_versions[py_version]["strip_prefix"] = dict(strip_prefix) + url = item["url"] + if type(url) == type(""): + available_versions[py_version]["url"] = { + platform: url + for platform in platforms + } + else: + available_versions[py_version]["url"] = dict(url) + default = { "base_url": DEFAULT_RELEASE_BASE_URL, "platforms": dict(PLATFORMS), # Copy so it's mutable. @@ -1084,10 +1225,48 @@ configuration, please use {obj}`single_version_override`. ::: """, 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}} VERSION_NEXT_FEATURE +::: +""", + ), "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}} VERSION_NEXT_FEATURE +::: """, ), "patch_strip": attr.int( @@ -1101,8 +1280,20 @@ The coverage tool to be used for a particular Python interpreter. This can overr ), "platform": attr.string( mandatory = True, - values = PLATFORMS.keys(), - doc = "The platform to override the values for, must be one of:\n{}.".format("\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS]))), + 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}} VERSION_NEXT_FEATURE +Arbitrary platform strings allowed. +::: +""".format( + platforms = "\n".join(sorted(["* `{}`".format(p) for p in PLATFORMS])), + ), ), "python_version": attr.string( mandatory = True, @@ -1117,6 +1308,36 @@ The coverage tool to be used for a particular Python interpreter. This can overr 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}} VERSION_NEXT_FEATURE +::: +""", + ), + "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}} VERSION_NEXT_FEATURE +::: +""", + ), "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.", diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl index fd86b415cc..cb0731e6eb 100644 --- a/python/private/python_repository.bzl +++ b/python/private/python_repository.bzl @@ -15,7 +15,7 @@ """This file contains repository rules and macros to support toolchain registration. """ -load("//python:versions.bzl", "FREETHREADED", "INSTALL_ONLY", "PLATFORMS") +load("//python:versions.bzl", "FREETHREADED", "INSTALL_ONLY") load(":auth.bzl", "get_auth") load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":text_util.bzl", "render") @@ -327,7 +327,6 @@ function defaults (e.g. `single_version_override` for `MODULE.bazel` files. "platform": attr.string( doc = "The platform name for the Python interpreter tarball.", mandatory = True, - values = PLATFORMS.keys(), ), "python_version": attr.string( doc = "The Python version.", diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index 53351cacb9..cc25b4ba1d 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -84,13 +84,7 @@ def _hub_build_file_content(rctx): ) _interpreters_bzl_template = """ -INTERPRETER_LABELS = {{ -{interpreter_labels} -}} -""" - -_line_for_hub_template = """\ - "{name}_host": Label("@{name}_host//:python"), +INTERPRETER_LABELS = {labels} """ _versions_bzl_template = """ @@ -110,15 +104,16 @@ def _hub_repo_impl(rctx): # Create a dict that is later used to create # a symlink to a interpreter. - interpreter_labels = "".join([ - _line_for_hub_template.format(name = name) - for name in rctx.attr.base_toolchain_repo_names - ]) - rctx.file( "interpreters.bzl", _interpreters_bzl_template.format( - interpreter_labels = interpreter_labels, + labels = render.dict( + { + name: 'Label("@{}//:python")'.format(name) + for name in rctx.attr.host_compatible_repo_names + }, + value_repr = str, + ), ), executable = False, ) @@ -144,15 +139,14 @@ This rule also writes out the various toolchains for the different Python versio """, implementation = _hub_repo_impl, attrs = { - "base_toolchain_repo_names": attr.string_list( - doc = "The base repo name for toolchains ('python_3_10', no " + - "platform suffix)", - mandatory = True, - ), "default_python_version": attr.string( doc = "Default Python version for the build in `X.Y` or `X.Y.Z` format.", mandatory = True, ), + "host_compatible_repo_names": attr.string_list( + doc = "Names of `host_compatible_python_repo` repos.", + mandatory = True, + ), "minor_mapping": attr.string_dict( doc = "The minor mapping of the `X.Y` to `X.Y.Z` format that is used in config settings.", mandatory = True, diff --git a/python/private/repo_utils.bzl b/python/private/repo_utils.bzl index eee56ec86c..32a5b70e15 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -31,13 +31,15 @@ def _is_repo_debug_enabled(mrctx): """ return _getenv(mrctx, REPO_DEBUG_ENV_VAR) == "1" -def _logger(mrctx, name = None): +def _logger(mrctx = None, name = None, verbosity_level = None): """Creates a logger instance for printing messages. Args: mrctx: repository_ctx or module_ctx object. If the attribute `_rule_name` is present, it will be included in log messages. name: name for the logger. Optional for repository_ctx usage. + verbosity_level: {type}`int | None` verbosity level. If not set, + taken from `mrctx` Returns: A struct with attributes logging: trace, debug, info, warn, fail. @@ -46,13 +48,14 @@ def _logger(mrctx, name = None): the logger injected into the function work as expected by terminating on the given line. """ - if _is_repo_debug_enabled(mrctx): - verbosity_level = "DEBUG" - else: - verbosity_level = "WARN" + if verbosity_level == None: + if _is_repo_debug_enabled(mrctx): + verbosity_level = "DEBUG" + else: + verbosity_level = "WARN" - env_var_verbosity = _getenv(mrctx, REPO_VERBOSITY_ENV_VAR) - verbosity_level = env_var_verbosity or verbosity_level + env_var_verbosity = _getenv(mrctx, REPO_VERBOSITY_ENV_VAR) + verbosity_level = env_var_verbosity or verbosity_level verbosity = { "DEBUG": 2, @@ -376,7 +379,7 @@ def _get_platforms_os_name(mrctx): """Return the name in @platforms//os for the host os. Args: - mrctx: module_ctx or repository_ctx. + mrctx: {type}`module_ctx | repository_ctx` Returns: `str`. The target name. @@ -405,6 +408,7 @@ def _get_platforms_cpu_name(mrctx): `str`. The target name. """ arch = mrctx.os.arch.lower() + if arch in ["i386", "i486", "i586", "i686", "i786", "x86"]: return "x86_32" if arch in ["amd64", "x86_64", "x64"]: diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index 2476889583..93bbb52108 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -309,11 +309,11 @@ actions.""", environ = [REPO_DEBUG_ENV_VAR], ) -def _host_compatible_python_repo(rctx): +def _host_compatible_python_repo_impl(rctx): rctx.file("BUILD.bazel", _HOST_TOOLCHAIN_BUILD_CONTENT) os_name = repo_utils.get_platforms_os_name(rctx) - host_platform = _get_host_platform( + impl_repo_name = _get_host_impl_repo_name( rctx = rctx, logger = repo_utils.logger(rctx), python_version = rctx.attr.python_version, @@ -321,10 +321,11 @@ def _host_compatible_python_repo(rctx): cpu_name = repo_utils.get_platforms_cpu_name(rctx), platforms = rctx.attr.platforms, ) - repo = "@@{py_repository}_{host_platform}".format( - py_repository = rctx.attr.name[:-len("_host")], - host_platform = host_platform, - ) + + # Bzlmod quirk: A repository rule can't, in its **implemention function**, + # resolve an apparent repo name referring to a repo created by the same + # bzlmod extension. To work around this, we use a canonical label. + repo = "@@{}".format(impl_repo_name) rctx.report_progress("Symlinking interpreter files to the target platform") host_python_repo = rctx.path(Label("{repo}//:BUILD.bazel".format(repo = repo))) @@ -380,26 +381,76 @@ def _host_compatible_python_repo(rctx): # NOTE: The term "toolchain" is a misnomer for this rule. This doesn't define # a repo with toolchains or toolchain implementations. host_compatible_python_repo = repository_rule( - _host_compatible_python_repo, + implementation = _host_compatible_python_repo_impl, doc = """\ Creates a repository with a shorter name meant to be used in the repository_ctx, which needs to have `symlinks` for the interpreter. This is separate from the toolchain_aliases repo because referencing the `python` interpreter target from this repo causes an eager fetch of the toolchain for the host platform. - """, + +This repo has two ways in which is it called: + +1. Workspace. The `platforms` attribute is set, which are keys into the + PLATFORMS global. It assumes `name` + is a + valid repo name which it can use as the backing repo. + +2. Bzlmod. All platform and backing repo information is passed in via the + arch_names, impl_repo_names, os_names, python_versions attributes. +""", attrs = { "arch_names": attr.string_dict( doc = """ -If set, overrides the platform metadata. Keyed by index in `platforms` +Arch (cpu) names. Only set in bzlmod. Keyed by index in `platforms` +""", + ), + "base_name": attr.string( + doc = """ +The name arg, but without bzlmod canonicalization applied. Only set in bzlmod. +""", + ), + "impl_repo_names": attr.string_dict( + doc = """ +The names of backing runtime repos. Only set in bzlmod. The names must be repos +in the same extension as creates the host repo. Keyed by index in `platforms`. """, ), "os_names": attr.string_dict( doc = """ -If set, overrides the platform metadata. Keyed by index in `platforms` +If set, overrides the platform metadata. Only set in bzlmod. Keyed by +index in `platforms` +""", + ), + "platforms": attr.string_list( + mandatory = True, + doc = """ +Platform names (workspace) or platform name-like keys (bzlmod) + +NOTE: The order of this list matters. The first platform that is compatible +with the host will be selected; this can be customized by using the +`RULES_PYTHON_REPO_TOOLCHAIN_*` env vars. + +The values passed vary depending on workspace vs bzlmod. + +Workspace: the values are keys into the `PLATFORMS` dict and are the suffix +to append to `name` to point to the backing repo name. + +Bzlmod: The values are arbitrary keys to create the platform map from the +other attributes (os_name, arch_names, et al). +""", + ), + "python_version": attr.string( + doc = """ +Full python version, Major.Minor.Micro. + +Only set in workspace calls. +""", + ), + "python_versions": attr.string_dict( + doc = """ +If set, the Python version for the corresponding selected platform. Values in +Major.Minor.Micro format. Keyed by index in `platforms`. """, ), - "platforms": attr.string_list(mandatory = True), - "python_version": attr.string(mandatory = True), "_rule_name": attr.string(default = "host_compatible_python_repo"), "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")), }, @@ -435,8 +486,8 @@ multi_toolchain_aliases = repository_rule( }, ) -def sorted_host_platforms(platform_map): - """Sort the keys in the platform map to give correct precedence. +def sorted_host_platform_names(platform_names): + """Sort platform names to give correct precedence. The order of keys in the platform mapping matters for the host toolchain selection. When multiple runtimes are compatible with the host, we take the @@ -453,11 +504,10 @@ def sorted_host_platforms(platform_map): is an innocous looking formatter disable directive. Args: - platform_map: a mapping of platforms and their metadata. + platform_names: a list of platform names Returns: - dict; the same values, but with the keys inserted in the desired - order so that iteration happens in the desired order. + list[str] the same values, but in the desired order. """ def platform_keyer(name): @@ -467,13 +517,26 @@ def sorted_host_platforms(platform_map): 1 if FREETHREADED in name else 0, ) - sorted_platform_keys = sorted(platform_map.keys(), key = platform_keyer) + return sorted(platform_names, key = platform_keyer) + +def sorted_host_platforms(platform_map): + """Sort the keys in the platform map to give correct precedence. + + See sorted_host_platform_names for explanation. + + Args: + platform_map: a mapping of platforms and their metadata. + + Returns: + dict; the same values, but with the keys inserted in the desired + order so that iteration happens in the desired order. + """ return { key: platform_map[key] - for key in sorted_platform_keys + for key in sorted_host_platform_names(platform_map.keys()) } -def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platforms): +def _get_host_impl_repo_name(*, rctx, logger, python_version, os_name, cpu_name, platforms): """Gets the host platform. Args: @@ -488,24 +551,40 @@ def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platf """ if rctx.attr.os_names: platform_map = {} + base_name = rctx.attr.base_name + if not base_name: + fail("The `base_name` attribute must be set under bzlmod") for i, platform_name in enumerate(platforms): key = str(i) + impl_repo_name = rctx.attr.impl_repo_names[key] + impl_repo_name = rctx.name.replace(base_name, impl_repo_name) platform_map[platform_name] = struct( os_name = rctx.attr.os_names[key], arch = rctx.attr.arch_names[key], + python_version = rctx.attr.python_versions[key], + impl_repo_name = impl_repo_name, ) else: - platform_map = sorted_host_platforms(PLATFORMS) + base_name = rctx.name.removesuffix("_host") + platform_map = {} + for platform_name, info in sorted_host_platforms(PLATFORMS).items(): + platform_map[platform_name] = struct( + os_name = info.os_name, + arch = info.arch, + python_version = python_version, + impl_repo_name = "{}_{}".format(base_name, platform_name), + ) candidates = [] for platform in platforms: meta = platform_map[platform] if meta.os_name == os_name and meta.arch == cpu_name: - candidates.append(platform) + candidates.append((platform, meta)) if len(candidates) == 1: - return candidates[0] + platform_name, meta = candidates[0] + return meta.impl_repo_name if candidates: env_var = "RULES_PYTHON_REPO_TOOLCHAIN_{}_{}_{}".format( @@ -525,7 +604,11 @@ def _get_host_platform(*, rctx, logger, python_version, os_name, cpu_name, platf candidates = [preference] if candidates: - return candidates[0] + platform_name, meta = candidates[0] + suffix = meta.impl_repo_name + if not suffix: + suffix = platform_name + return suffix return logger.fail("Could not find a compatible 'host' python for '{os_name}', '{cpu_name}' from the loaded platforms: {platforms}".format( os_name = os_name, diff --git a/python/versions.bzl b/python/versions.bzl index 166cc98851..e712a2e126 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -15,6 +15,8 @@ """The Python versions we use for the toolchains. """ +load("//python/private:platform_info.bzl", "platform_info") + # Values present in the @platforms//os package MACOS_NAME = "osx" LINUX_NAME = "linux" @@ -684,42 +686,12 @@ MINOR_MAPPING = { "3.13": "3.13.2", } -def _platform_info( - *, - compatible_with = [], - flag_values = {}, - target_settings = [], - os_name, - arch): - """Creates a struct of platform metadata. - - Args: - compatible_with: list[str], where the values are string labels. These - are the target_compatible_with values to use with the toolchain - flag_values: dict[str|Label, Any] of config_setting.flag_values - compatible values. DEPRECATED -- use target_settings instead - target_settings: list[str], where the values are string labels. These - are the target_settings values to use with the toolchain. - os_name: str, the os name; must match the name used in `@platfroms//os` - arch: str, the cpu name; must match the name used in `@platforms//cpu` - - Returns: - A struct with attributes and values matching the args. - """ - return struct( - compatible_with = compatible_with, - flag_values = flag_values, - target_settings = target_settings, - os_name = os_name, - arch = arch, - ) - def _generate_platforms(): is_libc_glibc = str(Label("//python/config_settings:_is_py_linux_libc_glibc")) is_libc_musl = str(Label("//python/config_settings:_is_py_linux_libc_musl")) platforms = { - "aarch64-apple-darwin": _platform_info( + "aarch64-apple-darwin": platform_info( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:aarch64", @@ -727,7 +699,7 @@ def _generate_platforms(): os_name = MACOS_NAME, arch = "aarch64", ), - "aarch64-unknown-linux-gnu": _platform_info( + "aarch64-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:aarch64", @@ -738,7 +710,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "aarch64", ), - "armv7-unknown-linux-gnu": _platform_info( + "armv7-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:armv7", @@ -749,7 +721,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "arm", ), - "i386-unknown-linux-gnu": _platform_info( + "i386-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:i386", @@ -760,7 +732,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "x86_32", ), - "ppc64le-unknown-linux-gnu": _platform_info( + "ppc64le-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:ppc", @@ -771,7 +743,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "ppc", ), - "riscv64-unknown-linux-gnu": _platform_info( + "riscv64-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:riscv64", @@ -782,7 +754,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "riscv64", ), - "s390x-unknown-linux-gnu": _platform_info( + "s390x-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:s390x", @@ -793,7 +765,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "s390x", ), - "x86_64-apple-darwin": _platform_info( + "x86_64-apple-darwin": platform_info( compatible_with = [ "@platforms//os:macos", "@platforms//cpu:x86_64", @@ -801,7 +773,7 @@ def _generate_platforms(): os_name = MACOS_NAME, arch = "x86_64", ), - "x86_64-pc-windows-msvc": _platform_info( + "x86_64-pc-windows-msvc": platform_info( compatible_with = [ "@platforms//os:windows", "@platforms//cpu:x86_64", @@ -809,7 +781,7 @@ def _generate_platforms(): os_name = WINDOWS_NAME, arch = "x86_64", ), - "x86_64-unknown-linux-gnu": _platform_info( + "x86_64-unknown-linux-gnu": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", @@ -820,7 +792,7 @@ def _generate_platforms(): os_name = LINUX_NAME, arch = "x86_64", ), - "x86_64-unknown-linux-musl": _platform_info( + "x86_64-unknown-linux-musl": platform_info( compatible_with = [ "@platforms//os:linux", "@platforms//cpu:x86_64", @@ -836,7 +808,7 @@ def _generate_platforms(): is_freethreaded_yes = str(Label("//python/config_settings:_is_py_freethreaded_yes")) is_freethreaded_no = str(Label("//python/config_settings:_is_py_freethreaded_no")) return { - p + suffix: _platform_info( + p + suffix: platform_info( compatible_with = v.compatible_with, target_settings = [ freethreadedness, diff --git a/tests/bootstrap_impls/bin.py b/tests/bootstrap_impls/bin.py index 1176107384..3d467dcf29 100644 --- a/tests/bootstrap_impls/bin.py +++ b/tests/bootstrap_impls/bin.py @@ -23,3 +23,4 @@ print("sys.flags.safe_path:", sys.flags.safe_path) print("file:", __file__) print("sys.executable:", sys.executable) +print("sys._base_executable:", sys._base_executable) diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 19be1c478e..116afa76ad 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -17,6 +17,7 @@ load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility _tests = [] @@ -131,6 +132,10 @@ def _single_version_platform_override( python_version = python_version, patch_strip = patch_strip, patches = patches, + target_compatible_with = [], + target_settings = [], + os_name = "", + arch = "", ) def _test_default(env): @@ -138,6 +143,7 @@ def _test_default(env): module_ctx = _mock_mctx( _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) # The value there should be consistent in bzlmod with the automatically @@ -168,6 +174,7 @@ def _test_default_some_module(env): module_ctx = _mock_mctx( _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11") @@ -186,6 +193,7 @@ def _test_default_with_patch_version(env): module_ctx = _mock_mctx( _mod(name = "rules_python", toolchain = [_toolchain("3.11.2")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11.2") @@ -207,6 +215,7 @@ def _test_default_non_rules_python(env): # does not make any calls to the extension. _mod(name = "rules_python", toolchain = [_toolchain("3.11")], is_root = False), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11") @@ -228,6 +237,7 @@ def _test_default_non_rules_python_ignore_root_user_error(env): ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_bool(py.config.default["ignore_root_user_error"]).equals(False) @@ -257,6 +267,7 @@ def _test_default_non_rules_python_ignore_root_user_error_non_root_module(env): _mod(name = "some_module", toolchain = [_toolchain("3.12", ignore_root_user_error = False)]), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -302,6 +313,7 @@ def _test_toolchain_ordering(env): ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) got_versions = [ t.python_version @@ -347,6 +359,7 @@ def _test_default_from_defaults(env): is_root = True, ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.11") @@ -374,6 +387,7 @@ def _test_default_from_defaults_env(env): ), environ = {"PYENV_VERSION": "3.12"}, ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.12") @@ -401,6 +415,7 @@ def _test_default_from_defaults_file(env): ), mocked_files = {"@@//:.python-version": "3.12\n"}, ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.12") @@ -427,6 +442,7 @@ def _test_first_occurance_of_the_toolchain_wins(env): "RULES_PYTHON_BZLMOD_DEBUG": "1", }, ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.12") @@ -472,6 +488,7 @@ def _test_auth_overrides(env): ), _mod(name = "rules_python", toolchain = [_toolchain("3.11")]), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_dict(py.config.default).contains_at_least({ @@ -541,6 +558,7 @@ def _test_add_new_version(env): ], ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -609,6 +627,7 @@ def _test_register_all_versions(env): ], ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -685,6 +704,7 @@ def _test_add_patches(env): ], ), ), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_str(py.default_python_version).equals("3.13") @@ -731,6 +751,7 @@ def _test_fail_two_overrides(env): ), ), _fail = errors.append, + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_collection(errors).contains_exactly([ "Only a single 'python.override' can be present", @@ -758,6 +779,7 @@ def _test_single_version_override_errors(env): ), ), _fail = errors.append, + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_collection(errors).contains_exactly([test.want_error]) @@ -795,6 +817,7 @@ def _test_single_version_platform_override_errors(env): ), ), _fail = lambda *a: errors.append(" ".join(a)), + logger = repo_utils.logger(verbosity_level = 0, name = "python"), ) env.expect.that_collection(errors).contains_exactly([test.want_error]) diff --git a/tests/support/BUILD.bazel b/tests/support/BUILD.bazel index 9fb5cd0760..303dbafbdf 100644 --- a/tests/support/BUILD.bazel +++ b/tests/support/BUILD.bazel @@ -18,6 +18,7 @@ # to force them to resolve in the proper context. # ==================== +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load(":sh_py_run_test.bzl", "current_build_settings") package( @@ -90,3 +91,15 @@ platform( current_build_settings( name = "current_build_settings", ) + +string_flag( + name = "custom_runtime", + build_setting_default = "", +) + +config_setting( + name = "is_custom_runtime_linux-x86-install-only-stripped", + flag_values = { + ":custom_runtime": "linux-x86-install-only-stripped", + }, +) diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl index 04a2883fde..69141fe8a4 100644 --- a/tests/support/sh_py_run_test.bzl +++ b/tests/support/sh_py_run_test.bzl @@ -42,6 +42,7 @@ def _perform_transition_impl(input_settings, attr, base_impl): # value into the output settings _RECONFIG_ATTR_SETTING_MAP = { "bootstrap_impl": "//python/config_settings:bootstrap_impl", + "custom_runtime": "//tests/support:custom_runtime", "extra_toolchains": "//command_line_option:extra_toolchains", "python_src": "//python/bin:python_src", "venvs_site_packages": "//python/config_settings:venvs_site_packages", @@ -58,6 +59,7 @@ _RECONFIG_INHERITED_OUTPUTS = [v for v in _RECONFIG_OUTPUTS if v in _RECONFIG_IN _RECONFIG_ATTRS = { "bootstrap_impl": attrb.String(), "build_python_zip": attrb.String(default = "auto"), + "custom_runtime": attrb.String(), "extra_toolchains": attrb.StringList( doc = """ Value for the --extra_toolchains flag. diff --git a/tests/toolchains/BUILD.bazel b/tests/toolchains/BUILD.bazel index c55dc92a7d..f346651d46 100644 --- a/tests/toolchains/BUILD.bazel +++ b/tests/toolchains/BUILD.bazel @@ -12,8 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") load(":defs.bzl", "define_toolchain_tests") define_toolchain_tests( name = "toolchain_tests", ) + +py_reconfig_test( + name = "custom_platform_toolchain_test", + srcs = ["custom_platform_toolchain_test.py"], + custom_runtime = "linux-x86-install-only-stripped", + python_version = "3.13.1", + target_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ] if BZLMOD_ENABLED else ["@platforms//:incompatible"], +) diff --git a/tests/toolchains/custom_platform_toolchain_test.py b/tests/toolchains/custom_platform_toolchain_test.py new file mode 100644 index 0000000000..d6c083a6a2 --- /dev/null +++ b/tests/toolchains/custom_platform_toolchain_test.py @@ -0,0 +1,15 @@ +import sys +import unittest + + +class VerifyCustomPlatformToolchainTest(unittest.TestCase): + + def test_custom_platform_interpreter_used(self): + # We expect the repo name, and thus path, to have the + # platform name in it. + self.assertIn("linux-x86-install-only-stripped", sys._base_executable) + print(sys._base_executable) + + +if __name__ == "__main__": + unittest.main()