Skip to content

Commit 80eac09

Browse files
committed
feat(toolchains): add toolchain_target_settings to python.override
Users who register custom toolchain families (e.g. gated behind a string_flag like "custom" vs "prebuilt") have no way to prevent the default-registered toolchains from acting as a silent fallback. When a user requests a custom toolchain at a version they didn't register, Bazel's toolchain resolution quietly falls back to the default prebuilt toolchain instead of producing an error. Add a `toolchain_target_settings` attribute to `python.override` that appends the given config_setting labels to the `target_settings` of every toolchain registered by the module extension. This lets users gate the default toolchains behind a config_setting so they only resolve when explicitly selected, giving a clean separation between toolchain families. These settings are appended to the `target_settings` of all toolchains registered by the extension, including any that already have settings from `python.single_version_platform_override`. Toolchains registered outside the extension (e.g. via `local_runtime_toolchains_repo`) are not affected. Fixes #3673
1 parent 8633255 commit 80eac09

13 files changed

Lines changed: 238 additions & 2 deletions

File tree

.bazelignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ sphinxdocs
3333
tests/integration/compile_pip_requirements/bazel-compile_pip_requirements
3434
tests/integration/local_toolchains/bazel-local_toolchains
3535
tests/integration/py_cc_toolchain_registered/bazel-py_cc_toolchain_registered
36+
tests/integration/toolchain_target_settings/bazel-module_under_test

.bazelrc.deleted_packages

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ common --deleted_packages=tests/integration/local_toolchains
3636
common --deleted_packages=tests/integration/pip_parse
3737
common --deleted_packages=tests/integration/pip_parse/empty
3838
common --deleted_packages=tests/integration/py_cc_toolchain_registered
39+
common --deleted_packages=tests/integration/toolchain_target_settings
3940
common --deleted_packages=tests/modules/another_module
4041
common --deleted_packages=tests/modules/other
4142
common --deleted_packages=tests/modules/other/nspkg_delta

python/private/python.bzl

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@ def _python_impl(module_ctx):
403403
# the PLATFORMS global for this toolchain
404404
toolchain_platform_keys = {}
405405

406+
# Extra target_settings to add to every registered toolchain, e.g. for
407+
# gating the default toolchains behind a custom config_setting.
408+
global_toolchain_target_settings = py.config.toolchain_target_settings
409+
406410
# Split the toolchain info into separate objects so they can be passed onto
407411
# the repository rule.
408412
for entry in toolchain_impls:
@@ -414,7 +418,7 @@ def _python_impl(module_ctx):
414418

415419
# The target_settings attribute may not be present for users
416420
# patching python/versions.bzl.
417-
toolchain_ts_map[key] = getattr(entry.platform, "target_settings", [])
421+
toolchain_ts_map[key] = getattr(entry.platform, "target_settings", []) + global_toolchain_target_settings
418422
toolchain_platform_keys[key] = entry.platform_name
419423
toolchain_python_versions[key] = entry.full_python_version
420424

@@ -702,6 +706,9 @@ def _process_global_overrides(*, tag, default, _fail = fail):
702706

703707
default["minor_mapping"] = tag.minor_mapping
704708

709+
if tag.toolchain_target_settings:
710+
default["toolchain_target_settings"] = list(tag.toolchain_target_settings)
711+
705712
forwarded_attrs = sorted(AUTH_ATTRS) + [
706713
"base_url",
707714
"register_all_versions",
@@ -809,6 +816,7 @@ def _get_toolchain_config(*, modules, _fail = fail):
809816
)
810817

811818
register_all_versions = default.pop("register_all_versions", False)
819+
toolchain_target_settings = default.pop("toolchain_target_settings", [])
812820
kwargs = default.pop("kwargs", {})
813821

814822
versions = {}
@@ -834,6 +842,7 @@ def _get_toolchain_config(*, modules, _fail = fail):
834842
minor_mapping = minor_mapping,
835843
default = default,
836844
register_all_versions = register_all_versions,
845+
toolchain_target_settings = toolchain_target_settings,
837846
)
838847

839848
def _compute_default_python_version(mctx):
@@ -1144,6 +1153,30 @@ The values in this mapping override the default values and do not replace them.
11441153
default = {},
11451154
),
11461155
"register_all_versions": attr.bool(default = False, doc = "Add all versions"),
1156+
"toolchain_target_settings": attr.string_list(
1157+
mandatory = False,
1158+
doc = """\
1159+
A list of `config_setting` labels to add to the `target_settings` of every
1160+
toolchain registered by this module extension. This is useful for creating
1161+
separate "families" of toolchains gated behind custom build settings.
1162+
1163+
For example, to ensure the default prebuilt toolchains are only resolved when
1164+
a `prebuilt` config setting is active:
1165+
1166+
```starlark
1167+
python.override(
1168+
toolchain_target_settings = ["@@//:python_toolchain_family_prebuilt"],
1169+
)
1170+
```
1171+
1172+
These settings are appended to the `target_settings` of all toolchains
1173+
registered by the extension, including any that already have settings
1174+
from `python.single_version_platform_override`.
1175+
1176+
:::{versionadded} VERSION_NEXT
1177+
:::
1178+
""",
1179+
),
11471180
} | AUTH_ATTRS,
11481181
)
11491182

tests/integration/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ rules_python_integration_test(
8888
py_main = "custom_commands_test.py",
8989
)
9090

91+
rules_python_integration_test(
92+
name = "toolchain_target_settings_test",
93+
py_main = "toolchain_target_settings_test.py",
94+
)
95+
9196
py_library(
9297
name = "runner_lib",
9398
srcs = ["runner.py"],
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
common --lockfile_mode=off
2+
test --test_output=errors
3+
build --enable_runfiles
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2025 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
16+
load("@rules_python//python:py_test.bzl", "py_test")
17+
18+
# A flag to select which "family" of toolchains to use.
19+
string_flag(
20+
name = "family",
21+
build_setting_default = "prebuilt",
22+
values = [
23+
"prebuilt",
24+
"custom",
25+
],
26+
)
27+
28+
# Matches when the "prebuilt" family is selected.
29+
# This is referenced in MODULE.bazel's python.override(toolchain_target_settings=...).
30+
config_setting(
31+
name = "is_prebuilt",
32+
flag_values = {
33+
":family": "prebuilt",
34+
},
35+
)
36+
37+
# Matches when the "custom" family is selected.
38+
# No toolchains use this setting, so selecting it should produce an error.
39+
config_setting(
40+
name = "is_custom",
41+
flag_values = {
42+
":family": "custom",
43+
},
44+
)
45+
46+
# This target selects the "prebuilt" family via config_settings transition.
47+
# Since python.override(toolchain_target_settings = ["@@//:is_prebuilt"]) gates
48+
# the default toolchains, this should succeed: the flag matches, the config_setting
49+
# is satisfied, and the default 3.13 toolchain resolves.
50+
py_test(
51+
name = "prebuilt_test",
52+
srcs = ["main.py"],
53+
config_settings = {
54+
"//:family": "prebuilt",
55+
},
56+
main = "main.py",
57+
)
58+
59+
# This target selects the "custom" family via config_settings transition.
60+
# No toolchains have target_settings = [":is_custom"], so toolchain resolution
61+
# should fail -- the default toolchains are gated behind ":is_prebuilt" and
62+
# won't match.
63+
py_test(
64+
name = "custom_no_toolchain_test",
65+
srcs = ["main.py"],
66+
config_settings = {
67+
"//:family": "custom",
68+
},
69+
main = "main.py",
70+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2025 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
module(name = "module_under_test")
15+
16+
bazel_dep(name = "rules_python", version = "0.0.0")
17+
bazel_dep(name = "bazel_skylib", version = "1.7.1")
18+
19+
local_path_override(
20+
module_name = "rules_python",
21+
path = "../../..",
22+
)
23+
24+
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
25+
python.toolchain(python_version = "3.13")
26+
27+
# Gate ALL default-registered toolchains behind the "prebuilt" config setting.
28+
# This prevents them from being a silent fallback when a different toolchain
29+
# family is requested.
30+
python.override(
31+
toolchain_target_settings = ["@@//:is_prebuilt"],
32+
)
33+
34+
# Register //:family as a transition setting so py_binary/py_test
35+
# config_settings can set it.
36+
config = use_extension("@rules_python//python/extensions:config.bzl", "config")
37+
config.add_transition_setting(setting = "//:family")

tests/integration/toolchain_target_settings/REPO.bazel

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Intentionally blank; bzlmod is used.

tests/integration/toolchain_target_settings/WORKSPACE.bzlmod

Whitespace-only changes.

0 commit comments

Comments
 (0)