From a93fcafc1b117d83f666e8b270b6696b2d259a21 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 18:56:26 -0700 Subject: [PATCH 01/14] feat(toolchains): let local toolchains point to a label By letting a local toolchain point to a label, it allows Bazel repository rules to manage the download and creation of the Python runtime itself. This makes it easy to customize where a runtime is coming from. --- CHANGELOG.md | 1 + docs/toolchains.md | 4 ++ python/private/local_runtime_repo.bzl | 64 +++++++++++++++---- .../integration/local_toolchains/BUILD.bazel | 22 ++++++- .../integration/local_toolchains/MODULE.bazel | 39 ++++++++++- .../{test.py => local_runtime_test.py} | 6 +- .../local_toolchains/pbs_archive.bzl | 59 +++++++++++++++++ .../local_toolchains/repo_runtime_test.py | 19 ++++++ 8 files changed, 196 insertions(+), 18 deletions(-) rename tests/integration/local_toolchains/{test.py => local_runtime_test.py} (98%) create mode 100644 tests/integration/local_toolchains/pbs_archive.bzl create mode 100644 tests/integration/local_toolchains/repo_runtime_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 469e9d3612..814fe159ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,7 @@ END_UNRELEASED_TEMPLATE {obj}`py_cc_toolchain.headers_abi3`, and {obj}`PyCcToolchainInfo.headers_abi3`. * {obj}`//python:features.bzl%features.headers_abi3` can be used to feature-detect the presense of the above. +* (toolchains) Local toolchains can use a label for the interpreter to use. {#v1-6-3} ## [1.6.3] - 2025-09-21 diff --git a/docs/toolchains.md b/docs/toolchains.md index 52e619a120..186ad11e73 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -460,6 +460,10 @@ local_runtime_toolchains_repo( register_toolchains("@local_toolchains//:all", dev_dependency = True) ``` +In the example above, `interpreter_path` is used to find Python via `PATH` +lookups. Alternatively, {obj}`interpreter_target` can be set, which can +refer to a Python in an arbitrary Bazel repository. + :::{important} Be sure to set `dev_dependency = True`. Using a local toolchain only makes sense for the root module. diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 27c90b1bc9..62a0148377 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -196,17 +196,41 @@ create thousands of files for every `py_test`), at the risk of having to rely on a system having the necessary Python installed. """, attrs = { + "interpreter_target": attr.label( +doc = """ +A label to a Python interpreter executable. + +*Mutually exclusive with `interpreter_path`.* + +On Windows, if the path doesn't exist, various suffixes will be tried to +find a usable path. + +:::{seealso} +The {obj}`interpreter_path` attribute for getting the interpreter from +a path or PATH environment lookup. +::: +""" +), "interpreter_path": attr.string( doc = """ An absolute path or program name on the `PATH` env var. +*Mutually exclusive with `interpreter_target`.* + Values with slashes are assumed to be the path to a program. Otherwise, it is treated as something to search for on `PATH` Note that, when a plain program name is used, the path to the interpreter is resolved at repository evalution time, not runtime of any resulting binaries. + +If not set, defaults to `python3`. + +:::{seealso} +The {obj}`interpreter_target` attribute for getting the interpreter from +a label +::: """, - default = "python3", + default = "", ), "on_failure": attr.string( default = _OnFailure.SKIP, @@ -260,20 +284,34 @@ def _resolve_interpreter_path(rctx): returns a description of why it couldn't be resolved A path object or None. The path may not exist. """ - if "/" not in rctx.attr.interpreter_path and "\\" not in rctx.attr.interpreter_path: - # Provide a bit nicer integration with pyenv: recalculate the runtime if the - # user changes the python version using e.g. `pyenv shell` - repo_utils.getenv(rctx, "PYENV_VERSION") - result = repo_utils.which_unchecked(rctx, rctx.attr.interpreter_path) - resolved_path = result.binary - describe_failure = result.describe_failure + if rctx.attr.interpreter_path and rctx.attr.interpreter_target: + fail("interpreter_path and interpreter_target are mutually exclusive") + + if rctx.attr.interpreter_target: + path = rctx.path(rctx.attr.interpreter_target) + if path.exists: + resolved_path = path + describe_failure = None + else: + resolved_path = None + describe_failure = lambda: "Target '{}' could not be resolved to a file that exists".format(rctx.attr.interpreter_target) + else: - rctx.watch(rctx.attr.interpreter_path) - resolved_path = rctx.path(rctx.attr.interpreter_path) - if not resolved_path.exists: - describe_failure = lambda: "Path not found: {}".format(repr(rctx.attr.interpreter_path)) + interpreter_path = rctx.attr.interpreter_path or "python3" + if "/" not in interpreter_path and "\\" not in interpreter_path: + # Provide a bit nicer integration with pyenv: recalculate the runtime if the + # user changes the python version using e.g. `pyenv shell` + repo_utils.getenv(rctx, "PYENV_VERSION") + result = repo_utils.which_unchecked(rctx, interpreter_path) + resolved_path = result.binary + describe_failure = result.describe_failure else: - describe_failure = None + rctx.watch(interpreter_path) + resolved_path = rctx.path(interpreter_path) + if not resolved_path.exists: + describe_failure = lambda: "Path not found: {}".format(repr(interpreter_path)) + else: + describe_failure = None return struct( resolved_path = resolved_path, diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index a0cb2b164d..bf47316027 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -18,12 +18,23 @@ load("@rules_python//python:py_test.bzl", "py_test") load(":py_extension.bzl", "py_extension") py_test( - name = "test", - srcs = ["test.py"], + name = "local_runtime_test", + srcs = ["local_runtime_test.py"], + config_settings = { + "//:py": "local", + }, # Make this test better respect pyenv env_inherit = ["PYENV_VERSION"], ) +py_test( + name = "repo_runtime_test", + srcs = ["repo_runtime_test.py"], + config_settings = { + "//:py": "repo", + }, +) + config_setting( name = "is_py_local", flag_values = { @@ -31,6 +42,13 @@ config_setting( }, ) +config_setting( + name = "is_py_repo", + flag_values = { + ":py": "repo", + }, +) + # Set `--//:py=local` to use the local toolchain # (This is set in this example's .bazelrc) string_flag( diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index e81c012c2d..3bef71c3d7 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -33,22 +33,57 @@ local_runtime_repo( on_failure = "fail", ) +pbs_archive = use_repo_rule("//:pbs_archive.bzl", "pbs_archive") + +pbs_archive( + name = "pbs_runtime", + sha256 = { + "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", + "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", + "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + }, + urls = { + "windows": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", + "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", + "mac os x": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + }, +) + +local_runtime_repo( + name = "repo_python3", + interpreter_target = "@pbs_runtime//:python/bin/python3", + on_failure = "fail", +) + local_runtime_toolchains_repo( name = "local_toolchains", - runtimes = ["local_python3"], + runtimes = [ + "local_python3", + "repo_python3", + ], target_compatible_with = { "local_python3": [ "HOST_CONSTRAINTS", ], + "repo_python3": [ + "HOST_CONSTRAINTS", + ], }, target_settings = { "local_python3": [ "@//:is_py_local", ], + "repo_python3": [ + "@//:is_py_repo", + ], }, ) +config = use_extension("@rules_python//python/extensions:config.bzl", "config") +config.add_transition_setting(setting = "//:py") + python = use_extension("@rules_python//python/extensions:python.bzl", "python") -use_repo(python, "rules_python_bzlmod_debug") +python.toolchain(python_version = "3.13") +use_repo(python, "python_3_13_host", "rules_python_bzlmod_debug") register_toolchains("@local_toolchains//:all") diff --git a/tests/integration/local_toolchains/test.py b/tests/integration/local_toolchains/local_runtime_test.py similarity index 98% rename from tests/integration/local_toolchains/test.py rename to tests/integration/local_toolchains/local_runtime_test.py index 0a0d6bedeb..41481dd20a 100644 --- a/tests/integration/local_toolchains/test.py +++ b/tests/integration/local_toolchains/local_runtime_test.py @@ -6,6 +6,10 @@ import unittest +import config + +print(f"{config.CONFIG=}") + class LocalToolchainTest(unittest.TestCase): maxDiff = None @@ -28,7 +32,7 @@ def test_python_from_path_used(self): import sys print(sys.executable) print(sys._base_executable) -""" += ) f.flush() output_lines = ( diff --git a/tests/integration/local_toolchains/pbs_archive.bzl b/tests/integration/local_toolchains/pbs_archive.bzl new file mode 100644 index 0000000000..7e1645ee35 --- /dev/null +++ b/tests/integration/local_toolchains/pbs_archive.bzl @@ -0,0 +1,59 @@ +"""A repository rule to download and extract a Python runtime archive.""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +BUILD_BAZEL = """ +# Generated by pbs_archive.bzl + +package( + default_visibility = ["//visibility:public"], +) + +exports_files(glob(["**"])) +""" + +def _pbs_archive_impl(repository_ctx): + """Implementation of the python_build_standalone_archive rule.""" + os_name = repository_ctx.os.name.lower() + urls = repository_ctx.attr.urls + sha256s = repository_ctx.attr.sha256 + + if os_name not in urls: + fail("Unsupported OS: '{}'. Available OSs are: {}".format( + os_name, + ", ".join(urls.keys()), + )) + + url = urls[os_name] + sha256 = sha256s.get(os_name) + + repository_ctx.download_and_extract( + url = url, + sha256 = sha256, + strip_prefix = repository_ctx.attr.strip_prefix, + ) + + repository_ctx.file("BUILD.bazel", BUILD_BAZEL) + +pbs_archive = repository_rule( + implementation = _pbs_archive_impl, + attrs = { + "urls": attr.string_dict( + doc = "A dictionary of URLs to the runtime archives, keyed by OS name (e.g., 'linux', 'windows').", + mandatory = True, + ), + "sha256": attr.string_dict( + doc = "A dictionary of SHA256 checksums for the archives, keyed by OS name.", + mandatory = True, + ), + "strip_prefix": attr.string( + doc = "The prefix to strip from the archive.", + ), + }, + doc = """ +Downloads and extracts a Python runtime archive for the current OS. + +This rule selects a URL from the `urls` attribute based on the host OS, +downloads the archive, and extracts it. +""", +) diff --git a/tests/integration/local_toolchains/repo_runtime_test.py b/tests/integration/local_toolchains/repo_runtime_test.py new file mode 100644 index 0000000000..4614407c4e --- /dev/null +++ b/tests/integration/local_toolchains/repo_runtime_test.py @@ -0,0 +1,19 @@ +import os.path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +class RepoToolchainTest(unittest.TestCase): + maxDiff = None + + def test_python_from_repo_used(self): + actual = os.path.realpath(sys._base_executable.lower()) + # Normalize case: Windows may have case differences + self.assertIn("pbs_runtime", actual.lower()) + + +if __name__ == "__main__": + unittest.main() From f028ccce193f27a2092f2bad53bef3d9b0e2c10e Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 20:25:11 -0700 Subject: [PATCH 02/14] fix sha key, add some doc, remove unneeded use_repo --- .../integration/local_toolchains/MODULE.bazel | 15 ++++++++++--- tests/integration/local_toolchains/WORKSPACE | 22 +++++++++++++++++++ .../local_toolchains/pbs_archive.bzl | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 3bef71c3d7..c357a4c9af 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -23,10 +23,12 @@ local_path_override( path = "../../..", ) +# Step 1: Define the python runtime local_runtime_repo = use_repo_rule("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_repo") local_runtime_toolchains_repo = use_repo_rule("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_toolchains_repo") +# This will use `python3` from the environment local_runtime_repo( name = "local_python3", interpreter_path = "python3", @@ -43,18 +45,24 @@ pbs_archive( "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", }, urls = { - "windows": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", + "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", - "mac os x": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + "mac osx": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", }, ) +# This will use Python from the `pbs_runtime` repository. +# The pbs_runtime is just an example; the repo just needs to be a valid Python +# installation. local_runtime_repo( name = "repo_python3", interpreter_target = "@pbs_runtime//:python/bin/python3", on_failure = "fail", ) +# Step 2: Create toolchains for the runtimes +# Below, we configure them to only activate if the `//:py` flag has particular +# values. local_runtime_toolchains_repo( name = "local_toolchains", runtimes = [ @@ -84,6 +92,7 @@ config.add_transition_setting(setting = "//:py") python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.toolchain(python_version = "3.13") -use_repo(python, "python_3_13_host", "rules_python_bzlmod_debug") +use_repo(python, "rules_python_bzlmod_debug") +# Step 3: Register the toolchains register_toolchains("@local_toolchains//:all") diff --git a/tests/integration/local_toolchains/WORKSPACE b/tests/integration/local_toolchains/WORKSPACE index 480cd2794a..f744a3140c 100644 --- a/tests/integration/local_toolchains/WORKSPACE +++ b/tests/integration/local_toolchains/WORKSPACE @@ -21,6 +21,28 @@ local_runtime_repo( # or interpreter_path = "C:\\path\\to\\python.exe" ) +load("//:pbs_archive.bzl", "pbs_archive") + +pbs_archive( + name = "pbs_runtime", + sha256 = { + "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", + "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", + "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + }, + urls = { + "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", + "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", + "mac osx": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + }, +) + +local_runtime_repo( + name = "repo_python3", + interpreter_target = "@pbs_runtime//:python/bin/python3", + on_failure = "fail", +) + # Step 2: Create toolchains for the runtimes local_runtime_toolchains_repo( name = "local_toolchains", diff --git a/tests/integration/local_toolchains/pbs_archive.bzl b/tests/integration/local_toolchains/pbs_archive.bzl index 7e1645ee35..bd69411317 100644 --- a/tests/integration/local_toolchains/pbs_archive.bzl +++ b/tests/integration/local_toolchains/pbs_archive.bzl @@ -25,7 +25,7 @@ def _pbs_archive_impl(repository_ctx): )) url = urls[os_name] - sha256 = sha256s.get(os_name) + sha256 = sha256s.get(os_name, "") repository_ctx.download_and_extract( url = url, From fe5859334b89821cf4ebf5cd2b90643c2e98b46f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 20:27:07 -0700 Subject: [PATCH 03/14] make error message nicer --- python/private/local_runtime_repo.bzl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 62a0148377..e867f44c17 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -197,7 +197,7 @@ a system having the necessary Python installed. """, attrs = { "interpreter_target": attr.label( -doc = """ + doc = """ A label to a Python interpreter executable. *Mutually exclusive with `interpreter_path`.* @@ -209,8 +209,8 @@ find a usable path. The {obj}`interpreter_path` attribute for getting the interpreter from a path or PATH environment lookup. ::: -""" -), +""", + ), "interpreter_path": attr.string( doc = """ An absolute path or program name on the `PATH` env var. @@ -294,7 +294,12 @@ def _resolve_interpreter_path(rctx): describe_failure = None else: resolved_path = None - describe_failure = lambda: "Target '{}' could not be resolved to a file that exists".format(rctx.attr.interpreter_target) + describe_failure = lambda: ( + "Target '{}' resolved to path '{}', but that file does't exist".format( + rctx.attr.interpreter_taret, + path, + ) + ) else: interpreter_path = rctx.attr.interpreter_path or "python3" From 933344ee81a755e253b1ce72f918c416d74d46f9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 20:30:00 -0700 Subject: [PATCH 04/14] fix attr spelling --- python/private/local_runtime_repo.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index e867f44c17..edbaad238c 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -296,7 +296,7 @@ def _resolve_interpreter_path(rctx): resolved_path = None describe_failure = lambda: ( "Target '{}' resolved to path '{}', but that file does't exist".format( - rctx.attr.interpreter_taret, + rctx.attr.interpreter_target, path, ) ) From 7156b576697a169ba1bf90ab09db42ad296dd6b3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 20:47:20 -0700 Subject: [PATCH 05/14] implement some probing for windows --- python/private/local_runtime_repo.bzl | 39 ++++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index edbaad238c..8a52b123ed 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -271,6 +271,31 @@ def _expand_incompatible_template(): os = "@platforms//:incompatible", ) +def _find_python_exe_from_target(rctx): + base_path = rctx.path(rctx.attr.interpreter_target) + if base_path.exists: + return base_path, None + attempted_paths = [base_path] + + # On Windows, python.exe is in the root, not under `bin/` + path = rctx.path("{}/{}.exe".format(base_path.dirname, base_path.basename)) + if path.exists: + return path, None + attempted_paths.append(path) + + # Try adding .exe to the base path + path = rctx.path("{}.exe".format(base_path)) + if path.exists: + return path, None + attempted_paths.append(path) + + resolved_path = None + describe_failure = lambda: ( + "Target '{}' could not be resolved to a valid path. " + + "Attempted paths: {paths}".format("\n".join(attempted_paths)) + ) + return None, describe_failure + def _resolve_interpreter_path(rctx): """Find the absolute path for an interpreter. @@ -288,19 +313,7 @@ def _resolve_interpreter_path(rctx): fail("interpreter_path and interpreter_target are mutually exclusive") if rctx.attr.interpreter_target: - path = rctx.path(rctx.attr.interpreter_target) - if path.exists: - resolved_path = path - describe_failure = None - else: - resolved_path = None - describe_failure = lambda: ( - "Target '{}' resolved to path '{}', but that file does't exist".format( - rctx.attr.interpreter_target, - path, - ) - ) - + resolved_path, describe_failure = _find_python_exe_from_target(rctx) else: interpreter_path = rctx.attr.interpreter_path or "python3" if "/" not in interpreter_path and "\\" not in interpreter_path: From d3d9f5f0a2f72240ac64f38f077808dc03460c02 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:28:27 -0700 Subject: [PATCH 06/14] fix error message --- python/private/local_runtime_repo.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 8a52b123ed..1dd31624bd 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -292,7 +292,7 @@ def _find_python_exe_from_target(rctx): resolved_path = None describe_failure = lambda: ( "Target '{}' could not be resolved to a valid path. " + - "Attempted paths: {paths}".format("\n".join(attempted_paths)) + "Attempted paths: {paths}".format("\n".join([str(p) for p in attempted_paths])) ) return None, describe_failure From 8b97dc1f8be7aa71b7e5f5bc143eb6cdd0f12d44 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:30:32 -0700 Subject: [PATCH 07/14] format and fix py code --- python/private/local_runtime_repo.bzl | 31 +++++++++---------- .../integration/local_toolchains/MODULE.bazel | 4 +-- tests/integration/local_toolchains/WORKSPACE | 4 +-- .../local_toolchains/local_runtime_test.py | 6 +--- .../local_toolchains/pbs_archive.bzl | 10 +++--- 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 1dd31624bd..fb09ee4b51 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -196,21 +196,6 @@ create thousands of files for every `py_test`), at the risk of having to rely on a system having the necessary Python installed. """, attrs = { - "interpreter_target": attr.label( - doc = """ -A label to a Python interpreter executable. - -*Mutually exclusive with `interpreter_path`.* - -On Windows, if the path doesn't exist, various suffixes will be tried to -find a usable path. - -:::{seealso} -The {obj}`interpreter_path` attribute for getting the interpreter from -a path or PATH environment lookup. -::: -""", - ), "interpreter_path": attr.string( doc = """ An absolute path or program name on the `PATH` env var. @@ -232,6 +217,21 @@ a label """, default = "", ), + "interpreter_target": attr.label( + doc = """ +A label to a Python interpreter executable. + +*Mutually exclusive with `interpreter_path`.* + +On Windows, if the path doesn't exist, various suffixes will be tried to +find a usable path. + +:::{seealso} +The {obj}`interpreter_path` attribute for getting the interpreter from +a path or PATH environment lookup. +::: +""", + ), "on_failure": attr.string( default = _OnFailure.SKIP, values = sorted(_OnFailure.__members__.values()), @@ -289,7 +289,6 @@ def _find_python_exe_from_target(rctx): return path, None attempted_paths.append(path) - resolved_path = None describe_failure = lambda: ( "Target '{}' could not be resolved to a valid path. " + "Attempted paths: {paths}".format("\n".join([str(p) for p in attempted_paths])) diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index c357a4c9af..4be0798dcc 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -40,14 +40,14 @@ pbs_archive = use_repo_rule("//:pbs_archive.bzl", "pbs_archive") pbs_archive( name = "pbs_runtime", sha256 = { - "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", }, urls = { - "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", "mac osx": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", }, ) diff --git a/tests/integration/local_toolchains/WORKSPACE b/tests/integration/local_toolchains/WORKSPACE index f744a3140c..be08ff0207 100644 --- a/tests/integration/local_toolchains/WORKSPACE +++ b/tests/integration/local_toolchains/WORKSPACE @@ -26,14 +26,14 @@ load("//:pbs_archive.bzl", "pbs_archive") pbs_archive( name = "pbs_runtime", sha256 = { - "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", }, urls = { - "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", "mac osx": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", }, ) diff --git a/tests/integration/local_toolchains/local_runtime_test.py b/tests/integration/local_toolchains/local_runtime_test.py index 41481dd20a..0a0d6bedeb 100644 --- a/tests/integration/local_toolchains/local_runtime_test.py +++ b/tests/integration/local_toolchains/local_runtime_test.py @@ -6,10 +6,6 @@ import unittest -import config - -print(f"{config.CONFIG=}") - class LocalToolchainTest(unittest.TestCase): maxDiff = None @@ -32,7 +28,7 @@ def test_python_from_path_used(self): import sys print(sys.executable) print(sys._base_executable) -= +""" ) f.flush() output_lines = ( diff --git a/tests/integration/local_toolchains/pbs_archive.bzl b/tests/integration/local_toolchains/pbs_archive.bzl index bd69411317..deedf2a21e 100644 --- a/tests/integration/local_toolchains/pbs_archive.bzl +++ b/tests/integration/local_toolchains/pbs_archive.bzl @@ -1,7 +1,5 @@ """A repository rule to download and extract a Python runtime archive.""" -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - BUILD_BAZEL = """ # Generated by pbs_archive.bzl @@ -38,10 +36,6 @@ def _pbs_archive_impl(repository_ctx): pbs_archive = repository_rule( implementation = _pbs_archive_impl, attrs = { - "urls": attr.string_dict( - doc = "A dictionary of URLs to the runtime archives, keyed by OS name (e.g., 'linux', 'windows').", - mandatory = True, - ), "sha256": attr.string_dict( doc = "A dictionary of SHA256 checksums for the archives, keyed by OS name.", mandatory = True, @@ -49,6 +43,10 @@ pbs_archive = repository_rule( "strip_prefix": attr.string( doc = "The prefix to strip from the archive.", ), + "urls": attr.string_dict( + doc = "A dictionary of URLs to the runtime archives, keyed by OS name (e.g., 'linux', 'windows').", + mandatory = True, + ), }, doc = """ Downloads and extracts a Python runtime archive for the current OS. From b1387c7bcdc9a9276bd162393fa3f78f7f2d9afd Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:33:21 -0700 Subject: [PATCH 08/14] fix error message more --- python/private/local_runtime_repo.bzl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index fb09ee4b51..d99f042822 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -290,8 +290,11 @@ def _find_python_exe_from_target(rctx): attempted_paths.append(path) describe_failure = lambda: ( - "Target '{}' could not be resolved to a valid path. " + - "Attempted paths: {paths}".format("\n".join([str(p) for p in attempted_paths])) + "Target '{target}' could not be resolved to a valid path. " + + "Attempted paths: {paths}".format( + target = rctx.attr.interpreter_target, + paths = "\n".join([str(p) for p in attempted_paths]), + ) ) return None, describe_failure From 9aee1c3f1fefba6c93356d3c69d06e020843a2c4 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:36:42 -0700 Subject: [PATCH 09/14] use correct mac os x string --- tests/integration/local_toolchains/MODULE.bazel | 4 ++-- tests/integration/local_toolchains/WORKSPACE | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 4be0798dcc..9f37d1581a 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -41,12 +41,12 @@ pbs_archive( name = "pbs_runtime", sha256 = { "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", - "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + "mac os x": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", }, urls = { "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", - "mac osx": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + "mac os x": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", }, ) diff --git a/tests/integration/local_toolchains/WORKSPACE b/tests/integration/local_toolchains/WORKSPACE index be08ff0207..242ce7ca27 100644 --- a/tests/integration/local_toolchains/WORKSPACE +++ b/tests/integration/local_toolchains/WORKSPACE @@ -27,12 +27,12 @@ pbs_archive( name = "pbs_runtime", sha256 = { "linux": "0a01bad99fd4a165a11335c29eb43015dfdb8bd5ba8e305538ebb54f3bf3146d", - "mac osx": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", + "mac os x": "4fb42ffc8aad2a42ca7646715b8926bc6b2e0d31f13d2fec25943dc236a6fd60", "windows": "005cb2abf4cfa4aaa48fb10ce4e33fe4335ea4d1f55202dbe4e20c852e45e0f9", }, urls = { "linux": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-unknown-linux-gnu-install_only.tar.gz", - "mac osx": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", + "mac os x": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-apple-darwin-install_only.tar.gz", "windows server 2022": "https://github.com/astral-sh/python-build-standalone/releases/download/20250918/cpython-3.13.7+20250918-x86_64-pc-windows-msvc-install_only.tar.gz", }, ) From 64be885a8102c400e8134c53b38c82f90d2eab8f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:39:48 -0700 Subject: [PATCH 10/14] actually go up a dir when looking --- python/private/local_runtime_repo.bzl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index d99f042822..cbad95a0bf 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -277,8 +277,11 @@ def _find_python_exe_from_target(rctx): return base_path, None attempted_paths = [base_path] - # On Windows, python.exe is in the root, not under `bin/` - path = rctx.path("{}/{}.exe".format(base_path.dirname, base_path.basename)) + # On Windows, python.exe is in the root, not under `bin/`, so + # go up a directory + + path = base_path.dirname.dirname.relative_paths(base_path.basename) + path = rctx.path("{}.exe".format(path)) if path.exists: return path, None attempted_paths.append(path) From c9ef8f8bfdffe671296b563aea47d4d7d67a0176 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:44:43 -0700 Subject: [PATCH 11/14] call correct path get_child function --- python/private/local_runtime_repo.bzl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index cbad95a0bf..a8dc4b39a5 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -280,7 +280,7 @@ def _find_python_exe_from_target(rctx): # On Windows, python.exe is in the root, not under `bin/`, so # go up a directory - path = base_path.dirname.dirname.relative_paths(base_path.basename) + path = base_path.dirname.dirname.get_child(base_path.basename) path = rctx.path("{}.exe".format(path)) if path.exists: return path, None @@ -294,10 +294,10 @@ def _find_python_exe_from_target(rctx): describe_failure = lambda: ( "Target '{target}' could not be resolved to a valid path. " + - "Attempted paths: {paths}".format( - target = rctx.attr.interpreter_target, - paths = "\n".join([str(p) for p in attempted_paths]), - ) + "Attempted paths: {paths}" + ).format( + target = rctx.attr.interpreter_target, + paths = "\n".join([str(p) for p in attempted_paths]), ) return None, describe_failure From 822f1d8d4d8e457946648e8c97e8601c268d3965 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 21:55:33 -0700 Subject: [PATCH 12/14] convert bin/python3 to python.exe --- python/private/local_runtime_repo.bzl | 9 +++++---- tests/integration/local_toolchains/MODULE.bazel | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index a8dc4b39a5..583926b15f 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -277,10 +277,11 @@ def _find_python_exe_from_target(rctx): return base_path, None attempted_paths = [base_path] - # On Windows, python.exe is in the root, not under `bin/`, so - # go up a directory - - path = base_path.dirname.dirname.get_child(base_path.basename) + # Try to convert a unix-y path to a Windows path. On Linux/Mac, + # the path is usually `bin/python3`. On Windows, it's simply + # `python.exe`. + basename = base_path.basename.rstrip("3") + path = base_path.dirname.dirname.get_child(basename) path = rctx.path("{}.exe".format(path)) if path.exists: return path, None diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 9f37d1581a..6c821c5bb0 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -56,7 +56,7 @@ pbs_archive( # installation. local_runtime_repo( name = "repo_python3", - interpreter_target = "@pbs_runtime//:python/bin/python3", + interpreter_target = "@pbs_runtime//:python/bin/python", on_failure = "fail", ) From 82c7b7ee2716543950a9a0d192d8aa0bb9a3078e Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 22:38:18 -0700 Subject: [PATCH 13/14] remove strip prefix arg, not used, causes error --- tests/integration/local_toolchains/pbs_archive.bzl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/local_toolchains/pbs_archive.bzl b/tests/integration/local_toolchains/pbs_archive.bzl index deedf2a21e..8bd0c1eb10 100644 --- a/tests/integration/local_toolchains/pbs_archive.bzl +++ b/tests/integration/local_toolchains/pbs_archive.bzl @@ -28,7 +28,6 @@ def _pbs_archive_impl(repository_ctx): repository_ctx.download_and_extract( url = url, sha256 = sha256, - strip_prefix = repository_ctx.attr.strip_prefix, ) repository_ctx.file("BUILD.bazel", BUILD_BAZEL) @@ -40,9 +39,6 @@ pbs_archive = repository_rule( doc = "A dictionary of SHA256 checksums for the archives, keyed by OS name.", mandatory = True, ), - "strip_prefix": attr.string( - doc = "The prefix to strip from the archive.", - ), "urls": attr.string_dict( doc = "A dictionary of URLs to the runtime archives, keyed by OS name (e.g., 'linux', 'windows').", mandatory = True, From 0ec5d079371406f4a5f2b7a3515149e5c3982d1c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sun, 28 Sep 2025 22:48:28 -0700 Subject: [PATCH 14/14] add missing workspace config --- tests/integration/local_toolchains/WORKSPACE | 27 ++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/integration/local_toolchains/WORKSPACE b/tests/integration/local_toolchains/WORKSPACE index 242ce7ca27..159f16deab 100644 --- a/tests/integration/local_toolchains/WORKSPACE +++ b/tests/integration/local_toolchains/WORKSPACE @@ -9,7 +9,11 @@ local_repository( load("@rules_python//python:repositories.bzl", "py_repositories") -py_repositories() +py_repositories( + transition_settings = [ + "@//:py", + ], +) load("@rules_python//python/local_toolchains:repos.bzl", "local_runtime_repo", "local_runtime_toolchains_repo") @@ -46,7 +50,26 @@ local_runtime_repo( # Step 2: Create toolchains for the runtimes local_runtime_toolchains_repo( name = "local_toolchains", - runtimes = ["local_python3"], + runtimes = [ + "local_python3", + "repo_python3", + ], + target_compatible_with = { + "local_python3": [ + "HOST_CONSTRAINTS", + ], + "repo_python3": [ + "HOST_CONSTRAINTS", + ], + }, + target_settings = { + "local_python3": [ + "@//:is_py_local", + ], + "repo_python3": [ + "@//:is_py_repo", + ], + }, ) # Step 3: Register the toolchains