Skip to content

Commit 1b875ff

Browse files
committed
feat: allow registering arbitrary settings for py_binary transitions
1 parent 7b88c87 commit 1b875ff

31 files changed

+620
-103
lines changed

MODULE.bazel

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ use_repo(
3434
"rules_python_internal",
3535
)
3636

37+
rules_python_config = use_extension(
38+
"//python/extensions:config.bzl",
39+
"rules_python_config",
40+
dev_dependency = True,
41+
)
42+
use_repo(rules_python_config, "rules_python_config")
43+
rules_python_config.add_transition_setting(
44+
# Intentionally add a setting already present for testing
45+
setting = "//python/config_settings:python_version",
46+
)
47+
rules_python_config.add_transition_setting(
48+
setting = "//tests/multi_pypi:external_deps_name",
49+
)
50+
3751
# We need to do another use_extension call to expose the "pythons_hub"
3852
# repo.
3953
python = use_extension("//python/extensions:python.bzl", "python")
@@ -291,7 +305,17 @@ dev_pip.parse(
291305
python_version = "3.11",
292306
requirements_lock = "//examples/wheel:requirements_server.txt",
293307
)
294-
use_repo(dev_pip, "dev_pip", "pypiserver")
308+
dev_pip.parse(
309+
hub_name = "pypi_alpha",
310+
python_version = "3.11",
311+
requirements_lock = "//tests/multi_pypi/alpha:requirements.txt",
312+
)
313+
dev_pip.parse(
314+
hub_name = "pypi_beta",
315+
python_version = "3.11",
316+
requirements_lock = "//tests/multi_pypi/beta:requirements.txt",
317+
)
318+
use_repo(dev_pip, "dev_pip", "pypi_alpha", "pypi_beta", "pypiserver")
295319

296320
# Bazel integration test setup below
297321

WORKSPACE

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ load("//:internal_dev_setup.bzl", "rules_python_internal_setup")
6969
rules_python_internal_setup()
7070

7171
load("@pythons_hub//:versions.bzl", "PYTHON_VERSIONS")
72-
load("//python:repositories.bzl", "python_register_multi_toolchains")
72+
load("//python:repositories.bzl", "py_repositories", "python_register_multi_toolchains")
73+
74+
py_repositories()
7375

7476
python_register_multi_toolchains(
7577
name = "python",
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# How to use a common set of dependencies with multiple PyPI versions
2+
3+
In this guide, we show how to handle a situation common to monorepos
4+
that extensively share code: How does a common library refer to the correct
5+
`@pypi_<name>` hub when binaries may have their own requirements (and thus
6+
PyPI hub name)? Stated as code, this situation:
7+
8+
```bzl
9+
10+
py_binary(
11+
name = "bin_alpha"
12+
deps = ["@pypi_alpha//requests", ":common"],
13+
)
14+
py_binary(
15+
name = "bin_alpha"
16+
deps = ["@pypi_beta//requests", ":common"],
17+
)
18+
19+
py_library(
20+
name = "common",
21+
deps = ["@pypi_???//more_itertools"] # <-- Which @pypi repo?
22+
)
23+
```
24+
25+
## Using flags to pick a hub
26+
27+
The basic trick to make `:common` pick the appropriate `@pypi_<name>` is to use
28+
`select()` to choose one based on build flags. To help this process, `py_binary`
29+
et al allow forcing particular build flags to be used, and custom flags can be
30+
registered to allow `py_binary` et al to set them.
31+
32+
In this example, we create a custom string flag named `//:pypi_hub`,
33+
register it to allow using it with `py_binary` directly, then use `select()`
34+
to pick different dependencies.
35+
36+
```
37+
# File: MODULE.bazel
38+
39+
rules_python_config.add_transition_setting(
40+
setting = "@//pick:pypi_hub",
41+
)
42+
43+
# File: BUILD.bazel
44+
45+
```bazel
46+
47+
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
48+
49+
string_flag(
50+
name = "pypi_hub",
51+
)
52+
53+
config_setting(
54+
name = "is_pypi_alpha",
55+
flag_values = {"//:pypi_hub": "alpha"},
56+
)
57+
58+
config_setting(
59+
name = "is_pypi_beta",
60+
flag_values = {"//:pypi_hub": "beta"}
61+
)
62+
63+
py_binary(
64+
name = "bin_alpha",
65+
srcs = ["bin_alpha.py"],
66+
config_settings = {
67+
"//:pypi_hub": "alpha",
68+
},
69+
deps ["@pypi_alpha//requests", ":common"],
70+
)
71+
py_binary(
72+
name = "bin_beta",
73+
srcs = ["bin_beta.py"],
74+
config_settings = {
75+
"//:pypi_hub": "beta",
76+
},
77+
deps ["@pypi_beta//requests", ":common"],
78+
)
79+
py_library(
80+
name = "common",
81+
deps = select({
82+
":is_pypi_alpha": ["@pypi_alpha//more_itertools"],
83+
":is_pypi_beta": ["@pypi_beta//more_itertools"],
84+
}),
85+
)
86+
```
87+
88+
When `bin_alpha` and `bin_beta` are built, they will have the `pypi_hub`
89+
flag force to their respective value. When `:common` is evaluated, it sees
90+
the flag value of the binary that is consuming it, and the `select()` resolves
91+
appropriately.

python/extensions/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@ bzl_library(
3939
"//python/private:python_bzl",
4040
],
4141
)
42+
43+
bzl_library(
44+
name = "config_bzl",
45+
srcs = ["config.bzl"],
46+
visibility = ["//:__subpackages__"],
47+
deps = [
48+
"//python/private:rules_python_config_repo_bzl",
49+
],
50+
)

python/extensions/config.bzl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Extension for configuring global settings of rules_python."""
2+
3+
load("//python/private:rules_python_config_repo.bzl", "rules_python_config_repo")
4+
5+
def _rules_python_config_impl(mctx):
6+
transition_setting_generators = {}
7+
for mod in mctx.modules:
8+
for tag in mod.tags.add_transition_setting:
9+
setting = str(tag.setting)
10+
if setting not in transition_setting_generators:
11+
transition_setting_generators[setting] = []
12+
transition_setting_generators[setting].append(mod.name)
13+
14+
rules_python_config_repo(
15+
name = "rules_python_config",
16+
transition_setting_generators = transition_setting_generators,
17+
transition_settings = transition_setting_generators.keys(),
18+
)
19+
20+
_add_transition_setting = tag_class(
21+
doc = """
22+
Specify a build setting that terminal rules transition on by default.
23+
24+
Terminal rules are rules such as py_binary, py_test, py_wheel, or similar
25+
rules that represent some deployable unit. Settings added here can
26+
then be used a keys with the {obj}`config_settings` attribute.
27+
28+
:::{note}
29+
This adds the label as a dependency of the Python rules. Take care to not refer
30+
to repositories that are expensive to create or invalidate frequently.
31+
:::
32+
""",
33+
attrs = {
34+
"setting": attr.label(doc = "The build setting to add."),
35+
},
36+
)
37+
38+
rules_python_config = module_extension(
39+
doc = """Global settings for rules_python.
40+
41+
:::{versionadded} VERSION_NEXT_FEATURE
42+
:::
43+
""",
44+
implementation = _rules_python_config_impl,
45+
tag_classes = {
46+
"add_transition_setting": _add_transition_setting,
47+
},
48+
)

python/private/BUILD.bazel

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ bzl_library(
135135
],
136136
)
137137

138+
bzl_library(
139+
name = "common_labels_bzl",
140+
srcs = ["common_labels.bzl"],
141+
)
142+
138143
bzl_library(
139144
name = "config_settings_bzl",
140145
srcs = ["config_settings.bzl"],
@@ -408,6 +413,7 @@ bzl_library(
408413
":py_runtime_info_bzl",
409414
":rules_cc_srcs_bzl",
410415
":toolchain_types_bzl",
416+
":transition_labels_bzl",
411417
"@bazel_skylib//lib:dicts",
412418
"@bazel_skylib//lib:paths",
413419
"@bazel_skylib//lib:structs",
@@ -583,6 +589,7 @@ bzl_library(
583589
deps = [
584590
":py_package_bzl",
585591
":stamp_bzl",
592+
":transition_labels_bzl",
586593
],
587594
)
588595

@@ -618,6 +625,14 @@ bzl_library(
618625
],
619626
)
620627

628+
bzl_library(
629+
name = "rules_python_config_repo_bzl",
630+
srcs = ["rules_python_config_repo.bzl"],
631+
deps = [
632+
":repo_utils_bzl",
633+
],
634+
)
635+
621636
bzl_library(
622637
name = "sentinel_bzl",
623638
srcs = ["sentinel.bzl"],
@@ -649,6 +664,16 @@ bzl_library(
649664
srcs = ["toolchain_types.bzl"],
650665
)
651666

667+
bzl_library(
668+
name = "transition_labels_bzl",
669+
srcs = ["transition_labels.bzl"],
670+
deps = [
671+
"common_labels_bzl",
672+
"@bazel_skylib//lib:collections",
673+
"@rules_python_config//:extra_transition_settings_bzl",
674+
],
675+
)
676+
652677
bzl_library(
653678
name = "util_bzl",
654679
srcs = ["util.bzl"],

python/private/attr_builders.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ load(
3131
"kwargs_setter",
3232
"kwargs_setter_doc",
3333
"kwargs_setter_mandatory",
34+
"normalize_transition_inout_values",
3435
"to_label_maybe",
3536
)
3637

@@ -167,6 +168,8 @@ def _AttrCfg_new(
167168
}
168169
kwargs_set_default_list(state, _INPUTS)
169170
kwargs_set_default_list(state, _OUTPUTS)
171+
normalize_transition_inout_values("input", state[_INPUTS])
172+
normalize_transition_inout_values("output", state[_OUTPUTS])
170173

171174
# buildifier: disable=uninitialized
172175
self = struct(

python/private/attributes.bzl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,58 @@ COVERAGE_ATTRS = {
405405
# Attributes specific to Python executable-equivalent rules. Such rules may not
406406
# accept Python sources (e.g. some packaged-version of a py_test/py_binary), but
407407
# still accept Python source-agnostic settings.
408+
CONFIG_SETTINGS_ATTR = {
409+
"config_settings": lambda: attrb.LabelKeyedStringDict(
410+
doc = """
411+
Config settings to change for this target.
412+
413+
The keys are labels for settings, and the values are strings for the new value
414+
to use. Pass `Label` objects or canonical label strings for the keys to ensure
415+
they resolve as expected (canonical labels start with `@@` and can be
416+
obtained by calling `str(Label(...))`).
417+
418+
Most `@rules_python//python/config_setting` settings can be used here, which
419+
allows, for example, making only a certain `py_binary` use
420+
{obj}`--boostrap_impl=script`.
421+
422+
Additional or custom config settings can be registered using the
423+
{obj}`add_transition_setting` API. This allows, for example, forcing a
424+
particular CPU, or defining a custom setting that `select()` uses elsewhere
425+
to pick between `pip.parse` hubs. See the [How to guide on multiple
426+
versions of a library] for a more concrete example.
427+
428+
:::{note}
429+
These values are transitioned on, so will affect the analysis graph and the
430+
associated memory overhead. The more unique configurations in your overall
431+
build, the more memory and (often unnecessary) re-analysis and re-building
432+
can occur. See
433+
https://bazel.build/extending/config#memory-performance-considerations for
434+
more information about risks and considerations.
435+
:::
436+
437+
:::{versionadded} VERSION_NEXT_FEATURE
438+
:::
439+
""",
440+
),
441+
}
442+
443+
def apply_config_settings_attr(settings, attr):
444+
"""Applies the config_settings attribute to the settings.
445+
446+
Args:
447+
settings: The settings dict to modify in-place.
448+
attr: The rule attributes struct.
449+
450+
Returns:
451+
{type}`dict[str, object]` the input `settings` value.
452+
"""
453+
for key, value in attr.config_settings.items():
454+
settings[str(key)] = value
455+
return settings
456+
408457
AGNOSTIC_EXECUTABLE_ATTRS = dicts.add(
409458
DATA_ATTRS,
459+
CONFIG_SETTINGS_ATTR,
410460
{
411461
"env": lambda: attrb.StringDict(
412462
doc = """\

0 commit comments

Comments
 (0)