8000 internal(config_settings): make config_setting creation reusable · bazel-contrib/rules_python@d2db50e · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit d2db50e

Browse files
committed
internal(config_settings): make config_setting creation reusable
The PR #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 #1744 to support whl-only hub repos more self-contained. Supersedes #1743.
1 parent a5e17e6 commit d2db50e

File tree

2 files changed

+116
-90
lines changed

2 files changed

+116
-90
lines changed

python/config_settings/BUILD.bazel

Lines changed: 27 additions & 5 deletions
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

Lines changed: 89 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -16,104 +16,108 @@
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.
29-
"""
34+
python_versions: A list of all versions.
3035
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.
40+
"""
3141
# 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-
)
42+
ret = {}
43+
44+
for micro_version in sorted(python_versions, key=_ver_key):
45+
minor_version, _, _ = micro_version.rpartition(".")
5246

53-
for minor_version, micro_versions in minor_to_micro_versions.items():
5447
# This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
5548
# It's private because matching the concept of e.g. "3.8" value is done
5649
# using the `is_python_X.Y` config setting group, which is aware of the
5750
# 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]
51+
ret.setdefault(minor_version, []).append(micro_version)
6452

53+
# Ensure that is_python_3.9.8 is matched if python_version is set
54+
# to 3.9 if MINOR_MAPPING points to 3.9.8
6555
default_micro_version = MINOR_MAPPING[minor_version]
56+
ret[micro_version] = [minor_version] if default_micro_version == micro_version else []
57+
58+
return ret
59+
60+
VERSION_FLAG_VALUES = _flag_values(TOOL_VERSIONS.keys())
6661

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,
62+
def is_python_config_setting(name, flag_values, match_extra, **kwargs):
63+
"""Create a config setting for matching 'python_version' configuration flag.
64+
65+
This function is mainly intended for internal use within the `whl_library` and `pip_parse`
66+
machinery.
67+
68+
Args:
69+
name: name for the target that will be created to be used in select statements.
70+
flag_values: The flag_values in the `config_setting`.
71+
match_extra: The extra flag values that we should matched when the `name` is used
72+
in the config setting. You can either pass a list of labels that will be included
73+
in the bazel-skylib selects.config_setting_group match_any clause or it can be a
74+
dict[str, dic], where the keys are the names of the extra config_setting targets
75+
to be created and the value is the `flag_values` attribute.
76+
**kwargs: extra kwargs passed to the `config_setting`
77+
"""
78+
visibility = kwargs.pop("visibility", [])
79+
if not match_extra:
80+
native.config_setting(
81+
name = name,
82+
flag_values = flag_values,
83+
visibility = visibility,
84+
**kwargs,
11385
)
86+
return
11487

115-
native.alias(
116-
name = "is_python_" + minor_version,
117-
actual = "_is_python_" + minor_version,
118-
visibility = ["//visibility:public"],
88+
create_config_settings = {"_" + name: flag_values}
89+
match_any = ["_" + name]
90+
if type(match_extra) == type([]):
91+
match_any.extend(match_extra)
92+
elif type(match_extra) == type({}):
93+
match_any.extend(match_extra.keys())
94+
create_config_settings.update(match_extra)
95+
else:
96+
fail("unsupported match_extra type, can be either a list or a dict of dicts")
97+
98+
# Create all of the necessary config setting values for the config_setting_group
99+
for name_, flag_values_ in create_config_settings.items():
100+
native.config_setting(
101+
name = name_,
102+
flag_values = flag_values_,
103+
# We need to pass the visibility here because of how `config_setting_group` is
104+
# implemented, it is using the internal aliases here, hence the need for making
105+
# them with the same visibility as the `alias` itself.
106+
visibility = visibility,
107+
**kwargs,
119108
)
109+
110+
# An alias pointing to an underscore-prefixed config_setting_group
111+
# is used because config_setting_group creates
112+
# `is_{version}_N` targets, which are easily confused with the
113+
# `is_{minor}.{micro}` (dot) targets.
114+
selects.config_setting_group(
115+
name = "_{}_group".format(name),
116+
match_any = match_any,
117+
visibility = ["//visibility:private"],
118+
)
119+
native.alias(
120+
name = name,
121+
actual = "_{}_group".format(name),
122+
visibility = visibility,
123+
)

0 commit comments

Comments
 (0)
0