1717
1818load ("@bazel_skylib//lib:selects.bzl" , "selects" )
1919load ("@bazel_skylib//rules:common_settings.bzl" , "BuildSettingInfo" )
20- load ("//python:versions .bzl" , "MINOR_MAPPING" , "TOOL_VERSIONS " )
20+ load (":semver .bzl" , "semver " )
2121
22- _PYTHON_VERSION_FLAG = str (Label ("//python/config_settings:python_version" ))
22+ _PYTHON_VERSION_FLAG = Label ("//python/config_settings:python_version" )
23+ _PYTHON_VERSION_MAJOR_MINOR_FLAG = Label ("//python/config_settings:_python_version_major_minor" )
2324
24- def _ver_key (s ):
25- major , _ , s = s .partition ("." )
26- minor , _ , s = s .partition ("." )
27- micro , _ , s = s .partition ("." )
28- return (int (major ), int (minor ), int (micro ))
29-
30- def _flag_values (* , python_versions , minor_mapping ):
31- """Construct a map of python_version to a list of toolchain values.
32-
33- This mapping maps the concept of a config setting to a list of compatible toolchain versions.
34- For using this in the code, the VERSION_FLAG_VALUES should be used instead.
35-
36- Args:
37- python_versions: {type}`list[str]` X.Y.Z` python versions.
38- minor_mapping: {type}`dict[str, str]` `X.Y` to `X.Y.Z` mapping.
39-
40- Returns:
41- A `map[str, list[str]]`. Each key is a python_version flag value. Each value
42- is a list of the python_version flag values that should match when for the
43- `key`. For example:
44- ```
45- "3.8" -> ["3.8", "3.8.1", "3.8.2", ..., "3.8.19"] # All 3.8 versions
46- "3.8.2" -> ["3.8.2"] # Only 3.8.2
47- "3.8.19" -> ["3.8.19", "3.8"] # The latest version should also match 3.8 so
48- as when the `3.8` toolchain is used we just use the latest `3.8` toolchain.
49- this makes the `select("is_python_3.8.19")` work no matter how the user
50- specifies the latest python version to use.
51- ```
52- """
53- ret = {}
54-
55- for micro_version in sorted (python_versions , key = _ver_key ):
56- minor_version , _ , _ = micro_version .rpartition ("." )
57-
58- # This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
59- # It's private because matching the concept of e.g. "3.8" value is done
60- # using the `is_python_X.Y` config setting group, which is aware of the
61- # minor versions that could match instead.
62- ret .setdefault (minor_version , [minor_version ]).append (micro_version )
63-
64- # Ensure that is_python_3.9.8 is matched if python_version is set
65- # to 3.9 if minor_mapping points to 3.9.8
66- default_micro_version = minor_mapping [minor_version ]
67- ret [micro_version ] = [micro_version , minor_version ] if default_micro_version == micro_version else [micro_version ]
68-
69- return ret
70-
71- VERSION_FLAG_VALUES = _flag_values (python_versions = TOOL_VERSIONS .keys (), minor_mapping = MINOR_MAPPING )
72-
73- def is_python_config_setting (name , * , python_version , reuse_conditions = None , ** kwargs ):
74- """Create a config setting for matching 'python_version' configuration flag.
75-
76- This function is mainly intended for internal use within the `whl_library` and `pip_parse`
77- machinery.
78-
79- The matching of the 'python_version' flag depends on the value passed in
80- `python_version` and here is the example for `3.8` (but the same applies
81- to other python versions present in @//python:versions.bzl#TOOL_VERSIONS):
82- * "3.8" -> ["3.8", "3.8.1", "3.8.2", ..., "3.8.19"] # All 3.8 versions
83- * "3.8.2" -> ["3.8.2"] # Only 3.8.2
84- * "3.8.19" -> ["3.8.19", "3.8"] # The latest version should also match 3.8 so
85- as when the `3.8` toolchain is used we just use the latest `3.8` toolchain.
86- this makes the `select("is_python_3.8.19")` work no matter how the user
87- specifies the latest python version to use.
88-
89- Args:
90- name: name for the target that will be created to be used in select statements.
91- python_version: The python_version to be passed in the `flag_values` in the
92- `config_setting`. Depending on the version, the matching python version list
93- can be as described above.
94- reuse_conditions: A dict of version to version label for which we should
95- reuse config_setting targets instead of creating them from scratch. This
96- is useful when using is_python_config_setting multiple times in the
97- same package with the same `major.minor` python versions.
98- **kwargs: extra kwargs passed to the `config_setting`.
99- """
100- if python_version not in name :
101- fail ("The name '{}' must have the python version '{}' in it" .format (name , python_version ))
102-
103- if python_version not in VERSION_FLAG_VALUES :
104- fail ("The 'python_version' must be known to 'rules_python', choose from the values: {}" .format (VERSION_FLAG_VALUES .keys ()))
105-
106- python_versions = VERSION_FLAG_VALUES [python_version ]
107- extra_flag_values = kwargs .pop ("flag_values" , {})
108- if _PYTHON_VERSION_FLAG in extra_flag_values :
109- fail ("Cannot set '{}' in the flag values" .format (_PYTHON_VERSION_FLAG ))
110-
111- if len (python_versions ) == 1 :
112- native .config_setting (
113- name = name ,
114- flag_values = {
115- _PYTHON_VERSION_FLAG : python_version ,
116- } | extra_flag_values ,
117- ** kwargs
118- )
119- return
120-
121- reuse_conditions = reuse_conditions or {}
122- create_config_settings = {
123- "_{}" .format (name ).replace (python_version , version ): {_PYTHON_VERSION_FLAG : version }
124- for version in python_versions
125- if not reuse_conditions or version not in reuse_conditions
126- }
127- match_any = list (create_config_settings .keys ())
128- for version , condition in reuse_conditions .items ():
129- if len (VERSION_FLAG_VALUES [version ]) == 1 :
130- match_any .append (condition )
131- continue
132-
133- # Convert the name to an internal label that this function would create,
134- # so that we are hitting the config_setting and not the config_setting_group.
135- condition = Label (condition )
136- if hasattr (condition , "same_package_label" ):
137- condition = condition .same_package_label ("_" + condition .name )
138- else :
139- condition = condition .relative ("_" + condition .name )
140-
141- match_any .append (condition )
142-
143- for name_ , flag_values_ in create_config_settings .items ():
144- native .config_setting (
145- name = name_ ,
146- flag_values = flag_values_ | extra_flag_values ,
147- ** kwargs
148- )
149-
150- # An alias pointing to an underscore-prefixed config_setting_group
151- # is used because config_setting_group creates
152- # `is_{version}_N` targets, which are easily confused with the
153- # `is_{minor}.{micro}` (dot) targets.
154- selects .config_setting_group (
155- name = "_{}_group" .format (name ),
156- match_any = match_any ,
157- visibility = ["//visibility:private" ],
158- )
159- native .alias (
160- name = name ,
161- actual = "_{}_group" .format (name ),
162- visibility = kwargs .get ("visibility" , []),
163- )
164-
165- def construct_config_settings (name = None ): # buildifier: disable=function-docstring
25+ def construct_config_settings (* , name , versions , minor_mapping ): # buildifier: disable=function-docstring
16626 """Create a 'python_version' config flag and construct all config settings used in rules_python.
16727
16828 This mainly includes the targets that are used in the toolchain and pip hub
16929 repositories that only match on the 'python_version' flag values.
17030
17131 Args:
172- name(str): A dummy name value that is no-op for now.
32+ name: {type}`str` A dummy name value that is no-op for now.
33+ versions: {type}`list[str]` A list of versions to build constraint settings for.
34+ minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions.
17335 """
36+ _ = name # @unused
17437 _python_version_flag (
175- name = "python_version" ,
38+ name = _PYTHON_VERSION_FLAG . name ,
17639 # TODO: The default here should somehow match the MODULE config. Until
17740 # then, use the empty string to indicate an unknown version. This
17841 # also prevents version-unaware targets from inadvertently matching
17942 # a select condition when they shouldn't.
18043 build_setting_default = "" ,
181- values = ["" ] + VERSION_FLAG_VALUES .keys (),
44+ visibility = ["//visibility:public" ],
45+ )
46+
47+ _python_version_major_minor_flag (
48+ name = _PYTHON_VERSION_MAJOR_MINOR_FLAG .name ,
49+ build_setting_default = "" ,
18250 visibility = ["//visibility:public" ],
18351 )
18452
18553 native .config_setting (
18654 name = "is_python_version_unset" ,
187- flag_values = {
188- Label ("//python/config_settings:python_version" ): "" ,
189- },
55+ flag_values = {_PYTHON_VERSION_FLAG : "" },
19056 visibility = ["//visibility:public" ],
19157 )
19258
193- for version , matching_versions in VERSION_FLAG_VALUES .items ():
194- is_python_config_setting (
195- name = "is_python_{}" .format (version ),
196- python_version = version ,
197- reuse_conditions = {
198- v : native .package_relative_label ("is_python_{}" .format (v ))
199- for v in matching_versions
200- if v != version
201- },
59+ _reverse_minor_mapping = {full : minor for minor , full in minor_mapping .items ()}
60+ for version in versions :
61+ minor_version = _reverse_minor_mapping .get (version )
62+ if not minor_version :
63+ native .config_setting (
64+ name = "is_python_{}" .format (version ),
65+ flag_values = {":python_version" : version },
66+ visibility = ["//visibility:public" ],
67+ )
68+ continue
69+
70+ # Also need to match the minor version when using
71+ name = "is_python_{}" .format (version )
72+ native .config_setting (
73+ name = "_" + name ,
74+ flag_values = {":python_version" : version },
75+ visibility = ["//visibility:public" ],
76+ )
77+
78+ # An alias pointing to an underscore-prefixed config_setting_group
79+ # is used because config_setting_group creates
80+ # `is_{version}_N` targets, which are easily confused with the
81+ # `is_{minor}.{micro}` (dot) targets.
82+ selects .config_setting_group (
83+ name = "_{}_group" .format (name ),
84+ match_any = [
85+ ":_is_python_{}" .format (version ),
86+ ":is_python_{}" .format (minor_version ),
87+ ],
88+ visibility = ["//visibility:private" ],
89+ )
90+ native .alias (
91+ name = name ,
92+ actual = "_{}_group" .format (name ),
93+ visibility = ["//visibility:public" ],
94+ )
95+
96+ # This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8
97+ # It's private because matching the concept of e.g. "3.8" value is done
98+ # using the `is_python_X.Y` config setting group, which is aware of the
99+ # minor versions that could match instead.
100+ for minor in minor_mapping .keys ():
101+ native .config_setting (
102+ name = "is_python_{}" .format (minor ),
103+ flag_values = {_PYTHON_VERSION_MAJOR_MINOR_FLAG : minor },
202104 visibility = ["//visibility:public" ],
203105 )
204106
205107def _python_version_flag_impl (ctx ):
206108 value = ctx .build_setting_value
207- if value not in ctx .attr .values :
208- fail ((
209- "Invalid --python_version value: {actual}\n Allowed values {allowed}"
210- ).format (
211- actual = value ,
212- allowed = ", " .join (sorted (ctx .attr .values )),
213- ))
214-
215109 return [
216110 # BuildSettingInfo is the original provider returned, so continue to
217111 # return it for compatibility
@@ -227,9 +121,25 @@ def _python_version_flag_impl(ctx):
227121_python_version_flag = rule (
228122 implementation = _python_version_flag_impl ,
229123 build_setting = config .string (flag = True ),
124+ attrs = {},
125+ )
126+
127+ def _python_version_major_minor_flag_impl (ctx ):
128+ input = ctx .attr ._python_version_flag [config_common .FeatureFlagInfo ].value
129+ if input :
130+ version = semver (input )
131+ value = "{}.{}" .format (version .major , version .minor )
132+ else :
133+ value = ""
134+
135+ return [config_common .FeatureFlagInfo (value = value )]
136+
137+ _python_version_major_minor_flag = rule (
138+ implementation = _python_version_major_minor_flag_impl ,
139+ build_setting = config .string (flag = False ),
230140 attrs = {
231- "values " : attr .string_list (
232- doc = "Allowed values." ,
141+ "_python_version_flag " : attr .label (
142+ default = _PYTHON_VERSION_FLAG ,
233143 ),
234144 },
235145)
0 commit comments