Skip to content

Commit 7b95ea6

Browse files
aignasrickeylev
andauthored
refactor(pkg_aliases): create a macro for creating whl aliases (#2391)
This just cleans up the code and moves more logic from the repository_rule (i.e. generation of `BUILD.bazel` files) to loading time (macro evaluation). This makes the unit testing easier and I plan to also move the code that is generating config setting names from filenames to this new macro, but wanted to submit this PR to reduce the review chunks. Summary: - Add a new `pkg_aliases` macro. - Move logic and tests for creating WORKSPACE aliases. - Move logic and tests bzlmod aliases. - Move logic and tests bzlmod aliases with groups. - Add a test for extra alias creation. - Use `whl_alias` in `pypi` extension integration tests. - Improve the serialization of `whl_alias` for passing to the pypi hub repo. Related to #260, #2386, #2337, #2319 - hopefully cleaning the code up will make it easier to address those feature requests later. --------- Co-authored-by: Richard Levasseur <[email protected]>
1 parent 0759322 commit 7b95ea6

File tree

9 files changed

+501
-443
lines changed

9 files changed

+501
-443
lines changed

examples/bzlmod/MODULE.bazel.lock

+84-84
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/private/pypi/BUILD.bazel

-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ bzl_library(
256256
srcs = ["render_pkg_aliases.bzl"],
257257
deps = [
258258
":generate_group_library_build_bazel_bzl",
259-
":labels_bzl",
260259
":parse_whl_name_bzl",
261260
":whl_target_platforms_bzl",
262261
"//python/private:normalize_name_bzl",

python/private/pypi/extension.bzl

+16-2
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,20 @@ You cannot use both the additive_build_content and additive_build_content_file a
571571
is_reproducible = is_reproducible,
572572
)
573573

574+
def _alias_dict(a):
575+
ret = {
576+
"repo": a.repo,
577+
}
578+
if a.config_setting:
579+
ret["config_setting"] = a.config_setting
580+
if a.filename:
581+
ret["filename"] = a.filename
582+
if a.target_platforms:
583+
ret["target_platforms"] = a.target_platforms
584+
if a.version:
585+
ret["version"] = a.version
586+
return ret
587+
574588
def _pip_impl(module_ctx):
575589
"""Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
576590
@@ -651,8 +665,8 @@ def _pip_impl(module_ctx):
651665
repo_name = hub_name,
652666
extra_hub_aliases = mods.extra_aliases.get(hub_name, {}),
653667
whl_map = {
654-
key: json.encode(value)
655-
for key, value in whl_map.items()
668+
key: json.encode([_alias_dict(a) for a in aliases])
669+
for key, aliases in whl_map.items()
656670
},
657671
packages = mods.exposed_packages.get(hub_name, []),
658672
groups = mods.hub_group_map.get(hub_name),

python/private/pypi/pkg_aliases.bzl

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""pkg_aliases is a macro to generate aliases for selecting the right wheel for the right target platform.
16+
17+
This is used in bzlmod and non-bzlmod setups."""
18+
19+
load("//python/private:text_util.bzl", "render")
20+
load(
21+
":labels.bzl",
22+
"DATA_LABEL",
23+
"DIST_INFO_LABEL",
24+
"PY_LIBRARY_IMPL_LABEL",
25+
"PY_LIBRARY_PUBLIC_LABEL",
26+
"WHEEL_FILE_IMPL_LABEL",
27+
"WHEEL_FILE_PUBLIC_LABEL",
28+
)
29+
30+
_NO_MATCH_ERROR_TEMPLATE = """\
31+
No matching wheel for current configuration's Python version.
32+
33+
The current build configuration's Python version doesn't match any of the Python
34+
wheels available for this wheel. This wheel supports the following Python
35+
configuration settings:
36+
{config_settings}
37+
38+
To determine the current configuration's Python version, run:
39+
`bazel config <config id>` (shown further below)
40+
and look for
41+
{rules_python}//python/config_settings:python_version
42+
43+
If the value is missing, then the "default" Python version is being used,
44+
which has a "null" version value and will not match version constraints.
45+
"""
46+
47+
def _no_match_error(actual):
48+
if type(actual) != type({}):
49+
return None
50+
51+
if "//conditions:default" in actual:
52+
return None
53+
54+
return _NO_MATCH_ERROR_TEMPLATE.format(
55+
config_settings = render.indent(
56+
"\n".join(sorted(actual.keys())),
57+
).lstrip(),
58+
rules_python = "rules_python",
59+
)
60+
61+
def pkg_aliases(
62+
*,
63+
name,
64+
actual,
65+
group_name = None,
66+
extra_aliases = None,
67+
native = native,
68+
select = select):
69+
"""Create aliases for an actual package.
70+
71+
Args:
72+
name: {type}`str` The name of the package.
73+
actual: {type}`dict[Label, str] | str` The name of the repo the aliases point to, or a dict of select conditions to repo names for the aliases to point to
74+
mapping to repositories.
75+
group_name: {type}`str` The group name that the pkg belongs to.
76+
extra_aliases: {type}`list[str]` The extra aliases to be created.
77+
native: {type}`struct` used in unit tests.
78+
select: {type}`select` used in unit tests.
79+
"""
80+
native.alias(
81+
name = name,
82+
actual = ":" + PY_LIBRARY_PUBLIC_LABEL,
83+
)
84+
85+
target_names = {
86+
PY_LIBRARY_PUBLIC_LABEL: PY_LIBRARY_IMPL_LABEL if group_name else PY_LIBRARY_PUBLIC_LABEL,
87+
WHEEL_FILE_PUBLIC_LABEL: WHEEL_FILE_IMPL_LABEL if group_name else WHEEL_FILE_PUBLIC_LABEL,
88+
DATA_LABEL: DATA_LABEL,
89+
DIST_INFO_LABEL: DIST_INFO_LABEL,
90+
} | {
91+
x: x
92+
for x in extra_aliases or []
93+
}
94+
no_match_error = _no_match_error(actual)
95+
96+
for name, target_name in target_names.items():
97+
if type(actual) == type(""):
98+
_actual = "@{repo}//:{target_name}".format(
99+
repo = actual,
100+
target_name = name,
101+
)
102+
elif type(actual) == type({}):
103+
_actual = select(
104+
{
105+
config_setting: "@{repo}//:{target_name}".format(
106+
repo = repo,
107+
target_name = name,
108+
)
109+
for config_setting, repo in actual.items()
110+
},
111+
no_match_error = no_match_error,
112+
)
113+
else:
114+
fail("The `actual` arg must be a dictionary or a string")
115+
116+
kwargs = {}
117+
if target_name.startswith("_"):
118+
kwargs["visibility"] = ["//_groups:__subpackages__"]
119+
120+
native.alias(
121+
name = target_name,
122+
actual = _actual,
123+
**kwargs
124+
)
125+
126+
if group_name:
127+
native.alias(
128+
name = PY_LIBRARY_PUBLIC_LABEL,
129+
actual = "//_groups:{}_pkg".format(group_name),
130+
)
131+
native.alias(
132+
name = WHEEL_FILE_PUBLIC_LABEL,
133+
actual = "//_groups:{}_whl".format(group_name),
134+
)

python/private/pypi/render_pkg_aliases.bzl

+23-121
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,6 @@ load(
2222
":generate_group_library_build_bazel.bzl",
2323
"generate_group_library_build_bazel",
2424
) # buildifier: disable=bzl-visibility
25-
load(
26-
":labels.bzl",
27-
"DATA_LABEL",
28-
"DIST_INFO_LABEL",
29-
"PY_LIBRARY_IMPL_LABEL",
30-
"PY_LIBRARY_PUBLIC_LABEL",
31-
"WHEEL_FILE_IMPL_LABEL",
32-
"WHEEL_FILE_PUBLIC_LABEL",
33-
)
3425
load(":parse_whl_name.bzl", "parse_whl_name")
3526
load(":whl_target_platforms.bzl", "whl_target_platforms")
3627

@@ -70,117 +61,32 @@ If the value is missing, then the "default" Python version is being used,
7061
which has a "null" version value and will not match version constraints.
7162
"""
7263

73-
def _render_whl_library_alias(
74-
*,
75-
name,
76-
aliases,
77-
target_name,
78-
**kwargs):
79-
"""Render an alias for common targets."""
80-
if len(aliases) == 1 and not aliases[0].version:
81-
alias = aliases[0]
82-
return render.alias(
83-
name = name,
84-
actual = repr("@{repo}//:{name}".format(
85-
repo = alias.repo,
86-
name = target_name,
87-
)),
88-
**kwargs
89-
)
90-
91-
# Create the alias repositories which contains different select
92-
# statements These select statements point to the different pip
93-
# whls that are based on a specific version of Python.
94-
selects = {}
95-
no_match_error = "_NO_MATCH_ERROR"
96-
for alias in sorted(aliases, key = lambda x: x.version):
97-
actual = "@{repo}//:{name}".format(repo = alias.repo, name = target_name)
98-
selects.setdefault(actual, []).append(alias.config_setting)
64+
def _repr_actual(aliases):
65+
if len(aliases) == 1 and not aliases[0].version and not aliases[0].config_setting:
66+
return repr(aliases[0].repo)
9967

100-
return render.alias(
101-
name = name,
102-
actual = render.select(
103-
{
104-
tuple(sorted(
105-
conditions,
106-
# Group `is_python` and other conditions for easier reading
107-
# when looking at the generated files.
108-
key = lambda condition: ("is_python" not in condition, condition),
109-
)): target
110-
for target, conditions in sorted(selects.items())
111-
},
112-
no_match_error = no_match_error,
113-
# This key_repr is used to render selects.with_or keys
114-
key_repr = lambda x: repr(x[0]) if len(x) == 1 else render.tuple(x),
115-
name = "selects.with_or",
116-
),
117-
**kwargs
118-
)
68+
actual = {}
69+
for alias in aliases:
70+
actual[alias.config_setting or ("//_config:is_python_" + alias.version)] = alias.repo
71+
return render.indent(render.dict(actual)).lstrip()
11972

12073
def _render_common_aliases(*, name, aliases, extra_aliases = [], group_name = None):
121-
lines = [
122-
"""load("@bazel_skylib//lib:selects.bzl", "selects")""",
123-
"""package(default_visibility = ["//visibility:public"])""",
124-
]
125-
126-
config_settings = None
127-
if aliases:
128-
config_settings = sorted([v.config_setting for v in aliases if v.config_setting])
129-
130-
if config_settings:
131-
error_msg = NO_MATCH_ERROR_MESSAGE_TEMPLATE_V2.format(
132-
config_settings = render.indent(
133-
"\n".join(config_settings),
134-
).lstrip(),
135-
rules_python = "rules_python",
136-
)
74+
return """\
75+
load("@rules_python//python/private/pypi:pkg_aliases.bzl", "pkg_aliases")
13776
138-
lines.append("_NO_MATCH_ERROR = \"\"\"\\\n{error_msg}\"\"\"".format(
139-
error_msg = error_msg,
140-
))
77+
package(default_visibility = ["//visibility:public"])
14178
142-
lines.append(
143-
render.alias(
144-
name = name,
145-
actual = repr(":pkg"),
146-
),
147-
)
148-
lines.extend(
149-
[
150-
_render_whl_library_alias(
151-
name = name,
152-
aliases = aliases,
153-
target_name = target_name,
154-
visibility = ["//_groups:__subpackages__"] if name.startswith("_") else None,
155-
)
156-
for target_name, name in (
157-
{
158-
PY_LIBRARY_PUBLIC_LABEL: PY_LIBRARY_IMPL_LABEL if group_name else PY_LIBRARY_PUBLIC_LABEL,
159-
WHEEL_FILE_PUBLIC_LABEL: WHEEL_FILE_IMPL_LABEL if group_name else WHEEL_FILE_PUBLIC_LABEL,
160-
DATA_LABEL: DATA_LABEL,
161-
DIST_INFO_LABEL: DIST_INFO_LABEL,
162-
} | {
163-
x: x
164-
for x in extra_aliases
165-
}
166-
).items()
167-
],
79+
pkg_aliases(
80+
name = "{name}",
81+
actual = {actual},
82+
group_name = {group_name},
83+
extra_aliases = {extra_aliases},
84+
)""".format(
85+
name = name,
86+
actual = _repr_actual(aliases),
87+
group_name = repr(group_name),
88+
extra_aliases = repr(extra_aliases),
16889
)
169-
if group_name:
170-
lines.extend(
171-
[
172-
render.alias(
173-
name = "pkg",
174-
actual = repr("//_groups:{}_pkg".format(group_name)),
175-
),
176-
render.alias(
177-
name = "whl",
178-
actual = repr("//_groups:{}_whl".format(group_name)),
179-
),
180-
],
181-
)
182-
183-
return "\n\n".join(lines)
18490

18591
def render_pkg_aliases(*, aliases, requirement_cycles = None, extra_hub_aliases = {}):
18692
"""Create alias declarations for each PyPI package.
@@ -222,7 +128,7 @@ def render_pkg_aliases(*, aliases, requirement_cycles = None, extra_hub_aliases
222128
"{}/BUILD.bazel".format(normalize_name(name)): _render_common_aliases(
223129
name = normalize_name(name),
224130
aliases = pkg_aliases,
225-
extra_aliases = extra_hub_aliases.get(name, []),
131+
extra_aliases = extra_hub_aliases.get(normalize_name(name), []),
226132
group_name = whl_group_mapping.get(normalize_name(name)),
227133
).strip()
228134
for name, pkg_aliases in aliases.items()
@@ -256,21 +162,17 @@ def whl_alias(*, repo, version = None, config_setting = None, filename = None, t
256162
if not repo:
257163
fail("'repo' must be specified")
258164

259-
if version:
260-
config_setting = config_setting or ("//_config:is_python_" + version)
261-
config_setting = str(config_setting)
262-
263165
if target_platforms:
264166
for p in target_platforms:
265167
if not p.startswith("cp"):
266168
fail("target_platform should start with 'cp' denoting the python version, got: " + p)
267169

268170
return struct(
269-
repo = repo,
270-
version = version,
271171
config_setting = config_setting,
272172
filename = filename,
173+
repo = repo,
273174
target_platforms = target_platforms,
175+
version = version,
274176
)
275177

276178
def render_multiplatform_pkg_aliases(*, aliases, **kwargs):

0 commit comments

Comments
 (0)