|
14 | 14 |
|
15 | 15 | "Python toolchain module extensions for use with bzlmod"
|
16 | 16 |
|
17 |
| -load("//python:repositories.bzl", "python_register_toolchains") |
18 |
| -load("//python/extensions/private:pythons_hub.bzl", "hub_repo") |
19 |
| -load("//python/private:toolchains_repo.bzl", "multi_toolchain_aliases") |
| 17 | +load("//python/private/bzlmod:python.bzl", _python = "python") |
20 | 18 |
|
21 |
| -# This limit can be increased essentially arbitrarily, but doing so will cause a rebuild of all |
22 |
| -# targets using any of these toolchains due to the changed repository name. |
23 |
| -_MAX_NUM_TOOLCHAINS = 9999 |
24 |
| -_TOOLCHAIN_INDEX_PAD_LENGTH = len(str(_MAX_NUM_TOOLCHAINS)) |
25 |
| - |
26 |
| -def _toolchain_prefix(index, name): |
27 |
| - """Prefixes the given name with the index, padded with zeros to ensure lexicographic sorting. |
28 |
| -
|
29 |
| - Examples: |
30 |
| - _toolchain_prefix( 2, "foo") == "_0002_foo_" |
31 |
| - _toolchain_prefix(2000, "foo") == "_2000_foo_" |
32 |
| - """ |
33 |
| - return "_{}_{}_".format(_left_pad_zero(index, _TOOLCHAIN_INDEX_PAD_LENGTH), name) |
34 |
| - |
35 |
| -def _left_pad_zero(index, length): |
36 |
| - if index < 0: |
37 |
| - fail("index must be non-negative") |
38 |
| - return ("0" * length + str(index))[-length:] |
39 |
| - |
40 |
| -# Printing a warning msg not debugging, so we have to disable |
41 |
| -# the buildifier check. |
42 |
| -# buildifier: disable=print |
43 |
| -def _print_warn(msg): |
44 |
| - print("WARNING:", msg) |
45 |
| - |
46 |
| -def _python_register_toolchains(name, toolchain_attr, version_constraint): |
47 |
| - """Calls python_register_toolchains and returns a struct used to collect the toolchains. |
48 |
| - """ |
49 |
| - python_register_toolchains( |
50 |
| - name = name, |
51 |
| - python_version = toolchain_attr.python_version, |
52 |
| - register_coverage_tool = toolchain_attr.configure_coverage_tool, |
53 |
| - ignore_root_user_error = toolchain_attr.ignore_root_user_error, |
54 |
| - set_python_version_constraint = version_constraint, |
55 |
| - ) |
56 |
| - return struct( |
57 |
| - python_version = toolchain_attr.python_version, |
58 |
| - set_python_version_constraint = str(version_constraint), |
59 |
| - name = name, |
60 |
| - ) |
61 |
| - |
62 |
| -def _python_impl(module_ctx): |
63 |
| - # The toolchain info structs to register, in the order to register them in. |
64 |
| - toolchains = [] |
65 |
| - |
66 |
| - # We store the default toolchain separately to ensure it is the last |
67 |
| - # toolchain added to toolchains. |
68 |
| - default_toolchain = None |
69 |
| - |
70 |
| - # Map of string Major.Minor to the toolchain name and module name |
71 |
| - global_toolchain_versions = {} |
72 |
| - |
73 |
| - for mod in module_ctx.modules: |
74 |
| - module_toolchain_versions = [] |
75 |
| - |
76 |
| - for toolchain_attr in mod.tags.toolchain: |
77 |
| - toolchain_version = toolchain_attr.python_version |
78 |
| - toolchain_name = "python_" + toolchain_version.replace(".", "_") |
79 |
| - |
80 |
| - # Duplicate versions within a module indicate a misconfigured module. |
81 |
| - if toolchain_version in module_toolchain_versions: |
82 |
| - _fail_duplicate_module_toolchain_version(toolchain_version, mod.name) |
83 |
| - module_toolchain_versions.append(toolchain_version) |
84 |
| - |
85 |
| - # Ignore version collisions in the global scope because there isn't |
86 |
| - # much else that can be done. Modules don't know and can't control |
87 |
| - # what other modules do, so the first in the dependency graph wins. |
88 |
| - if toolchain_version in global_toolchain_versions: |
89 |
| - # If the python version is explicitly provided by the root |
90 |
| - # module, they should not be warned for choosing the same |
91 |
| - # version that rules_python provides as default. |
92 |
| - first = global_toolchain_versions[toolchain_version] |
93 |
| - if mod.name != "rules_python" or not first.is_root: |
94 |
| - _warn_duplicate_global_toolchain_version( |
95 |
| - toolchain_version, |
96 |
| - first = first, |
97 |
| - second_toolchain_name = toolchain_name, |
98 |
| - second_module_name = mod.name, |
99 |
| - ) |
100 |
| - continue |
101 |
| - global_toolchain_versions[toolchain_version] = struct( |
102 |
| - toolchain_name = toolchain_name, |
103 |
| - module_name = mod.name, |
104 |
| - is_root = mod.is_root, |
105 |
| - ) |
106 |
| - |
107 |
| - # Only the root module and rules_python are allowed to specify the default |
108 |
| - # toolchain for a couple reasons: |
109 |
| - # * It prevents submodules from specifying different defaults and only |
110 |
| - # one of them winning. |
111 |
| - # * rules_python needs to set a soft default in case the root module doesn't, |
112 |
| - # e.g. if the root module doesn't use Python itself. |
113 |
| - # * The root module is allowed to override the rules_python default. |
114 |
| - if mod.is_root: |
115 |
| - # A single toolchain is treated as the default because it's unambiguous. |
116 |
| - is_default = toolchain_attr.is_default or len(mod.tags.toolchain) == 1 |
117 |
| - elif mod.name == "rules_python" and not default_toolchain: |
118 |
| - # We don't do the len() check because we want the default that rules_python |
119 |
| - # sets to be clearly visible. |
120 |
| - is_default = toolchain_attr.is_default |
121 |
| - else: |
122 |
| - is_default = False |
123 |
| - |
124 |
| - # We have already found one default toolchain, and we can only have |
125 |
| - # one. |
126 |
| - if is_default and default_toolchain != None: |
127 |
| - _fail_multiple_default_toolchains( |
128 |
| - first = default_toolchain.name, |
129 |
| - second = toolchain_name, |
130 |
| - ) |
131 |
| - |
132 |
| - toolchain_info = _python_register_toolchains( |
133 |
| - toolchain_name, |
134 |
| - toolchain_attr, |
135 |
| - version_constraint = not is_default, |
136 |
| - ) |
137 |
| - |
138 |
| - if is_default: |
139 |
| - default_toolchain = toolchain_info |
140 |
| - else: |
141 |
| - toolchains.append(toolchain_info) |
142 |
| - |
143 |
| - # A default toolchain is required so that the non-version-specific rules |
144 |
| - # are able to match a toolchain. |
145 |
| - if default_toolchain == None: |
146 |
| - fail("No default Python toolchain configured. Is rules_python missing `is_default=True`?") |
147 |
| - |
148 |
| - # The last toolchain in the BUILD file is set as the default |
149 |
| - # toolchain. We need the default last. |
150 |
| - toolchains.append(default_toolchain) |
151 |
| - |
152 |
| - if len(toolchains) > _MAX_NUM_TOOLCHAINS: |
153 |
| - fail("more than {} python versions are not supported".format(_MAX_NUM_TOOLCHAINS)) |
154 |
| - |
155 |
| - # Create the pythons_hub repo for the interpreter meta data and the |
156 |
| - # the various toolchains. |
157 |
| - hub_repo( |
158 |
| - name = "pythons_hub", |
159 |
| - default_python_version = default_toolchain.python_version, |
160 |
| - toolchain_prefixes = [ |
161 |
| - _toolchain_prefix(index, toolchain.name) |
162 |
| - for index, toolchain in enumerate(toolchains) |
163 |
| - ], |
164 |
| - toolchain_python_versions = [t.python_version for t in toolchains], |
165 |
| - toolchain_set_python_version_constraints = [t.set_python_version_constraint for t in toolchains], |
166 |
| - toolchain_user_repository_names = [t.name for t in toolchains], |
167 |
| - ) |
168 |
| - |
169 |
| - # This is require in order to support multiple version py_test |
170 |
| - # and py_binary |
171 |
| - multi_toolchain_aliases( |
172 |
| - name = "python_versions", |
173 |
| - python_versions = { |
174 |
| - version: entry.toolchain_name |
175 |
| - for version, entry in global_toolchain_versions.items() |
176 |
| - }, |
177 |
| - ) |
178 |
| - |
179 |
| -def _fail_duplicate_module_toolchain_version(version, module): |
180 |
| - fail(("Duplicate module toolchain version: module '{module}' attempted " + |
181 |
| - "to use version '{version}' multiple times in itself").format( |
182 |
| - version = version, |
183 |
| - module = module, |
184 |
| - )) |
185 |
| - |
186 |
| -def _warn_duplicate_global_toolchain_version(version, first, second_toolchain_name, second_module_name): |
187 |
| - _print_warn(( |
188 |
| - "Ignoring toolchain '{second_toolchain}' from module '{second_module}': " + |
189 |
| - "Toolchain '{first_toolchain}' from module '{first_module}' " + |
190 |
| - "already registered Python version {version} and has precedence" |
191 |
| - ).format( |
192 |
| - first_toolchain = first.toolchain_name, |
193 |
| - first_module = first.module_name, |
194 |
| - second_module = second_module_name, |
195 |
| - second_toolchain = second_toolchain_name, |
196 |
| - version = version, |
197 |
| - )) |
198 |
| - |
199 |
| -def _fail_multiple_default_toolchains(first, second): |
200 |
| - fail(("Multiple default toolchains: only one toolchain " + |
201 |
| - "can have is_default=True. First default " + |
202 |
| - "was toolchain '{first}'. Second was '{second}'").format( |
203 |
| - first = first, |
204 |
| - second = second, |
205 |
| - )) |
206 |
| - |
207 |
| -python = module_extension( |
208 |
| - doc = """Bzlmod extension that is used to register Python toolchains. |
209 |
| -""", |
210 |
| - implementation = _python_impl, |
211 |
| - tag_classes = { |
212 |
| - "toolchain": tag_class( |
213 |
| - doc = """Tag class used to register Python toolchains. |
214 |
| -Use this tag class to register one or more Python toolchains. This class |
215 |
| -is also potentially called by sub modules. The following covers different |
216 |
| -business rules and use cases. |
217 |
| -
|
218 |
| -Toolchains in the Root Module |
219 |
| -
|
220 |
| -This class registers all toolchains in the root module. |
221 |
| -
|
222 |
| -Toolchains in Sub Modules |
223 |
| -
|
224 |
| -It will create a toolchain that is in a sub module, if the toolchain |
225 |
| -of the same name does not exist in the root module. The extension stops name |
226 |
| -clashing between toolchains in the root module and toolchains in sub modules. |
227 |
| -You cannot configure more than one toolchain as the default toolchain. |
228 |
| -
|
229 |
| -Toolchain set as the default version |
230 |
| -
|
231 |
| -This extension will not create a toolchain that exists in a sub module, |
232 |
| -if the sub module toolchain is marked as the default version. If you have |
233 |
| -more than one toolchain in your root module, you need to set one of the |
234 |
| -toolchains as the default version. If there is only one toolchain it |
235 |
| -is set as the default toolchain. |
236 |
| -
|
237 |
| -Toolchain repository name |
238 |
| -
|
239 |
| -A toolchain's repository name uses the format `python_{major}_{minor}`, e.g. |
240 |
| -`python_3_10`. The `major` and `minor` components are |
241 |
| -`major` and `minor` are the Python version from the `python_version` attribute. |
242 |
| -""", |
243 |
| - attrs = { |
244 |
| - "configure_coverage_tool": attr.bool( |
245 |
| - mandatory = False, |
246 |
| - doc = "Whether or not to configure the default coverage tool for the toolchains.", |
247 |
| - ), |
248 |
| - "ignore_root_user_error": attr.bool( |
249 |
| - default = False, |
250 |
| - doc = "Whether the check for root should be ignored or not. This causes cache misses with .pyc files.", |
251 |
| - mandatory = False, |
252 |
| - ), |
253 |
| - "is_default": attr.bool( |
254 |
| - mandatory = False, |
255 |
| - doc = "Whether the toolchain is the default version", |
256 |
| - ), |
257 |
| - "python_version": attr.string( |
258 |
| - mandatory = True, |
259 |
| - doc = "The Python version, in `major.minor` format, e.g " + |
260 |
| - "'3.12', to create a toolchain for. Patch level " + |
261 |
| - "granularity (e.g. '3.12.1') is not supported.", |
262 |
| - ), |
263 |
| - }, |
264 |
| - ), |
265 |
| - }, |
266 |
| -) |
| 19 | +python = _python |
0 commit comments