fix(whl_library): fix the dependency generation for multi-python depenency closures (#1875)
We start using the recently introduced `is_python_config_setting` to
make
it possible to have a working select statement when multiple python
version selection needs to happen in a `whl_library`.
This adds further fixes so that the correct dependencies are pulled in
when the
`python_version` string flag is unset thus making this implementation
suitable
for `bzlmod` use case where we would use a single `whl_library` instance
for
multiple python versions within the hub.
Work towards #735.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61e9d95..1040ded 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,10 @@
be automatically deleted correctly. For example, if `python_generation_mode`
is set to package, when `__init__.py` is deleted, the `py_library` generated
for this package before will be deleted automatically.
+* (whl_library): Use `is_python_config_setting` to correctly handle multi-python
+ version dependency select statements when the `experimental_target_platforms`
+ includes the Python ABI. The default python version case within the select is
+ also now handled correctly, stabilizing the implementation.
### Added
* (rules) Precompiling Python source at build time is available. but is
diff --git a/python/pip_install/private/generate_whl_library_build_bazel.bzl b/python/pip_install/private/generate_whl_library_build_bazel.bzl
index 8010ccb..f3ddd3b 100644
--- a/python/pip_install/private/generate_whl_library_build_bazel.bzl
+++ b/python/pip_install/private/generate_whl_library_build_bazel.bzl
@@ -92,12 +92,14 @@
"""
def _plat_label(plat):
+ if plat.endswith("default"):
+ return plat
if plat.startswith("@//"):
return "@@" + str(Label("//:BUILD.bazel")).partition("//")[0].strip("@") + plat.strip("@")
elif plat.startswith("@"):
return str(Label(plat))
else:
- return ":is_" + plat
+ return ":is_" + plat.replace("cp3", "python_3.")
def _render_list_and_select(deps, deps_by_platform, tmpl):
deps = render.list([tmpl.format(d) for d in sorted(deps)])
@@ -115,14 +117,7 @@
# Add the default, which means that we will be just using the dependencies in
# `deps` for platforms that are not handled in a special way by the packages
- #
- # FIXME @aignas 2024-01-24: This currently works as expected only if the default
- # value of the @rules_python//python/config_settings:python_version is set in
- # the `.bazelrc`. If it is unset, then the we don't get the expected behaviour
- # in cases where we are using a simple `py_binary` using the default toolchain
- # without forcing any transitions. If the `python_version` config setting is set
- # via .bazelrc, then everything works correctly.
- deps_by_platform["//conditions:default"] = []
+ deps_by_platform.setdefault("//conditions:default", [])
deps_by_platform = render.select(deps_by_platform, value_repr = render.list)
if deps == "[]":
@@ -131,81 +126,63 @@
return "{} + {}".format(deps, deps_by_platform)
def _render_config_settings(dependencies_by_platform):
- py_version_by_os_arch = {}
+ loads = []
+ additional_content = []
for p in dependencies_by_platform:
# p can be one of the following formats:
+ # * //conditions:default
# * @platforms//os:{value}
# * @platforms//cpu:{value}
# * @//python/config_settings:is_python_3.{minor_version}
# * {os}_{cpu}
# * cp3{minor_version}_{os}_{cpu}
- if p.startswith("@"):
+ if p.startswith("@") or p.endswith("default"):
continue
abi, _, tail = p.partition("_")
if not abi.startswith("cp"):
tail = p
abi = ""
+
os, _, arch = tail.partition("_")
os = "" if os == "anyos" else os
arch = "" if arch == "anyarch" else arch
- py_version_by_os_arch.setdefault((os, arch), []).append(abi)
-
- if not py_version_by_os_arch:
- return None, None
-
- loads = []
- additional_content = []
- for (os, arch), abis in py_version_by_os_arch.items():
constraint_values = []
- if os:
- constraint_values.append("@platforms//os:{}".format(os))
if arch:
constraint_values.append("@platforms//cpu:{}".format(arch))
+ if os:
+ constraint_values.append("@platforms//os:{}".format(os))
- os_arch = (os or "anyos") + "_" + (arch or "anyarch")
- additional_content.append(
- """\
-config_setting(
- name = "is_{name}",
- constraint_values = {values},
- visibility = ["//visibility:private"],
-)""".format(
- name = os_arch,
- values = render.indent(render.list(sorted([str(Label(c)) for c in constraint_values]))).strip(),
- ),
- )
+ constraint_values_str = render.indent(render.list(constraint_values)).lstrip()
- if abis == [""]:
- if not os or not arch:
- fail("BUG: both os and arch should be set in this case")
- continue
-
- for abi in abis:
+ if abi:
if not loads:
- loads.append("""load("@bazel_skylib//lib:selects.bzl", "selects")""")
- minor_version = int(abi[len("cp3"):])
- setting = "@@{rules_python}//python/config_settings:is_python_3.{version}".format(
- rules_python = str(Label("//:BUILD.bazel")).partition("//")[0].strip("@"),
- version = minor_version,
- )
- settings = [
- ":is_" + os_arch,
- setting,
- ]
-
- plat = "{}_{}".format(abi, os_arch)
+ loads.append("""load("@rules_python//python/config_settings:config_settings.bzl", "is_python_config_setting")""")
additional_content.append(
"""\
-selects.config_setting_group(
- name = "{name}",
- match_all = {values},
+is_python_config_setting(
+ name = "is_{name}",
+ python_version = "3.{minor_version}",
+ constraint_values = {constraint_values},
visibility = ["//visibility:private"],
)""".format(
- name = _plat_label(plat).lstrip(":"),
- values = render.indent(render.list(sorted(settings))).strip(),
+ name = p.replace("cp3", "python_3."),
+ minor_version = abi[len("cp3"):],
+ constraint_values = constraint_values_str,
+ ),
+ )
+ else:
+ additional_content.append(
+ """\
+config_setting(
+ name = "is_{name}",
+ constraint_values = {constraint_values},
+ visibility = ["//visibility:private"],
+)""".format(
+ name = p.replace("cp3", "python_3."),
+ constraint_values = constraint_values_str,
),
)
@@ -379,7 +356,7 @@
contents = "\n".join(
[
_BUILD_TEMPLATE.format(
- loads = "\n".join(loads),
+ loads = "\n".join(sorted(loads)),
py_library_label = py_library_label,
dependencies = render.indent(lib_dependencies, " " * 4).lstrip(),
whl_file_deps = render.indent(whl_file_deps, " " * 4).lstrip(),
diff --git a/python/pip_install/tools/wheel_installer/arguments_test.py b/python/pip_install/tools/wheel_installer/arguments_test.py
index cafb85f..fa018da 100644
--- a/python/pip_install/tools/wheel_installer/arguments_test.py
+++ b/python/pip_install/tools/wheel_installer/arguments_test.py
@@ -56,7 +56,6 @@
parser = arguments.parser()
args = parser.parse_args(
args=[
- "--platform=host",
"--platform=linux_*",
"--platform=osx_*",
"--platform=windows_*",
diff --git a/python/pip_install/tools/wheel_installer/wheel.py b/python/pip_install/tools/wheel_installer/wheel.py
index f7c686d..d355bfe 100644
--- a/python/pip_install/tools/wheel_installer/wheel.py
+++ b/python/pip_install/tools/wheel_installer/wheel.py
@@ -59,7 +59,7 @@
ppc64le = ppc
@classmethod
- def interpreter(cls) -> "OS":
+ def interpreter(cls) -> "Arch":
"Return the currently running interpreter architecture."
# FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6
# is returning an empty string here, so lets default to x86_64
@@ -94,12 +94,6 @@
arch: Optional[Arch] = None
minor_version: Optional[int] = None
- def __post_init__(self):
- if not self.os and not self.arch and not self.minor_version:
- raise ValueError(
- "At least one of os, arch, minor_version must be specified"
- )
-
@classmethod
def all(
cls,
@@ -125,7 +119,13 @@
A list of parsed values which makes the signature the same as
`Platform.all` and `Platform.from_string`.
"""
- return [cls(os=OS.interpreter(), arch=Arch.interpreter())]
+ return [
+ Platform(
+ os=OS.interpreter(),
+ arch=Arch.interpreter(),
+ minor_version=host_interpreter_minor_version(),
+ )
+ ]
def all_specializations(self) -> Iterator["Platform"]:
"""Return the platform itself and all its unambiguous specializations.
@@ -160,9 +160,9 @@
def __str__(self) -> str:
if self.minor_version is None:
- assert (
- self.os is not None
- ), f"if minor_version is None, OS must be specified, got {repr(self)}"
+ if self.os is None and self.arch is None:
+ return "//conditions:default"
+
if self.arch is None:
return f"@platforms//os:{self.os}"
else:
@@ -207,6 +207,7 @@
minor_version=minor_version,
)
)
+
else:
ret.update(
cls.all(
@@ -252,6 +253,8 @@
return "Darwin"
elif self.os == OS.windows:
return "Windows"
+ else:
+ return ""
# derived from OS and Arch
@property
@@ -416,7 +419,9 @@
if len(self._target_versions) < 2:
return
- platforms = [Platform(minor_version=v) for v in self._target_versions]
+ platforms = [Platform()] + [
+ Platform(minor_version=v) for v in self._target_versions
+ ]
# If the dep is targeting all target python versions, lets add it to
# the common dependency list to simplify the select statements.
@@ -534,14 +539,22 @@
):
continue
- if match_arch:
+ if match_arch and self._add_version_select:
+ self._add(req.name, plat)
+ if plat.minor_version == host_interpreter_minor_version():
+ self._add(req.name, Platform(plat.os, plat.arch))
+ elif match_arch:
self._add(req.name, plat)
elif match_os and self._add_version_select:
self._add(req.name, Platform(plat.os, minor_version=plat.minor_version))
+ if plat.minor_version == host_interpreter_minor_version():
+ self._add(req.name, Platform(plat.os))
elif match_os:
self._add(req.name, Platform(plat.os))
elif match_version and self._add_version_select:
self._add(req.name, Platform(minor_version=plat.minor_version))
+ if plat.minor_version == host_interpreter_minor_version():
+ self._add(req.name, Platform())
elif match_version:
self._add(req.name, None)
diff --git a/python/pip_install/tools/wheel_installer/wheel_test.py b/python/pip_install/tools/wheel_installer/wheel_test.py
index 20141e2..acf2315 100644
--- a/python/pip_install/tools/wheel_installer/wheel_test.py
+++ b/python/pip_install/tools/wheel_installer/wheel_test.py
@@ -1,5 +1,6 @@
import unittest
from random import shuffle
+from unittest import mock
from python.pip_install.tools.wheel_installer import wheel
@@ -216,22 +217,28 @@
self.assertEqual(["bar"], py38_deps.deps)
self.assertEqual({"@platforms//os:linux": ["posix_dep"]}, py38_deps.deps_select)
- def test_can_get_version_select(self):
+ @mock.patch(
+ "python.pip_install.tools.wheel_installer.wheel.host_interpreter_minor_version"
+ )
+ def test_can_get_version_select(self, mock_host_interpreter_version):
requires_dist = [
"bar",
"baz; python_version < '3.8'",
+ "baz_new; python_version >= '3.8'",
"posix_dep; os_name=='posix'",
"posix_dep_with_version; os_name=='posix' and python_version >= '3.8'",
]
+ mock_host_interpreter_version.return_value = 7
+
+ self.maxDiff = None
deps = wheel.Deps(
"foo",
requires_dist=requires_dist,
platforms=[
- wheel.Platform(
- os=wheel.OS.linux, arch=wheel.Arch.x86_64, minor_version=minor
- )
+ wheel.Platform(os=os, arch=wheel.Arch.x86_64, minor_version=minor)
for minor in [7, 8, 9]
+ for os in [wheel.OS.linux, wheel.OS.windows]
],
)
got = deps.build()
@@ -239,20 +246,38 @@
self.assertEqual(["bar"], got.deps)
self.assertEqual(
{
+ "//conditions:default": ["baz"],
"@//python/config_settings:is_python_3.7": ["baz"],
+ "@//python/config_settings:is_python_3.8": ["baz_new"],
+ "@//python/config_settings:is_python_3.9": ["baz_new"],
+ "@platforms//os:linux": ["baz", "posix_dep"],
"cp37_linux_anyarch": ["baz", "posix_dep"],
- "cp38_linux_anyarch": ["posix_dep", "posix_dep_with_version"],
- "cp39_linux_anyarch": ["posix_dep", "posix_dep_with_version"],
+ "cp38_linux_anyarch": [
+ "baz_new",
+ "posix_dep",
+ "posix_dep_with_version",
+ ],
+ "cp39_linux_anyarch": [
+ "baz_new",
+ "posix_dep",
+ "posix_dep_with_version",
+ ],
},
got.deps_select,
)
- def test_deps_spanning_all_target_py_versions_are_added_to_common(self):
+ @mock.patch(
+ "python.pip_install.tools.wheel_installer.wheel.host_interpreter_minor_version"
+ )
+ def test_deps_spanning_all_target_py_versions_are_added_to_common(
+ self, mock_host_version
+ ):
requires_dist = [
"bar",
"baz (<2,>=1.11) ; python_version < '3.8'",
"baz (<2,>=1.14) ; python_version >= '3.8'",
]
+ mock_host_version.return_value = 8
deps = wheel.Deps(
"foo",
@@ -264,7 +289,12 @@
self.assertEqual(["bar", "baz"], got.deps)
self.assertEqual({}, got.deps_select)
- def test_deps_are_not_duplicated(self):
+ @mock.patch(
+ "python.pip_install.tools.wheel_installer.wheel.host_interpreter_minor_version"
+ )
+ def test_deps_are_not_duplicated(self, mock_host_version):
+ mock_host_version.return_value = 7
+
# See an example in
# https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata
requires_dist = [
@@ -281,14 +311,21 @@
deps = wheel.Deps(
"foo",
requires_dist=requires_dist,
- platforms=wheel.Platform.from_string(["cp310_*"]),
+ platforms=wheel.Platform.from_string(["cp37_*", "cp310_*"]),
)
got = deps.build()
self.assertEqual(["bar"], got.deps)
self.assertEqual({}, got.deps_select)
- def test_deps_are_not_duplicated_when_encountering_platform_dep_first(self):
+ @mock.patch(
+ "python.pip_install.tools.wheel_installer.wheel.host_interpreter_minor_version"
+ )
+ def test_deps_are_not_duplicated_when_encountering_platform_dep_first(
+ self, mock_host_version
+ ):
+ mock_host_version.return_value = 7
+
# Note, that we are sorting the incoming `requires_dist` and we need to ensure that we are not getting any
# issues even if the platform-specific line comes first.
requires_dist = [
@@ -299,7 +336,7 @@
deps = wheel.Deps(
"foo",
requires_dist=requires_dist,
- platforms=wheel.Platform.from_string(["cp310_*"]),
+ platforms=wheel.Platform.from_string(["cp37_*", "cp310_*"]),
)
got = deps.build()
diff --git a/tests/pip_install/whl_library/generate_build_bazel_tests.bzl b/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
index 66126cf..62858af 100644
--- a/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
+++ b/tests/pip_install/whl_library/generate_build_bazel_tests.bzl
@@ -21,8 +21,8 @@
def _test_simple(env):
want = """\
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
package(default_visibility = ["//visibility:public"])
@@ -86,9 +86,9 @@
def _test_dep_selects(env):
want = """\
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-load("@bazel_skylib//lib:selects.bzl", "selects")
+load("@rules_python//python/config_settings:config_settings.bzl", "is_python_config_setting")
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
package(default_visibility = ["//visibility:public"])
@@ -113,9 +113,9 @@
"@//python/config_settings:is_python_3.9": ["@pypi_py39_dep//:whl"],
"@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"],
"@platforms//os:windows": ["@pypi_win_dep//:whl"],
- ":is_cp310_linux_ppc": ["@pypi_py310_linux_ppc_dep//:whl"],
- ":is_cp39_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"],
- ":is_cp39_linux_anyarch": ["@pypi_py39_linux_dep//:whl"],
+ ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:whl"],
+ ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"],
+ ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"],
":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"],
"//conditions:default": [],
},
@@ -147,9 +147,9 @@
"@//python/config_settings:is_python_3.9": ["@pypi_py39_dep//:pkg"],
"@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"],
"@platforms//os:windows": ["@pypi_win_dep//:pkg"],
- ":is_cp310_linux_ppc": ["@pypi_py310_linux_ppc_dep//:pkg"],
- ":is_cp39_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"],
- ":is_cp39_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"],
+ ":is_python_3.10_linux_ppc": ["@pypi_py310_linux_ppc_dep//:pkg"],
+ ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"],
+ ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"],
":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"],
"//conditions:default": [],
},
@@ -158,8 +158,9 @@
visibility = ["//visibility:public"],
)
-config_setting(
- name = "is_linux_ppc",
+is_python_config_setting(
+ name = "is_python_3.10_linux_ppc",
+ python_version = "3.10",
constraint_values = [
"@platforms//cpu:ppc",
"@platforms//os:linux",
@@ -167,45 +168,20 @@
visibility = ["//visibility:private"],
)
-selects.config_setting_group(
- name = "is_cp310_linux_ppc",
- match_all = [
- ":is_linux_ppc",
- "@//python/config_settings:is_python_3.10",
- ],
- visibility = ["//visibility:private"],
-)
-
-config_setting(
- name = "is_anyos_aarch64",
+is_python_config_setting(
+ name = "is_python_3.9_anyos_aarch64",
+ python_version = "3.9",
constraint_values = ["@platforms//cpu:aarch64"],
visibility = ["//visibility:private"],
)
-selects.config_setting_group(
- name = "is_cp39_anyos_aarch64",
- match_all = [
- ":is_anyos_aarch64",
- "@//python/config_settings:is_python_3.9",
- ],
- visibility = ["//visibility:private"],
-)
-
-config_setting(
- name = "is_linux_anyarch",
+is_python_config_setting(
+ name = "is_python_3.9_linux_anyarch",
+ python_version = "3.9",
constraint_values = ["@platforms//os:linux"],
visibility = ["//visibility:private"],
)
-selects.config_setting_group(
- name = "is_cp39_linux_anyarch",
- match_all = [
- ":is_linux_anyarch",
- "@//python/config_settings:is_python_3.9",
- ],
- visibility = ["//visibility:private"],
-)
-
config_setting(
name = "is_linux_x86_64",
constraint_values = [
@@ -239,8 +215,8 @@
def _test_with_annotation(env):
want = """\
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
package(default_visibility = ["//visibility:public"])
@@ -327,8 +303,8 @@
def _test_with_entry_points(env):
want = """\
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
package(default_visibility = ["//visibility:public"])
@@ -401,8 +377,8 @@
def _test_group_member(env):
want = """\
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
package(default_visibility = ["//visibility:public"])
@@ -503,8 +479,8 @@
def _test_group_member_deps_to_hub(env):
want = """\
-load("@rules_python//python:defs.bzl", "py_library", "py_binary")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
+load("@rules_python//python:defs.bzl", "py_library", "py_binary")
package(default_visibility = ["//visibility:public"])