Skip to content

Commit d80c814

Browse files
committed
Merge branch 'main' of https://github.com/bazel-contrib/rules_python into pep508.env.marker.config
2 parents e8bccc1 + 4ccf5b2 commit d80c814

File tree

9 files changed

+278
-14
lines changed

9 files changed

+278
-14
lines changed

.bazelci/presubmit.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ buildifier:
5151
test_flags:
5252
- "--noenable_bzlmod"
5353
- "--enable_workspace"
54+
- "--test_tag_filters=-integration-test"
5455
build_flags:
5556
- "--noenable_bzlmod"
5657
- "--enable_workspace"
58+
- "--build_tag_filters=-integration-test"
5759
bazel: 7.x
5860
.common_bazelinbazel_config: &common_bazelinbazel_config
5961
build_flags:

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ END_UNRELEASED_TEMPLATE
8484
* Repo utilities `execute_unchecked`, `execute_checked`, and `execute_checked_stdout` now
8585
support `log_stdout` and `log_stderr` keyword arg booleans. When these are `True`
8686
(the default), the subprocess's stdout/stderr will be logged.
87+
* (toolchains) Local toolchains can be activated with custom flags. See
88+
[Conditionally using local toolchains] docs for how to configure.
8789

8890
{#v0-0-0-removed}
8991
### Removed

docs/toolchains.md

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,15 +377,14 @@ local_runtime_repo(
377377
local_runtime_toolchains_repo(
378378
name = "local_toolchains",
379379
runtimes = ["local_python3"],
380+
# TIP: The `target_settings` arg can be used to activate them based on
381+
# command line flags; see docs below.
380382
)
381383

382384
# Step 3: Register the toolchains
383385
register_toolchains("@local_toolchains//:all", dev_dependency = True)
384386
```
385387

386-
Note that `register_toolchains` will insert the local toolchain earlier in the
387-
toolchain ordering, so it will take precedence over other registered toolchains.
388-
389388
:::{important}
390389
Be sure to set `dev_dependency = True`. Using a local toolchain only makes sense
391390
for the root module.
@@ -397,6 +396,72 @@ downstream modules.
397396

398397
Multiple runtimes and/or toolchains can be defined, which allows for multiple
399398
Python versions and/or platforms to be configured in a single `MODULE.bazel`.
399+
Note that `register_toolchains` will insert the local toolchain earlier in the
400+
toolchain ordering, so it will take precedence over other registered toolchains.
401+
To better control when the toolchain is used, see [Conditionally using local
402+
toolchains]
403+
404+
### Conditionally using local toolchains
405+
406+
By default, a local toolchain has few constraints and is early in the toolchain
407+
ordering, which means it will usually be used no matter what. This can be
408+
problematic for CI (where it shouldn't be used), expensive for CI (CI must
409+
initialize/download the repository to determine its Python version), and
410+
annoying for iterative development (enabling/disabling it requires modifying
411+
MODULE.bazel).
412+
413+
These behaviors can be mitigated, but it requires additional configuration
414+
to avoid triggering the local toolchain repository to initialize (i.e. run
415+
local commands and perform downloads).
416+
417+
The two settings to change are
418+
{obj}`local_runtime_toolchains_repo.target_compatible_with` and
419+
{obj}`local_runtime_toolchains_repo.target_settings`, which control how Bazel
420+
decides if a toolchain should match. By default, they point to targets *within*
421+
the local runtime repository (trigger repo initialization). We have to override
422+
them to *not* reference the local runtime repository at all.
423+
424+
In the example below, we reconfigure the local toolchains so they are only
425+
activated if the custom flag `--//:py=local` is set and the target platform
426+
matches the Bazel host platform. The net effect is CI won't use the local
427+
toolchain (nor initialize its repository), and developers can easily
428+
enable/disable the local toolchain with a command line flag.
429+
430+
```
431+
# File: MODULE.bazel
432+
bazel_dep(name = "bazel_skylib", version = "1.7.1")
433+
434+
local_runtime_toolchains_repo(
435+
name = "local_toolchains",
436+
runtimes = ["local_python3"],
437+
target_compatible_with = {
438+
"local_python3": ["HOST_CONSTRAINTS"],
439+
},
440+
target_settings = {
441+
"local_python3": ["@//:is_py_local"]
442+
}
443+
)
444+
445+
# File: BUILD.bazel
446+
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
447+
448+
config_setting(
449+
name = "is_py_local",
450+
flag_values = {":py": "local"},
451+
)
452+
453+
string_flag(
454+
name = "py",
455+
build_setting_default = "",
456+
)
457+
```
458+
459+
:::{tip}
460+
Easily switching between *multiple* local toolchains can be accomplished by
461+
adding additional `:is_py_X` targets and setting `--//:py` to match.
462+
to easily switch between different local toolchains.
463+
:::
464+
400465

401466
## Runtime environment toolchain
402467

@@ -425,7 +490,7 @@ locally installed Python.
425490
### Autodetecting toolchain
426491

427492
The autodetecting toolchain is a deprecated toolchain that is built into Bazel.
428-
It's name is a bit misleading: it doesn't autodetect anything. All it does is
493+
**It's name is a bit misleading: it doesn't autodetect anything**. All it does is
429494
use `python3` from the environment a binary runs within. This provides extremely
430495
limited functionality to the rules (at build time, nothing is knowable about
431496
the Python runtime).

python/private/local_runtime_toolchains_repo.bzl

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ define_local_toolchain_suites(
2626
name = "toolchains",
2727
version_aware_repo_names = {version_aware_names},
2828
version_unaware_repo_names = {version_unaware_names},
29+
repo_exec_compatible_with = {repo_exec_compatible_with},
30+
repo_target_compatible_with = {repo_target_compatible_with},
31+
repo_target_settings = {repo_target_settings},
2932
)
3033
"""
3134

@@ -39,6 +42,9 @@ def _local_runtime_toolchains_repo(rctx):
3942

4043
rctx.file("BUILD.bazel", _TOOLCHAIN_TEMPLATE.format(
4144
version_aware_names = render.list(rctx.attr.runtimes),
45+
repo_target_settings = render.string_list_dict(rctx.attr.target_settings),
46+
repo_target_compatible_with = render.string_list_dict(rctx.attr.target_compatible_with),
47+
repo_exec_compatible_with = render.string_list_dict(rctx.attr.exec_compatible_with),
4248
version_unaware_names = render.list(rctx.attr.default_runtimes or rctx.attr.runtimes),
4349
))
4450

@@ -62,8 +68,36 @@ These will be defined as *version-unaware* toolchains. This means they will
6268
match any Python version. As such, they are registered after the version-aware
6369
toolchains defined by the `runtimes` attribute.
6470
71+
If not set, then the `runtimes` values will be used.
72+
6573
Note that order matters: it determines the toolchain priority within the
6674
package.
75+
""",
76+
),
77+
"exec_compatible_with": attr.string_list_dict(
78+
doc = """
79+
Constraints that must be satisfied by an exec platform for a toolchain to be used.
80+
81+
This is a `dict[str, list[str]]`, where the keys are repo names from the
82+
`runtimes` or `default_runtimes` args, and the values are constraint
83+
target labels (e.g. OS, CPU, etc).
84+
85+
:::{note}
86+
Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is
87+
needed because the strings are evaluated in a different context than where
88+
they originate.
89+
:::
90+
91+
The list of settings become the {obj}`toolchain.exec_compatible_with` value for
92+
each respective repo.
93+
94+
This allows a local toolchain to only be used if certain exec platform
95+
conditions are met, typically values from `@platforms`.
96+
97+
See the [Local toolchains] docs for examples and further information.
98+
99+
:::{versionadded} VERSION_NEXT_FEATURE
100+
:::
67101
""",
68102
),
69103
"runtimes": attr.string_list(
@@ -76,6 +110,81 @@ are registered before `default_runtimes`.
76110
77111
Note that order matters: it determines the toolchain priority within the
78112
package.
113+
""",
114+
),
115+
"target_compatible_with": attr.string_list_dict(
116+
doc = """
117+
Constraints that must be satisfied for a toolchain to be used.
118+
119+
120+
This is a `dict[str, list[str]]`, where the keys are repo names from the
121+
`runtimes` or `default_runtimes` args, and the values are constraint
122+
target labels (e.g. OS, CPU, etc), or the special string `"HOST_CONSTRAINTS"`
123+
(which will be replaced with the current Bazel hosts's constraints).
124+
125+
If a repo's entry is missing or empty, it defaults to the supported OS the
126+
underlying runtime repository detects as compatible.
127+
128+
:::{note}
129+
Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is
130+
needed because the strings are evaluated in a different context than where
131+
they originate.
132+
:::
133+
134+
The list of settings **becomes the** the {obj}`toolchain.target_compatible_with`
135+
value for each respective repo; i.e. they _replace_ the auto-detected values
136+
the local runtime itself computes.
137+
138+
This allows a local toolchain to only be used if certain target platform
139+
conditions are met, typically values from `@platforms`.
140+
141+
See the [Local toolchains] docs for examples and further information.
142+
143+
:::{seealso}
144+
The `target_settings` attribute, which handles `config_setting` values,
145+
instead of constraints.
146+
:::
147+
148+
:::{versionadded} VERSION_NEXT_FEATURE
149+
:::
150+
""",
151+
),
152+
"target_settings": attr.string_list_dict(
153+
doc = """
154+
Config settings that must be satisfied for a toolchain to be used.
155+
156+
This is a `dict[str, list[str]]`, where the keys are repo names from the
157+
`runtimes` or `default_runtimes` args, and the values are {obj}`config_setting()`
158+
target labels.
159+
160+
If a repo's entry is missing or empty, it will default to
161+
`@<repo>//:is_match_python_version` (for repos in `runtimes`) or an empty list
162+
(for repos in `default_runtimes`).
163+
164+
:::{note}
165+
Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is
166+
needed because the strings are evaluated in a different context than where
167+
they originate.
168+
:::
169+
170+
The list of settings will be applied atop of any of the local runtime's
171+
settings that are used for {obj}`toolchain.target_settings`. i.e. they are
172+
evaluated first and guard the checking of the local runtime's auto-detected
173+
conditions.
174+
175+
This allows a local toolchain to only be used if certain flags or
176+
config setting conditions are met. Such conditions can include user-defined
177+
flags, platform constraints, etc.
178+
179+
See the [Local toolchains] docs for examples and further information.
180+
181+
:::{seealso}
182+
The `target_compatible_with` attribute, which handles *constraint* values,
183+
instead of `config_settings`.
184+
:::
185+
186+
:::{versionadded} VERSION_NEXT_FEATURE
187+
:::
79188
""",
80189
),
81190
"_rule_name": attr.string(default = "local_toolchains_repo"),

python/private/py_toolchain_suite.bzl

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Create the toolchain defs in a BUILD.bazel file."""
1616

1717
load("@bazel_skylib//lib:selects.bzl", "selects")
18+
load("@platforms//host:constraints.bzl", "HOST_CONSTRAINTS")
1819
load(":text_util.bzl", "render")
1920
load(
2021
":toolchain_types.bzl",
@@ -95,9 +96,15 @@ def py_toolchain_suite(
9596
runtime_repo_name = user_repository_name,
9697
target_settings = target_settings,
9798
target_compatible_with = target_compatible_with,
99+
exec_compatible_with = [],
98100
)
99101

100-
def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, target_settings):
102+
def _internal_toolchain_suite(
103+
prefix,
104+
runtime_repo_name,
105+
target_compatible_with,
106+
target_settings,
107+
exec_compatible_with):
101108
native.toolchain(
102109
name = "{prefix}_toolchain".format(prefix = prefix),
103110
toolchain = "@{runtime_repo_name}//:python_runtimes".format(
@@ -106,6 +113,7 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with,
106113
toolchain_type = TARGET_TOOLCHAIN_TYPE,
107114
target_settings = target_settings,
108115
target_compatible_with = target_compatible_with,
116+
exec_compatible_with = exec_compatible_with,
109117
)
110118

111119
native.toolchain(
@@ -116,6 +124,7 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with,
116124
toolchain_type = PY_CC_TOOLCHAIN_TYPE,
117125
target_settings = target_settings,
118126
target_compatible_with = target_compatible_with,
127+
exec_compatible_with = exec_compatible_with,
119128
)
120129

121130
native.toolchain(
@@ -142,7 +151,13 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with,
142151
# call in python/repositories.bzl. Bzlmod doesn't need anything; it will
143152
# register `:all`.
144153

145-
def define_local_toolchain_suites(name, version_aware_repo_names, version_unaware_repo_names):
154+
def define_local_toolchain_suites(
155+
name,
156+
version_aware_repo_names,
157+
version_unaware_repo_names,
158+
repo_exec_compatible_with,
159+
repo_target_compatible_with,
160+
repo_target_settings):
146161
"""Define toolchains for `local_runtime_repo` backed toolchains.
147162
148163
This generates `toolchain` targets that can be registered using `:all`. The
@@ -156,24 +171,60 @@ def define_local_toolchain_suites(name, version_aware_repo_names, version_unawar
156171
version-aware toolchains defined.
157172
version_unaware_repo_names: `list[str]` of the repo names that will have
158173
version-unaware toolchains defined.
174+
repo_target_settings: {type}`dict[str, list[str]]` mapping of repo names
175+
to string labels that are added to the `target_settings` for the
176+
respective repo's toolchain.
177+
repo_target_compatible_with: {type}`dict[str, list[str]]` mapping of repo names
178+
to string labels that are added to the `target_compatible_with` for
179+
the respective repo's toolchain.
180+
repo_exec_compatible_with: {type}`dict[str, list[str]]` mapping of repo names
181+
to string labels that are added to the `exec_compatible_with` for
182+
the respective repo's toolchain.
159183
"""
184+
160185
i = 0
161186
for i, repo in enumerate(version_aware_repo_names, start = i):
162-
prefix = render.left_pad_zero(i, 4)
187+
target_settings = ["@{}//:is_matching_python_version".format(repo)]
188+
189+
if repo_target_settings.get(repo):
190+
selects.config_setting_group(
191+
name = "_{}_user_guard".format(repo),
192+
match_all = repo_target_settings.get(repo, []) + target_settings,
193+
)
194+
target_settings = ["_{}_user_guard".format(repo)]
163195
_internal_toolchain_suite(
164-
prefix = prefix,
196+
prefix = render.left_pad_zero(i, 4),
165197
runtime_repo_name = repo,
166-
target_compatible_with = ["@{}//:os".format(repo)],
167-
target_settings = ["@{}//:is_matching_python_version".format(repo)],
198+
target_compatible_with = _get_local_toolchain_target_compatible_with(
199+
repo,
200+
repo_target_compatible_with,
201+
),
202+
target_settings = target_settings,
203+
exec_compatible_with = repo_exec_compatible_with.get(repo, []),
168204
)
169205

170206
# The version unaware entries must go last because they will match any Python
171207
# version.
172208
for i, repo in enumerate(version_unaware_repo_names, start = i + 1):
173-
prefix = render.left_pad_zero(i, 4)
174209
_internal_toolchain_suite(
175-
prefix = prefix,
210+
prefix = render.left_pad_zero(i, 4) + "_default",
176211
runtime_repo_name = repo,
177-
target_settings = [],
178-
target_compatible_with = ["@{}//:os".format(repo)],
212+
target_compatible_with = _get_local_toolchain_target_compatible_with(
213+
repo,
214+
repo_target_compatible_with,
215+
),
216+
# We don't call _get_local_toolchain_target_settings because that
217+
# will add the version matching condition by default.
218+
target_settings = repo_target_settings.get(repo, []),
219+
exec_compatible_with = repo_exec_compatible_with.get(repo, []),
179220
)
221+
222+
def _get_local_toolchain_target_compatible_with(repo, repo_target_compatible_with):
223+
if repo in repo_target_compatible_with:
224+
target_compatible_with = repo_target_compatible_with[repo]
225+
if "HOST_CONSTRAINTS" in target_compatible_with:
226+
target_compatible_with.remove("HOST_CONSTRAINTS")
227+
target_compatible_with.extend(HOST_CONSTRAINTS)
228+
else:
229+
target_compatible_with = ["@{}//:os".format(repo)]
230+
return target_compatible_with

0 commit comments

Comments
 (0)