refactor: support rendering pkg aliases without whl_library_alias (#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 #1294 and work towards #1262 with ideas taken from #1320.
diff --git a/python/pip.bzl b/python/pip.bzl
index 708cd6b..0c6e90f 100644
--- a/python/pip.bzl
+++ b/python/pip.bzl
@@ -17,30 +17,12 @@
load("//python/pip_install:repositories.bzl", "pip_install_dependencies")
load("//python/pip_install:requirements.bzl", _compile_pip_requirements = "compile_pip_requirements")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
+load("//python/private:render_pkg_aliases.bzl", "NO_MATCH_ERROR_MESSAGE_TEMPLATE")
load(":versions.bzl", "MINOR_MAPPING")
compile_pip_requirements = _compile_pip_requirements
package_annotation = _package_annotation
-_NO_MATCH_ERROR_MESSAGE_TEMPLATE = """\
-No matching wheel for current configuration's Python version.
-
-The current build configuration's Python version doesn't match any of the Python
-versions available for this wheel. This wheel supports the following Python versions:
- {supported_versions}
-
-As matched by the `@{rules_python}//python/config_settings:is_python_<version>`
-configuration settings.
-
-To determine the current configuration's Python version, run:
- `bazel config <config id>` (shown further below)
-and look for
- {rules_python}//python/config_settings:python_version
-
-If the value is missing, then the "default" Python version is being used,
-which has a "null" version value and will not match version constraints.
-"""
-
def pip_install(requirements = None, name = "pip", **kwargs):
"""Accepts a locked/compiled requirements file and installs the dependencies listed within.
@@ -335,7 +317,7 @@
if not default_repo_prefix:
supported_versions = sorted([python_version for python_version, _ in version_map])
alias.append(' no_match_error="""{}""",'.format(
- _NO_MATCH_ERROR_MESSAGE_TEMPLATE.format(
+ NO_MATCH_ERROR_MESSAGE_TEMPLATE.format(
supported_versions = ", ".join(supported_versions),
rules_python = rules_python,
),
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index 1f392ee..d4ccd43 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -22,6 +22,7 @@
load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:render_pkg_aliases.bzl", "render_pkg_aliases")
load("//python/private:toolchains_repo.bzl", "get_host_os_arch")
CPPFLAGS = "CPPFLAGS"
@@ -271,56 +272,12 @@
""")
return requirements_txt
-def _pkg_aliases(rctx, repo_name, bzl_packages):
- """Create alias declarations for each python dependency.
-
- The aliases should be appended to the pip_repository BUILD.bazel file. These aliases
- allow users to use requirement() without needed a corresponding `use_repo()` for each dep
- when using bzlmod.
-
- Args:
- rctx: the repository context.
- repo_name: the repository name of the parent that is visible to the users.
- bzl_packages: the list of packages to setup.
- """
- for name in bzl_packages:
- build_content = """package(default_visibility = ["//visibility:public"])
-
-alias(
- name = "{name}",
- actual = "@{repo_name}_{dep}//:pkg",
-)
-
-alias(
- name = "pkg",
- actual = "@{repo_name}_{dep}//:pkg",
-)
-
-alias(
- name = "whl",
- actual = "@{repo_name}_{dep}//:whl",
-)
-
-alias(
- name = "data",
- actual = "@{repo_name}_{dep}//:data",
-)
-
-alias(
- name = "dist_info",
- actual = "@{repo_name}_{dep}//:dist_info",
-)
-""".format(
- name = name,
- repo_name = repo_name,
- dep = name,
- )
- rctx.file("{}/BUILD.bazel".format(name), build_content)
-
def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements):
repo_name = rctx.attr.repo_name
build_contents = _BUILD_FILE_CONTENTS
- _pkg_aliases(rctx, repo_name, bzl_packages)
+ aliases = render_pkg_aliases(repo_name = repo_name, bzl_packages = bzl_packages)
+ for path, contents in aliases.items():
+ rctx.file(path, contents)
# NOTE: we are using the canonical name with the double '@' in order to
# always uniquely identify a repository, as the labels are being passed as
@@ -461,7 +418,9 @@
config["python_interpreter_target"] = str(rctx.attr.python_interpreter_target)
if rctx.attr.incompatible_generate_aliases:
- _pkg_aliases(rctx, rctx.attr.name, bzl_packages)
+ aliases = render_pkg_aliases(repo_name = rctx.attr.name, bzl_packages = bzl_packages)
+ for path, contents in aliases.items():
+ rctx.file(path, contents)
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
rctx.template("requirements.bzl", rctx.attr._template, substitutions = {
diff --git a/python/private/render_pkg_aliases.bzl b/python/private/render_pkg_aliases.bzl
new file mode 100644
index 0000000..bcbfc8c
--- /dev/null
+++ b/python/private/render_pkg_aliases.bzl
@@ -0,0 +1,182 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""render_pkg_aliases is a function to generate BUILD.bazel contents used to create user-friendly aliases.
+
+This is used in bzlmod and non-bzlmod setups."""
+
+load("//python/private:normalize_name.bzl", "normalize_name")
+load(":text_util.bzl", "render")
+load(":version_label.bzl", "version_label")
+
+NO_MATCH_ERROR_MESSAGE_TEMPLATE = """\
+No matching wheel for current configuration's Python version.
+
+The current build configuration's Python version doesn't match any of the Python
+versions available for this wheel. This wheel supports the following Python versions:
+ {supported_versions}
+
+As matched by the `@{rules_python}//python/config_settings:is_python_<version>`
+configuration settings.
+
+To determine the current configuration's Python version, run:
+ `bazel config <config id>` (shown further below)
+and look for
+ {rules_python}//python/config_settings:python_version
+
+If the value is missing, then the "default" Python version is being used,
+which has a "null" version value and will not match version constraints.
+"""
+
+def _render_whl_library_alias(
+ *,
+ name,
+ repo_name,
+ dep,
+ target,
+ default_version,
+ versions,
+ rules_python):
+ """Render an alias for common targets
+
+ If the versions is passed, then the `rules_python` must be passed as well and
+ an alias with a select statement based on the python version is going to be
+ generated.
+ """
+ if versions == None:
+ return render.alias(
+ name = name,
+ actual = repr("@{repo_name}_{dep}//:{target}".format(
+ repo_name = repo_name,
+ dep = dep,
+ target = target,
+ )),
+ )
+
+ # Create the alias repositories which contains different select
+ # statements These select statements point to the different pip
+ # whls that are based on a specific version of Python.
+ selects = {}
+ for full_version in versions:
+ condition = "@@{rules_python}//python/config_settings:is_python_{full_python_version}".format(
+ rules_python = rules_python,
+ full_python_version = full_version,
+ )
+ actual = "@{repo_name}_{version}_{dep}//:{target}".format(
+ repo_name = repo_name,
+ version = version_label(full_version),
+ dep = dep,
+ target = target,
+ )
+ selects[condition] = actual
+
+ if default_version:
+ no_match_error = None
+ default_actual = "@{repo_name}_{version}_{dep}//:{target}".format(
+ repo_name = repo_name,
+ version = version_label(default_version),
+ dep = dep,
+ target = target,
+ )
+ selects["//conditions:default"] = default_actual
+ else:
+ no_match_error = "_NO_MATCH_ERROR"
+
+ return render.alias(
+ name = name,
+ actual = render.select(
+ selects,
+ no_match_error = no_match_error,
+ ),
+ )
+
+def _render_common_aliases(repo_name, name, versions = None, default_version = None, rules_python = None):
+ lines = [
+ """package(default_visibility = ["//visibility:public"])""",
+ ]
+
+ if versions:
+ versions = sorted(versions)
+
+ if versions and not default_version:
+ error_msg = NO_MATCH_ERROR_MESSAGE_TEMPLATE.format(
+ supported_versions = ", ".join(versions),
+ rules_python = rules_python,
+ )
+
+ lines.append("_NO_MATCH_ERROR = \"\"\"\\\n{error_msg}\"\"\"".format(
+ error_msg = error_msg,
+ ))
+
+ lines.append(
+ render.alias(
+ name = name,
+ actual = repr(":pkg"),
+ ),
+ )
+ lines.extend(
+ [
+ _render_whl_library_alias(
+ name = target,
+ repo_name = repo_name,
+ dep = name,
+ target = target,
+ versions = versions,
+ default_version = default_version,
+ rules_python = rules_python,
+ )
+ for target in ["pkg", "whl", "data", "dist_info"]
+ ],
+ )
+
+ return "\n\n".join(lines)
+
+def render_pkg_aliases(*, repo_name, bzl_packages = None, whl_map = None, rules_python = None, default_version = None):
+ """Create alias declarations for each PyPI package.
+
+ The aliases should be appended to the pip_repository BUILD.bazel file. These aliases
+ allow users to use requirement() without needed a corresponding `use_repo()` for each dep
+ when using bzlmod.
+
+ Args:
+ repo_name: the repository name of the hub repository that is visible to the users that is
+ also used as the prefix for the spoke repo names (e.g. "pip", "pypi").
+ bzl_packages: the list of packages to setup, if not specified, whl_map.keys() will be used instead.
+ whl_map: the whl_map for generating Python version aware aliases.
+ default_version: the default version to be used for the aliases.
+ rules_python: the name of the rules_python workspace.
+
+ Returns:
+ A dict of file paths and their contents.
+ """
+ if not bzl_packages and whl_map:
+ bzl_packages = list(whl_map.keys())
+
+ contents = {}
+ for name in bzl_packages:
+ versions = None
+ if whl_map != None:
+ versions = whl_map[name]
+ name = normalize_name(name)
+
+ filename = "{}/BUILD.bazel".format(name)
+ contents[filename] = _render_common_aliases(
+ repo_name = repo_name,
+ name = name,
+ versions = versions,
+ rules_python = rules_python,
+ default_version = default_version,
+ ).strip()
+
+ return contents
diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl
new file mode 100644
index 0000000..3d72b8d
--- /dev/null
+++ b/python/private/text_util.bzl
@@ -0,0 +1,65 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Text manipulation utilities useful for repository rule writing."""
+
+def _indent(text, indent = " " * 4):
+ if "\n" not in text:
+ return indent + text
+
+ return "\n".join([indent + line for line in text.splitlines()])
+
+def _render_alias(name, actual):
+ return "\n".join([
+ "alias(",
+ _indent("name = \"{}\",".format(name)),
+ _indent("actual = {},".format(actual)),
+ ")",
+ ])
+
+def _render_dict(d):
+ return "\n".join([
+ "{",
+ _indent("\n".join([
+ "{}: {},".format(repr(k), repr(v))
+ for k, v in d.items()
+ ])),
+ "}",
+ ])
+
+def _render_select(selects, *, no_match_error = None):
+ dict_str = _render_dict(selects) + ","
+
+ if no_match_error:
+ args = "\n".join([
+ "",
+ _indent(dict_str),
+ _indent("no_match_error = {},".format(no_match_error)),
+ "",
+ ])
+ else:
+ args = "\n".join([
+ "",
+ _indent(dict_str),
+ "",
+ ])
+
+ return "select({})".format(args)
+
+render = struct(
+ indent = _indent,
+ alias = _render_alias,
+ dict = _render_dict,
+ select = _render_select,
+)
diff --git a/tests/pip_hub_repository/render_pkg_aliases/BUILD.bazel b/tests/pip_hub_repository/render_pkg_aliases/BUILD.bazel
new file mode 100644
index 0000000..f2e0126
--- /dev/null
+++ b/tests/pip_hub_repository/render_pkg_aliases/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":render_pkg_aliases_test.bzl", "render_pkg_aliases_test_suite")
+
+render_pkg_aliases_test_suite(name = "render_pkg_aliases_tests")
diff --git a/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl b/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl
new file mode 100644
index 0000000..28d95ff
--- /dev/null
+++ b/tests/pip_hub_repository/render_pkg_aliases/render_pkg_aliases_test.bzl
@@ -0,0 +1,251 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""render_pkg_aliases tests"""
+
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/private:render_pkg_aliases.bzl", "render_pkg_aliases") # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_legacy_aliases(env):
+ actual = render_pkg_aliases(
+ bzl_packages = ["foo"],
+ repo_name = "pypi",
+ )
+
+ want = {
+ "foo/BUILD.bazel": """\
+package(default_visibility = ["//visibility:public"])
+
+alias(
+ name = "foo",
+ actual = ":pkg",
+)
+
+alias(
+ name = "pkg",
+ actual = "@pypi_foo//:pkg",
+)
+
+alias(
+ name = "whl",
+ actual = "@pypi_foo//:whl",
+)
+
+alias(
+ name = "data",
+ actual = "@pypi_foo//:data",
+)
+
+alias(
+ name = "dist_info",
+ actual = "@pypi_foo//:dist_info",
+)""",
+ }
+
+ env.expect.that_dict(actual).contains_exactly(want)
+
+_tests.append(_test_legacy_aliases)
+
+def _test_all_legacy_aliases_are_created(env):
+ actual = render_pkg_aliases(
+ bzl_packages = ["foo", "bar"],
+ repo_name = "pypi",
+ )
+
+ want_files = ["bar/BUILD.bazel", "foo/BUILD.bazel"]
+
+ env.expect.that_dict(actual).keys().contains_exactly(want_files)
+
+_tests.append(_test_all_legacy_aliases_are_created)
+
+def _test_bzlmod_aliases(env):
+ actual = render_pkg_aliases(
+ default_version = "3.2.3",
+ repo_name = "pypi",
+ rules_python = "rules_python",
+ whl_map = {
+ "bar-baz": ["3.2.3"],
+ },
+ )
+
+ want = {
+ "bar_baz/BUILD.bazel": """\
+package(default_visibility = ["//visibility:public"])
+
+alias(
+ name = "bar_baz",
+ actual = ":pkg",
+)
+
+alias(
+ name = "pkg",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:pkg",
+ "//conditions:default": "@pypi_32_bar_baz//:pkg",
+ },
+ ),
+)
+
+alias(
+ name = "whl",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:whl",
+ "//conditions:default": "@pypi_32_bar_baz//:whl",
+ },
+ ),
+)
+
+alias(
+ name = "data",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:data",
+ "//conditions:default": "@pypi_32_bar_baz//:data",
+ },
+ ),
+)
+
+alias(
+ name = "dist_info",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:dist_info",
+ "//conditions:default": "@pypi_32_bar_baz//:dist_info",
+ },
+ ),
+)""",
+ }
+
+ env.expect.that_dict(actual).contains_exactly(want)
+
+_tests.append(_test_bzlmod_aliases)
+
+def _test_bzlmod_aliases_with_no_default_version(env):
+ actual = render_pkg_aliases(
+ default_version = None,
+ repo_name = "pypi",
+ rules_python = "rules_python",
+ whl_map = {
+ "bar-baz": ["3.2.3", "3.1.3"],
+ },
+ )
+
+ want_key = "bar_baz/BUILD.bazel"
+ want_content = """\
+package(default_visibility = ["//visibility:public"])
+
+_NO_MATCH_ERROR = \"\"\"\\
+No matching wheel for current configuration's Python version.
+
+The current build configuration's Python version doesn't match any of the Python
+versions available for this wheel. This wheel supports the following Python versions:
+ 3.1.3, 3.2.3
+
+As matched by the `@rules_python//python/config_settings:is_python_<version>`
+configuration settings.
+
+To determine the current configuration's Python version, run:
+ `bazel config <config id>` (shown further below)
+and look for
+ rules_python//python/config_settings:python_version
+
+If the value is missing, then the "default" Python version is being used,
+which has a "null" version value and will not match version constraints.
+\"\"\"
+
+alias(
+ name = "bar_baz",
+ actual = ":pkg",
+)
+
+alias(
+ name = "pkg",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:pkg",
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:pkg",
+ },
+ no_match_error = _NO_MATCH_ERROR,
+ ),
+)
+
+alias(
+ name = "whl",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:whl",
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:whl",
+ },
+ no_match_error = _NO_MATCH_ERROR,
+ ),
+)
+
+alias(
+ name = "data",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:data",
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:data",
+ },
+ no_match_error = _NO_MATCH_ERROR,
+ ),
+)
+
+alias(
+ name = "dist_info",
+ actual = select(
+ {
+ "@@rules_python//python/config_settings:is_python_3.1.3": "@pypi_31_bar_baz//:dist_info",
+ "@@rules_python//python/config_settings:is_python_3.2.3": "@pypi_32_bar_baz//:dist_info",
+ },
+ no_match_error = _NO_MATCH_ERROR,
+ ),
+)"""
+
+ env.expect.that_collection(actual.keys()).contains_exactly([want_key])
+ env.expect.that_str(actual[want_key]).equals(want_content)
+
+_tests.append(_test_bzlmod_aliases_with_no_default_version)
+
+def _test_bzlmod_aliases_are_created_for_all_wheels(env):
+ actual = render_pkg_aliases(
+ default_version = "3.2.3",
+ repo_name = "pypi",
+ rules_python = "rules_python",
+ whl_map = {
+ "bar": ["3.1.2", "3.2.3"],
+ "foo": ["3.1.2", "3.2.3"],
+ },
+ )
+
+ want_files = [
+ "bar/BUILD.bazel",
+ "foo/BUILD.bazel",
+ ]
+
+ env.expect.that_dict(actual).keys().contains_exactly(want_files)
+
+_tests.append(_test_bzlmod_aliases_are_created_for_all_wheels)
+
+def render_pkg_aliases_test_suite(name):
+ """Create the test suite.
+
+ Args:
+ name: the name of the test suite
+ """
+ test_suite(name = name, basic_tests = _tests)