feat: allow custom platform when overriding (#2880)
This basically allows using any python-build-standalone archive and
using it
if custom flags are set. This is done through the
`single_version_platform_override()`
API, because such archives are inherently version and platform specific.
Key changes:
* The `platform` arg can be any value (mostly; it ends up in repo names)
* Added `target_compatible_with` and `target_settings` args, which
become the
settings used on the generated toolchain() definition.
The platform settings are version specific, i.e. the key
`(python_version, platform)`
is what maps to the TCW/TS values.
If an existing platform is used, it'll override the defaults that
normally come
from the PLATFORMS global for the particular version. If a new platform
is used,
it creates a new platform entry with those settings.
Along the way:
* Added various docs about internal variables so they're easier to grok
at a glance.
Work towards https://github.com/bazel-contrib/rules_python/issues/2081
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a6bdf0..a113c74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -107,6 +107,10 @@
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 d3a9535..144e130 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -125,6 +125,22 @@
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 ada887c..57d43d2 100644
--- a/docs/toolchains.md
+++ b/docs/toolchains.md
@@ -243,6 +243,73 @@
* 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 62a11ab..c37c59a 100644
--- a/internal_dev_setup.bzl
+++ b/internal_dev_setup.bzl
@@ -42,7 +42,7 @@
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 867c434..58cff5b 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -247,6 +247,7 @@
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 ce22421..b319919 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -242,10 +242,16 @@
)
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 0000000..3f7dc00
--- /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 b5bd93b..10bc066 100644
--- a/python/private/py_repositories.bzl
+++ b/python/private/py_repositories.bzl
@@ -47,7 +47,7 @@
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 a7e2576..8e23668 100644
--- a/python/private/python.bzl
+++ b/python/private/python.bzl
@@ -18,29 +18,44 @@
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 @@
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 _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 @@
_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 @@
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_platforms = sorted_host_platforms(host_platforms)
+ 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 = 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())
+ 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)
},
- arch_names = {
- str(i): platform_info.arch
- for i, platform_info in enumerate(host_platforms.values())
- },
- 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 @@
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 @@
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 @@
"""
# 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,12 +1225,50 @@
:::
""",
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(
mandatory = False,
doc = "Same as the --strip argument of Unix patch.",
@@ -1101,8 +1280,20 @@
),
"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 @@
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 fd86b41..cb0731e 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 @@
"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 53351ca..cc25b4b 100644
--- a/python/private/pythons_hub.bzl
+++ b/python/private/pythons_hub.bzl
@@ -84,13 +84,7 @@
)
_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 @@
# 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 @@
""",
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 eee56ec..32a5b70 100644
--- a/python/private/repo_utils.bzl
+++ b/python/private/repo_utils.bzl
@@ -31,13 +31,15 @@
"""
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 @@
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 @@
"""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 @@
`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 2476889..93bbb52 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -309,11 +309,11 @@
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 @@
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 @@
# 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` + <matching platform 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),
- "python_version": attr.string(mandatory = True),
+ "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`.
+""",
+ ),
"_rule_name": attr.string(default = "host_compatible_python_repo"),
"_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")),
},
@@ -435,8 +486,8 @@
},
)
-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 @@
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 @@
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 @@
"""
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 @@
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 166cc98..e712a2e 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 @@
"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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 1176107..3d467dc 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 19be1c4..116afa7 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 @@
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 @@
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 @@
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 @@
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 @@
# 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 @@
),
_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 @@
_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 @@
),
_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 @@
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 @@
),
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 @@
),
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 @@
"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 @@
),
_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 @@
],
),
),
+ logger = repo_utils.logger(verbosity_level = 0, name = "python"),
)
env.expect.that_str(py.default_python_version).equals("3.13")
@@ -609,6 +627,7 @@
],
),
),
+ logger = repo_utils.logger(verbosity_level = 0, name = "python"),
)
env.expect.that_str(py.default_python_version).equals("3.13")
@@ -685,6 +704,7 @@
],
),
),
+ logger = repo_utils.logger(verbosity_level = 0, name = "python"),
)
env.expect.that_str(py.default_python_version).equals("3.13")
@@ -731,6 +751,7 @@
),
),
_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 @@
),
),
_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 @@
),
),
_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 9fb5cd0..303dbaf 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 @@
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 04a2883..69141fe 100644
--- a/tests/support/sh_py_run_test.bzl
+++ b/tests/support/sh_py_run_test.bzl
@@ -42,6 +42,7 @@
# 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_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 c55dc92..f346651 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 0000000..d6c083a
--- /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()