diff --git a/docs/interpreter.md b/docs/interpreter.md index 20bda70ea..c0d850e17 100644 --- a/docs/interpreter.md +++ b/docs/interpreter.md @@ -273,8 +273,25 @@ This interpreter provisioning is designed to coexist with `rules_python`: registration, so these interpreters work with all existing Python rules. - The `@rules_python//python/config_settings:python_version` flag is kept in sync with our own version flag via build transitions. -- `py_runtime` and `py_runtime_pair` from `rules_python` are used to create - the runtime providers. +- Runtimes registered with `rules_python`'s `py_runtime` / `py_runtime_pair` + (for example a system interpreter) remain usable by rules_py rules, which + read the runtime fields structurally. +- Build actions that run an interpreter (wheel installation, site-packages + merging) resolve `@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type`, + registered by `interpreters.toolchain()`. The exec interpreter follows the + Python version flags when the hub provisions that version; otherwise it + falls back to the hub's highest provisioned version — including the hub + rules_py itself registers, so this resolves even in modules that provision + interpreters only through `rules_python`'s `python.toolchain()`. rules_py + registers nothing under `rules_python`'s exec-tools type, leaving it — + including precompiling — entirely to `rules_python`. + +Note that runtimes provisioned by `interpreters.toolchain()` carry +`rules_python`'s public `PyRuntimeInfo` (re-exported from +`@aspect_rules_py//py:defs.bzl`), so `rules_python`-defined executables and +their downstream consumers (`py_zipapp_binary`, `py_interpreter`) analyze and +run on them. No coverage tool is bundled, so `rules_python`-rule coverage on +these runtimes is unavailable. You can migrate incrementally: replace `python.toolchain()` calls with `interpreters.toolchain()` and remove the `rules_python` interpreter diff --git a/e2e/cases/rules-python-consumers/BUILD.bazel b/e2e/cases/rules-python-consumers/BUILD.bazel index 390c64a91..6178af188 100644 --- a/e2e/cases/rules-python-consumers/BUILD.bazel +++ b/e2e/cases/rules-python-consumers/BUILD.bazel @@ -1,5 +1,7 @@ load("@aspect_rules_py//py:defs.bzl", "py_library", "py_test", "py_wheel", "whl_filegroup") +load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") +load("@rules_python//python/zipapp:py_zipapp_binary.bzl", "py_zipapp_binary") load("@rules_shell//shell:sh_test.bzl", "sh_test") load(":exec_tools_facts.bzl", "exec_tools_facts", "with_python_version") load(":report_exec_version.bzl", "report_exec_version") @@ -85,3 +87,19 @@ sh_test( args = ["$(rootpath :pycowsay)"], data = [":pycowsay"], ) + +# rules_python's py_zipapp_binary requires and indexes rules_python's +# PyRuntimeInfo on the wrapped binary, which forwards the runtime it resolved +# from the rules_py-provisioned toolchain — pinning that toolchains carry +# rules_python's provider by identity, not a structurally compatible one. +# Only PyRuntimeInfo is under test here: the binary's dependency graph is +# pip-hub-internal, so no PyInfo crosses the ruleset boundary. +py_zipapp_binary( + name = "pycowsay_zipapp", + binary = ":pycowsay", +) + +build_test( + name = "zipapp_test", + targets = [":pycowsay_zipapp"], +) diff --git a/e2e/cases/rules-python-consumers/consumers_test.py b/e2e/cases/rules-python-consumers/consumers_test.py index 04a41e47c..40c91558b 100644 --- a/e2e/cases/rules-python-consumers/consumers_test.py +++ b/e2e/cases/rules-python-consumers/consumers_test.py @@ -22,11 +22,9 @@ def read(rel): python3 = read("python3_var.txt") assert "python_interpreters+python_3_13" in python3, python3 -# The exec-tools payload carries rules_python's expected shape: an exec -# runtime from a rules_py-provisioned repo, and a precompiler field (None). +# The exec-tools payload serves an exec runtime from a rules_py-provisioned repo. facts = read("exec_tools_facts.txt").splitlines() assert "python_interpreters+" in facts[0], facts -assert facts[1] == "None", facts assert read("python_launcher.txt") == "3.11" diff --git a/e2e/cases/rules-python-consumers/exec_tools_facts.bzl b/e2e/cases/rules-python-consumers/exec_tools_facts.bzl index 1a2d2c62a..b88a5d43c 100644 --- a/e2e/cases/rules-python-consumers/exec_tools_facts.bzl +++ b/e2e/cases/rules-python-consumers/exec_tools_facts.bzl @@ -1,19 +1,15 @@ """Materialises the resolved exec-tools toolchain payload for assertion. -Reads the fields rules_python's consumers access — `exec_tools.exec_runtime` -and `exec_tools.precompiler` — so the test pins that rules_py's registration -under rules_python's toolchain type keeps their expected shape. +Reads the runtime from rules_py's exec-tools toolchain type so the test pins +that it serves a rules_py-provisioned interpreter. """ -EXEC_TOOLS_TOOLCHAIN = "@rules_python//python:exec_tools_toolchain_type" +EXEC_TOOLS_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type" def _exec_tools_facts_impl(ctx): - exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_tools + exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_runtime out = ctx.actions.declare_file(ctx.label.name + ".txt") - ctx.actions.write(out, "{}\n{}\n".format( - exec_tools.exec_runtime.interpreter.path, - exec_tools.precompiler, - )) + ctx.actions.write(out, exec_runtime.interpreter.path + "\n") return [DefaultInfo(files = depset([out]))] exec_tools_facts = rule( diff --git a/e2e/cases/rules-python-consumers/report_exec_version.bzl b/e2e/cases/rules-python-consumers/report_exec_version.bzl index 609b5dd97..84b890127 100644 --- a/e2e/cases/rules-python-consumers/report_exec_version.bzl +++ b/e2e/cases/rules-python-consumers/report_exec_version.bzl @@ -1,14 +1,14 @@ """Runnable that prints the resolved exec-tools runtime version. -Resolves rules_python's exec-tools toolchain directly (no py_* version +Resolves rules_py's exec-tools toolchain directly (no py_* version transition), so the reported version reflects the version flag only if the flag is authoritative in the interpreter hub. `bazel run` it and assert on stdout. """ -_EXEC_TOOLS_TOOLCHAIN = "@rules_python//python:exec_tools_toolchain_type" +_EXEC_TOOLS_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type" def _report_exec_version_impl(ctx): - version_info = ctx.toolchains[_EXEC_TOOLS_TOOLCHAIN].exec_tools.exec_runtime.interpreter_version_info + version_info = ctx.toolchains[_EXEC_TOOLS_TOOLCHAIN].exec_runtime.interpreter_version_info launcher = ctx.actions.declare_file(ctx.label.name + ".sh") ctx.actions.write( output = launcher, diff --git a/e2e/cases/rules-python-consumers/test.sh b/e2e/cases/rules-python-consumers/test.sh index 7dd0e7324..86f4248d0 100755 --- a/e2e/cases/rules-python-consumers/test.sh +++ b/e2e/cases/rules-python-consumers/test.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# report_exec_version resolves rules_python's exec-tools toolchain without the +# report_exec_version resolves rules_py's exec-tools toolchain without the # py_* version transition, so it prints the requested version only if the # version flag is authoritative in the interpreter hub. Both the native flag # and @rules_python's fallback must select the requested version. diff --git a/e2e/cases/uv-deps-650/crossbuild/toolchain_test.bzl b/e2e/cases/uv-deps-650/crossbuild/toolchain_test.bzl index ef4652203..0bc5ffc6d 100644 --- a/e2e/cases/uv-deps-650/crossbuild/toolchain_test.bzl +++ b/e2e/cases/uv-deps-650/crossbuild/toolchain_test.bzl @@ -5,10 +5,10 @@ path into a file, so sh_test scripts can verify which interpreter was selected under cross-compilation (target platform ≠ exec platform). """ -EXEC_TOOLS_TOOLCHAIN = "@rules_python//python:exec_tools_toolchain_type" +EXEC_TOOLS_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type" def _exec_python_path_impl(ctx): - exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_tools.exec_runtime + exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_runtime out = ctx.actions.declare_file(ctx.label.name + ".txt") ctx.actions.write(out, exec_runtime.interpreter.path) return [DefaultInfo(files = depset([out]))] diff --git a/e2e/interpreter-runtime-metadata/BUILD.bazel b/e2e/interpreter-runtime-metadata/BUILD.bazel index 567144629..818da6bda 100644 --- a/e2e/interpreter-runtime-metadata/BUILD.bazel +++ b/e2e/interpreter-runtime-metadata/BUILD.bazel @@ -3,11 +3,11 @@ load(":runtime_metadata_test.bzl", "runtime_metadata_test") runtime_metadata_test( name = "regular", abi_flags = "", - runtime = "@python_3_15_x86_64_unknown_linux_gnu//:py3_runtime", + runtime = "@python_3_15_x86_64_unknown_linux_gnu//:runtime", ) runtime_metadata_test( name = "freethreaded", abi_flags = "t", - runtime = "@python_3_15_x86_64_unknown_linux_gnu_freethreaded//:py3_runtime", + runtime = "@python_3_15_x86_64_unknown_linux_gnu_freethreaded//:runtime", ) diff --git a/e2e/interpreter-runtime-metadata/runtime_metadata_test.bzl b/e2e/interpreter-runtime-metadata/runtime_metadata_test.bzl index 2fa19282e..e0248d0a5 100644 --- a/e2e/interpreter-runtime-metadata/runtime_metadata_test.bzl +++ b/e2e/interpreter-runtime-metadata/runtime_metadata_test.bzl @@ -1,6 +1,6 @@ """Checks metadata on a provisioned PBS Python runtime.""" -load("@rules_python//python:py_runtime_info.bzl", "PyRuntimeInfo") +load("@aspect_rules_py//py:defs.bzl", "PyRuntimeInfo") def _assert_equal(description, expected, actual): if actual != expected: diff --git a/e2e/rules-python-interop/BUILD.bazel b/e2e/rules-python-interop/BUILD.bazel index 148104d4e..669411aac 100644 --- a/e2e/rules-python-interop/BUILD.bazel +++ b/e2e/rules-python-interop/BUILD.bazel @@ -79,3 +79,15 @@ rules_py_test( main = "pip_deps_test.py", deps = ["@pip//six"], ) + +# Deps from a rules_py uv hub: whl_install's exec-tools requirement must be +# satisfiable with only rules_python-provisioned target interpreters, via +# the exec-tools fallback of the PBS hub bundled in rules_py's own MODULE. +rules_py_test( + name = "uv_deps_test", + srcs = ["uv_deps_test.py"], + dep_group = "py-binary-example", + main = "uv_deps_test.py", + python_version = "3.12", + deps = ["@rpy_pypi//cowsay"], +) diff --git a/e2e/rules-python-interop/MODULE.bazel b/e2e/rules-python-interop/MODULE.bazel index 59824cb45..62247736a 100644 --- a/e2e/rules-python-interop/MODULE.bazel +++ b/e2e/rules-python-interop/MODULE.bazel @@ -27,3 +27,15 @@ pip.parse( requirements_lock = "//:requirements.txt", ) use_repo(pip, "pip") + +# A rules_py uv hub: whl_install must find an exec-tools runtime even though +# only rules_python provisions target interpreters here — rules_py's own +# MODULE bundles a PBS interpreter hub whose exec-tools fallback serves it. +uv = use_extension("@aspect_rules_py//uv:extensions.bzl", "uv") +uv.declare_hub(hub_name = "rpy_pypi") +uv.project( + hub_name = "rpy_pypi", + lock = "//:uv.lock", + pyproject = "//:pyproject.toml", +) +use_repo(uv, "rpy_pypi") diff --git a/e2e/rules-python-interop/pyproject.toml b/e2e/rules-python-interop/pyproject.toml new file mode 100644 index 000000000..434a44b5d --- /dev/null +++ b/e2e/rules-python-interop/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "py-binary-example" +version = "0.0.0" +requires-python = ">=3.11" +dependencies = [ + "cowsay", +] diff --git a/e2e/rules-python-interop/uv.lock b/e2e/rules-python-interop/uv.lock new file mode 100644 index 000000000..28b60bc8f --- /dev/null +++ b/e2e/rules-python-interop/uv.lock @@ -0,0 +1,22 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "cowsay" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/13/63c0a02c44024ee16f664e0b36eefeb22d54e93531630bd99e237986f534/cowsay-6.1-py3-none-any.whl", hash = "sha256:274b1e6fc1b966d53976333eb90ac94cb07a450a700b455af9fbdf882244b30a", size = 25560, upload-time = "2023-09-25T16:30:01.619Z" }, +] + +[[package]] +name = "py-binary-example" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "cowsay" }, +] + +[package.metadata] +requires-dist = [{ name = "cowsay" }] diff --git a/e2e/rules-python-interop/uv_deps_test.py b/e2e/rules-python-interop/uv_deps_test.py new file mode 100644 index 000000000..601fcae60 --- /dev/null +++ b/e2e/rules-python-interop/uv_deps_test.py @@ -0,0 +1,9 @@ +"""Asserts a uv-hub wheel imports inside a venv assembled over the +rules_python-provisioned runtime — exercising whl_install's exec-tools +resolution in a module with no rules_py interpreters. +""" + +import cowsay + +assert cowsay.get_output_string("cow", "moo") +print("OK") diff --git a/py/BUILD.bazel b/py/BUILD.bazel index 23b6a5c35..076117615 100644 --- a/py/BUILD.bazel +++ b/py/BUILD.bazel @@ -64,6 +64,7 @@ bzl_library( "//py/private:py_wheel", "//py/private:virtual", "//py/private/interpreter:current_py_toolchain", + "//py/private/interpreter:runtime", "//py/private/py_venv:defs", "@bazel_lib//lib:utils", "@rules_python//python:packaging_bzl", diff --git a/py/defs.bzl b/py/defs.bzl index dac0a5c9c..a8566802b 100644 --- a/py/defs.bzl +++ b/py/defs.bzl @@ -9,7 +9,9 @@ which has been registered in the `MODULE.bazel` file, e.g.: ```starlark interpreters = use_extension("@aspect_rules_py//py:extensions.bzl", "python_interpreters") interpreters.toolchain(python_version = "3.9") +interpreters.toolchain(python_version = "3.12") use_repo(interpreters, "python_interpreters") + register_toolchains("@python_interpreters//:all") ``` """ @@ -31,6 +33,7 @@ load("//py/private:py_pytest_main.bzl", _py_pytest_main = "py_pytest_main", _pyt load("//py/private:py_unpacked_wheel.bzl", _py_unpacked_wheel = "py_unpacked_wheel") load("//py/private:virtual.bzl", _resolutions = "resolutions") load("//py/private/interpreter:current_py_toolchain.bzl", _current_py_toolchain = "current_py_toolchain") +load("//py/private/interpreter:runtime.bzl", _PyRuntimeInfo = "PyRuntimeInfo") load( "//py/private/py_venv:defs.bzl", _py_binary_with_venv = "py_binary_with_venv", @@ -61,6 +64,10 @@ PyLayerTierInfo = _PyLayerTierInfo # The PyInfo provider used by rules_py PyInfo = _PyInfo +# The runtime provider carried by rules_py-provisioned interpreter toolchains: +# rules_python's public PyRuntimeInfo, the shared standard-toolchain contract. +PyRuntimeInfo = _PyRuntimeInfo + resolutions = _resolutions def _resolve_main(name, srcs, main): diff --git a/py/private/interpreter/BUILD.bazel b/py/private/interpreter/BUILD.bazel index 44b216b2c..2affd8c38 100644 --- a/py/private/interpreter/BUILD.bazel +++ b/py/private/interpreter/BUILD.bazel @@ -121,6 +121,12 @@ bzl_library( deps = [":sanitize"], ) +bzl_library( + name = "runtime", + srcs = ["runtime.bzl"], + deps = ["@rules_python//python:py_runtime_info_bzl"], +) + bzl_library( name = "sanitize", srcs = ["sanitize.bzl"], diff --git a/py/private/interpreter/repository.bzl b/py/private/interpreter/repository.bzl index 21bbda371..44c661a53 100644 --- a/py/private/interpreter/repository.bzl +++ b/py/private/interpreter/repository.bzl @@ -201,9 +201,7 @@ cc_library( return """\ load("@rules_cc//cc:cc_import.bzl", "cc_import") load("@rules_cc//cc:cc_library.bzl", "cc_library") -load("@rules_python//python:py_runtime.bzl", "py_runtime") -load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair") -load("@rules_python//python:py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain") +load("@aspect_rules_py//py/private/interpreter:runtime.bzl", "py_runtime_toolchain") load("@rules_python//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") package(default_visibility = ["//visibility:public"]) @@ -244,8 +242,8 @@ filegroup( deps = {header_interface_deps}, ) -py_runtime( - name = "py3_runtime", +py_runtime_toolchain( + name = "runtime", abi_flags = "{abi_flags}", files = [":files"], interpreter = "{python_bin}", @@ -256,17 +254,6 @@ py_runtime( "releaselevel": "{releaselevel}", "serial": "{serial}", }}, - python_version = "PY3", -) - -py_runtime_pair( - name = "runtime_pair", - py2_runtime = None, - py3_runtime = ":py3_runtime", -) - -py_exec_tools_toolchain( - name = "exec_tools_toolchain", ) py_cc_toolchain( @@ -433,6 +420,7 @@ config_setting( # offers a matching pair: https://github.com/aspect-build/rules_py/issues/1095 # # Second pass: emit toolchain() registrations. + exec_tools_fallbacks = {} # repr(exec_compatible_with) -> (version tuple, name, repo, constraints) for info, platform_setting_names in toolchain_infos: extra_config_settings = info.get("config_settings", []) extra_target_compatible = info.get("target_compatible_with", []) @@ -461,7 +449,7 @@ toolchain( name = "{name}", target_compatible_with = {target_compatible_with}, target_settings = {target_settings}, - toolchain = "@{repo}//:runtime_pair", + toolchain = "@{repo}//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -483,18 +471,47 @@ toolchain( content.append("""# Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "{name}_exec_tools", exec_compatible_with = {exec_compatible_with}, - toolchain = "@{repo}//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = ["{version_setting}"], + toolchain = "@{repo}//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) """.format( name = info["name"], repo = info["repo"], exec_compatible_with = exec_compatible_with, + version_setting = version_setting, )) + # Track the platform's highest provisioned version for the + # ungated fallback entry emitted after the loop. + version_key = tuple([int(part) for part in info["python_version"].split(".")]) + prior = exec_tools_fallbacks.get(repr(exec_compatible_with)) + if prior == None or version_key > prior[0]: + exec_tools_fallbacks[repr(exec_compatible_with)] = (version_key, info["name"], info["repo"], exec_compatible_with) + + # Ungated exec-tools fallbacks: build actions only need *a* runnable host + # interpreter, so configurations matching no version-gated entry (e.g. the + # version flags at defaults not provisioned by this hub) fall back to the + # platform's highest provisioned version. Registration is lexicographic by + # name, so the `zz_` prefix sorts these after every gated entry. + for _, name, repo, constraints in exec_tools_fallbacks.values(): + content.append(""" +toolchain( + name = "zz_{name}_exec_tools_fallback", + exec_compatible_with = {exec_compatible_with}, + toolchain = "@{repo}//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) +""".format( + name = name, + repo = repo, + exec_compatible_with = constraints, + )) + content.append(""" exports_files( ["BUILD.bazel"], diff --git a/py/private/interpreter/runtime.bzl b/py/private/interpreter/runtime.bzl new file mode 100644 index 000000000..0da23f745 --- /dev/null +++ b/py/private/interpreter/runtime.bzl @@ -0,0 +1,100 @@ +"""Python runtime toolchain rule for provisioned interpreter repositories. + +A minimal replacement for the rules_python `py_runtime` + `py_runtime_pair` + +`py_exec_tools_toolchain` rule stack: one target provides the runtime and the +ToolchainInfo payloads for both the standard Python toolchain type and +rules_py's exec-tools toolchain type. + +The runtime is an instance of rules_python's public PyRuntimeInfo (re-exported +here and from //py:defs.bzl): `@bazel_tools//tools/python:toolchain_type` is +the shared target-runtime contract, and rules_python's py_binary forwards the +resolved runtime provider onto built binaries, where downstream consumers +(py_zipapp_binary, py_interpreter) index it by that provider symbol. +""" + +load("@rules_python//python:py_runtime_info.bzl", _PyRuntimeInfo = "PyRuntimeInfo") + +PyRuntimeInfo = _PyRuntimeInfo + +def _py_runtime_toolchain_impl(ctx): + version_info = ctx.attr.interpreter_version_info + for key in ("major", "minor", "micro"): + if key not in version_info: + fail("interpreter_version_info must contain '{}'".format(key)) + + runtime = PyRuntimeInfo( + interpreter = ctx.file.interpreter, + files = depset(ctx.files.files), + interpreter_version_info = version_info, + implementation_name = "cpython", + abi_flags = ctx.attr.abi_flags, + # Freethreaded CPython keeps the same cache tag without its `t` ABI flag: + # https://github.com/python/cpython/blob/v3.15.0a5/Python/sysmodule.c#L3570-L3576 + pyc_tag = "cpython-{}{}".format(int(version_info["major"]), int(version_info["minor"])), + supports_build_time_venv = True, + python_version = "PY3", + # rules_python's own public-for-implicit-deps template files — label + # references, not .bzl loads. + bootstrap_template = ctx.file._bootstrap_template, + stage2_bootstrap_template = ctx.file._stage2_bootstrap_template, + site_init_template = ctx.file._site_init_template, + zip_main_template = ctx.file._zip_main_template, + ) + + return [ + runtime, + platform_common.ToolchainInfo( + # The standard Python toolchain contract + # (@bazel_tools//tools/python:toolchain_type). + py2_runtime = None, + py3_runtime = runtime, + # The //py/private/toolchain:exec_tools_toolchain_type contract. + exec_runtime = runtime, + ), + DefaultInfo(files = depset([ctx.file.interpreter], transitive = [runtime.files])), + ] + +py_runtime_toolchain = rule( + doc = """Declares a provisioned Python runtime and its toolchain payloads. + +One target serves both toolchain registrations: selected by target platform +for the standard Python toolchain type, and by exec platform for the +exec-tools toolchain type (build actions get an interpreter runnable on the +build host regardless of the target platform being built for).""", + implementation = _py_runtime_toolchain_impl, + attrs = { + "interpreter": attr.label( + doc = "The interpreter executable within `files`.", + allow_single_file = True, + mandatory = True, + ), + "files": attr.label_list( + doc = "The complete runtime tree.", + allow_files = True, + ), + "interpreter_version_info": attr.string_dict( + doc = "Static version info: major/minor/micro required, releaselevel/serial optional.", + mandatory = True, + ), + "abi_flags": attr.string( + doc = "CPython ABI flag suffix, e.g. \"t\" for freethreaded.", + ), + "_bootstrap_template": attr.label( + allow_single_file = True, + default = "@rules_python//python/private:bootstrap_template", + ), + "_stage2_bootstrap_template": attr.label( + allow_single_file = True, + default = "@rules_python//python/private:stage2_bootstrap_template", + ), + "_site_init_template": attr.label( + allow_single_file = True, + default = "@rules_python//python/private:site_init_template", + ), + "_zip_main_template": attr.label( + allow_single_file = True, + default = "@rules_python//python/private/zipapp:zip_main_template", + ), + }, + provides = [PyRuntimeInfo], +) diff --git a/py/private/py_semantics.bzl b/py/private/py_semantics.bzl index dd13b4a18..56451c8b1 100644 --- a/py/private/py_semantics.bzl +++ b/py/private/py_semantics.bzl @@ -14,15 +14,9 @@ _INTERPRETER_FLAGS = [ ] _MUST_SET_TOOLCHAIN_INTERPRETER_VERSION_INFO = """ -ERROR: In Bazel 7.x and later, the python toolchain py_runtime interpreter_version_info must be set \ -to a dict with keys "major", "minor", and "micro". - -`PyRuntimeInfo` requires that this field contains the static version information for the given -interpreter. This can be set via `py_runtime` when registering an interpreter toolchain, and will -done automatically for the builtin interpreter versions registered via `python_register_toolchains`. -Note that this only available on the Starlark implementation of the provider. - -For example: +ERROR: The resolved Python toolchain's py3_runtime must set interpreter_version_info to \ +a dict with keys "major", "minor", and "micro". Set it on the py_runtime when registering \ +an interpreter toolchain, for example: py_runtime( name = "system_runtime", @@ -51,25 +45,25 @@ def _resolve_toolchain(ctx): if not toolchain_info.py3_runtime: fail("A py3_runtime must be set on the Python toolchain") - py3_toolchain = toolchain_info.py3_runtime + py3_runtime = toolchain_info.py3_runtime runfiles_interpreter = True - if py3_toolchain.interpreter != None: - files = depset([py3_toolchain.interpreter], transitive = [py3_toolchain.files]) - interpreter = py3_toolchain.interpreter + if py3_runtime.interpreter != None: + files = depset([py3_runtime.interpreter], transitive = [py3_runtime.files]) + interpreter = py3_runtime.interpreter else: interpreter = struct( - path = py3_toolchain.interpreter_path, - short_path = py3_toolchain.interpreter_path, + path = py3_runtime.interpreter_path, + short_path = py3_runtime.interpreter_path, ) files = depset([]) runfiles_interpreter = False for attr in ["major", "minor", "micro"]: - if not hasattr(py3_toolchain.interpreter_version_info, attr): + if not hasattr(py3_runtime.interpreter_version_info, attr): fail(_MUST_SET_TOOLCHAIN_INTERPRETER_VERSION_INFO) - interpreter_version_info = py3_toolchain.interpreter_version_info + interpreter_version_info = py3_runtime.interpreter_version_info # Read the freethreaded build setting if the consuming rule exposed # the attr. Freethreaded Python uses `lib/python.t/site-packages/` @@ -80,7 +74,7 @@ def _resolve_toolchain(ctx): freethreaded = ctx.attr._freethreaded_flag[BuildSettingInfo].value return struct( - toolchain = py3_toolchain, + toolchain = py3_runtime, files = files, python = interpreter, interpreter_version_info = interpreter_version_info, diff --git a/py/private/py_unpacked_wheel.bzl b/py/private/py_unpacked_wheel.bzl index 28a142804..e4e3d45d2 100644 --- a/py/private/py_unpacked_wheel.bzl +++ b/py/private/py_unpacked_wheel.bzl @@ -9,7 +9,7 @@ load("//py/private/toolchain:types.bzl", "EXEC_TOOLS_TOOLCHAIN", "PY_TOOLCHAIN") def _py_unpacked_wheel_impl(ctx): py_toolchain = _py_semantics.resolve_toolchain(ctx) - exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_tools.exec_runtime + exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_runtime unpack_script = ctx.file._unpack_script unpack_directory = ctx.actions.declare_directory("{}".format(ctx.attr.name)) diff --git a/py/private/py_venv/venv.bzl b/py/private/py_venv/venv.bzl index e92c66332..0f8899499 100644 --- a/py/private/py_venv/venv.bzl +++ b/py/private/py_venv/venv.bzl @@ -173,7 +173,7 @@ def assemble_venv( # so each contributing wheel resolves in tree_by_sp. for group in merge_groups: exec_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN] - exec_runtime = exec_toolchain.exec_tools.exec_runtime if exec_toolchain else None + exec_runtime = exec_toolchain.exec_runtime if exec_toolchain else None if exec_runtime == None: fail(("{}: wheels {} all contribute to the regular package `{}` — merging it " + "requires an exec-configuration Python interpreter, but no `{}` toolchain " + diff --git a/py/private/toolchain/BUILD.bazel b/py/private/toolchain/BUILD.bazel index eee268d32..17e883da8 100644 --- a/py/private/toolchain/BUILD.bazel +++ b/py/private/toolchain/BUILD.bazel @@ -14,6 +14,13 @@ toolchain_type( visibility = ["//visibility:public"], ) +# Resolved by exec platform to provide an interpreter for build actions +# (wheel unpacking, site-packages merging) under cross-compilation. +toolchain_type( + name = "exec_tools_toolchain_type", + visibility = ["//visibility:public"], +) + bzl_library( name = "repo", srcs = ["repo.bzl"], diff --git a/py/private/toolchain/types.bzl b/py/private/toolchain/types.bzl index d16a0cbf6..1431dcf29 100644 --- a/py/private/toolchain/types.bzl +++ b/py/private/toolchain/types.bzl @@ -1,5 +1,5 @@ """Constants for toolchain types""" PY_TOOLCHAIN = "@bazel_tools//tools/python:toolchain_type" -EXEC_TOOLS_TOOLCHAIN = "@rules_python//python:exec_tools_toolchain_type" +EXEC_TOOLS_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type" NATIVE_BUILD_TOOLCHAIN = "@aspect_rules_py//py/private/toolchain:native_build_toolchain_type" diff --git a/uv/private/uv_hub/snapshots/python_interpreters.BUILD.bazel b/uv/private/uv_hub/snapshots/python_interpreters.BUILD.bazel index f3a699b6d..28377f146 100644 --- a/uv/private/uv_hub/snapshots/python_interpreters.BUILD.bazel +++ b/uv/private/uv_hub/snapshots/python_interpreters.BUILD.bazel @@ -61,7 +61,7 @@ toolchain( name = "python_3_13_aarch64_apple_darwin", target_compatible_with = ["@platforms//os:macos", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false"], - toolchain = "@python_3_13_aarch64_apple_darwin//:runtime_pair", + toolchain = "@python_3_13_aarch64_apple_darwin//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -76,11 +76,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_aarch64_apple_darwin_exec_tools", exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:aarch64"], - toolchain = "@python_3_13_aarch64_apple_darwin//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_aarch64_apple_darwin//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -95,7 +97,7 @@ toolchain( name = "python_3_13_aarch64_unknown_linux_gnu", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false", ":platform_libc_is_glibc"], - toolchain = "@python_3_13_aarch64_unknown_linux_gnu//:runtime_pair", + toolchain = "@python_3_13_aarch64_unknown_linux_gnu//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -110,11 +112,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_aarch64_unknown_linux_gnu_exec_tools", exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], - toolchain = "@python_3_13_aarch64_unknown_linux_gnu//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_aarch64_unknown_linux_gnu//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -129,7 +133,7 @@ toolchain( name = "python_3_13_aarch64_unknown_linux_musl", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false", ":platform_libc_is_musl"], - toolchain = "@python_3_13_aarch64_unknown_linux_musl//:runtime_pair", + toolchain = "@python_3_13_aarch64_unknown_linux_musl//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -153,7 +157,7 @@ toolchain( name = "python_3_13_x86_64_apple_darwin", target_compatible_with = ["@platforms//os:macos", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false"], - toolchain = "@python_3_13_x86_64_apple_darwin//:runtime_pair", + toolchain = "@python_3_13_x86_64_apple_darwin//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -168,11 +172,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_x86_64_apple_darwin_exec_tools", exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:x86_64"], - toolchain = "@python_3_13_x86_64_apple_darwin//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_x86_64_apple_darwin//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -187,7 +193,7 @@ toolchain( name = "python_3_13_x86_64_unknown_linux_gnu", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false", ":platform_libc_is_glibc"], - toolchain = "@python_3_13_x86_64_unknown_linux_gnu//:runtime_pair", + toolchain = "@python_3_13_x86_64_unknown_linux_gnu//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -202,11 +208,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_x86_64_unknown_linux_gnu_exec_tools", exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], - toolchain = "@python_3_13_x86_64_unknown_linux_gnu//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_x86_64_unknown_linux_gnu//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -221,7 +229,7 @@ toolchain( name = "python_3_13_x86_64_unknown_linux_musl", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false", ":platform_libc_is_musl"], - toolchain = "@python_3_13_x86_64_unknown_linux_musl//:runtime_pair", + toolchain = "@python_3_13_x86_64_unknown_linux_musl//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -245,7 +253,7 @@ toolchain( name = "python_3_13_x86_64_pc_windows_msvc", target_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false"], - toolchain = "@python_3_13_x86_64_pc_windows_msvc//:runtime_pair", + toolchain = "@python_3_13_x86_64_pc_windows_msvc//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -260,11 +268,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_x86_64_pc_windows_msvc_exec_tools", exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_64"], - toolchain = "@python_3_13_x86_64_pc_windows_msvc//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_x86_64_pc_windows_msvc//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -279,7 +289,7 @@ toolchain( name = "python_3_13_aarch64_pc_windows_msvc", target_compatible_with = ["@platforms//os:windows", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false"], - toolchain = "@python_3_13_aarch64_pc_windows_msvc//:runtime_pair", + toolchain = "@python_3_13_aarch64_pc_windows_msvc//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -294,11 +304,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_aarch64_pc_windows_msvc_exec_tools", exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:aarch64"], - toolchain = "@python_3_13_aarch64_pc_windows_msvc//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_aarch64_pc_windows_msvc//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -313,7 +325,7 @@ toolchain( name = "python_3_13_i686_pc_windows_msvc", target_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_32"], target_settings = [":python_version_is_3_13", ":freethreaded_is_false"], - toolchain = "@python_3_13_i686_pc_windows_msvc//:runtime_pair", + toolchain = "@python_3_13_i686_pc_windows_msvc//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -328,11 +340,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_i686_pc_windows_msvc_exec_tools", exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_32"], - toolchain = "@python_3_13_i686_pc_windows_msvc//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_i686_pc_windows_msvc//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -347,7 +361,7 @@ toolchain( name = "python_3_13_aarch64_apple_darwin_freethreaded", target_compatible_with = ["@platforms//os:macos", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true"], - toolchain = "@python_3_13_aarch64_apple_darwin_freethreaded//:runtime_pair", + toolchain = "@python_3_13_aarch64_apple_darwin_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -362,11 +376,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_aarch64_apple_darwin_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:aarch64"], - toolchain = "@python_3_13_aarch64_apple_darwin_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_aarch64_apple_darwin_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -381,7 +397,7 @@ toolchain( name = "python_3_13_aarch64_unknown_linux_gnu_freethreaded", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true", ":platform_libc_is_glibc"], - toolchain = "@python_3_13_aarch64_unknown_linux_gnu_freethreaded//:runtime_pair", + toolchain = "@python_3_13_aarch64_unknown_linux_gnu_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -396,11 +412,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_aarch64_unknown_linux_gnu_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], - toolchain = "@python_3_13_aarch64_unknown_linux_gnu_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_aarch64_unknown_linux_gnu_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -415,7 +433,7 @@ toolchain( name = "python_3_13_aarch64_unknown_linux_musl_freethreaded", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true", ":platform_libc_is_musl"], - toolchain = "@python_3_13_aarch64_unknown_linux_musl_freethreaded//:runtime_pair", + toolchain = "@python_3_13_aarch64_unknown_linux_musl_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -439,7 +457,7 @@ toolchain( name = "python_3_13_x86_64_apple_darwin_freethreaded", target_compatible_with = ["@platforms//os:macos", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true"], - toolchain = "@python_3_13_x86_64_apple_darwin_freethreaded//:runtime_pair", + toolchain = "@python_3_13_x86_64_apple_darwin_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -454,11 +472,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_x86_64_apple_darwin_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:x86_64"], - toolchain = "@python_3_13_x86_64_apple_darwin_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_x86_64_apple_darwin_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -473,7 +493,7 @@ toolchain( name = "python_3_13_x86_64_unknown_linux_gnu_freethreaded", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true", ":platform_libc_is_glibc"], - toolchain = "@python_3_13_x86_64_unknown_linux_gnu_freethreaded//:runtime_pair", + toolchain = "@python_3_13_x86_64_unknown_linux_gnu_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -488,11 +508,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_x86_64_unknown_linux_gnu_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], - toolchain = "@python_3_13_x86_64_unknown_linux_gnu_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_x86_64_unknown_linux_gnu_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -507,7 +529,7 @@ toolchain( name = "python_3_13_x86_64_unknown_linux_musl_freethreaded", target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true", ":platform_libc_is_musl"], - toolchain = "@python_3_13_x86_64_unknown_linux_musl_freethreaded//:runtime_pair", + toolchain = "@python_3_13_x86_64_unknown_linux_musl_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -531,7 +553,7 @@ toolchain( name = "python_3_13_x86_64_pc_windows_msvc_freethreaded", target_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true"], - toolchain = "@python_3_13_x86_64_pc_windows_msvc_freethreaded//:runtime_pair", + toolchain = "@python_3_13_x86_64_pc_windows_msvc_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -546,11 +568,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_x86_64_pc_windows_msvc_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_64"], - toolchain = "@python_3_13_x86_64_pc_windows_msvc_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_x86_64_pc_windows_msvc_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -565,7 +589,7 @@ toolchain( name = "python_3_13_aarch64_pc_windows_msvc_freethreaded", target_compatible_with = ["@platforms//os:windows", "@platforms//cpu:aarch64"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true"], - toolchain = "@python_3_13_aarch64_pc_windows_msvc_freethreaded//:runtime_pair", + toolchain = "@python_3_13_aarch64_pc_windows_msvc_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -580,11 +604,13 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_aarch64_pc_windows_msvc_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:aarch64"], - toolchain = "@python_3_13_aarch64_pc_windows_msvc_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_aarch64_pc_windows_msvc_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) @@ -599,7 +625,7 @@ toolchain( name = "python_3_13_i686_pc_windows_msvc_freethreaded", target_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_32"], target_settings = [":python_version_is_3_13", ":freethreaded_is_true"], - toolchain = "@python_3_13_i686_pc_windows_msvc_freethreaded//:runtime_pair", + toolchain = "@python_3_13_i686_pc_windows_msvc_freethreaded//:runtime", toolchain_type = "@bazel_tools//tools/python:toolchain_type", ) @@ -614,11 +640,69 @@ toolchain( # Exec tools toolchain: selected by exec platform (not target platform) so # that build actions using the interpreter (e.g. compileall) get a runnable # binary on the build host regardless of the target platform being built for. +# Version-gated so the exec interpreter follows the version flags. toolchain( name = "python_3_13_i686_pc_windows_msvc_freethreaded_exec_tools", exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_32"], - toolchain = "@python_3_13_i686_pc_windows_msvc_freethreaded//:exec_tools_toolchain", - toolchain_type = "@rules_python//python:exec_tools_toolchain_type", + target_settings = [":python_version_is_3_13"], + toolchain = "@python_3_13_i686_pc_windows_msvc_freethreaded//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_aarch64_apple_darwin_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:aarch64"], + toolchain = "@python_3_13_aarch64_apple_darwin//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_aarch64_unknown_linux_gnu_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:aarch64"], + toolchain = "@python_3_13_aarch64_unknown_linux_gnu//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_x86_64_apple_darwin_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:x86_64"], + toolchain = "@python_3_13_x86_64_apple_darwin//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_x86_64_unknown_linux_gnu_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"], + toolchain = "@python_3_13_x86_64_unknown_linux_gnu//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_x86_64_pc_windows_msvc_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_64"], + toolchain = "@python_3_13_x86_64_pc_windows_msvc//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_aarch64_pc_windows_msvc_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:aarch64"], + toolchain = "@python_3_13_aarch64_pc_windows_msvc//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", +) + + +toolchain( + name = "zz_python_3_13_i686_pc_windows_msvc_exec_tools_fallback", + exec_compatible_with = ["@platforms//os:windows", "@platforms//cpu:x86_32"], + toolchain = "@python_3_13_i686_pc_windows_msvc//:runtime", + toolchain_type = "@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type", ) diff --git a/uv/private/whl_install/BUILD.bazel b/uv/private/whl_install/BUILD.bazel index 7f409a98f..b99dca260 100644 --- a/uv/private/whl_install/BUILD.bazel +++ b/uv/private/whl_install/BUILD.bazel @@ -1,5 +1,5 @@ load("@bazel_lib//:bzl_library.bzl", "bzl_library") -load(":test.bzl", "metadata_selection_test_suite", "whl_install_suite") +load(":test.bzl", "compile_pyc_version_test_suite", "metadata_selection_test_suite", "whl_install_suite") package(default_visibility = [ "//docs:__pkg__", @@ -33,6 +33,8 @@ whl_install_suite() metadata_selection_test_suite(name = "metadata_selection") +compile_pyc_version_test_suite(name = "compile_pyc_version") + bzl_library( name = "test", srcs = ["test.bzl"], diff --git a/uv/private/whl_install/rule.bzl b/uv/private/whl_install/rule.bzl index 5fa3b9bba..13289607d 100644 --- a/uv/private/whl_install/rule.bzl +++ b/uv/private/whl_install/rule.bzl @@ -37,9 +37,21 @@ source_built_wheel = rule( provides = [SourceBuiltWheelInfo], ) +def pyc_compile_version_compatible(exec_info, target_info): + """Whether .pyc built by `exec_info` is loadable by `target_info`. + + Requires the full version to match: CPython changes the bytecode magic + number even between same-minor prereleases (e.g. 3.15.0a2 vs 3.15.0a6). + """ + + def tuple_of(info): + return (info.major, info.minor, info.micro, info.releaselevel, info.serial) + + return tuple_of(exec_info) == tuple_of(target_info) + def _whl_install(ctx): py_toolchain = ctx.toolchains[PY_TOOLCHAIN].py3_runtime - exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_tools.exec_runtime + exec_runtime = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN].exec_runtime # Name the install tree after the target rather than a fixed "install" # so several whl_install targets can coexist in one package without @@ -105,10 +117,17 @@ def _whl_install(ctx): transitive_inputs.append(depset(patch_files)) # Optional .pyc pre-compilation (runs after patching). - # Use the exec-configured interpreter from EXEC_TOOLS_TOOLCHAIN so cross-arch - # builds work (the target interpreter isn't runnable on the build host). This is - # safe because .pyc bytecode varies by Python version, not by architecture. - if ctx.attr.compile_pyc: + # Use the exec-configured interpreter from the exec-tools toolchain so cross-arch + # builds work (the target interpreter isn't runnable on the build host). Skip it + # when the exec runtime isn't the exact same interpreter version as the target: + # CPython changes the bytecode magic number even between same-minor prereleases + # (e.g. 3.15.0a2 vs 3.15.0a6), so a mismatched exec runtime writes .pyc unusable + # by the target under lib/python{version}/. + exec_matches_target = pyc_compile_version_compatible( + exec_runtime.interpreter_version_info, + py_toolchain.interpreter_version_info, + ) + if ctx.attr.compile_pyc and exec_matches_target: arguments.add("--compile-pyc") arguments.add("--pyc-invalidation-mode", ctx.attr.pyc_invalidation_mode) arguments.add("--python", exec_runtime.interpreter) diff --git a/uv/private/whl_install/test.bzl b/uv/private/whl_install/test.bzl index 53bf6a6ca..72feab74d 100644 --- a/uv/private/whl_install/test.bzl +++ b/uv/private/whl_install/test.bzl @@ -4,7 +4,7 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") load("@bazel_skylib//rules:write_file.bzl", "write_file") load("//py/private:providers.bzl", "PyWheelsInfo") load(":repository.bzl", "compatible_python_tags", "native_roots_for_segments", "parse_console_script", "parse_record_path", "select_key", "site_packages_segments", "sort_select_arms", "source_specificity") -load(":rule.bzl", "source_built_wheel", "whl_install") +load(":rule.bzl", "pyc_compile_version_compatible", "source_built_wheel", "whl_install") def _whl_sorting_test_impl(ctx): env = unittest.begin(ctx) @@ -546,7 +546,108 @@ def metadata_selection_test_suite(name): leaked_top_levels = [], ) +# --- compile_pyc exec/target version agreement ----------------------------- +# +# WhlInstall lays out lib/python{target}/ from the standard toolchain but runs +# compileall on the exec-tools runtime. A fallback exec runtime of another +# version would emit bytecode whose magic is wrong for the target layout, so +# the action must omit --compile-pyc unless the versions agree. + +def _compile_pyc_args_test_impl(ctx): + env = analysistest.begin(ctx) + argv = [] + for action in analysistest.target_actions(env): + if action.mnemonic == "WhlInstall": + argv = action.argv + asserts.equals( + env, + ctx.attr.expect_compile_pyc, + "--compile-pyc" in argv, + "WhlInstall argv: {}".format(argv), + ) + return analysistest.end(env) + +_compile_pyc_matched_test = analysistest.make( + _compile_pyc_args_test_impl, + attrs = {"expect_compile_pyc": attr.bool()}, + config_settings = { + # The hub provisions 3.13, so exec and target versions agree. + "@@//py/private/interpreter:python_version": "3.13", + }, +) + +_compile_pyc_mismatched_test = analysistest.make( + _compile_pyc_args_test_impl, + attrs = {"expect_compile_pyc": attr.bool()}, + config_settings = { + # The hub provisions no 3.9: the target runtime comes from + # rules_python's dev toolchain while the exec fallback stays 3.13. + "@@//py/private/interpreter:python_version": "3.9", + }, +) + +def _version_info(major, minor, micro, releaselevel = "final", serial = 0): + return struct( + major = major, + minor = minor, + micro = micro, + releaselevel = releaselevel, + serial = serial, + ) + +def _pyc_version_compatible_test_impl(ctx): + env = unittest.begin(ctx) + + v3_13 = _version_info(3, 13, 5) + asserts.true(env, pyc_compile_version_compatible(v3_13, _version_info(3, 13, 5))) + + # Differing minor, micro, or — the reported regression — same-minor + # prerelease serial each change the bytecode magic. + asserts.false(env, pyc_compile_version_compatible(v3_13, _version_info(3, 12, 5))) + asserts.false(env, pyc_compile_version_compatible(v3_13, _version_info(3, 13, 4))) + asserts.false( + env, + pyc_compile_version_compatible( + _version_info(3, 15, 0, "alpha", 2), + _version_info(3, 15, 0, "alpha", 6), + ), + ) + + return unittest.end(env) + +pyc_version_compatible_test = unittest.make(_pyc_version_compatible_test_impl) + +def compile_pyc_version_test_suite(name): + """Fixture + analysis tests for exec/target pyc version agreement. + + Args: + name: prefix for the generated test targets. + """ + whl_install( + name = "__compile_pyc_fixture", + testonly = True, + src = _SBUILD_WHL, + compile_pyc = True, + tags = ["manual"], + ) + + _compile_pyc_matched_test( + name = name + "_matched_test", + target_under_test = ":__compile_pyc_fixture", + expect_compile_pyc = True, + ) + + _compile_pyc_mismatched_test( + name = name + "_mismatched_test", + target_under_test = ":__compile_pyc_fixture", + expect_compile_pyc = False, + ) + def whl_install_suite(): + unittest.suite( + "pyc_version_compatible_tests", + pyc_version_compatible_test, + ) unittest.suite( "whl_sorting_tests", whl_sorting_test,