Skip to content

Commit dbc080a

Browse files
committed
internal(config_settings): make config_setting creation reusable
The PR bazel-contrib#1743 explored the idea of creating extra config settings for each target platform that our toolchain is targetting, however that has a drawback of not being usable in `bzlmod` if someone built Python for a platform that we don't provide a toolchain for and tried to use the `pip.parse` machinery with that by providing the `python_interpreter_target`. That is a niche usecase, but `rules_python` is a core ruleset that should only provide abstractions/helpers that work in all cases or make it possible to extend things. This explores a way to decouple the definition of the available `config_settings` values and how they are constructed by adding an extra `is_python_config_setting` macro, that could be used to declare the config settings from within the `pip.parse` hub repo. This makes the work in bazel-contrib#1744 to support whl-only hub repos more self-contained. Supersedes bazel-contrib#1743.
1 parent 565a531 commit dbc080a

File tree

2 files changed

+116
-89
lines changed

2 files changed

+116
-89
lines changed

python/config_settings/BUILD.bazel

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
load("//python:versions.bzl", "TOOL_VERSIONS")
2-
load(":config_settings.bzl", "construct_config_settings")
1+
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
2+
load(":config_settings.bzl", "VERSION_FLAG_VALUES", "is_python_config_setting")
33

44
filegroup(
55
name = "distribution",
@@ -10,7 +10,29 @@ filegroup(
1010
visibility = ["//python:__pkg__"],
1111
)
1212

13-
construct_config_settings(
14-
name = "construct_config_settings",
15-
python_versions = TOOL_VERSIONS.keys(),
13+
string_flag(
14+
name = "python_version",
15+
# TODO: The default here should somehow match the MODULE config. Until
16+
# then, use the empty string to indicate an unknown version. This
17+
# also prevents version-unaware targets from inadvertently matching
18+
# a select condition when they shouldn't.
19+
build_setting_default = "",
20+
values = [""] + VERSION_FLAG_VALUES.keys(),
21+
visibility = ["//visibility:public"],
1622
)
23+
24+
[
25+
is_python_config_setting(
26+
name = "is_python_{}".format(version),
27+
flag_values = {":python_version": version},
28+
match_extra = [
29+
# Use the internal labels created by this macro in order to handle matching
30+
# 3.8 config value if using the 3.8 version from MINOR_MAPPING. If we used the
31+
# public labels we would have a circular dependency loop.
32+
("_is_python_{}" if VERSION_FLAG_VALUES[x] else "is_python_{}").format(x)
33+
for x in extras
34+
],
35+
visibility = ["//visibility:public"],
36+
)
37+
for version, extras in VERSION_FLAG_VALUES.items()
38+
]

python/config_settings/config_settings.bzl

+89-84
Original file line numberDiff line numberDiff line change
@@ -16,104 +16,109 @@
1616
"""
1717

1818
load("@bazel_skylib//lib:selects.bzl", "selects")
19-
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
20-
load("//python:versions.bzl", "MINOR_MAPPING")
19+
load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
2120

22-
def construct_config_settings(name, python_versions):
23-
"""Constructs a set of configs for all Python versions.
21+
def _ver_key(s):
22+
_, _, s = s.partition(".") # All are 3
23+
minor, _, s = s.partition(".")
24+
micro, _, s = s.partition(".")
25+
return 100 * int(minor) + int(micro)
26+
27+
def _flag_values(python_versions):
28+
"""Construct a map of python_version to a list of toolchain values.
29+
30+
This mapping maps the concept of a config setting to a list of compatible toolchain versions.
31+
For using this in the code, the VERSION_FLAG_VALUES should be used instead.
2432
2533
Args:
26-
name: str, unused; only specified to satisfy buildifier lint checks
27-
and allow programatic modification of the target.
28-
python_versions: list of all (x.y.z) Python versions supported by rules_python.
34+
python_versions: A list of all versions.
35+
36+
Returns:
37+
A map with config settings as keys and values as extra flag values to be included in
38+
the config_setting_group if they should be also matched, which is used for generating
39+
correct entries for matching the latest 3.8 version, etc.
2940
"""
3041

3142
# Maps e.g. "3.8" -> ["3.8.1", "3.8.2", etc]
32-
minor_to_micro_versions = {}
33-
34-
allowed_flag_values = []
35-
for micro_version in python_versions:
36-
minor, _, _ = micro_version.rpartition(".")
37-
minor_to_micro_versions.setdefault(minor, []).append(micro_version)
38-
allowed_flag_values.append(micro_version)
39-
40-
allowed_flag_values.extend(list(minor_to_micro_versions))
41-
42-
string_flag(
43-
name = "python_version",
44-
# TODO: The default here should somehow match the MODULE config. Until
45-
# then, use the empty string to indicate an unknown version. This
46-
# also prevents version-unaware targets from inadvertently matching
47-
# a select condition when they shouldn't.
48-
build_setting_default = "",
49-
values = [""] + sorted(allowed_flag_values),
50-
visibility = ["//visibility:public"],
51-
)
43+
ret = {}
44+
45+
for micro_version in sorted(python_versions, key = _ver_key):
46+
minor_version, _, _ = micro_version.rpartition(".")
5247

53-
for minor_version, micro_versions in minor_to_micro_versions.items():
5448
# This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
5549
# It's private because matching the concept of e.g. "3.8" value is done
5650
# using the `is_python_X.Y` config setting group, which is aware of the
5751
# minor versions that could match instead.
58-
equals_minor_version_name = "_python_version_flag_equals_" + minor_version
59-
native.config_setting(
60-
name = equals_minor_version_name,
61-
flag_values = {":python_version": minor_version},
62-
)
63-
matches_minor_version_names = [equals_minor_version_name]
52+
ret.setdefault(minor_version, []).append(micro_version)
6453

54+
# Ensure that is_python_3.9.8 is matched if python_version is set
55+
# to 3.9 if MINOR_MAPPING points to 3.9.8
6556
default_micro_version = MINOR_MAPPING[minor_version]
57+
ret[micro_version] = [minor_version] if default_micro_version == micro_version else []
58+
59+
return ret
60+
61+
VERSION_FLAG_VALUES = _flag_values(TOOL_VERSIONS.keys())
62+
63+
def is_python_config_setting(name, flag_values, match_extra, **kwargs):
64+
"""Create a config setting for matching 'python_version' configuration flag.
6665
67-
for micro_version in micro_versions:
68-
is_micro_version_name = "is_python_" + micro_version
69-
if default_micro_version != micro_version:
70-
native.config_setting(
71-
name = is_micro_version_name,
72-
flag_values = {":python_version": micro_version},
73-
visibility = ["//visibility:public"],
74-
)
75-
matches_minor_version_names.append(is_micro_version_name)
76-
continue
77-
78-
# Ensure that is_python_3.9.8 is matched if python_version is set
79-
# to 3.9 if MINOR_MAPPING points to 3.9.8
80-
equals_micro_name = "_python_version_flag_equals_" + micro_version
81-
native.config_setting(
82-
name = equals_micro_name,
83-
flag_values = {":python_version": micro_version},
84-
)
85-
86-
# An alias pointing to an underscore-prefixed config_setting_group
87-
# is used because config_setting_group creates
88-
# `is_{minor}_N` targets, which are easily confused with the
89-
# `is_{minor}.{micro}` (dot) targets.
90-
selects.config_setting_group(
91-
name = "_" + is_micro_version_name,
92-
match_any = [
93-
equals_micro_name,
94-
equals_minor_version_name,
95-
],
96-
)
97-
native.alias(
98-
name = is_micro_version_name,
99-
actual = "_" + is_micro_version_name,
100-
visibility = ["//visibility:public"],
101-
)
102-
matches_minor_version_names.append(equals_micro_name)
103-
104-
# This is prefixed with an underscore to prevent confusion due to how
105-
# config_setting_group is implemented and how our micro-version targets
106-
# are named. config_setting_group will generate targets like
107-
# "is_python_3.10_1" (where the `_N` suffix is len(match_any).
108-
# Meanwhile, the micro-version tarets are named "is_python_3.10.1" --
109-
# just a single dot vs underscore character difference.
110-
selects.config_setting_group(
111-
name = "_is_python_" + minor_version,
112-
match_any = matches_minor_version_names,
66+
This function is mainly intended for internal use within the `whl_library` and `pip_parse`
67+
machinery.
68+
69+
Args:
70+
name: name for the target that will be created to be used in select statements.
71+
flag_values: The flag_values in the `config_setting`.
72+
match_extra: The extra flag values that we should matched when the `name` is used
73+
in the config setting. You can either pass a list of labels that will be included
74+
in the bazel-skylib selects.config_setting_group match_any clause or it can be a
75+
dict[str, dic], where the keys are the names of the extra config_setting targets
76+
to be created and the value is the `flag_values` attribute.
77+
**kwargs: extra kwargs passed to the `config_setting`
78+
"""
79+
visibility = kwargs.pop("visibility", [])
80+
if not match_extra:
81+
native.config_setting(
82+
name = name,
83+
flag_values = flag_values,
84+
visibility = visibility,
85+
**kwargs
11386
)
87+
return
88+
89+
create_config_settings = {"_" + name: flag_values}
90+
match_any = ["_" + name]
91+
if type(match_extra) == type([]):
92+
match_any.extend(match_extra)
93+
elif type(match_extra) == type({}):
94+
match_any.extend(match_extra.keys())
95+
create_config_settings.update(match_extra)
96+
else:
97+
fail("unsupported match_extra type, can be either a list or a dict of dicts")
11498

115-
native.alias(
116-
name = "is_python_" + minor_version,
117-
actual = "_is_python_" + minor_version,
118-
visibility = ["//visibility:public"],
99+
# Create all of the necessary config setting values for the config_setting_group
100+
for name_, flag_values_ in create_config_settings.items():
101+
native.config_setting(
102+
name = name_,
103+
flag_values = flag_values_,
104+
# We need to pass the visibility here because of how `config_setting_group` is
105+
# implemented, it is using the internal aliases here, hence the need for making
106+
# them with the same visibility as the `alias` itself.
107+
visibility = visibility,
108+
**kwargs
119109
)
110+
111+
# An alias pointing to an underscore-prefixed config_setting_group
112+
# is used because config_setting_group creates
113+
# `is_{version}_N` targets, which are easily confused with the
114+
# `is_{minor}.{micro}` (dot) targets.
115+
selects.config_setting_group(
116+
name = "_{}_group".format(name),
117+
match_any = match_any,
118+
visibility = ["//visibility:private"],
119+
)
120+
native.alias(
121+
name = name,
122+
actual = "_{}_group".format(name),
123+
visibility = visibility,
124+
)

0 commit comments

Comments
 (0)