From 7f5126b8bfa690537953534f095899e28125e46a Mon Sep 17 00:00:00 2001 From: Stephan Dollberg Date: Wed, 11 Nov 2026 14:04:02 +0000 Subject: [PATCH 1/2] fix: preserve bootstrap_impl in exec config With Bazel 10, `--bootstrap_impl=script` no longer applies to Python executables built as tools, e.g. a `py_binary` run by a `genrule`. They fall back to `system_python` and fail to start on hosts without a `python3`. Give the flag universal scope, as 159b6c2 did for `python_version` and 05916c1 for `venvs_use_declare_symlink`. --- docs/api/rules_python/python/config_settings/index.md | 4 ++++ news/bootstrap-impl-exec-config.fixed.md | 3 +++ python/config_settings/BUILD.bazel | 6 ++++++ python/private/flags.bzl | 5 +++++ 4 files changed, 18 insertions(+) create mode 100644 news/bootstrap-impl-exec-config.fixed.md diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index 4d9169a4ef..3158ef5608 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -405,6 +405,10 @@ instead. :::{versionadded} 0.33.0 ::: +:::{versionchanged} VERSION_NEXT_PATCH +The flag also applies to tools built in the exec configuration under Bazel 10. +::: + :::: ::::{bzl:flag} current_config diff --git a/news/bootstrap-impl-exec-config.fixed.md b/news/bootstrap-impl-exec-config.fixed.md new file mode 100644 index 0000000000..d51c3353b0 --- /dev/null +++ b/news/bootstrap-impl-exec-config.fixed.md @@ -0,0 +1,3 @@ +Fixed {obj}`--bootstrap_impl` resetting to `system_python` for build tools, +such as a `py_binary` run by a `genrule`, under Bazel 10. Tools no longer need +a system `python3` when `--bootstrap_impl=script` is set. diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index f519a38c23..4f52951fef 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -114,6 +114,12 @@ rp_string_flag( ":_is_windows": BootstrapImplFlag.SYSTEM_PYTHON, "//conditions:default": "", }), + # Propagate the flag into exec configurations so that tools, e.g. a + # py_binary run by a genrule, use the same bootstrap as the target + # configuration. Bazel 10 resets Starlark flags in exec configurations + # unless they have universal scope + # (https://github.com/bazelbuild/bazel/issues/26909). + scope = "universal", values = sorted(BootstrapImplFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency visibility = NOT_ACTUALLY_PUBLIC, diff --git a/python/private/flags.bzl b/python/private/flags.bzl index 372a779e56..c26415ca3a 100644 --- a/python/private/flags.bzl +++ b/python/private/flags.bzl @@ -163,6 +163,11 @@ string_flag = rule( build_setting = config.string(flag = True), attrs = { "override": attr.string(), + # Read by Bazel, not the implementation, to decide whether the flag + # propagates into exec configurations. No default because Bazel only + # honors an explicitly set value + # (https://github.com/bazelbuild/bazel/issues/31806). + "scope": attr.string(), "values": attr.string_list(), }, ) From 2da5b269f54c24ea1e7870c9375ad9ba803a78de Mon Sep 17 00:00:00 2001 From: Stephan Dollberg Date: Thu, 12 Nov 2026 10:18:12 +0000 Subject: [PATCH 2/2] test: cover bootstrap_impl in the exec config Builds a py_binary genrule tool with --bootstrap_impl=script set on the command line and checks that it gets the script bootstrap. --- .bazelignore | 1 + .bazelrc.deleted_packages | 1 + tests/integration/BUILD.bazel | 5 +++ .../bootstrap_impl_exec_config/.bazelrc | 7 ++++ .../bootstrap_impl_exec_config/BUILD.bazel | 15 ++++++++ .../bootstrap_impl_exec_config/MODULE.bazel | 10 ++++++ .../bootstrap_impl_exec_config/REPO.bazel | 0 .../bootstrap_impl_exec_config/WORKSPACE | 1 + .../WORKSPACE.bzlmod | 0 .../bootstrap_impl_exec_config/tool.py | 1 + .../bootstrap_impl_exec_config_test.py | 34 +++++++++++++++++++ 11 files changed, 75 insertions(+) create mode 100644 tests/integration/bootstrap_impl_exec_config/.bazelrc create mode 100644 tests/integration/bootstrap_impl_exec_config/BUILD.bazel create mode 100644 tests/integration/bootstrap_impl_exec_config/MODULE.bazel create mode 100644 tests/integration/bootstrap_impl_exec_config/REPO.bazel create mode 100644 tests/integration/bootstrap_impl_exec_config/WORKSPACE create mode 100644 tests/integration/bootstrap_impl_exec_config/WORKSPACE.bzlmod create mode 100644 tests/integration/bootstrap_impl_exec_config/tool.py create mode 100644 tests/integration/bootstrap_impl_exec_config_test.py diff --git a/.bazelignore b/.bazelignore index 8a3cbf9068..93b1ea9696 100644 --- a/.bazelignore +++ b/.bazelignore @@ -30,6 +30,7 @@ gazelle/examples/bzlmod_build_file_generation/bazel-bzlmod_build_file_generation gazelle/examples/bzlmod_build_file_generation/bazel-out gazelle/examples/bzlmod_build_file_generation/bazel-testlog sphinxdocs +tests/integration/bootstrap_impl_exec_config/bazel-bootstrap_impl_exec_config tests/integration/bzlmod_lockfile/bazel-bzlmod_lockfile tests/integration/compile_pip_requirements/bazel-compile_pip_requirements tests/integration/local_toolchains/bazel-local_toolchains diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages index e17e1f250f..0d2affa350 100644 --- a/.bazelrc.deleted_packages +++ b/.bazelrc.deleted_packages @@ -31,6 +31,7 @@ common --deleted_packages=gazelle/modules_mapping common --deleted_packages=gazelle/python common --deleted_packages=gazelle/python/private common --deleted_packages=gazelle/pythonconfig +common --deleted_packages=tests/integration/bootstrap_impl_exec_config common --deleted_packages=tests/integration/bzlmod_lockfile common --deleted_packages=tests/integration/compile_pip_requirements common --deleted_packages=tests/integration/compile_pip_requirements_test_from_external_repo diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index 950de14368..e648cbdcb9 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -39,6 +39,11 @@ default_test_runner( visibility = ["//visibility:public"], ) +rules_python_integration_test( + name = "bootstrap_impl_exec_config_test", + py_main = "bootstrap_impl_exec_config_test.py", +) + rules_python_integration_test( name = "bzlmod_lockfile_test", bazel_versions = ["9.3.1"], diff --git a/tests/integration/bootstrap_impl_exec_config/.bazelrc b/tests/integration/bootstrap_impl_exec_config/.bazelrc new file mode 100644 index 0000000000..cc0428f57c --- /dev/null +++ b/tests/integration/bootstrap_impl_exec_config/.bazelrc @@ -0,0 +1,7 @@ +common --lockfile_mode=off +test --test_output=errors +build --enable_runfiles +# Mitigate transient CI network failures via mirror fallback and retries. +common --experimental_downloader_config=../../../downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 diff --git a/tests/integration/bootstrap_impl_exec_config/BUILD.bazel b/tests/integration/bootstrap_impl_exec_config/BUILD.bazel new file mode 100644 index 0000000000..e858f231e6 --- /dev/null +++ b/tests/integration/bootstrap_impl_exec_config/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_python//python:py_binary.bzl", "py_binary") + +py_binary( + name = "tool", + srcs = ["tool.py"], +) + +# Records the first line of the launcher of :tool as built for the exec +# configuration, which shows the bootstrap it uses. +genrule( + name = "tool_launcher", + outs = ["tool_launcher.txt"], + cmd = "head -n 1 $(execpath :tool) > $@", + tools = [":tool"], +) diff --git a/tests/integration/bootstrap_impl_exec_config/MODULE.bazel b/tests/integration/bootstrap_impl_exec_config/MODULE.bazel new file mode 100644 index 0000000000..c1e126ba1f --- /dev/null +++ b/tests/integration/bootstrap_impl_exec_config/MODULE.bazel @@ -0,0 +1,10 @@ +module(name = "bootstrap_impl_exec_config") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") diff --git a/tests/integration/bootstrap_impl_exec_config/REPO.bazel b/tests/integration/bootstrap_impl_exec_config/REPO.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/bootstrap_impl_exec_config/WORKSPACE b/tests/integration/bootstrap_impl_exec_config/WORKSPACE new file mode 100644 index 0000000000..d7be0c96c7 --- /dev/null +++ b/tests/integration/bootstrap_impl_exec_config/WORKSPACE @@ -0,0 +1 @@ +# Intentionally blank; bzlmod is used. diff --git a/tests/integration/bootstrap_impl_exec_config/WORKSPACE.bzlmod b/tests/integration/bootstrap_impl_exec_config/WORKSPACE.bzlmod new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/bootstrap_impl_exec_config/tool.py b/tests/integration/bootstrap_impl_exec_config/tool.py new file mode 100644 index 0000000000..9b929dcc17 --- /dev/null +++ b/tests/integration/bootstrap_impl_exec_config/tool.py @@ -0,0 +1 @@ +print("tool") diff --git a/tests/integration/bootstrap_impl_exec_config_test.py b/tests/integration/bootstrap_impl_exec_config_test.py new file mode 100644 index 0000000000..b841667f5c --- /dev/null +++ b/tests/integration/bootstrap_impl_exec_config_test.py @@ -0,0 +1,34 @@ +"""Integration test for --bootstrap_impl in the exec configuration. + +Verifies that a py_binary run by a genrule uses the bootstrap set with +--bootstrap_impl on the command line. Bazel 10 resets Starlark flags in the +exec configuration unless they have universal scope. +""" + +import pathlib +import unittest + +from tests.integration import runner + + +class BootstrapImplExecConfigTest(runner.TestCase): + def test_genrule_tool_uses_bootstrap_script(self): + """The launcher of the genrule tool is the script bootstrap.""" + self.run_bazel( + "build", + "--@rules_python//python/config_settings:bootstrap_impl=script", + "//:tool_launcher", + ) + execution_root = self.run_bazel("info", "execution_root").stdout.strip() + result = self.run_bazel( + "cquery", + "--@rules_python//python/config_settings:bootstrap_impl=script", + "--output=files", + "//:tool_launcher", + ) + launcher = pathlib.Path(execution_root, result.stdout.strip()) + self.assertEqual(launcher.read_text().strip(), "#!/usr/bin/env bash") + + +if __name__ == "__main__": + unittest.main()