Skip to content

Commit fabb65f

Browse files
authored
refactor: support rendering pkg aliases without whl_library_alias (bazel-contrib#1346)
Before this PR the only way to render aliases for PyPI package repos using the version-aware toolchain was to use the `whl_library_alias` repo. However, we have code that is creating aliases for packages within the hub repo and it is natural to merge the two approaches to keep the number of layers of indirection to minimum. - feat: support alias rendering for python aware toolchain targets. - refactor: use render_pkg_aliases everywhere. - refactor: move the function to a private `.bzl` file. - test: add unit tests for rendering of the aliases. Split from bazel-contrib#1294 and work towards bazel-contrib#1262 with ideas taken from bazel-contrib#1320.
1 parent c99aaec commit fabb65f

File tree

6 files changed

+510
-68
lines changed

6 files changed

+510
-68
lines changed

python/pip.bzl

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,12 @@ load("//python/pip_install:pip_repository.bzl", "pip_repository", _package_annot
1717
load("//python/pip_install:repositories.bzl", "pip_install_dependencies")
1818
load("//python/pip_install:requirements.bzl", _compile_pip_requirements = "compile_pip_requirements")
1919
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
20+
load("//python/private:render_pkg_aliases.bzl", "NO_MATCH_ERROR_MESSAGE_TEMPLATE")
2021
load(":versions.bzl", "MINOR_MAPPING")
2122

2223
compile_pip_requirements = _compile_pip_requirements
2324
package_annotation = _package_annotation
2425

25-
_NO_MATCH_ERROR_MESSAGE_TEMPLATE = """\
26-
No matching wheel for current configuration's Python version.
27-
28-
The current build configuration's Python version doesn't match any of the Python
29-
versions available for this wheel. This wheel supports the following Python versions:
30-
{supported_versions}
31-
32-
As matched by the `@{rules_python}//python/config_settings:is_python_<version>`
33-
configuration settings.
34-
35-
To determine the current configuration's Python version, run:
36-
`bazel config <config id>` (shown further below)
37-
and look for
38-
{rules_python}//python/config_settings:python_version
39-
40-
If the value is missing, then the "default" Python version is being used,
41-
which has a "null" version value and will not match version constraints.
42-
"""
43-
4426
def pip_install(requirements = None, name = "pip", **kwargs):
4527
"""Accepts a locked/compiled requirements file and installs the dependencies listed within.
4628
@@ -335,7 +317,7 @@ alias(
335317
if not default_repo_prefix:
336318
supported_versions = sorted([python_version for python_version, _ in version_map])
337319
alias.append(' no_match_error="""{}""",'.format(
338-
_NO_MATCH_ERROR_MESSAGE_TEMPLATE.format(
320+
NO_MATCH_ERROR_MESSAGE_TEMPLATE.format(
339321
supported_versions = ", ".join(supported_versions),
340322
rules_python = rules_python,
341323
),

python/pip_install/pip_repository.bzl

Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "gener
2222
load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
2323
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
2424
load("//python/private:normalize_name.bzl", "normalize_name")
25+
load("//python/private:render_pkg_aliases.bzl", "render_pkg_aliases")
2526
load("//python/private:toolchains_repo.bzl", "get_host_os_arch")
2627

2728
CPPFLAGS = "CPPFLAGS"
@@ -271,56 +272,12 @@ A requirements_lock attribute must be specified, or a platform-specific lockfile
271272
""")
272273
return requirements_txt
273274

274-
def _pkg_aliases(rctx, repo_name, bzl_packages):
275-
"""Create alias declarations for each python dependency.
276-
277-
The aliases should be appended to the pip_repository BUILD.bazel file. These aliases
278-
allow users to use requirement() without needed a corresponding `use_repo()` for each dep
279-
when using bzlmod.
280-
281-
Args:
282-
rctx: the repository context.
283-
repo_name: the repository name of the parent that is visible to the users.
284-
bzl_packages: the list of packages to setup.
285-
"""
286-
for name in bzl_packages:
287-
build_content = """package(default_visibility = ["//visibility:public"])
288-
289-
alias(
290-
name = "{name}",
291-
actual = "@{repo_name}_{dep}//:pkg",
292-
)
293-
294-
alias(
295-
name = "pkg",
296-
actual = "@{repo_name}_{dep}//:pkg",
297-
)
298-
299-
alias(
300-
name = "whl",
301-
actual = "@{repo_name}_{dep}//:whl",
302-
)
303-
304-
alias(
305-
name = "data",
306-
actual = "@{repo_name}_{dep}//:data",
307-
)
308-
309-
alias(
310-
name = "dist_info",
311-
actual = "@{repo_name}_{dep}//:dist_info",
312-
)
313-
""".format(
314-
name = name,
315-
repo_name = repo_name,
316-
dep = name,
317-
)
318-
rctx.file("{}/BUILD.bazel".format(name), build_content)
319-
320275
def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements):
321276
repo_name = rctx.attr.repo_name
322277
build_contents = _BUILD_FILE_CONTENTS
323-
_pkg_aliases(rctx, repo_name, bzl_packages)
278+
aliases = render_pkg_aliases(repo_name = repo_name, bzl_packages = bzl_packages)
279+
for path, contents in aliases.items():
280+
rctx.file(path, contents)
324281

325282
# NOTE: we are using the canonical name with the double '@' in order to
326283
# always uniquely identify a repository, as the labels are being passed as
@@ -461,7 +418,9 @@ def _pip_repository_impl(rctx):
461418
config["python_interpreter_target"] = str(rctx.attr.python_interpreter_target)
462419

463420
if rctx.attr.incompatible_generate_aliases:
464-
_pkg_aliases(rctx, rctx.attr.name, bzl_packages)
421+
aliases = render_pkg_aliases(repo_name = rctx.attr.name, bzl_packages = bzl_packages)
422+
for path, contents in aliases.items():
423+
rctx.file(path, contents)
465424

466425
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
467426
rctx.template("requirements.bzl", rctx.attr._template, substitutions = {

python/private/render_pkg_aliases.bzl

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Copyright 2023 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+
"""render_pkg_aliases is a function to generate BUILD.bazel contents used to create user-friendly aliases.
16+
17+
This is used in bzlmod and non-bzlmod setups."""
18+
19+
load("//python/private:normalize_name.bzl", "normalize_name")
20+
load(":text_util.bzl", "render")
21+
load(":version_label.bzl", "version_label")
22+
23+
NO_MATCH_ERROR_MESSAGE_TEMPLATE = """\
24+
No matching wheel for current configuration's Python version.
25+
26+
The current build configuration's Python version doesn't match any of the Python
27+
versions available for this wheel. This wheel supports the following Python versions:
28+
{supported_versions}
29+
30+
As matched by the `@{rules_python}//python/config_settings:is_python_<version>`
31+
configuration settings.
32+
33+
To determine the current configuration's Python version, run:
34+
`bazel config <config id>` (shown further below)
35+
and look for
36+
{rules_python}//python/config_settings:python_version
37+
38+
If the value is missing, then the "default" Python version is being used,
39+
which has a "null" version value and will not match version constraints.
40+
"""
41+
42+
def _render_whl_library_alias(
43+
*,
44+
name,
45+
repo_name,
46+
dep,
47+
target,
48+
default_version,
49+
versions,
50+
rules_python):
51+
"""Render an alias for common targets
52+
53+
If the versions is passed, then the `rules_python` must be passed as well and
54+
an alias with a select statement based on the python version is going to be
55+
generated.
56+
"""
57+
if versions == None:
58+
return render.alias(
59+
name = name,
60+
actual = repr("@{repo_name}_{dep}//:{target}".format(
61+
repo_name = repo_name,
62+
dep = dep,
63+
target = target,
64+
)),
65+
)
66+
67+
# Create the alias repositories which contains different select
68+
# statements These select statements point to the different pip
69+
# whls that are based on a specific version of Python.
70+
selects = {}
71+
for full_version in versions:
72+
condition = "@@{rules_python}//python/config_settings:is_python_{full_python_version}".format(
73+
rules_python = rules_python,
74+
full_python_version = full_version,
75+
)
76+
actual = "@{repo_name}_{version}_{dep}//:{target}".format(
77+
repo_name = repo_name,
78+
version = version_label(full_version),
79+
dep = dep,
80+
target = target,
81+
)
82+
selects[condition] = actual
83+
84+
if default_version:
85+
no_match_error = None
86+
default_actual = "@{repo_name}_{version}_{dep}//:{target}".format(
87+
repo_name = repo_name,
88+
version = version_label(default_version),
89+
dep = dep,
90+
target = target,
91+
)
92+
selects["//conditions:default"] = default_actual
93+
else:
94+
no_match_error = "_NO_MATCH_ERROR"
95+
96+
return render.alias(
97+
name = name,
98+
actual = render.select(
99+
selects,
100+
no_match_error = no_match_error,
101+
),
102+
)
103+
104+
def _render_common_aliases(repo_name, name, versions = None, default_version = None, rules_python = None):
105+
lines = [
106+
"""package(default_visibility = ["//visibility:public"])""",
107+
]
108+
109+
if versions:
110+
versions = sorted(versions)
111+
112+
if versions and not default_version:
113+
error_msg = NO_MATCH_ERROR_MESSAGE_TEMPLATE.format(
114+
supported_versions = ", ".join(versions),
115+
rules_python = rules_python,
116+
)
117+
118+
lines.append("_NO_MATCH_ERROR = \"\"\"\\\n{error_msg}\"\"\"".format(
119+
error_msg = error_msg,
120+
))
121+
122+
lines.append(
123+
render.alias(
124+
name = name,
125+
actual = repr(":pkg"),
126+
),
127+
)
128+
lines.extend(
129+
[
130+
_render_whl_library_alias(
131+
name = target,
132+
repo_name = repo_name,
133+
dep = name,
134+
target = target,
135+
versions = versions,
136+
default_version = default_version,
137+
rules_python = rules_python,
138+
)
139+
for target in ["pkg", "whl", "data", "dist_info"]
140+
],
141+
)
142+
143+
return "\n\n".join(lines)
144+
145+
def render_pkg_aliases(*, repo_name, bzl_packages = None, whl_map = None, rules_python = None, default_version = None):
146+
"""Create alias declarations for each PyPI package.
147+
148+
The aliases should be appended to the pip_repository BUILD.bazel file. These aliases
149+
allow users to use requirement() without needed a corresponding `use_repo()` for each dep
150+
when using bzlmod.
151+
152+
Args:
153+
repo_name: the repository name of the hub repository that is visible to the users that is
154+
also used as the prefix for the spoke repo names (e.g. "pip", "pypi").
155+
bzl_packages: the list of packages to setup, if not specified, whl_map.keys() will be used instead.
156+
whl_map: the whl_map for generating Python version aware aliases.
157+
default_version: the default version to be used for the aliases.
158+
rules_python: the name of the rules_python workspace.
159+
160+
Returns:
161+
A dict of file paths and their contents.
162+
"""
163+
if not bzl_packages and whl_map:
164+
bzl_packages = list(whl_map.keys())
165+
166+
contents = {}
167+
for name in bzl_packages:
168+
versions = None
169+
if whl_map != None:
170+
versions = whl_map[name]
171+
name = normalize_name(name)
172+
173+
filename = "{}/BUILD.bazel".format(name)
174+
contents[filename] = _render_common_aliases(
175+
repo_name = repo_name,
176+
name = name,
177+
versions = versions,
178+
rules_python = rules_python,
179+
default_version = default_version,
180+
).strip()
181+
182+
return contents

python/private/text_util.bzl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright 2023 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+
"""Text manipulation utilities useful for repository rule writing."""
16+
17+
def _indent(text, indent = " " * 4):
18+
if "\n" not in text:
19+
return indent + text
20+
21+
return "\n".join([indent + line for line in text.splitlines()])
22+
23+
def _render_alias(name, actual):
24+
return "\n".join([
25+
"alias(",
26+
_indent("name = \"{}\",".format(name)),
27+
_indent("actual = {},".format(actual)),
28+
")",
29+
])
30+
31+
def _render_dict(d):
32+
return "\n".join([
33+
"{",
34+
_indent("\n".join([
35+
"{}: {},".format(repr(k), repr(v))
36+
for k, v in d.items()
37+
])),
38+
"}",
39+
])
40+
41+
def _render_select(selects, *, no_match_error = None):
42+
dict_str = _render_dict(selects) + ","
43+
44+
if no_match_error:
45+
args = "\n".join([
46+
"",
47+
_indent(dict_str),
48+
_indent("no_match_error = {},".format(no_match_error)),
49+
"",
50+
])
51+
else:
52+
args = "\n".join([
53+
"",
54+
_indent(dict_str),
55+
"",
56+
])
57+
58+
return "select({})".format(args)
59+
60+
render = struct(
61+
indent = _indent,
62+
alias = _render_alias,
63+
dict = _render_dict,
64+
select = _render_select,
65+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
load(":render_pkg_aliases_test.bzl", "render_pkg_aliases_test_suite")
2+
3+
render_pkg_aliases_test_suite(name = "render_pkg_aliases_tests")

0 commit comments

Comments
 (0)