From 9e613d58cecda3f370698f37f7ca26bf38486db3 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:44:32 +0900 Subject: [PATCH 01/23] fix(pypi) backport python_full_version fix to Python (#2833) Handling of `python_full_version` correctly has been fixed in the Starlark implementation in #2793 and in this PR I am backporting the changes to handle the full python version target platform strings so that we can have the same behaviour for now. At the same time I have simplified and got rid of the specialization handling in the Python algorithm just like I did in the starlark, which simplifies the tests and makes the algorithm more correct. Summary: * Handle `cp3x.y_os_arch` strings in the `platform.py` * Produce correct strings when the `micro_version` is unset. Note, that we use version `0` in evaluating but we use the default version in the config setting. This is to keep compatibility with the current behaviour when the target platform is not fully specified (which would be the case for WORKSPACE users). * Adjust the tests and the code to be more similar to the starlark impl. Work towards #2830 --- python/private/pypi/whl_installer/platform.py | 90 ++++---- python/private/pypi/whl_installer/wheel.py | 140 +++-------- tests/pypi/whl_installer/platform_test.py | 73 +----- tests/pypi/whl_installer/wheel_test.py | 218 ++++++++---------- 4 files changed, 185 insertions(+), 336 deletions(-) diff --git a/python/private/pypi/whl_installer/platform.py b/python/private/pypi/whl_installer/platform.py index 11dd6e37..ff267fe4 100644 --- a/python/private/pypi/whl_installer/platform.py +++ b/python/private/pypi/whl_installer/platform.py @@ -18,7 +18,7 @@ import sys from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union class OS(Enum): @@ -77,8 +77,8 @@ def _as_int(value: Optional[Union[OS, Arch]]) -> int: return int(value.value) -def host_interpreter_minor_version() -> int: - return sys.version_info.minor +def host_interpreter_version() -> Tuple[int, int]: + return (sys.version_info.minor, sys.version_info.micro) @dataclass(frozen=True) @@ -86,16 +86,23 @@ class Platform: os: Optional[OS] = None arch: Optional[Arch] = None minor_version: Optional[int] = None + micro_version: Optional[int] = None @classmethod def all( cls, want_os: Optional[OS] = None, minor_version: Optional[int] = None, + micro_version: Optional[int] = None, ) -> List["Platform"]: return sorted( [ - cls(os=os, arch=arch, minor_version=minor_version) + cls( + os=os, + arch=arch, + minor_version=minor_version, + micro_version=micro_version, + ) for os in OS for arch in Arch if not want_os or want_os == os @@ -112,32 +119,16 @@ def host(cls) -> List["Platform"]: A list of parsed values which makes the signature the same as `Platform.all` and `Platform.from_string`. """ + minor, micro = host_interpreter_version() return [ Platform( os=OS.interpreter(), arch=Arch.interpreter(), - minor_version=host_interpreter_minor_version(), + minor_version=minor, + micro_version=micro, ) ] - def all_specializations(self) -> Iterator["Platform"]: - """Return the platform itself and all its unambiguous specializations. - - For more info about specializations see - https://bazel.build/docs/configurable-attributes - """ - yield self - if self.arch is None: - for arch in Arch: - yield Platform(os=self.os, arch=arch, minor_version=self.minor_version) - if self.os is None: - for os in OS: - yield Platform(os=os, arch=self.arch, minor_version=self.minor_version) - if self.arch is None and self.os is None: - for os in OS: - for arch in Arch: - yield Platform(os=os, arch=arch, minor_version=self.minor_version) - def __lt__(self, other: Any) -> bool: """Add a comparison method, so that `sorted` returns the most specialized platforms first.""" if not isinstance(other, Platform) or other is None: @@ -153,24 +144,15 @@ def __lt__(self, other: Any) -> bool: def __str__(self) -> str: if self.minor_version is None: - 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: - return f"{self.os}_{self.arch}" - - if self.arch is None and self.os is None: - return f"@//python/config_settings:is_python_3.{self.minor_version}" + return f"{self.os}_{self.arch}" - if self.arch is None: - return f"cp3{self.minor_version}_{self.os}_anyarch" + minor_version = self.minor_version + micro_version = self.micro_version - if self.os is None: - return f"cp3{self.minor_version}_anyos_{self.arch}" - - return f"cp3{self.minor_version}_{self.os}_{self.arch}" + if micro_version is None: + return f"cp3{minor_version}_{self.os}_{self.arch}" + else: + return f"cp3{minor_version}.{micro_version}_{self.os}_{self.arch}" @classmethod def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: @@ -190,7 +172,17 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: os, _, arch = tail.partition("_") arch = arch or "*" - minor_version = int(abi[len("cp3") :]) if abi else None + if abi: + tail = abi[len("cp3") :] + minor_version, _, micro_version = tail.partition(".") + minor_version = int(minor_version) + if micro_version == "": + micro_version = None + else: + micro_version = int(micro_version) + else: + minor_version = None + micro_version = None if arch != "*": ret.add( @@ -198,6 +190,7 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: os=OS[os] if os != "*" else None, arch=Arch[arch], minor_version=minor_version, + micro_version=micro_version, ) ) @@ -206,6 +199,7 @@ def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]: cls.all( want_os=OS[os] if os != "*" else None, minor_version=minor_version, + micro_version=micro_version, ) ) @@ -282,7 +276,12 @@ def platform_machine(self) -> str: def env_markers(self, extra: str) -> Dict[str, str]: # If it is None, use the host version - minor_version = self.minor_version or host_interpreter_minor_version() + if self.minor_version is None: + minor, micro = host_interpreter_version() + else: + minor, micro = self.minor_version, self.micro_version + + micro = micro or 0 return { "extra": extra, @@ -292,12 +291,9 @@ def env_markers(self, extra: str) -> Dict[str, str]: "platform_system": self.platform_system, "platform_release": "", # unset "platform_version": "", # unset - "python_version": f"3.{minor_version}", - # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should - # use `20` or something else to avoid having weird issues where the full version is used for - # matching and the author decides to only support 3.y.5 upwards. - "implementation_version": f"3.{minor_version}.0", - "python_full_version": f"3.{minor_version}.0", + "python_version": f"3.{minor}", + "implementation_version": f"3.{minor}.{micro}", + "python_full_version": f"3.{minor}.{micro}", # we assume that the following are the same as the interpreter used to setup the deps: # "implementation_name": "cpython" # "platform_python_implementation: "CPython", diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index d95b33a1..fce706ac 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -27,7 +27,7 @@ from python.private.pypi.whl_installer.platform import ( Platform, - host_interpreter_minor_version, + host_interpreter_version, ) @@ -62,12 +62,13 @@ def __init__( """ self.name: str = Deps._normalize(name) self._platforms: Set[Platform] = platforms or set() - self._target_versions = {p.minor_version for p in platforms or {}} - self._default_minor_version = None - if platforms and len(self._target_versions) > 2: + self._target_versions = {(p.minor_version, p.micro_version) for p in platforms or {}} + if platforms and len(self._target_versions) > 1: # TODO @aignas 2024-06-23: enable this to be set via a CLI arg # for being more explicit. - self._default_minor_version = host_interpreter_minor_version() + self._default_minor_version, _ = host_interpreter_version() + else: + self._default_minor_version = None if None in self._target_versions and len(self._target_versions) > 2: raise ValueError( @@ -88,8 +89,13 @@ def __init__( # Then add all of the requirements in order self._deps: Set[str] = set() self._select: Dict[Platform, Set[str]] = defaultdict(set) + + reqs_by_name = {} for req in reqs: - self._add_req(req, want_extras) + reqs_by_name.setdefault(req.name, []).append(req) + + for reqs in reqs_by_name.values(): + self._add_req(reqs, want_extras) def _add(self, dep: str, platform: Optional[Platform]): dep = Deps._normalize(dep) @@ -123,50 +129,6 @@ def _add(self, dep: str, platform: Optional[Platform]): # Add the platform-specific dep self._select[platform].add(dep) - # Add the dep to specializations of the given platform if they - # exist in the select statement. - for p in platform.all_specializations(): - if p not in self._select: - continue - - self._select[p].add(dep) - - if len(self._select[platform]) == 1: - # We are adding a new item to the select and we need to ensure that - # existing dependencies from less specialized platforms are propagated - # to the newly added dependency set. - for p, deps in self._select.items(): - # Check if the existing platform overlaps with the given platform - if p == platform or platform not in p.all_specializations(): - continue - - self._select[platform].update(self._select[p]) - - def _maybe_add_common_dep(self, dep): - if len(self._target_versions) < 2: - return - - 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. - for p in platforms: - if p not in self._select: - return - - if dep not in self._select[p]: - return - - # All of the python version-specific branches have the dep, so lets add - # it to the common deps. - self._deps.add(dep) - for p in platforms: - self._select[p].remove(dep) - if not self._select[p]: - self._select.pop(p) - @staticmethod def _normalize(name: str) -> str: return re.sub(r"[-_.]+", "_", name).lower() @@ -227,66 +189,40 @@ def _resolve_extras( return extras - def _add_req(self, req: Requirement, extras: Set[str]) -> None: - if req.marker is None: - self._add(req.name, None) - return + def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None: + platforms_to_add = set() + for req in reqs: + if req.marker is None: + self._add(req.name, None) + return - marker_str = str(req.marker) + for plat in self._platforms: + if plat in platforms_to_add: + # marker evaluation is more expensive than this check + continue - if not self._platforms: - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) - return + added = False + for extra in extras: + if added: + break - # NOTE @aignas 2023-12-08: in order to have reasonable select statements - # we do have to have some parsing of the markers, so it begs the question - # if packaging should be reimplemented in Starlark to have the best solution - # for now we will implement it in Python and see what the best parsing result - # can be before making this decision. - match_os = any( - tag in marker_str - for tag in [ - "os_name", - "sys_platform", - "platform_system", - ] - ) - match_arch = "platform_machine" in marker_str - match_version = "version" in marker_str + if req.marker.evaluate(plat.env_markers(extra)): + platforms_to_add.add(plat) + added = True + break - if not (match_os or match_arch or match_version): - if any(req.marker.evaluate({"extra": extra}) for extra in extras): - self._add(req.name, None) + if len(platforms_to_add) == len(self._platforms): + # the dep is in all target platforms, let's just add it to the regular + # list + self._add(req.name, None) return - for plat in self._platforms: - if not any( - req.marker.evaluate(plat.env_markers(extra)) for extra in extras - ): - continue - - if match_arch and self._default_minor_version: + for plat in platforms_to_add: + if self._default_minor_version is not None: self._add(req.name, plat) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(plat.os, plat.arch)) - elif match_arch: - self._add(req.name, Platform(plat.os, plat.arch)) - elif match_os and self._default_minor_version: - self._add(req.name, Platform(plat.os, minor_version=plat.minor_version)) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(plat.os)) - elif match_os: - self._add(req.name, Platform(plat.os)) - elif match_version and self._default_minor_version: - self._add(req.name, Platform(minor_version=plat.minor_version)) - if plat.minor_version == self._default_minor_version: - self._add(req.name, Platform()) - elif match_version: - self._add(req.name, None) - # Merge to common if possible after processing all platforms - self._maybe_add_common_dep(req.name) + if self._default_minor_version is None or plat.minor_version == self._default_minor_version: + self._add(req.name, Platform(os = plat.os, arch = plat.arch)) def build(self) -> FrozenDeps: return FrozenDeps( diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py index 2aeb4caa..ad656507 100644 --- a/tests/pypi/whl_installer/platform_test.py +++ b/tests/pypi/whl_installer/platform_test.py @@ -5,13 +5,13 @@ OS, Arch, Platform, - host_interpreter_minor_version, + host_interpreter_version, ) class MinorVersionTest(unittest.TestCase): def test_host(self): - host = host_interpreter_minor_version() + host = host_interpreter_version() self.assertIsNotNone(host) @@ -32,10 +32,14 @@ def test_can_get_specific_from_string(self): want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3) self.assertEqual(want, got[0]) + got = Platform.from_string("cp33.0_linux_x86_64") + want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3, micro_version=0) + self.assertEqual(want, got[0]) + def test_can_get_all_for_py_version(self): - cp39 = Platform.all(minor_version=9) + cp39 = Platform.all(minor_version=9, micro_version=0) self.assertEqual(21, len(cp39), f"Got {cp39}") - self.assertEqual(cp39, Platform.from_string("cp39_*")) + self.assertEqual(cp39, Platform.from_string("cp39.0_*")) def test_can_get_all_for_os(self): linuxes = Platform.all(OS.linux, minor_version=9) @@ -47,67 +51,6 @@ def test_can_get_all_for_os_for_host_python(self): self.assertEqual(7, len(linuxes)) self.assertEqual(linuxes, Platform.from_string("linux_*")) - def test_specific_version_specializations(self): - any_py33 = Platform(minor_version=3) - - # When - all_specializations = list(any_py33.all_specializations()) - - want = ( - [any_py33] - + [ - Platform(arch=arch, minor_version=any_py33.minor_version) - for arch in Arch - ] - + [Platform(os=os, minor_version=any_py33.minor_version) for os in OS] - + Platform.all(minor_version=any_py33.minor_version) - ) - self.assertEqual(want, all_specializations) - - def test_aarch64_specializations(self): - any_aarch64 = Platform(arch=Arch.aarch64) - all_specializations = list(any_aarch64.all_specializations()) - want = [ - Platform(os=None, arch=Arch.aarch64), - Platform(os=OS.linux, arch=Arch.aarch64), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.windows, arch=Arch.aarch64), - ] - self.assertEqual(want, all_specializations) - - def test_linux_specializations(self): - any_linux = Platform(os=OS.linux) - all_specializations = list(any_linux.all_specializations()) - want = [ - Platform(os=OS.linux, arch=None), - Platform(os=OS.linux, arch=Arch.x86_64), - Platform(os=OS.linux, arch=Arch.x86_32), - Platform(os=OS.linux, arch=Arch.aarch64), - Platform(os=OS.linux, arch=Arch.ppc), - Platform(os=OS.linux, arch=Arch.ppc64le), - Platform(os=OS.linux, arch=Arch.s390x), - Platform(os=OS.linux, arch=Arch.arm), - ] - self.assertEqual(want, all_specializations) - - def test_osx_specializations(self): - any_osx = Platform(os=OS.osx) - all_specializations = list(any_osx.all_specializations()) - # NOTE @aignas 2024-01-14: even though in practice we would only have - # Python on osx aarch64 and osx x86_64, we return all arch posibilities - # to make the code simpler. - want = [ - Platform(os=OS.osx, arch=None), - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.x86_32), - Platform(os=OS.osx, arch=Arch.aarch64), - Platform(os=OS.osx, arch=Arch.ppc), - Platform(os=OS.osx, arch=Arch.ppc64le), - Platform(os=OS.osx, arch=Arch.s390x), - Platform(os=OS.osx, arch=Arch.arm), - ] - self.assertEqual(want, all_specializations) - def test_platform_sort(self): platforms = [ Platform(os=OS.linux, arch=None), diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py index 404218e1..6921fe6d 100644 --- a/tests/pypi/whl_installer/wheel_test.py +++ b/tests/pypi/whl_installer/wheel_test.py @@ -5,7 +5,7 @@ from python.private.pypi.whl_installer.platform import OS, Arch, Platform _HOST_INTERPRETER_FN = ( - "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version" + "python.private.pypi.whl_installer.wheel.host_interpreter_version" ) @@ -20,108 +20,56 @@ def test_simple(self): self.assertEqual({}, got.deps_select) def test_can_add_os_specific_deps(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms={ + for platforms in [ + { Platform(os=OS.linux, arch=Arch.x86_64), Platform(os=OS.osx, arch=Arch.x86_64), Platform(os=OS.osx, arch=Arch.aarch64), Platform(os=OS.windows, arch=Arch.x86_64), }, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( { - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }, - got.deps_select, - ) - - def test_can_add_os_specific_deps_with_specific_python_version(self): - deps = wheel.Deps( - "foo", - requires_dist=[ - "bar", - "an_osx_dep; sys_platform=='darwin'", - "posix_dep; os_name=='posix'", - "win_dep; os_name=='nt'", - ], - platforms={ Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8), Platform(os=OS.osx, arch=Arch.aarch64, minor_version=8), Platform(os=OS.windows, arch=Arch.x86_64, minor_version=8), }, - ) - - got = deps.build() - - self.assertEqual(["bar"], got.deps) - self.assertEqual( { - "@platforms//os:linux": ["posix_dep"], - "@platforms//os:osx": ["an_osx_dep", "posix_dep"], - "@platforms//os:windows": ["win_dep"], - }, - got.deps_select, - ) - - def test_deps_are_added_to_more_specialized_platforms(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - "mac_dep; sys_platform=='darwin'", - ], - platforms={ - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), + Platform( + os=OS.linux, arch=Arch.x86_64, minor_version=8, micro_version=1 + ), + Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8, micro_version=1), + Platform( + os=OS.osx, arch=Arch.aarch64, minor_version=8, micro_version=1 + ), + Platform( + os=OS.windows, arch=Arch.x86_64, minor_version=8, micro_version=1 + ), }, - ).build() - - self.assertEqual( - wheel.FrozenDeps( - deps=[], - deps_select={ - "osx_aarch64": ["m1_dep", "mac_dep"], - "@platforms//os:osx": ["mac_dep"], - }, - ), - got, - ) - - def test_deps_from_more_specialized_platforms_are_propagated(self): - got = wheel.Deps( - "foo", - requires_dist=[ - "a_mac_dep; sys_platform=='darwin'", - "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'", - ], - platforms={ - Platform(os=OS.osx, arch=Arch.x86_64), - Platform(os=OS.osx, arch=Arch.aarch64), - }, - ).build() - - self.assertEqual([], got.deps) - self.assertEqual( - { - "osx_aarch64": ["a_mac_dep", "m1_dep"], - "@platforms//os:osx": ["a_mac_dep"], - }, - got.deps_select, - ) + ]: + with self.subTest(): + deps = wheel.Deps( + "foo", + requires_dist=[ + "bar", + "an_osx_dep; sys_platform=='darwin'", + "posix_dep; os_name=='posix'", + "win_dep; os_name=='nt'", + ], + platforms=platforms, + ) + + got = deps.build() + + self.assertEqual(["bar"], got.deps) + self.assertEqual( + { + "linux_x86_64": ["posix_dep"], + "osx_aarch64": ["an_osx_dep", "posix_dep"], + "osx_x86_64": ["an_osx_dep", "posix_dep"], + "windows_x86_64": ["win_dep"], + }, + got.deps_select, + ) def test_non_platform_markers_are_added_to_common_deps(self): got = wheel.Deps( @@ -185,7 +133,7 @@ def test_self_dependencies_can_come_in_any_order(self): def test_can_get_deps_based_on_specific_python_version(self): requires_dist = [ "bar", - "baz; python_version < '3.8'", + "baz; python_full_version < '3.7.3'", "posix_dep; os_name=='posix' and python_version >= '3.8'", ] @@ -196,6 +144,15 @@ def test_can_get_deps_based_on_specific_python_version(self): Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8), ], ).build() + py373_deps = wheel.Deps( + "foo", + requires_dist=requires_dist, + platforms=[ + Platform( + os=OS.linux, arch=Arch.x86_64, minor_version=7, micro_version=3 + ), + ], + ).build() py37_deps = wheel.Deps( "foo", requires_dist=requires_dist, @@ -206,11 +163,12 @@ def test_can_get_deps_based_on_specific_python_version(self): self.assertEqual(["bar", "baz"], py37_deps.deps) self.assertEqual({}, py37_deps.deps_select) - self.assertEqual(["bar"], py38_deps.deps) - self.assertEqual({"@platforms//os:linux": ["posix_dep"]}, py38_deps.deps_select) + self.assertEqual(["bar"], py373_deps.deps) + self.assertEqual({}, py37_deps.deps_select) + self.assertEqual(["bar", "posix_dep"], py38_deps.deps) + self.assertEqual({}, py38_deps.deps_select) - @mock.patch(_HOST_INTERPRETER_FN) - def test_no_version_select_when_single_version(self, mock_host_interpreter_version): + def test_no_version_select_when_single_version(self): requires_dist = [ "bar", "baz; python_version >= '3.8'", @@ -218,7 +176,6 @@ def test_no_version_select_when_single_version(self, mock_host_interpreter_versi "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", "arch_dep; platform_machine=='x86_64' and python_version >= '3.8'", ] - mock_host_interpreter_version.return_value = 7 self.maxDiff = None @@ -226,19 +183,19 @@ def test_no_version_select_when_single_version(self, mock_host_interpreter_versi "foo", requires_dist=requires_dist, platforms=[ - Platform(os=os, arch=Arch.x86_64, minor_version=minor) - for minor in [8] + Platform( + os=os, arch=Arch.x86_64, minor_version=minor, micro_version=micro + ) + for minor, micro in [(8, 4)] for os in [OS.linux, OS.windows] ], ) got = deps.build() - self.assertEqual(["bar", "baz"], got.deps) + self.assertEqual(["arch_dep", "bar", "baz"], got.deps) self.assertEqual( { - "@platforms//os:linux": ["posix_dep", "posix_dep_with_version"], - "linux_x86_64": ["arch_dep", "posix_dep", "posix_dep_with_version"], - "windows_x86_64": ["arch_dep"], + "linux_x86_64": ["posix_dep", "posix_dep_with_version"], }, got.deps_select, ) @@ -253,7 +210,7 @@ def test_can_get_version_select(self, mock_host_interpreter_version): "posix_dep_with_version; os_name=='posix' and python_version >= '3.8'", "arch_dep; platform_machine=='x86_64' and python_version < '3.8'", ] - mock_host_interpreter_version.return_value = 7 + mock_host_interpreter_version.return_value = (7, 4) self.maxDiff = None @@ -261,8 +218,10 @@ def test_can_get_version_select(self, mock_host_interpreter_version): "foo", requires_dist=requires_dist, platforms=[ - Platform(os=os, arch=Arch.x86_64, minor_version=minor) - for minor in [7, 8, 9] + Platform( + os=os, arch=Arch.x86_64, minor_version=minor, micro_version=micro + ) + for minor, micro in [(7, 4), (8, 8), (9, 8)] for os in [OS.linux, OS.windows] ], ) @@ -271,24 +230,20 @@ def test_can_get_version_select(self, mock_host_interpreter_version): 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_x86_64": ["arch_dep", "baz", "posix_dep"], - "cp37_windows_x86_64": ["arch_dep", "baz"], - "cp37_linux_anyarch": ["baz", "posix_dep"], - "cp38_linux_anyarch": [ + "cp37.4_linux_x86_64": ["arch_dep", "baz", "posix_dep"], + "cp37.4_windows_x86_64": ["arch_dep", "baz"], + "cp38.8_linux_x86_64": [ "baz_new", "posix_dep", "posix_dep_with_version", ], - "cp39_linux_anyarch": [ + "cp38.8_windows_x86_64": ["baz_new"], + "cp39.8_linux_x86_64": [ "baz_new", "posix_dep", "posix_dep_with_version", ], + "cp39.8_windows_x86_64": ["baz_new"], "linux_x86_64": ["arch_dep", "baz", "posix_dep"], "windows_x86_64": ["arch_dep", "baz"], }, @@ -304,7 +259,9 @@ def test_deps_spanning_all_target_py_versions_are_added_to_common( "baz (<2,>=1.11) ; python_version < '3.8'", "baz (<2,>=1.14) ; python_version >= '3.8'", ] - mock_host_version.return_value = 8 + mock_host_version.return_value = (8, 4) + + self.maxDiff = None deps = wheel.Deps( "foo", @@ -313,12 +270,12 @@ def test_deps_spanning_all_target_py_versions_are_added_to_common( ) got = deps.build() - self.assertEqual(["bar", "baz"], got.deps) self.assertEqual({}, got.deps_select) + self.assertEqual(["bar", "baz"], got.deps) @mock.patch(_HOST_INTERPRETER_FN) def test_deps_are_not_duplicated(self, mock_host_version): - mock_host_version.return_value = 7 + mock_host_version.return_value = (7, 4) # See an example in # https://files.pythonhosted.org/packages/76/9e/db1c2d56c04b97981c06663384f45f28950a73d9acf840c4006d60d0a1ff/opencv_python-4.9.0.80-cp37-abi3-win32.whl.metadata @@ -347,7 +304,7 @@ def test_deps_are_not_duplicated(self, mock_host_version): def test_deps_are_not_duplicated_when_encountering_platform_dep_first( self, mock_host_version ): - mock_host_version.return_value = 7 + mock_host_version.return_value = (7, 1) # 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. @@ -356,15 +313,32 @@ def test_deps_are_not_duplicated_when_encountering_platform_dep_first( "bar >=0.5.0 ; python_version >= '3.9'", ] + self.maxDiff = None + deps = wheel.Deps( "foo", requires_dist=requires_dist, - platforms=Platform.from_string(["cp37_*", "cp310_*"]), + platforms=Platform.from_string( + [ + "cp37.1_linux_x86_64", + "cp37.1_linux_aarch64", + "cp310_linux_x86_64", + "cp310_linux_aarch64", + ] + ), ) got = deps.build() - self.assertEqual(["bar"], got.deps) - self.assertEqual({}, got.deps_select) + self.assertEqual([], got.deps) + self.assertEqual( + { + "cp310_linux_aarch64": ["bar"], + "cp310_linux_x86_64": ["bar"], + "cp37.1_linux_aarch64": ["bar"], + "linux_aarch64": ["bar"], + }, + got.deps_select, + ) if __name__ == "__main__": From 5b9d545220e5956e0686de91a14e6ded89df651a Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 29 Apr 2025 05:37:37 +0900 Subject: [PATCH 02/23] revert(pypi): use Python for marker eval and METADATA parsing (#2834) Summary: - Revert to using Python for marker evaluation during parsing of requirements (partial revert of #2692). - Use Python to parse whl METADATA. - Bugfix the new simpler algorithm and add a new unit test. Fixes #2830 --- CHANGELOG.md | 9 -- python/private/pypi/evaluate_markers.bzl | 62 ++++++++++ python/private/pypi/extension.bzl | 42 ++++++- .../pypi/generate_whl_library_build_bazel.bzl | 35 ++++-- python/private/pypi/parse_requirements.bzl | 4 +- python/private/pypi/pip_repository.bzl | 40 +++---- python/private/pypi/whl_installer/wheel.py | 33 ++++-- python/private/pypi/whl_library.bzl | 59 ++++------ tests/pypi/extension/extension_tests.bzl | 110 ++++++++++++++++++ ...generate_whl_library_build_bazel_tests.bzl | 2 - .../parse_requirements_tests.bzl | 2 +- tests/pypi/whl_installer/wheel_test.py | 2 +- 12 files changed, 304 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc00ca2..a8cac4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,8 +103,6 @@ END_UNRELEASED_TEMPLATE * 3.12.9 * 3.13.2 * (pypi) Use `xcrun xcodebuild --showsdks` to find XCode root. -* (pypi) The `bzlmod` extension will now generate smaller lock files for when - using `experimental_index_url`. * (toolchains) Remove all but `3.8.20` versions of the Python `3.8` interpreter who has reached EOL. If users still need other versions of the `3.8` interpreter, please supply the URLs manually {bzl:obj}`python.toolchain` or {bzl:obj}`python_register_toolchains` calls. @@ -120,13 +118,6 @@ END_UNRELEASED_TEMPLATE [PR #2746](https://github.com/bazel-contrib/rules_python/pull/2746). * (rules) {attr}`py_binary.srcs` and {attr}`py_test.srcs` is no longer mandatory when `main_module` is specified (for `--bootstrap_impl=script`) -* (pypi) From now on the `Requires-Dist` from the wheel metadata is analysed in - the loading phase instead of repository rule phase giving better caching - performance when the target platforms are changed (e.g. target python - versions). This is preparatory work for stabilizing the cross-platform wheel - support. From now on the usage of `experimental_target_platforms` should be - avoided and the `requirements_by_platform` values should be instead used to - specify the target platforms for the given dependencies. [20250317]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250317 diff --git a/python/private/pypi/evaluate_markers.bzl b/python/private/pypi/evaluate_markers.bzl index f966aa32..19193359 100644 --- a/python/private/pypi/evaluate_markers.bzl +++ b/python/private/pypi/evaluate_markers.bzl @@ -14,10 +14,21 @@ """A simple function that evaluates markers using a python interpreter.""" +load(":deps.bzl", "record_files") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") load(":pep508_platform.bzl", "platform_from_str") load(":pep508_requirement.bzl", "requirement") +load(":pypi_repo_utils.bzl", "pypi_repo_utils") + +# Used as a default value in a rule to ensure we fetch the dependencies. +SRCS = [ + # When the version, or any of the files in `packaging` package changes, + # this file will change as well. + record_files["pypi__packaging"], + Label("//python/private/pypi/requirements_parser:resolve_target_platforms.py"), + Label("//python/private/pypi/whl_installer:platform.py"), +] def evaluate_markers(requirements, python_version = None): """Return the list of supported platforms per requirements line. @@ -37,3 +48,54 @@ def evaluate_markers(requirements, python_version = None): ret.setdefault(req_string, []).append(platform) return ret + +def evaluate_markers_py(mrctx, *, requirements, python_interpreter, python_interpreter_target, srcs, logger = None): + """Return the list of supported platforms per requirements line. + + Args: + mrctx: repository_ctx or module_ctx. + requirements: list[str] of the requirement file lines to evaluate. + python_interpreter: str, path to the python_interpreter to use to + evaluate the env markers in the given requirements files. It will + be only called if the requirements files have env markers. This + should be something that is in your PATH or an absolute path. + python_interpreter_target: Label, same as python_interpreter, but in a + label format. + srcs: list[Label], the value of SRCS passed from the `rctx` or `mctx` to this function. + logger: repo_utils.logger or None, a simple struct to log diagnostic + messages. Defaults to None. + + Returns: + dict of string lists with target platforms + """ + if not requirements: + return {} + + in_file = mrctx.path("requirements_with_markers.in.json") + out_file = mrctx.path("requirements_with_markers.out.json") + mrctx.file(in_file, json.encode(requirements)) + + pypi_repo_utils.execute_checked( + mrctx, + op = "ResolveRequirementEnvMarkers({})".format(in_file), + python = pypi_repo_utils.resolve_python_interpreter( + mrctx, + python_interpreter = python_interpreter, + python_interpreter_target = python_interpreter_target, + ), + arguments = [ + "-m", + "python.private.pypi.requirements_parser.resolve_target_platforms", + in_file, + out_file, + ], + srcs = srcs, + environment = { + "PYTHONPATH": [ + Label("@pypi__packaging//:BUILD.bazel"), + Label("//:BUILD.bazel"), + ], + }, + logger = logger, + ) + return json.decode(mrctx.read(out_file)) diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index e9eba684..647407f1 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -24,7 +24,7 @@ load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:semver.bzl", "semver") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") -load(":evaluate_markers.bzl", "evaluate_markers") +load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json") load(":parse_requirements.bzl", "parse_requirements") load(":parse_whl_name.bzl", "parse_whl_name") @@ -71,6 +71,7 @@ def _create_whl_repos( whl_overrides, available_interpreters = INTERPRETER_LABELS, minor_mapping = MINOR_MAPPING, + evaluate_markers = evaluate_markers_py, get_index_urls = None): """create all of the whl repositories @@ -85,6 +86,7 @@ def _create_whl_repos( used during the `repository_rule` and must be always compatible with the host. minor_mapping: {type}`dict[str, str]` The dictionary needed to resolve the full python version used to parse package METADATA files. + evaluate_markers: the function used to evaluate the markers. Returns a {type}`struct` with the following attributes: whl_map: {type}`dict[str, list[struct]]` the output is keyed by the @@ -172,7 +174,28 @@ def _create_whl_repos( ), extra_pip_args = pip_attr.extra_pip_args, get_index_urls = get_index_urls, - evaluate_markers = evaluate_markers, + # NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either + # in the PATH or if specified as a label. We will configure the env + # markers when evaluating the requirement lines based on the output + # from the `requirements_files_by_platform` which should have something + # similar to: + # { + # "//:requirements.txt": ["cp311_linux_x86_64", ...] + # } + # + # We know the target python versions that we need to evaluate the + # markers for and thus we don't need to use multiple python interpreter + # instances to perform this manipulation. This function should be executed + # only once by the underlying code to minimize the overhead needed to + # spin up a Python interpreter. + evaluate_markers = lambda module_ctx, requirements: evaluate_markers( + module_ctx, + requirements = requirements, + python_interpreter = pip_attr.python_interpreter, + python_interpreter_target = python_interpreter_target, + srcs = pip_attr._evaluate_markers_srcs, + logger = logger, + ), logger = logger, ) @@ -193,6 +216,7 @@ def _create_whl_repos( enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs, environment = pip_attr.environment, envsubst = pip_attr.envsubst, + experimental_target_platforms = pip_attr.experimental_target_platforms, group_deps = group_deps, group_name = group_name, pip_data_exclude = pip_attr.pip_data_exclude, @@ -281,6 +305,13 @@ def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patt args["urls"] = [distribution.url] args["sha256"] = distribution.sha256 args["filename"] = distribution.filename + args["experimental_target_platforms"] = [ + # Get rid of the version fot the target platforms because we are + # passing the interpreter any way. Ideally we should search of ways + # how to pass the target platforms through the hub repo. + p.partition("_")[2] + for p in requirement.target_platforms + ] # Pure python wheels or sdists may need to have a platform here target_platforms = None @@ -775,6 +806,13 @@ EXPERIMENTAL: this may be removed without notice. doc = """\ A dict of labels to wheel names that is typically generated by the whl_modifications. The labels are JSON config files describing the modifications. +""", + ), + "_evaluate_markers_srcs": attr.label_list( + default = EVALUATE_MARKERS_SRCS, + doc = """\ +The list of labels to use as SRCS for the marker evaluation code. This ensures that the +code will be re-evaluated when any of files in the default changes. """, ), }, **ATTRS) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index 7988aca1..31c9d4da 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -21,11 +21,14 @@ _RENDER = { "copy_files": render.dict, "data": render.list, "data_exclude": render.list, + "dependencies": render.list, + "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list), "entry_points": render.dict, "extras": render.list, "group_deps": render.list, "requires_dist": render.list, "srcs_exclude": render.list, + "tags": render.list, "target_platforms": lambda x: render.list(x) if x else "target_platforms", } @@ -37,7 +40,7 @@ _TEMPLATE = """\ package(default_visibility = ["//visibility:public"]) -whl_library_targets_from_requires( +{fn}( {kwargs} ) """ @@ -59,17 +62,28 @@ def generate_whl_library_build_bazel( A complete BUILD file as a string """ + fn = "whl_library_targets" + if kwargs.get("tags"): + # legacy path + unsupported_args = [ + "requires", + "metadata_name", + "metadata_version", + ] + else: + fn = "{}_from_requires".format(fn) + unsupported_args = [ + "dependencies", + "dependencies_by_platform", + ] + + for arg in unsupported_args: + if kwargs.get(arg): + fail("BUG, unsupported arg: '{}'".format(arg)) + loads = [ - """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires")""", + """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "{}")""".format(fn), ] - if not kwargs.setdefault("target_platforms", None): - dep_template = kwargs["dep_template"] - loads.append( - "load(\"{}\", \"{}\")".format( - dep_template.format(name = "", target = "config.bzl"), - "target_platforms", - ), - ) additional_content = [] if annotation: @@ -87,6 +101,7 @@ def generate_whl_library_build_bazel( [ _TEMPLATE.format( loads = "\n".join(loads), + fn = fn, kwargs = render.indent("\n".join([ "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 1cbf094f..5633328c 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -80,7 +80,7 @@ def parse_requirements( The second element is extra_pip_args should be passed to `whl_library`. """ - evaluate_markers = evaluate_markers or (lambda _: {}) + evaluate_markers = evaluate_markers or (lambda _ctx, _requirements: {}) options = {} requirements = {} for file, plats in requirements_by_platform.items(): @@ -156,7 +156,7 @@ def parse_requirements( # to do, we could use Python to parse the requirement lines and infer the # URL of the files to download things from. This should be important for # VCS package references. - env_marker_target_platforms = evaluate_markers(reqs_with_env_markers) + env_marker_target_platforms = evaluate_markers(ctx, reqs_with_env_markers) if logger: logger.debug(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format( reqs_with_env_markers, diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index b7ed1659..8ca94f7f 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -16,12 +16,11 @@ load("@bazel_skylib//lib:sets.bzl", "sets") load("//python/private:normalize_name.bzl", "normalize_name") -load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") +load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR") load("//python/private:text_util.bzl", "render") -load(":evaluate_markers.bzl", "evaluate_markers") +load(":evaluate_markers.bzl", "evaluate_markers_py", EVALUATE_MARKERS_SRCS = "SRCS") load(":parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement") load(":pip_repository_attrs.bzl", "ATTRS") -load(":pypi_repo_utils.bzl", "pypi_repo_utils") load(":render_pkg_aliases.bzl", "render_pkg_aliases") load(":requirements_files_by_platform.bzl", "requirements_files_by_platform") @@ -71,27 +70,7 @@ package(default_visibility = ["//visibility:public"]) exports_files(["requirements.bzl"]) """ -def _evaluate_markers(rctx, requirements, logger = None): - python_interpreter = _get_python_interpreter_attr(rctx) - stdout = pypi_repo_utils.execute_checked_stdout( - rctx, - op = "GetPythonVersionForMarkerEval", - python = python_interpreter, - arguments = [ - # Run the interpreter in isolated mode, this options implies -E, -P and -s. - # Ensures environment variables are ignored that are set in userspace, such as PYTHONPATH, - # which may interfere with this invocation. - "-I", - "-c", - "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}', end='')", - ], - srcs = [], - logger = logger, - ) - return evaluate_markers(requirements, python_version = stdout) - def _pip_repository_impl(rctx): - logger = repo_utils.logger(rctx) requirements_by_platform = parse_requirements( rctx, requirements_by_platform = requirements_files_by_platform( @@ -103,7 +82,13 @@ def _pip_repository_impl(rctx): extra_pip_args = rctx.attr.extra_pip_args, ), extra_pip_args = rctx.attr.extra_pip_args, - evaluate_markers = lambda requirements: _evaluate_markers(rctx, requirements, logger), + evaluate_markers = lambda rctx, requirements: evaluate_markers_py( + rctx, + requirements = requirements, + python_interpreter = rctx.attr.python_interpreter, + python_interpreter_target = rctx.attr.python_interpreter_target, + srcs = rctx.attr._evaluate_markers_srcs, + ), ) selected_requirements = {} options = None @@ -249,6 +234,13 @@ file](https://github.com/bazel-contrib/rules_python/blob/main/examples/pip_repos _template = attr.label( default = ":requirements.bzl.tmpl.workspace", ), + _evaluate_markers_srcs = attr.label_list( + default = EVALUATE_MARKERS_SRCS, + doc = """\ +The list of labels to use as SRCS for the marker evaluation code. This ensures that the +code will be re-evaluated when any of files in the default changes. +""", + ), **ATTRS ), doc = """Accepts a locked/compiled requirements file and installs the dependencies listed within. diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py index fce706ac..25003e62 100644 --- a/python/private/pypi/whl_installer/wheel.py +++ b/python/private/pypi/whl_installer/wheel.py @@ -62,7 +62,9 @@ def __init__( """ self.name: str = Deps._normalize(name) self._platforms: Set[Platform] = platforms or set() - self._target_versions = {(p.minor_version, p.micro_version) for p in platforms or {}} + self._target_versions = { + (p.minor_version, p.micro_version) for p in platforms or {} + } if platforms and len(self._target_versions) > 1: # TODO @aignas 2024-06-23: enable this to be set via a CLI arg # for being more explicit. @@ -94,8 +96,8 @@ def __init__( for req in reqs: reqs_by_name.setdefault(req.name, []).append(req) - for reqs in reqs_by_name.values(): - self._add_req(reqs, want_extras) + for req_name, reqs in reqs_by_name.items(): + self._add_req(req_name, reqs, want_extras) def _add(self, dep: str, platform: Optional[Platform]): dep = Deps._normalize(dep) @@ -134,7 +136,7 @@ def _normalize(name: str) -> str: return re.sub(r"[-_.]+", "_", name).lower() def _resolve_extras( - self, reqs: List[Requirement], extras: Optional[Set[str]] + self, reqs: List[Requirement], want_extras: Optional[Set[str]] ) -> Set[str]: """Resolve extras which are due to depending on self[some_other_extra]. @@ -156,7 +158,7 @@ def _resolve_extras( # extras The empty string in the set is just a way to make the handling # of no extras and a single extra easier and having a set of {"", "foo"} # is equivalent to having {"foo"}. - extras = extras or {""} + extras: Set[str] = want_extras or {""} self_reqs = [] for req in reqs: @@ -189,13 +191,18 @@ def _resolve_extras( return extras - def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None: + def _add_req(self, req_name, reqs: List[Requirement], extras: Set[str]) -> None: platforms_to_add = set() for req in reqs: if req.marker is None: self._add(req.name, None) return + if not self._platforms: + if any(req.marker.evaluate({"extra": extra}) for extra in extras): + self._add(req.name, None) + return + for plat in self._platforms: if plat in platforms_to_add: # marker evaluation is more expensive than this check @@ -211,18 +218,24 @@ def _add_req(self, reqs: List[Requirement], extras: Set[str]) -> None: added = True break + if not self._platforms: + return + if len(platforms_to_add) == len(self._platforms): # the dep is in all target platforms, let's just add it to the regular # list - self._add(req.name, None) + self._add(req_name, None) return for plat in platforms_to_add: if self._default_minor_version is not None: - self._add(req.name, plat) + self._add(req_name, plat) - if self._default_minor_version is None or plat.minor_version == self._default_minor_version: - self._add(req.name, Platform(os = plat.os, arch = plat.arch)) + if ( + self._default_minor_version is None + or plat.minor_version == self._default_minor_version + ): + self._add(req_name, Platform(os=plat.os, arch=plat.arch)) def build(self) -> FrozenDeps: return FrozenDeps( diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 630dc851..0c09f796 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -15,18 +15,16 @@ "" load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") -load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter") load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") -load(":parse_requirements.bzl", "host_platform") +load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") -load(":pep508_requirement.bzl", "requirement") load(":pypi_repo_utils.bzl", "pypi_repo_utils") -load(":whl_metadata.bzl", "whl_metadata") +load(":whl_target_platforms.bzl", "whl_target_platforms") _CPPFLAGS = "CPPFLAGS" _COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools" @@ -342,6 +340,21 @@ def _whl_library_impl(rctx): timeout = rctx.attr.timeout, ) + target_platforms = rctx.attr.experimental_target_platforms or [] + if target_platforms: + parsed_whl = parse_whl_name(whl_path.basename) + + # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we + # only include deps for that target platform + if parsed_whl.platform_tag != "any": + target_platforms = [ + p.target_platform + for p in whl_target_platforms( + platform_tag = parsed_whl.platform_tag, + abi_tag = parsed_whl.abi_tag.strip("tm"), + ) + ] + pypi_repo_utils.execute_checked( rctx, op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), @@ -349,7 +362,7 @@ def _whl_library_impl(rctx): arguments = args + [ "--whl-file", whl_path, - ], + ] + ["--platform={}".format(p) for p in target_platforms], srcs = rctx.attr._python_srcs, environment = environment, quiet = rctx.attr.quiet, @@ -384,45 +397,21 @@ def _whl_library_impl(rctx): ) entry_points[entry_point_without_py] = entry_point_script_name - if BZLMOD_ENABLED: - # The following attributes are unset on bzlmod and we pass data through - # the hub via load statements. - default_python_version = None - target_platforms = [] - else: - # NOTE @aignas 2025-04-16: if BZLMOD_ENABLED, we should use - # DEFAULT_PYTHON_VERSION since platforms always come with the actual - # python version otherwise we should use the version of the interpreter - # here. In WORKSPACE `multi_pip_parse` is using an interpreter for each - # `pip_parse` invocation, so we will have the host target platform - # only. Even if somebody would change the code to support - # `experimental_target_platforms`, they would be for a single python - # version. Hence, using the `default_python_version` that we get from the - # interpreter is correct. Hence, we unset the argument if we are on bzlmod. - default_python_version = metadata["python_version"] - target_platforms = rctx.attr.experimental_target_platforms or [host_platform(rctx)] - - metadata = whl_metadata( - install_dir = rctx.path("site-packages"), - read_fn = rctx.read, - logger = logger, - ) - build_file_contents = generate_whl_library_build_bazel( name = whl_path.basename, - metadata_name = metadata.name, - metadata_version = metadata.version, - requires_dist = metadata.requires_dist, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), entry_points = entry_points, - target_platforms = target_platforms, - default_python_version = default_python_version, # TODO @aignas 2025-04-14: load through the hub: + dependencies = metadata["deps"], + dependencies_by_platform = metadata["deps_by_platform"], annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), data_exclude = rctx.attr.pip_data_exclude, - extras = requirement(rctx.attr.requirement).extras, group_deps = rctx.attr.group_deps, group_name = rctx.attr.group_name, + tags = [ + "pypi_name={}".format(metadata["name"]), + "pypi_version={}".format(metadata["version"]), + ], ) rctx.file("BUILD.bazel", build_file_contents) diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 5de3bb58..1cd6869c 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -136,6 +136,7 @@ def _parse( parallel_download = False, experimental_index_url_overrides = {}, simpleapi_skip = simpleapi_skip, + _evaluate_markers_srcs = [], **kwargs ) @@ -273,6 +274,14 @@ torch==2.4.1 ; platform_machine != 'x86_64' \ "python_3_15_host": "unit_test_interpreter_target", }, minor_mapping = {"3.15": "3.15.19"}, + evaluate_markers = lambda _, requirements, **__: { + key: [ + platform + for platform in platforms + if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) + ] + for key, platforms in requirements.items() + }, ) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) @@ -397,6 +406,15 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, minor_mapping = {"3.12": "3.12.19"}, simpleapi_download = mocksimpleapi_download, + evaluate_markers = lambda _, requirements, **__: { + # todo once 2692 is merged, this is going to be easier to test. + key: [ + platform + for platform in platforms + if ("x86_64" in platform and "platform_machine ==" in key) or ("x86_64" not in platform and "platform_machine !=" in key) + ] + for key, platforms in requirements.items() + }, ) pypi.exposed_packages().contains_exactly({"pypi": ["torch"]}) @@ -440,6 +458,11 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ pypi.whl_libraries().contains_exactly({ "pypi_312_torch_cp312_cp312_linux_x86_64_8800deef": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_x86_64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1+cpu", @@ -448,6 +471,13 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "osx_aarch64", + ], "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1", @@ -456,6 +486,11 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_x86_64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1+cpu", @@ -464,6 +499,13 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ }, "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "osx_aarch64", + ], "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "torch==2.4.1", @@ -751,6 +793,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef pypi.whl_libraries().contains_exactly({ "pypi_315_any_name": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -760,6 +812,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "direct_without_sha-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "direct_without_sha==0.0.1 @ example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl", @@ -780,6 +842,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_simple_py3_none_any_deadb00f": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "simple-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "simple==0.0.1", @@ -788,6 +860,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_simple_sdist_deadbeef": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "simple-0.0.1.tar.gz", "python_interpreter_target": "unit_test_interpreter_target", @@ -797,6 +879,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_some_pkg_py3_none_any_deadbaaf": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "some_pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl --hash=sha256:deadbaaf", @@ -805,6 +897,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef }, "pypi_315_some_py3_none_any_deadb33f": { "dep_template": "@pypi//{name}:{target}", + "experimental_target_platforms": [ + "linux_aarch64", + "linux_arm", + "linux_ppc", + "linux_s390x", + "linux_x86_64", + "osx_aarch64", + "osx_x86_64", + "windows_x86_64", + ], "filename": "some-other-pkg-0.0.1-py3-none-any.whl", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "some_other_pkg==0.0.1", @@ -856,6 +958,14 @@ optimum[onnxruntime-gpu]==1.17.1 ; sys_platform == 'linux' "python_3_15_host": "unit_test_interpreter_target", }, minor_mapping = {"3.15": "3.15.19"}, + evaluate_markers = lambda _, requirements, **__: { + key: [ + platform + for platform in platforms + if ("darwin" in key and "osx" in platform) or ("linux" in key and "linux" in platform) + ] + for key, platforms in requirements.items() + }, ) pypi.exposed_packages().contains_exactly({"pypi": []}) diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 7bd19b65..83be7395 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -86,7 +86,6 @@ _tests.append(_test_all) def _test_all_with_loads(env): want = """\ load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") -load("@pypi//:config.bzl", "target_platforms") package(default_visibility = ["//visibility:public"]) @@ -119,7 +118,6 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], - target_platforms = target_platforms, ) # SOMETHING SPECIAL AT THE END diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index c5048212..723bb605 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -458,7 +458,7 @@ def _test_select_requirement_none_platform(env): _tests.append(_test_select_requirement_none_platform) def _test_env_marker_resolution(env): - def _mock_eval_markers(input): + def _mock_eval_markers(_, input): ret = { "foo[extra]==0.0.1 ;marker --hash=sha256:deadbeef": ["cp311_windows_x86_64"], } diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py index 6921fe6d..3599fd18 100644 --- a/tests/pypi/whl_installer/wheel_test.py +++ b/tests/pypi/whl_installer/wheel_test.py @@ -11,7 +11,7 @@ class DepsTest(unittest.TestCase): def test_simple(self): - deps = wheel.Deps("foo", requires_dist=["bar"]) + deps = wheel.Deps("foo", requires_dist=["bar", 'baz; extra=="foo"']) got = deps.build() From 1c35e4c84674ce25c9d9963125d335258f257ce7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 28 Apr 2025 20:07:31 -0700 Subject: [PATCH 03/23] feat: implement less/greater operators for string for env marker evaluation (#2827) Right now, if two strings are compared, it results in an error. Per spec, strings are suppose to "use the python behavior". Starlark is going to use Java semantics underneath, but it should behave close enough for the (almost exclusively) ASCII input that will be used. Work towards https://github.com/bazel-contrib/rules_python/issues/2826 --- python/private/pypi/pep508_evaluate.bzl | 8 ++++++++ tests/pypi/pep508/evaluate_tests.bzl | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/python/private/pypi/pep508_evaluate.bzl b/python/private/pypi/pep508_evaluate.bzl index f8ef5530..70840c76 100644 --- a/python/private/pypi/pep508_evaluate.bzl +++ b/python/private/pypi/pep508_evaluate.bzl @@ -344,6 +344,14 @@ def _env_expr(left, op, right): return left in right elif op == "not in": return left not in right + elif op == "<": + return left < right + elif op == "<=": + return left <= right + elif op == ">": + return left > right + elif op == ">=": + return left >= right else: return fail("TODO: op unsupported: '{}'".format(op)) diff --git a/tests/pypi/pep508/evaluate_tests.bzl b/tests/pypi/pep508/evaluate_tests.bzl index 14e5e40b..303c1679 100644 --- a/tests/pypi/pep508/evaluate_tests.bzl +++ b/tests/pypi/pep508/evaluate_tests.bzl @@ -68,18 +68,28 @@ def _evaluate_non_version_env_tests(env): # When for input, want in { - "{} == 'osx'".format(var_name): True, - "{} != 'osx'".format(var_name): False, - "'osx' == {}".format(var_name): True, "'osx' != {}".format(var_name): False, - "'x' in {}".format(var_name): True, + "'osx' < {}".format(var_name): False, + "'osx' <= {}".format(var_name): True, + "'osx' == {}".format(var_name): True, + "'osx' >= {}".format(var_name): True, "'w' not in {}".format(var_name): True, - }.items(): # buildifier: @unsorted-dict-items + "'x' in {}".format(var_name): True, + "{} != 'osx'".format(var_name): False, + "{} < 'osx'".format(var_name): False, + "{} <= 'osx'".format(var_name): True, + "{} == 'osx'".format(var_name): True, + "{} > 'osx'".format(var_name): False, + "{} >= 'osx'".format(var_name): True, + }.items(): got = evaluate( input, env = marker_env, ) - env.expect.that_bool(got).equals(want) + env.expect.where( + expr = input, + env = marker_env, + ).that_bool(got).equals(want) # Check that the non-strict eval gives us back the input when no # env is supplied. From 704ecdd835c8a79ac415c81567eae5785df4b7e3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 28 Apr 2025 20:07:57 -0700 Subject: [PATCH 04/23] docs: doc version when RULES_PYTHON_ENABLE_PYSTAR was introduced (#2838) While figuring out an upgrade from an old rules_python version, I had to look up when the environment variable first became available. Also note what version it defaulted to 1. --- docs/environment-variables.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 9500fa82..49fdf766 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -46,11 +46,19 @@ When `1`, the rules_python will warn users about deprecated functionality that w be removed in a subsequent major `rules_python` version. Defaults to `0` if unset. ::: -:::{envvar} RULES_PYTHON_ENABLE_PYSTAR +::::{envvar} RULES_PYTHON_ENABLE_PYSTAR When `1`, the rules_python Starlark implementation of the core rules is used -instead of the Bazel-builtin rules. Note this requires Bazel 7+. +instead of the Bazel-builtin rules. Note this requires Bazel 7+. Defaults +to `1`. + +:::{versionadded} 0.26.0 +Defaults to `0` if unspecified. +::: +:::{versionchanged} 0.40.0 +The default became `1` if unspecified ::: +:::: ::::{envvar} RULES_PYTHON_EXTRACT_ROOT From a79bbfaece3e41f361b7d5baf89aec269184eb4d Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:52:46 +0900 Subject: [PATCH 05/23] fix(pypi): handle more URL patterns for requirement sources (#2843) Summary: - Better handle git references for sdists. - Better handle direct whl references. - Add an extra test that turned out to be not needed in the end, but I left it to increase the code coverage. Work towards #2363 Fixes #2828 --- python/private/pypi/parse_requirements.bzl | 5 ++ .../index_sources/index_sources_tests.bzl | 14 ++++- .../parse_requirements_tests.bzl | 59 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 5633328c..1583c891 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -285,12 +285,17 @@ def _add_dists(*, requirement, index_urls, logger = None): if requirement.srcs.url: url = requirement.srcs.url _, _, filename = url.rpartition("/") + filename, _, _ = filename.partition("#sha256=") if "." not in filename: # detected filename has no extension, it might be an sdist ref # TODO @aignas 2025-04-03: should be handled if the following is fixed: # https://github.com/bazel-contrib/rules_python/issues/2363 return [], None + if "@" in filename: + # this is most likely foo.git@git_sha, skip special handling of these + return [], None + direct_url_dist = struct( url = url, filename = filename, diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index ffeed87a..9d12bc63 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -21,38 +21,50 @@ _tests = [] def _test_no_simple_api_sources(env): inputs = { + "foo @ git+https://github.com/org/foo.git@deadbeef": struct( + requirement = "foo @ git+https://github.com/org/foo.git@deadbeef", + marker = "", + url = "git+https://github.com/org/foo.git@deadbeef", + shas = [], + version = "", + ), "foo==0.0.1": struct( requirement = "foo==0.0.1", marker = "", url = "", + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org": struct( requirement = "foo==0.0.1 @ https://someurl.org", marker = "", url = "https://someurl.org", + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org/package.whl": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl", marker = "", url = "https://someurl.org/package.whl", + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "", url = "https://someurl.org/package.whl", shas = ["deadbeef"], + version = "0.0.1", ), "foo==0.0.1 @ https://someurl.org/package.whl; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", url = "https://someurl.org/package.whl", shas = ["deadbeef"], + version = "0.0.1", ), } for input, want in inputs.items(): got = index_sources(input) env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else []) - env.expect.that_str(got.version).equals("0.0.1") + env.expect.that_str(got.version).equals(want.version) env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement) env.expect.that_str(got.marker).equals(want.marker) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 723bb605..c5b24870 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -30,12 +30,16 @@ foo[extra] @ https://some-url/package.whl bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef baz @ https://test.com/baz-2.0.whl; python_version < "3.8" --hash=sha256:deadb00f qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f +torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc """, "requirements_extra_args": """\ --index-url=example.org foo[extra]==0.0.1 \ --hash=sha256:deadbeef +""", + "requirements_git": """ +foo @ git+https://github.com/org/foo.git@deadbeef """, "requirements_linux": """\ foo==0.0.3 --hash=sha256:deadbaaf @@ -232,6 +236,31 @@ def _test_direct_urls(env): whls = [], ), ], + "torch": [ + struct( + distribution = "torch", + extra_pip_args = [], + is_exposed = True, + sdist = None, + srcs = struct( + marker = "", + requirement = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + requirement_line = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + shas = [], + url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + version = "", + ), + target_platforms = ["linux_x86_64"], + whls = [ + struct( + filename = "torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl", + sha256 = "", + url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=5b6ae523bfb67088a17ca7734d131548a2e60346c622621e4248ed09dd0790cc", + yanked = False, + ), + ], + ), + ], }) _tests.append(_test_direct_urls) @@ -623,6 +652,36 @@ def _test_optional_hash(env): _tests.append(_test_optional_hash) +def _test_git_sources(env): + got = parse_requirements( + ctx = _mock_ctx(), + requirements_by_platform = { + "requirements_git": ["linux_x86_64"], + }, + ) + env.expect.that_dict(got).contains_exactly({ + "foo": [ + struct( + distribution = "foo", + extra_pip_args = [], + is_exposed = True, + sdist = None, + srcs = struct( + marker = "", + requirement = "foo @ git+https://github.com/org/foo.git@deadbeef", + requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", + shas = [], + url = "git+https://github.com/org/foo.git@deadbeef", + version = "", + ), + target_platforms = ["linux_x86_64"], + whls = [], + ), + ], + }) + +_tests.append(_test_git_sources) + def parse_requirements_test_suite(name): """Create the test suite. From 189e30df4001d34aba590e0267d3e5f72e6d8b19 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 29 Apr 2025 10:08:37 -0700 Subject: [PATCH 06/23] docs: document some of our project styles/conventions (#2816) Spurred by the discussion to converge on using `.` to separate generated targets, I wrote down some of the conventions we've adopted. --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- .editorconfig | 17 +++++++++++++++++ CONTRIBUTING.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..26bb52ff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Set default charset +[*] +charset = utf-8 + +# Line width +[*] +max_line_length = 100 + +# 4 space indentation +[*.{py,bzl}] +indent_style = space +indent_size = 4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17558e1b..b087119d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,6 +173,55 @@ The `legacy_foo` arg was removed ::: ``` +## Style and idioms + +For the most part, we just accept whatever the code formatters do, so there +isn't much style to enforce. + +Some miscellanous style, idioms, and conventions we have are: + +### Markdown/Sphinx Style + +* Use colons for prose sections of text, e.g. `:::{note}`, not backticks. +* Use backticks for code blocks. +* Max line length: 100. + +### BUILD/bzl Style + +* When a macro generates public targets, use a dot (`.`) to separate the + user-provided name from the generted name. e.g. `foo(name="x")` generates + `x.test`. The `.` is our convention to communicate that it's a generated + target, and thus one should look for `name="x"` when searching for the + definition. +* The different build phases shouldn't load code that defines objects that + aren't valid for their phase. e.g. + * The bzlmod phase shouldn't load code defining regular rules or providers. + * The repository phase shouldn't load code defining module extensions, regular + rules, or providers. + * The loading phase shouldn't load code defining module extensions or + repository rules. + * Loading utility libraries or generic code is OK, but should strive to load + code that is usable for its phase. e.g. loading-phase code shouldn't + load utility code that is predominately only usable to the bzlmod phase. +* Providers should be in their own files. This allows implementing a custom rule + that implements the provider without loading a specific implementation. +* One rule per file is preferred, but not required. The goal is that defining an + e.g. library shouldn't incur loading all the code for binaries, tests, + packaging, etc; things that may be niche or uncommonly used. +* Separate files should be used to expose public APIs. This ensures our public + API is well defined and prevents accidentally exposing a package-private + symbol as a public symbol. + + :::{note} + The public API file's docstring becomes part of the user-facing docs. That + file's docstring must be used for module-level API documentation. + ::: +* Repository rules should have name ending in `_repo`. This helps distinguish + them from regular rules. +* Each bzlmod extension, the "X" of `use_repo("//foo:foo.bzl", "X")` should be + in its own file. The path given in the `use_repo()` expression is the identity + Bazel uses and cannot be changed. + ## Generated files Some checked-in files are generated and need to be updated when a new PR is From 76b221e668d7038b8a069bf44b81682876dbea38 Mon Sep 17 00:00:00 2001 From: Vein Kong Date: Thu, 1 May 2025 23:36:56 -0700 Subject: [PATCH 07/23] fix: requires_file preserves extras that package depends on (#2807) When requirements are passed in through `requires_file` the extras are not preserved. eg if the contents of requires file is `example[extras]==1.1.1`, bazel will currently write to the METADATA file `Requires-Dist: example==1.1.1`. This PR attempts to fix that by adding that back if there are any extras. The expected output should be `Requires-Dist: example[extras]==1.1.1` --- .bazelrc | 4 +-- CHANGELOG.md | 2 ++ examples/wheel/BUILD.bazel | 29 +++++++++++++++++++++ examples/wheel/wheel_test.py | 50 ++++++++++++++++++++++++++++++++++++ tools/wheelmaker.py | 7 ++--- 5 files changed, 87 insertions(+), 5 deletions(-) diff --git a/.bazelrc b/.bazelrc index 4e6f2fa1..d2e07215 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,8 +4,8 @@ # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/pythonconfig,gazelle/python/private,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/local_toolchains,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered,tests/modules/other,tests/modules/other/nspkg_delta,tests/modules/other/nspkg_gamma test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index a8cac4c5..19fe636b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ END_UNRELEASED_TEMPLATE * The {obj}`//python/runtime_env_toolchains:all` toolchain now works with it. * (rules) Better handle flakey platform.win32_ver() calls by calling them multiple times. +* (tools/wheelmaker.py) Extras are now preserved in Requires-Dist metadata when using requires_file + to specify the requirements. {#v0-0-0-added} ### Added diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index b434e674..e52e0fc3 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -313,6 +313,17 @@ wheel; python_version == "3.11" or python_version == "3.12" # Example comment """.splitlines(), ) +write_file( + name = "requires_dist_depends_on_extras_file", + out = "requires_dist_depends_on_extras.txt", + content = """\ +# Requirements file +--index-url https://pypi.com + +extra_requires[example]==0.0.1 +""".splitlines(), +) + # py_wheel can use text files to specify their requirements. This # can be convenient for users of `compile_pip_requirements` who have # granular `requirements.in` files per package. This target shows @@ -374,6 +385,22 @@ py_wheel( deps = [":example_pkg"], ) +py_wheel( + name = "requires_dist_depends_on_extras", + distribution = "requires_dist_depends_on_extras", + requires = [ + "extra_requires[example]==0.0.1", + ], + version = "0.0.1", +) + +py_wheel( + name = "requires_dist_depends_on_extras_using_file", + distribution = "requires_dist_depends_on_extras_using_file", + requires_file = ":requires_dist_depends_on_extras.txt", + version = "0.0.1", +) + py_test( name = "wheel_test", srcs = ["wheel_test.py"], @@ -391,6 +418,8 @@ py_test( ":minimal_with_py_package", ":python_abi3_binary_wheel", ":python_requires_in_a_package", + ":requires_dist_depends_on_extras", + ":requires_dist_depends_on_extras_using_file", ":requires_files", ":use_rule_with_dir_in_outs", ], diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 35803da7..43e56cfc 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -565,6 +565,56 @@ def test_extra_requires(self): requires, ) + def test_requires_dist_depends_on_extras(self): + filename = self._get_path("requires_dist_depends_on_extras-0.0.1-py3-none-any.whl") + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + metadata_file = None + for f in zf.namelist(): + if os.path.basename(f) == "METADATA": + metadata_file = f + self.assertIsNotNone(metadata_file) + + requires = [] + with zf.open(metadata_file) as fp: + for line in fp: + if line.startswith(b"Requires-Dist:"): + requires.append(line.decode("utf-8").strip()) + + print(requires) + self.assertEqual( + [ + "Requires-Dist: extra_requires[example]==0.0.1", + ], + requires, + ) + + def test_requires_dist_depends_on_extras_file(self): + filename = self._get_path("requires_dist_depends_on_extras_using_file-0.0.1-py3-none-any.whl") + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + metadata_file = None + for f in zf.namelist(): + if os.path.basename(f) == "METADATA": + metadata_file = f + self.assertIsNotNone(metadata_file) + + requires = [] + with zf.open(metadata_file) as fp: + for line in fp: + if line.startswith(b"Requires-Dist:"): + requires.append(line.decode("utf-8").strip()) + + print(requires) + self.assertEqual( + [ + "Requires-Dist: extra_requires[example]==0.0.1", + ], + requires, + ) + if __name__ == "__main__": unittest.main() diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 28ec0397..de584650 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -562,13 +562,14 @@ def main() -> None: def get_new_requirement_line(reqs_text, extra): req = Requirement(reqs_text.strip()) + req_extra_deps = f"[{','.join(req.extras)}]" if req.extras else "" if req.marker: if extra: - return f"Requires-Dist: {req.name}{req.specifier}; ({req.marker}) and {extra}" + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; ({req.marker}) and {extra}" else: - return f"Requires-Dist: {req.name}{req.specifier}; {req.marker}" + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {req.marker}" else: - return f"Requires-Dist: {req.name}{req.specifier}; {extra}".strip(" ;") + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip(" ;") for meta_line in metadata.splitlines(): if not meta_line.startswith("Requires-Dist: "): From 8e76bd451a29d2728008a7094e850141a172cfe9 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 2 May 2025 14:46:20 -0700 Subject: [PATCH 08/23] refactor: add rule to do analysis time evaluation of environment markers (#2832) wip/prototype to help bootstrap the impl of an analysis-time flag that evaluates the pep508 dep specs Creating a PR to make collab easier (maintainers can directly edit) TODO: * Remove the todo markers after discussion Work towards https://github.com/bazel-contrib/rules_python/issues/2826 --------- Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com> --- python/private/pypi/env_marker_setting.bzl | 186 ++++++++++++++++++ python/private/pypi/pep508_env.bzl | 94 ++++++++- tests/pypi/env_marker_setting/BUILD.bazel | 5 + .../env_marker_setting_tests.bzl | 69 +++++++ 4 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 python/private/pypi/env_marker_setting.bzl create mode 100644 tests/pypi/env_marker_setting/BUILD.bazel create mode 100644 tests/pypi/env_marker_setting/env_marker_setting_tests.bzl diff --git a/python/private/pypi/env_marker_setting.bzl b/python/private/pypi/env_marker_setting.bzl new file mode 100644 index 00000000..bbc59ab1 --- /dev/null +++ b/python/private/pypi/env_marker_setting.bzl @@ -0,0 +1,186 @@ +"""Implement a flag for matching the dependency specifiers at analysis time.""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") +load( + ":pep508_env.bzl", + "env_aliases", + "os_name_select_map", + "platform_machine_select_map", + "platform_system_select_map", + "sys_platform_select_map", +) +load(":pep508_evaluate.bzl", "evaluate") + +# Use capitals to hint its not an actual boolean type. +_ENV_MARKER_TRUE = "TRUE" +_ENV_MARKER_FALSE = "FALSE" + +def env_marker_setting(*, name, expression, **kwargs): + """Creates an env_marker setting. + + Generated targets: + + * `is_{name}_true`: config_setting that matches when the expression is true. + * `{name}`: env marker target that evalutes the expression. + + Args: + name: {type}`str` target name + expression: {type}`str` the environment marker string to evaluate + **kwargs: {type}`dict` additional common kwargs. + """ + native.config_setting( + name = "is_{}_true".format(name), + flag_values = { + ":{}".format(name): _ENV_MARKER_TRUE, + }, + **kwargs + ) + _env_marker_setting( + name = name, + expression = expression, + os_name = select(os_name_select_map), + sys_platform = select(sys_platform_select_map), + platform_machine = select(platform_machine_select_map), + platform_system = select(platform_system_select_map), + platform_release = select({ + "@platforms//os:osx": "USE_OSX_VERSION_FLAG", + "//conditions:default": "", + }), + **kwargs + ) + +def _env_marker_setting_impl(ctx): + env = {} + + runtime = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime + if runtime.interpreter_version_info: + version_info = runtime.interpreter_version_info + env["python_version"] = "{major}.{minor}".format( + major = version_info.major, + minor = version_info.minor, + ) + full_version = _format_full_version(version_info) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + else: + env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag) + full_version = _get_flag(ctx.attr._python_full_version_flag) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + + # We assume cpython if the toolchain doesn't specify because it's most + # likely to be true. + env["implementation_name"] = runtime.implementation_name or "cpython" + env["os_name"] = ctx.attr.os_name + env["sys_platform"] = ctx.attr.sys_platform + env["platform_machine"] = ctx.attr.platform_machine + + # The `platform_python_implementation` marker value is supposed to come + # from `platform.python_implementation()`, however, PEP 421 introduced + # `sys.implementation.name` and the `implementation_name` env marker to + # replace it. Per the platform.python_implementation docs, there's now + # essentially just two possible "registered" values: CPython or PyPy. + # Rather than add a field to the toolchain, we just special case the value + # from `sys.implementation.name` to handle the two documented values. + platform_python_impl = runtime.implementation_name + if platform_python_impl == "cpython": + platform_python_impl = "CPython" + elif platform_python_impl == "pypy": + platform_python_impl = "PyPy" + env["platform_python_implementation"] = platform_python_impl + + # NOTE: Platform release for Android will be Android version: + # https://peps.python.org/pep-0738/#platform + # Similar for iOS: + # https://peps.python.org/pep-0730/#platform + platform_release = ctx.attr.platform_release + if platform_release == "USE_OSX_VERSION_FLAG": + platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag) + env["platform_release"] = platform_release + env["platform_system"] = ctx.attr.platform_system + + # For lack of a better option, just use an empty string for now. + env["platform_version"] = "" + + env.update(env_aliases()) + + if evaluate(ctx.attr.expression, env = env): + value = _ENV_MARKER_TRUE + else: + value = _ENV_MARKER_FALSE + return [config_common.FeatureFlagInfo(value = value)] + +_env_marker_setting = rule( + doc = """ +Evaluates an environment marker expression using target configuration info. + +See +https://packaging.python.org/en/latest/specifications/dependency-specifiers +for the specification of behavior. +""", + implementation = _env_marker_setting_impl, + attrs = { + "expression": attr.string( + mandatory = True, + doc = "Environment marker expression to evaluate.", + ), + "os_name": attr.string(), + "platform_machine": attr.string(), + "platform_release": attr.string(), + "platform_system": attr.string(), + "sys_platform": attr.string(), + "_pip_whl_osx_version_flag": attr.label( + default = "//python/config_settings:pip_whl_osx_version", + providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]], + ), + "_python_full_version_flag": attr.label( + default = "//python/config_settings:python_version", + providers = [config_common.FeatureFlagInfo], + ), + "_python_version_major_minor_flag": attr.label( + default = "//python/config_settings:python_version_major_minor", + providers = [config_common.FeatureFlagInfo], + ), + }, + provides = [config_common.FeatureFlagInfo], + toolchains = [ + TARGET_TOOLCHAIN_TYPE, + ], +) + +def _format_full_version(info): + """Format the full python interpreter version. + + Adapted from spec code at: + https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers + + Args: + info: The provider from the Python runtime. + + Returns: + a {type}`str` with the version + """ + kind = info.releaselevel + if kind == "final": + kind = "" + serial = "" + else: + kind = kind[0] if kind else "" + serial = str(info.serial) if info.serial else "" + + return "{major}.{minor}.{micro}{kind}{serial}".format( + v = info, + major = info.major, + minor = info.minor, + micro = info.micro, + kind = kind, + serial = serial, + ) + +def _get_flag(t): + if config_common.FeatureFlagInfo in t: + return t[config_common.FeatureFlagInfo].value + if BuildSettingInfo in t: + return t[BuildSettingInfo].value + fail("Should not occur: {} does not have necessary providers") diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 265a8e9b..3708c46f 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -18,7 +18,7 @@ load(":pep508_platform.bzl", "platform_from_str") # See https://stackoverflow.com/a/45125525 -_platform_machine_aliases = { +platform_machine_aliases = { # These pairs mean the same hardware, but different values may be used # on different host platforms. "amd64": "x86_64", @@ -27,6 +27,41 @@ _platform_machine_aliases = { "i686": "x86_32", } +# NOTE: There are many cpus, and unfortunately, the value isn't directly +# accessible to Starlark. Using CcToolchain.cpu might work, though. +platform_machine_select_map = { + "@platforms//cpu:aarch32": "aarch32", + "@platforms//cpu:aarch64": "aarch64", + "@platforms//cpu:arm": "arm", + "@platforms//cpu:arm64": "arm64", + "@platforms//cpu:arm64_32": "arm64_32", + "@platforms//cpu:arm64e": "arm64e", + "@platforms//cpu:armv6-m": "armv6-m", + "@platforms//cpu:armv7": "armv7", + "@platforms//cpu:armv7-m": "armv7-m", + "@platforms//cpu:armv7e-m": "armv7e-m", + "@platforms//cpu:armv7e-mf": "armv7e-mf", + "@platforms//cpu:armv7k": "armv7k", + "@platforms//cpu:armv8-m": "armv8-m", + "@platforms//cpu:cortex-r52": "cortex-r52", + "@platforms//cpu:cortex-r82": "cortex-r82", + "@platforms//cpu:i386": "i386", + "@platforms//cpu:mips64": "mips64", + "@platforms//cpu:ppc": "ppc", + "@platforms//cpu:ppc32": "ppc32", + "@platforms//cpu:ppc64le": "ppc64le", + "@platforms//cpu:riscv32": "riscv32", + "@platforms//cpu:riscv64": "riscv64", + "@platforms//cpu:s390x": "s390x", + "@platforms//cpu:wasm32": "wasm32", + "@platforms//cpu:wasm64": "wasm64", + "@platforms//cpu:x86_32": "x86_32", + "@platforms//cpu:x86_64": "x86_64", + # The value is empty string if it cannot be determined: + # https://docs.python.org/3/library/platform.html#platform.machine + "//conditions:default": "", +} + # Platform system returns results from the `uname` call. _platform_system_values = { "linux": "Linux", @@ -34,6 +69,23 @@ _platform_system_values = { "windows": "Windows", } +platform_system_select_map = { + # See https://peps.python.org/pep-0738/#platform + "@platforms//os:android": "Android", + "@platforms//os:freebsd": "FreeBSD", + # See https://peps.python.org/pep-0730/#platform + # NOTE: Per Pep 730, "iPadOS" is also an acceptable value + "@platforms//os:ios": "iOS", + "@platforms//os:linux": "Linux", + "@platforms//os:netbsd": "NetBSD", + "@platforms//os:openbsd": "OpenBSD", + "@platforms//os:osx": "Darwin", + "@platforms//os:windows": "Windows", + # The value is empty string if it cannot be determined: + # https://docs.python.org/3/library/platform.html#platform.machine + "//conditions:default": "", +} + # The copy of SO [answer](https://stackoverflow.com/a/13874620) containing # all of the platforms: # ┍━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━┑ @@ -64,12 +116,45 @@ _sys_platform_values = { "osx": "darwin", "windows": "win32", } + +# Taken from +# https://docs.python.org/3/library/sys.html#sys.platform +sys_platform_select_map = { + # These values are decided by the sys.platform docs. + "@platforms//os:android": "android", + "@platforms//os:emscripten": "emscripten", + # NOTE: The below values are approximations. The sys.platform() docs + # don't have documented values for these OSes. Per docs, the + # sys.platform() value reflects the OS at the time Python was *built* + # instead of the runtime (target) OS value. + "@platforms//os:freebsd": "freebsd", + "@platforms//os:ios": "ios", + "@platforms//os:linux": "linux", + "@platforms//os:openbsd": "openbsd", + "@platforms//os:osx": "darwin", + "@platforms//os:wasi": "wasi", + "@platforms//os:windows": "win32", + # For lack of a better option, use empty string. No standard doc/spec + # about sys_platform value. + "//conditions:default": "", +} + _os_name_values = { "linux": "posix", "osx": "posix", "windows": "nt", } +os_name_select_map = { + # The "java" value is documented, but with Jython defunct, + # shouldn't occur in practice. + # The os.name value is technically a property of the runtime, not the + # targetted runtime OS, but the distinction shouldn't matter if + # things are properly configured. + "@platforms//os:windows": "nt", + "//conditions:default": "posix", +} + def env(target_platform, *, extra = None): """Return an env target platform @@ -113,8 +198,11 @@ def env(target_platform, *, extra = None): } # This is split by topic - return env | { + return env | env_aliases() + +def env_aliases(): + return { "_aliases": { - "platform_machine": _platform_machine_aliases, + "platform_machine": platform_machine_aliases, }, } diff --git a/tests/pypi/env_marker_setting/BUILD.bazel b/tests/pypi/env_marker_setting/BUILD.bazel new file mode 100644 index 00000000..9605e650 --- /dev/null +++ b/tests/pypi/env_marker_setting/BUILD.bazel @@ -0,0 +1,5 @@ +load(":env_marker_setting_tests.bzl", "env_marker_setting_test_suite") + +env_marker_setting_test_suite( + name = "env_marker_setting_tests", +) diff --git a/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl new file mode 100644 index 00000000..549c15c2 --- /dev/null +++ b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl @@ -0,0 +1,69 @@ +"""env_marker_setting tests.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", "TestingAspectInfo") +load("//python/private/pypi:env_marker_setting.bzl", "env_marker_setting") # buildifier: disable=bzl-visibility +load("//tests/support:support.bzl", "PYTHON_VERSION") + +_tests = [] + +def _test_expr(name): + def impl(env, target): + env.expect.where( + expression = target[TestingAspectInfo].attrs.expression, + ).that_str( + target[config_common.FeatureFlagInfo].value, + ).equals( + env.ctx.attr.expected, + ) + + cases = { + "python_full_version_lt_negative": { + "config_settings": { + PYTHON_VERSION: "3.12.0", + }, + "expected": "FALSE", + "expression": "python_full_version < '3.8'", + }, + "python_version_gte": { + "config_settings": { + PYTHON_VERSION: "3.12.0", + }, + "expected": "TRUE", + "expression": "python_version >= '3.12.0'", + }, + } + + tests = [] + for case_name, case in cases.items(): + test_name = name + "_" + case_name + tests.append(test_name) + env_marker_setting( + name = test_name + "_subject", + expression = case["expression"], + ) + analysis_test( + name = test_name, + impl = impl, + target = test_name + "_subject", + config_settings = case["config_settings"], + attr_values = { + "expected": case["expected"], + }, + attrs = { + "expected": attr.string(), + }, + ) + native.test_suite( + name = name, + tests = tests, + ) + +_tests.append(_test_expr) + +def env_marker_setting_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) From ccbe5dcdb84a2c194deaf34165e43201e17a3826 Mon Sep 17 00:00:00 2001 From: Tobias Fuchs <9053039+devtbi@users.noreply.github.com> Date: Sat, 3 May 2025 05:22:48 +0200 Subject: [PATCH 09/23] py_wheel: always generate zip64-capable wheels (#2711) Currently, there is no possibility to pass the force zip64 option to the wheel creation. This hinders creation of packages that contain >2Gb files (e.g. large projects with debug symbols). To fix, always generate zip64 capable wheels. zip64 support is wide spread. Fixes https://github.com/bazel-contrib/rules_python/issues/2852 --------- Co-authored-by: Richard Levasseur Co-authored-by: Richard Levasseur --- CHANGELOG.md | 2 ++ examples/wheel/test_publish.py | 2 +- examples/wheel/wheel_test.py | 16 ++++++++-------- tools/wheelmaker.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fe636b..17e3cd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,12 +54,14 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed + * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform environments. * (rules) {obj}`pip_compile` now generates a `.test` target. The `_test` target is deprecated and will be removed in the next major release. ([#2794](https://github.com/bazel-contrib/rules_python/issues/2794) +* (py_wheel) py_wheel always creates zip64-capable wheel zips {#v0-0-0-fixed} ### Fixed diff --git a/examples/wheel/test_publish.py b/examples/wheel/test_publish.py index e6ec8072..7665629c 100644 --- a/examples/wheel/test_publish.py +++ b/examples/wheel/test_publish.py @@ -104,7 +104,7 @@ def test_upload_and_query_simple_api(self):

Links for example-minimal-library

- example_minimal_library-0.0.1-py3-none-any.whl
+ example_minimal_library-0.0.1-py3-none-any.whl
""" self.assertEqual( diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 43e56cfc..7f19ecd9 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -85,7 +85,7 @@ def test_py_library_wheel(self): ], ) self.assertFileSha256Equal( - filename, "a73acae23590c7a8d4365c888c1f12f0399b7af27169ea99fc7a00f402833926" + filename, "ef5afd9f6c3ff569ef7e5b2799d3a2ec9675d029414f341e0abd7254d6b9a25d" ) def test_py_package_wheel(self): @@ -110,7 +110,7 @@ def test_py_package_wheel(self): ], ) self.assertFileSha256Equal( - filename, "a76001500453dbd1d778821dcaba165d56db502c854cef9381dd3f8f89caee11" + filename, "39bec133cf79431e8d057eae550cd91aa9dfbddfedb53d98ebd36e3ade2753d0" ) def test_customized_wheel(self): @@ -206,7 +206,7 @@ def test_customized_wheel(self): second = second.main:s""", ) self.assertFileSha256Equal( - filename, "941c0d79f4ca67cfa0028248bd0606db7fc69953ff9c7c73ac26a3e6d3c23587" + filename, "685f68fc6665f53c9b769fd1ba12cce9937ab7f40ef4e60c82ef2de8653935de" ) def test_filename_escaping(self): @@ -278,7 +278,7 @@ def test_custom_package_root_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "7bd959b7efe9e325b30a6559177a1a4f22ac7a68fade310845916276110e9287" + filename, "2fbfc3baaf6fccca0f97d02316b8344507fe6c8136991a66ee5f162235adb19f" ) def test_custom_package_root_multi_prefix_wheel(self): @@ -312,7 +312,7 @@ def test_custom_package_root_multi_prefix_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "caf51e22bdcd3c6c766c8903319ce717daeb6caac577d14e16326a8597981854" + filename, "3e67971ca1e8a9ba36a143df7532e641f5661c56235e41d818309316c955ba58" ) def test_custom_package_root_multi_prefix_reverse_order_wheel(self): @@ -346,7 +346,7 @@ def test_custom_package_root_multi_prefix_reverse_order_wheel(self): for line in record_contents.splitlines(): self.assertFalse(line.startswith("/")) self.assertFileSha256Equal( - filename, "9e8c0baa408b829dec691a5e8d3bc040be0bbfcc95c0eee19e1e5ffadea4a059" + filename, "372ef9e11fb79f1952172993718a326b5adda192d94884b54377c34b44394982" ) def test_python_requires_wheel(self): @@ -371,7 +371,7 @@ def test_python_requires_wheel(self): """, ) self.assertFileSha256Equal( - filename, "b47f3eaf4f9fa4685a58c7415ba1feddd39635ae26c18473504f7d7e62e8ce07" + filename, "10a325ba8f77428b5cfcff6345d508f5eb77c140889eb62490d7382f60d4ebfe" ) def test_python_abi3_binary_wheel(self): @@ -436,7 +436,7 @@ def test_rule_creates_directory_and_is_included_in_wheel(self): ], ) self.assertFileSha256Equal( - filename, "d8e874b807e5574bd11a9312c58ce7fe7055afb80412d0d0e7ed21fc9223cd53" + filename, "85e44c43cc19ccae9fe2e1d629230203aa11791bed1f7f68a069fb58d1c93cd2" ) def test_rule_expands_workspace_status_keys_in_wheel_metadata(self): diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index de584650..8b775e15 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -154,7 +154,7 @@ def arcname_from(name): hash = hashlib.sha256() size = 0 with open(real_filename, "rb") as fsrc: - with self.open(zinfo, "w") as fdst: + with self.open(zinfo, "w", force_zip64=True) as fdst: while True: block = fsrc.read(2**20) if not block: From 4ccf5b23be8e2396b1fc358f1d83d1b7923c5ea7 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 3 May 2025 01:37:15 -0700 Subject: [PATCH 10/23] feat: allow specifying arbitrary constraints for local toolchains (#2829) This adds the ability for local toolchains to have arbitrary constraints set on them. This allows accomplishing two goals: 1. Makes it easier to enable/disable them on the command line, instead of having them entirely override an existing config and having to comment/uncomment the MODULE.bazel file sections. 2. Allows configuring them so that the repository is never initialized, which avoids the repository from being initialized during toolchain resolution, even if it will never match because of (1). --- .bazelci/presubmit.yml | 2 + CHANGELOG.md | 2 + docs/toolchains.md | 73 +++++++++++- .../private/local_runtime_toolchains_repo.bzl | 109 ++++++++++++++++++ python/private/py_toolchain_suite.bzl | 71 ++++++++++-- python/private/text_util.bzl | 5 + tests/integration/local_toolchains/.bazelrc | 2 + .../integration/local_toolchains/BUILD.bazel | 15 +++ .../integration/local_toolchains/MODULE.bazel | 13 +++ 9 files changed, 278 insertions(+), 14 deletions(-) diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 3b70734e..7e9d4dea 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -51,9 +51,11 @@ buildifier: test_flags: - "--noenable_bzlmod" - "--enable_workspace" + - "--test_tag_filters=-integration-test" build_flags: - "--noenable_bzlmod" - "--enable_workspace" + - "--build_tag_filters=-integration-test" bazel: 7.x .common_bazelinbazel_config: &common_bazelinbazel_config build_flags: diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e3cd3c..d9cb1445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ END_UNRELEASED_TEMPLATE * Repo utilities `execute_unchecked`, `execute_checked`, and `execute_checked_stdout` now support `log_stdout` and `log_stderr` keyword arg booleans. When these are `True` (the default), the subprocess's stdout/stderr will be logged. +* (toolchains) Local toolchains can be activated with custom flags. See + [Conditionally using local toolchains] docs for how to configure. {#v0-0-0-removed} ### Removed diff --git a/docs/toolchains.md b/docs/toolchains.md index 2f8db665..c8305e8f 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -377,15 +377,14 @@ local_runtime_repo( local_runtime_toolchains_repo( name = "local_toolchains", runtimes = ["local_python3"], + # TIP: The `target_settings` arg can be used to activate them based on + # command line flags; see docs below. ) # Step 3: Register the toolchains register_toolchains("@local_toolchains//:all", dev_dependency = True) ``` -Note that `register_toolchains` will insert the local toolchain earlier in the -toolchain ordering, so it will take precedence over other registered toolchains. - :::{important} Be sure to set `dev_dependency = True`. Using a local toolchain only makes sense for the root module. @@ -397,6 +396,72 @@ downstream modules. Multiple runtimes and/or toolchains can be defined, which allows for multiple Python versions and/or platforms to be configured in a single `MODULE.bazel`. +Note that `register_toolchains` will insert the local toolchain earlier in the +toolchain ordering, so it will take precedence over other registered toolchains. +To better control when the toolchain is used, see [Conditionally using local +toolchains] + +### Conditionally using local toolchains + +By default, a local toolchain has few constraints and is early in the toolchain +ordering, which means it will usually be used no matter what. This can be +problematic for CI (where it shouldn't be used), expensive for CI (CI must +initialize/download the repository to determine its Python version), and +annoying for iterative development (enabling/disabling it requires modifying +MODULE.bazel). + +These behaviors can be mitigated, but it requires additional configuration +to avoid triggering the local toolchain repository to initialize (i.e. run +local commands and perform downloads). + +The two settings to change are +{obj}`local_runtime_toolchains_repo.target_compatible_with` and +{obj}`local_runtime_toolchains_repo.target_settings`, which control how Bazel +decides if a toolchain should match. By default, they point to targets *within* +the local runtime repository (trigger repo initialization). We have to override +them to *not* reference the local runtime repository at all. + +In the example below, we reconfigure the local toolchains so they are only +activated if the custom flag `--//:py=local` is set and the target platform +matches the Bazel host platform. The net effect is CI won't use the local +toolchain (nor initialize its repository), and developers can easily +enable/disable the local toolchain with a command line flag. + +``` +# File: MODULE.bazel +bazel_dep(name = "bazel_skylib", version = "1.7.1") + +local_runtime_toolchains_repo( + name = "local_toolchains", + runtimes = ["local_python3"], + target_compatible_with = { + "local_python3": ["HOST_CONSTRAINTS"], + }, + target_settings = { + "local_python3": ["@//:is_py_local"] + } +) + +# File: BUILD.bazel +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") + +config_setting( + name = "is_py_local", + flag_values = {":py": "local"}, +) + +string_flag( + name = "py", + build_setting_default = "", +) +``` + +:::{tip} +Easily switching between *multiple* local toolchains can be accomplished by +adding additional `:is_py_X` targets and setting `--//:py` to match. +to easily switch between different local toolchains. +::: + ## Runtime environment toolchain @@ -425,7 +490,7 @@ locally installed Python. ### Autodetecting toolchain The autodetecting toolchain is a deprecated toolchain that is built into Bazel. -It's name is a bit misleading: it doesn't autodetect anything. All it does is +**It's name is a bit misleading: it doesn't autodetect anything**. All it does is use `python3` from the environment a binary runs within. This provides extremely limited functionality to the rules (at build time, nothing is knowable about the Python runtime). diff --git a/python/private/local_runtime_toolchains_repo.bzl b/python/private/local_runtime_toolchains_repo.bzl index adb3bb56..004ca664 100644 --- a/python/private/local_runtime_toolchains_repo.bzl +++ b/python/private/local_runtime_toolchains_repo.bzl @@ -26,6 +26,9 @@ define_local_toolchain_suites( name = "toolchains", version_aware_repo_names = {version_aware_names}, version_unaware_repo_names = {version_unaware_names}, + repo_exec_compatible_with = {repo_exec_compatible_with}, + repo_target_compatible_with = {repo_target_compatible_with}, + repo_target_settings = {repo_target_settings}, ) """ @@ -39,6 +42,9 @@ def _local_runtime_toolchains_repo(rctx): rctx.file("BUILD.bazel", _TOOLCHAIN_TEMPLATE.format( version_aware_names = render.list(rctx.attr.runtimes), + repo_target_settings = render.string_list_dict(rctx.attr.target_settings), + repo_target_compatible_with = render.string_list_dict(rctx.attr.target_compatible_with), + repo_exec_compatible_with = render.string_list_dict(rctx.attr.exec_compatible_with), version_unaware_names = render.list(rctx.attr.default_runtimes or rctx.attr.runtimes), )) @@ -62,8 +68,36 @@ These will be defined as *version-unaware* toolchains. This means they will match any Python version. As such, they are registered after the version-aware toolchains defined by the `runtimes` attribute. +If not set, then the `runtimes` values will be used. + Note that order matters: it determines the toolchain priority within the package. +""", + ), + "exec_compatible_with": attr.string_list_dict( + doc = """ +Constraints that must be satisfied by an exec platform for a toolchain to be used. + +This is a `dict[str, list[str]]`, where the keys are repo names from the +`runtimes` or `default_runtimes` args, and the values are constraint +target labels (e.g. OS, CPU, etc). + +:::{note} +Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is +needed because the strings are evaluated in a different context than where +they originate. +::: + +The list of settings become the {obj}`toolchain.exec_compatible_with` value for +each respective repo. + +This allows a local toolchain to only be used if certain exec platform +conditions are met, typically values from `@platforms`. + +See the [Local toolchains] docs for examples and further information. + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "runtimes": attr.string_list( @@ -76,6 +110,81 @@ are registered before `default_runtimes`. Note that order matters: it determines the toolchain priority within the package. +""", + ), + "target_compatible_with": attr.string_list_dict( + doc = """ +Constraints that must be satisfied for a toolchain to be used. + + +This is a `dict[str, list[str]]`, where the keys are repo names from the +`runtimes` or `default_runtimes` args, and the values are constraint +target labels (e.g. OS, CPU, etc), or the special string `"HOST_CONSTRAINTS"` +(which will be replaced with the current Bazel hosts's constraints). + +If a repo's entry is missing or empty, it defaults to the supported OS the +underlying runtime repository detects as compatible. + +:::{note} +Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is +needed because the strings are evaluated in a different context than where +they originate. +::: + +The list of settings **becomes the** the {obj}`toolchain.target_compatible_with` +value for each respective repo; i.e. they _replace_ the auto-detected values +the local runtime itself computes. + +This allows a local toolchain to only be used if certain target platform +conditions are met, typically values from `@platforms`. + +See the [Local toolchains] docs for examples and further information. + +:::{seealso} +The `target_settings` attribute, which handles `config_setting` values, +instead of constraints. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + ), + "target_settings": attr.string_list_dict( + doc = """ +Config settings that must be satisfied for a toolchain to be used. + +This is a `dict[str, list[str]]`, where the keys are repo names from the +`runtimes` or `default_runtimes` args, and the values are {obj}`config_setting()` +target labels. + +If a repo's entry is missing or empty, it will default to +`@//:is_match_python_version` (for repos in `runtimes`) or an empty list +(for repos in `default_runtimes`). + +:::{note} +Specify `@//foo:bar`, not simply `//foo:bar` or `:bar`. The additional `@` is +needed because the strings are evaluated in a different context than where +they originate. +::: + +The list of settings will be applied atop of any of the local runtime's +settings that are used for {obj}`toolchain.target_settings`. i.e. they are +evaluated first and guard the checking of the local runtime's auto-detected +conditions. + +This allows a local toolchain to only be used if certain flags or +config setting conditions are met. Such conditions can include user-defined +flags, platform constraints, etc. + +See the [Local toolchains] docs for examples and further information. + +:::{seealso} +The `target_compatible_with` attribute, which handles *constraint* values, +instead of `config_settings`. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "_rule_name": attr.string(default = "local_toolchains_repo"), diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl index a69be376..e71882da 100644 --- a/python/private/py_toolchain_suite.bzl +++ b/python/private/py_toolchain_suite.bzl @@ -15,6 +15,7 @@ """Create the toolchain defs in a BUILD.bazel file.""" load("@bazel_skylib//lib:selects.bzl", "selects") +load("@platforms//host:constraints.bzl", "HOST_CONSTRAINTS") load(":text_util.bzl", "render") load( ":toolchain_types.bzl", @@ -95,9 +96,15 @@ def py_toolchain_suite( runtime_repo_name = user_repository_name, target_settings = target_settings, target_compatible_with = target_compatible_with, + exec_compatible_with = [], ) -def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, target_settings): +def _internal_toolchain_suite( + prefix, + runtime_repo_name, + target_compatible_with, + target_settings, + exec_compatible_with): native.toolchain( name = "{prefix}_toolchain".format(prefix = prefix), toolchain = "@{runtime_repo_name}//:python_runtimes".format( @@ -106,6 +113,7 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, toolchain_type = TARGET_TOOLCHAIN_TYPE, target_settings = target_settings, target_compatible_with = target_compatible_with, + exec_compatible_with = exec_compatible_with, ) native.toolchain( @@ -116,6 +124,7 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, toolchain_type = PY_CC_TOOLCHAIN_TYPE, target_settings = target_settings, target_compatible_with = target_compatible_with, + exec_compatible_with = exec_compatible_with, ) native.toolchain( @@ -142,7 +151,13 @@ def _internal_toolchain_suite(prefix, runtime_repo_name, target_compatible_with, # call in python/repositories.bzl. Bzlmod doesn't need anything; it will # register `:all`. -def define_local_toolchain_suites(name, version_aware_repo_names, version_unaware_repo_names): +def define_local_toolchain_suites( + name, + version_aware_repo_names, + version_unaware_repo_names, + repo_exec_compatible_with, + repo_target_compatible_with, + repo_target_settings): """Define toolchains for `local_runtime_repo` backed toolchains. This generates `toolchain` targets that can be registered using `:all`. The @@ -156,24 +171,60 @@ def define_local_toolchain_suites(name, version_aware_repo_names, version_unawar version-aware toolchains defined. version_unaware_repo_names: `list[str]` of the repo names that will have version-unaware toolchains defined. + repo_target_settings: {type}`dict[str, list[str]]` mapping of repo names + to string labels that are added to the `target_settings` for the + respective repo's toolchain. + repo_target_compatible_with: {type}`dict[str, list[str]]` mapping of repo names + to string labels that are added to the `target_compatible_with` for + the respective repo's toolchain. + repo_exec_compatible_with: {type}`dict[str, list[str]]` mapping of repo names + to string labels that are added to the `exec_compatible_with` for + the respective repo's toolchain. """ + i = 0 for i, repo in enumerate(version_aware_repo_names, start = i): - prefix = render.left_pad_zero(i, 4) + target_settings = ["@{}//:is_matching_python_version".format(repo)] + + if repo_target_settings.get(repo): + selects.config_setting_group( + name = "_{}_user_guard".format(repo), + match_all = repo_target_settings.get(repo, []) + target_settings, + ) + target_settings = ["_{}_user_guard".format(repo)] _internal_toolchain_suite( - prefix = prefix, + prefix = render.left_pad_zero(i, 4), runtime_repo_name = repo, - target_compatible_with = ["@{}//:os".format(repo)], - target_settings = ["@{}//:is_matching_python_version".format(repo)], + target_compatible_with = _get_local_toolchain_target_compatible_with( + repo, + repo_target_compatible_with, + ), + target_settings = target_settings, + exec_compatible_with = repo_exec_compatible_with.get(repo, []), ) # The version unaware entries must go last because they will match any Python # version. for i, repo in enumerate(version_unaware_repo_names, start = i + 1): - prefix = render.left_pad_zero(i, 4) _internal_toolchain_suite( - prefix = prefix, + prefix = render.left_pad_zero(i, 4) + "_default", runtime_repo_name = repo, - target_settings = [], - target_compatible_with = ["@{}//:os".format(repo)], + target_compatible_with = _get_local_toolchain_target_compatible_with( + repo, + repo_target_compatible_with, + ), + # We don't call _get_local_toolchain_target_settings because that + # will add the version matching condition by default. + target_settings = repo_target_settings.get(repo, []), + exec_compatible_with = repo_exec_compatible_with.get(repo, []), ) + +def _get_local_toolchain_target_compatible_with(repo, repo_target_compatible_with): + if repo in repo_target_compatible_with: + target_compatible_with = repo_target_compatible_with[repo] + if "HOST_CONSTRAINTS" in target_compatible_with: + target_compatible_with.remove("HOST_CONSTRAINTS") + target_compatible_with.extend(HOST_CONSTRAINTS) + else: + target_compatible_with = ["@{}//:os".format(repo)] + return target_compatible_with diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl index a64b5d62..28979d89 100644 --- a/python/private/text_util.bzl +++ b/python/private/text_util.bzl @@ -108,6 +108,10 @@ def _render_list(items, *, hanging_indent = ""): def _render_str(value): return repr(value) +def _render_string_list_dict(value): + """Render an attr.string_list_dict value (`dict[str, list[str]`)""" + return _render_dict(value, value_repr = _render_list) + def _render_tuple(items, *, value_repr = repr): if not items: return "tuple()" @@ -166,4 +170,5 @@ render = struct( str = _render_str, toolchain_prefix = _toolchain_prefix, tuple = _render_tuple, + string_list_dict = _render_string_list_dict, ) diff --git a/tests/integration/local_toolchains/.bazelrc b/tests/integration/local_toolchains/.bazelrc index 39df41d9..aed08b07 100644 --- a/tests/integration/local_toolchains/.bazelrc +++ b/tests/integration/local_toolchains/.bazelrc @@ -4,3 +4,5 @@ test --test_output=errors # Windows requires these for multi-python support: build --enable_runfiles common:bazel7.x --incompatible_python_disallow_native_rules +build --//:py=local +common --announce_rc diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index 02b126b0..6b731181 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@rules_python//python:py_test.bzl", "py_test") py_test( @@ -20,3 +21,17 @@ py_test( # Make this test better respect pyenv env_inherit = ["PYENV_VERSION"], ) + +config_setting( + name = "is_py_local", + flag_values = { + ":py": "local", + }, +) + +# Set `--//:py=local` to use the local toolchain +# (This is set in this example's .bazelrc) +string_flag( + name = "py", + build_setting_default = "", +) diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 98f1ed9a..6c06909c 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -14,6 +14,9 @@ module(name = "module_under_test") bazel_dep(name = "rules_python", version = "0.0.0") +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "platforms", version = "0.0.11") + local_path_override( module_name = "rules_python", path = "../../..", @@ -32,6 +35,16 @@ local_runtime_repo( local_runtime_toolchains_repo( name = "local_toolchains", runtimes = ["local_python3"], + target_compatible_with = { + "local_python3": [ + "HOST_CONSTRAINTS", + ], + }, + target_settings = { + "local_python3": [ + "@//:is_py_local", + ], + }, ) python = use_extension("@rules_python//python/extensions:python.bzl", "python") From a4b946bbe1b3e83ca4602a0d059fea823b0ded65 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Mon, 5 May 2025 14:22:38 +0900 Subject: [PATCH 11/23] feat: add an env variable to toggle pipstar (#2855) This is a flag to start leveraging of the new code paths. The Starlark implementation has been added in 1.4 and has been reverted in the latest release candidates. The `env` variable will be a good way to roll it out more gradually and get more testing. For now we are switching only the `whl_library` internals as the `requirements.txt` files from `uv` may use `*` in `python_full_version` and `platform_version` that are not yet fully supported (#2826). Main goals for this is to start using Starlark implementation so that we don't have any hidden variables. What is more, having this in Starlark is the most maintainable long-term solution for supporting cross-platform builds. Work towards #260 --------- Co-authored-by: Richard Levasseur --- CHANGELOG.md | 3 + docs/environment-variables.md | 9 + python/private/internal_config_repo.bzl | 4 + .../private/pypi/whl_installer/arguments.py | 5 + .../pypi/whl_installer/wheel_installer.py | 44 ++-- python/private/pypi/whl_library.bzl | 207 ++++++++++++------ .../whl_installer/wheel_installer_test.py | 1 + 7 files changed, 187 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9cb1445..7d73613a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,9 @@ END_UNRELEASED_TEMPLATE (the default), the subprocess's stdout/stderr will be logged. * (toolchains) Local toolchains can be activated with custom flags. See [Conditionally using local toolchains] docs for how to configure. +* (pypi) `RULES_PYTHON_ENABLE_PIPSTAR` environment variable: when `1`, the Starlark + implementation of wheel METADATA parsing is used (which has improved multi-platform + build support). {#v0-0-0-removed} ### Removed diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 49fdf766..26c17109 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -60,6 +60,15 @@ The default became `1` if unspecified ::: :::: +::::{envvar} RULES_PYTHON_ENABLE_PIPSTAR + +When `1`, the rules_python Starlark implementation of the pypi/pip integration is used +instead of the legacy Python scripts. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +:::: + ::::{envvar} RULES_PYTHON_EXTRACT_ROOT Directory to use as the root for creating files necessary for bootstrapping so diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl index a5c47871..cfe2fdfd 100644 --- a/python/private/internal_config_repo.bzl +++ b/python/private/internal_config_repo.bzl @@ -20,6 +20,8 @@ settings for rules to later use. load(":repo_utils.bzl", "repo_utils") +_ENABLE_PIPSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PIPSTAR" +_ENABLE_PIPSTAR_DEFAULT = "0" _ENABLE_PYSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PYSTAR" _ENABLE_PYSTAR_DEFAULT = "1" _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME = "RULES_PYTHON_DEPRECATION_WARNINGS" @@ -28,6 +30,7 @@ _ENABLE_DEPRECATION_WARNINGS_DEFAULT = "0" _CONFIG_TEMPLATE = """\ config = struct( enable_pystar = {enable_pystar}, + enable_pipstar = {enable_pipstar}, enable_deprecation_warnings = {enable_deprecation_warnings}, BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}), BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}), @@ -84,6 +87,7 @@ def _internal_config_repo_impl(rctx): rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format( enable_pystar = enable_pystar, + enable_pipstar = _bool_from_environ(rctx, _ENABLE_PIPSTAR_ENVVAR_NAME, _ENABLE_PIPSTAR_DEFAULT), enable_deprecation_warnings = _bool_from_environ(rctx, _ENABLE_DEPRECATION_WARNINGS_ENVVAR_NAME, _ENABLE_DEPRECATION_WARNINGS_DEFAULT), builtin_py_info_symbol = builtin_py_info_symbol, builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol, diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index 29bea802..ea609bef 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -47,6 +47,11 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: type=Platform.from_string, help="Platforms to target dependencies. Can be used multiple times.", ) + parser.add_argument( + "--enable-pipstar", + action="store_true", + help="Disable certain code paths if we expect to process the whl in Starlark.", + ) parser.add_argument( "--pip_data_exclude", action="store", diff --git a/python/private/pypi/whl_installer/wheel_installer.py b/python/private/pypi/whl_installer/wheel_installer.py index a48df699..2db03e03 100644 --- a/python/private/pypi/whl_installer/wheel_installer.py +++ b/python/private/pypi/whl_installer/wheel_installer.py @@ -104,6 +104,7 @@ def _setup_namespace_pkg_compatibility(wheel_dir: str) -> None: def _extract_wheel( wheel_file: str, extras: Dict[str, Set[str]], + enable_pipstar: bool, enable_implicit_namespace_pkgs: bool, platforms: List[wheel.Platform], installation_dir: Path = Path("."), @@ -114,6 +115,7 @@ def _extract_wheel( wheel_file: the filepath of the .whl installation_dir: the destination directory for installation of the wheel. extras: a list of extras to add as dependencies for the installed wheel + enable_pipstar: if true, turns off certain operations. enable_implicit_namespace_pkgs: if true, disables conversion of implicit namespace packages and will unzip as-is """ @@ -123,26 +125,31 @@ def _extract_wheel( if not enable_implicit_namespace_pkgs: _setup_namespace_pkg_compatibility(installation_dir) - extras_requested = extras[whl.name] if whl.name in extras else set() - - dependencies = whl.dependencies(extras_requested, platforms) + metadata = { + "python_version": f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}", + "entry_points": [ + { + "name": name, + "module": module, + "attribute": attribute, + } + for name, (module, attribute) in sorted(whl.entry_points().items()) + ], + } + if not enable_pipstar: + extras_requested = extras[whl.name] if whl.name in extras else set() + dependencies = whl.dependencies(extras_requested, platforms) + + metadata.update( + { + "name": whl.name, + "version": whl.version, + "deps": dependencies.deps, + "deps_by_platform": dependencies.deps_select, + } + ) with open(os.path.join(installation_dir, "metadata.json"), "w") as f: - metadata = { - "name": whl.name, - "version": whl.version, - "deps": dependencies.deps, - "python_version": f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}", - "deps_by_platform": dependencies.deps_select, - "entry_points": [ - { - "name": name, - "module": module, - "attribute": attribute, - } - for name, (module, attribute) in sorted(whl.entry_points().items()) - ], - } json.dump(metadata, f) @@ -161,6 +168,7 @@ def main() -> None: _extract_wheel( wheel_file=whl, extras=extras, + enable_pipstar=args.enable_pipstar, enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, platforms=arguments.get_platforms(args), ) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 0c09f796..160bb5b7 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -14,6 +14,7 @@ "" +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth") load("//python/private:envsubst.bzl", "envsubst") load("//python/private:is_standalone_interpreter.bzl", "is_standalone_interpreter") @@ -21,9 +22,11 @@ load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") load(":attrs.bzl", "ATTRS", "use_isolated") load(":deps.bzl", "all_repo_names", "record_files") load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") +load(":parse_requirements.bzl", "host_platform") load(":parse_whl_name.bzl", "parse_whl_name") load(":patch_whl.bzl", "patch_whl") load(":pypi_repo_utils.bzl", "pypi_repo_utils") +load(":whl_metadata.bzl", "whl_metadata") load(":whl_target_platforms.bzl", "whl_target_platforms") _CPPFLAGS = "CPPFLAGS" @@ -340,79 +343,147 @@ def _whl_library_impl(rctx): timeout = rctx.attr.timeout, ) - target_platforms = rctx.attr.experimental_target_platforms or [] - if target_platforms: - parsed_whl = parse_whl_name(whl_path.basename) - - # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we - # only include deps for that target platform - if parsed_whl.platform_tag != "any": - target_platforms = [ - p.target_platform - for p in whl_target_platforms( - platform_tag = parsed_whl.platform_tag, - abi_tag = parsed_whl.abi_tag.strip("tm"), - ) - ] - - pypi_repo_utils.execute_checked( - rctx, - op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), - python = python_interpreter, - arguments = args + [ - "--whl-file", - whl_path, - ] + ["--platform={}".format(p) for p in target_platforms], - srcs = rctx.attr._python_srcs, - environment = environment, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - logger = logger, - ) + if rp_config.enable_pipstar: + pypi_repo_utils.execute_checked( + rctx, + op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), + python = python_interpreter, + arguments = args + [ + "--whl-file", + whl_path, + "--enable-pipstar", + ], + srcs = rctx.attr._python_srcs, + environment = environment, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + logger = logger, + ) - metadata = json.decode(rctx.read("metadata.json")) - rctx.delete("metadata.json") + metadata = json.decode(rctx.read("metadata.json")) + rctx.delete("metadata.json") + python_version = metadata["python_version"] - # NOTE @aignas 2024-06-22: this has to live on until we stop supporting - # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. - # - # See ../../packaging.bzl line 190 - entry_points = {} - for item in metadata["entry_points"]: - name = item["name"] - module = item["module"] - attribute = item["attribute"] - - # There is an extreme edge-case with entry_points that end with `.py` - # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174 - entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name - entry_point_target_name = ( - _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py + # NOTE @aignas 2024-06-22: this has to live on until we stop supporting + # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. + # + # See ../../packaging.bzl line 190 + entry_points = {} + for item in metadata["entry_points"]: + name = item["name"] + module = item["module"] + attribute = item["attribute"] + + # There is an extreme edge-case with entry_points that end with `.py` + # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174 + entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name + entry_point_target_name = ( + _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py + ) + entry_point_script_name = entry_point_target_name + ".py" + + rctx.file( + entry_point_script_name, + _generate_entry_point_contents(module, attribute), + ) + entry_points[entry_point_without_py] = entry_point_script_name + + metadata = whl_metadata( + install_dir = whl_path.dirname.get_child("site-packages"), + read_fn = rctx.read, + logger = logger, ) - entry_point_script_name = entry_point_target_name + ".py" - rctx.file( - entry_point_script_name, - _generate_entry_point_contents(module, attribute), + build_file_contents = generate_whl_library_build_bazel( + name = whl_path.basename, + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), + entry_points = entry_points, + metadata_name = metadata.name, + metadata_version = metadata.version, + default_python_version = python_version, + requires_dist = metadata.requires_dist, + target_platforms = rctx.attr.experimental_target_platforms or [host_platform(rctx)], + # TODO @aignas 2025-04-14: load through the hub: + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, ) - entry_points[entry_point_without_py] = entry_point_script_name - - build_file_contents = generate_whl_library_build_bazel( - name = whl_path.basename, - dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), - entry_points = entry_points, - # TODO @aignas 2025-04-14: load through the hub: - dependencies = metadata["deps"], - dependencies_by_platform = metadata["deps_by_platform"], - annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), - data_exclude = rctx.attr.pip_data_exclude, - group_deps = rctx.attr.group_deps, - group_name = rctx.attr.group_name, - tags = [ - "pypi_name={}".format(metadata["name"]), - "pypi_version={}".format(metadata["version"]), - ], - ) + else: + target_platforms = rctx.attr.experimental_target_platforms or [] + if target_platforms: + parsed_whl = parse_whl_name(whl_path.basename) + + # NOTE @aignas 2023-12-04: if the wheel is a platform specific wheel, we + # only include deps for that target platform + if parsed_whl.platform_tag != "any": + target_platforms = [ + p.target_platform + for p in whl_target_platforms( + platform_tag = parsed_whl.platform_tag, + abi_tag = parsed_whl.abi_tag.strip("tm"), + ) + ] + + pypi_repo_utils.execute_checked( + rctx, + op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path), + python = python_interpreter, + arguments = args + [ + "--whl-file", + whl_path, + ] + ["--platform={}".format(p) for p in target_platforms], + srcs = rctx.attr._python_srcs, + environment = environment, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + logger = logger, + ) + + metadata = json.decode(rctx.read("metadata.json")) + rctx.delete("metadata.json") + + # NOTE @aignas 2024-06-22: this has to live on until we stop supporting + # passing `twine` as a `:pkg` library via the `WORKSPACE` builds. + # + # See ../../packaging.bzl line 190 + entry_points = {} + for item in metadata["entry_points"]: + name = item["name"] + module = item["module"] + attribute = item["attribute"] + + # There is an extreme edge-case with entry_points that end with `.py` + # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174 + entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name + entry_point_target_name = ( + _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py + ) + entry_point_script_name = entry_point_target_name + ".py" + + rctx.file( + entry_point_script_name, + _generate_entry_point_contents(module, attribute), + ) + entry_points[entry_point_without_py] = entry_point_script_name + + build_file_contents = generate_whl_library_build_bazel( + name = whl_path.basename, + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix), + entry_points = entry_points, + # TODO @aignas 2025-04-14: load through the hub: + dependencies = metadata["deps"], + dependencies_by_platform = metadata["deps_by_platform"], + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, + tags = [ + "pypi_name={}".format(metadata["name"]), + "pypi_version={}".format(metadata["version"]), + ], + ) + rctx.file("BUILD.bazel", build_file_contents) return diff --git a/tests/pypi/whl_installer/wheel_installer_test.py b/tests/pypi/whl_installer/wheel_installer_test.py index b736877e..e8380479 100644 --- a/tests/pypi/whl_installer/wheel_installer_test.py +++ b/tests/pypi/whl_installer/wheel_installer_test.py @@ -72,6 +72,7 @@ def test_wheel_exists(self) -> None: extras={}, enable_implicit_namespace_pkgs=False, platforms=[], + enable_pipstar = False, ) want_files = [ From 78647318f94b3a94e11b77f03e0314bd77e1e0fe Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 5 May 2025 18:27:27 +0200 Subject: [PATCH 12/23] fix: add target platform to extra exec platforms in analysis tests (#2861) This is required as of https://github.com/bazelbuild/bazel/commit/2780393d35ad0607cf5e344ae082b00a5569a964 as tests now require an execution platform that matches their target constraints by default. Fixes #2850 --- tests/base_rules/py_executable_base_tests.bzl | 2 ++ tests/base_rules/py_test/py_test_tests.bzl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index 37707831..55a8958b 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -51,6 +51,7 @@ def _test_basic_windows(name, config): "//command_line_option:build_python_zip": "true", "//command_line_option:cpu": "windows_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [WINDOWS_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], "//command_line_option:platforms": [WINDOWS_X86_64], }, @@ -96,6 +97,7 @@ def _test_basic_zip(name, config): "//command_line_option:build_python_zip": "true", "//command_line_option:cpu": "linux_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [LINUX_X86_64], "//command_line_option:extra_toolchains": [CC_TOOLCHAIN], "//command_line_option:platforms": [LINUX_X86_64], }, diff --git a/tests/base_rules/py_test/py_test_tests.bzl b/tests/base_rules/py_test/py_test_tests.bzl index d4d839b3..c51aa53a 100644 --- a/tests/base_rules/py_test/py_test_tests.bzl +++ b/tests/base_rules/py_test/py_test_tests.bzl @@ -59,6 +59,7 @@ def _test_mac_requires_darwin_for_execution(name, config): config_settings = { "//command_line_option:cpu": "darwin_x86_64", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [MAC_X86_64], "//command_line_option:extra_toolchains": CC_TOOLCHAIN, "//command_line_option:platforms": [MAC_X86_64], }, @@ -92,6 +93,7 @@ def _test_non_mac_doesnt_require_darwin_for_execution(name, config): config_settings = { "//command_line_option:cpu": "k8", "//command_line_option:crosstool_top": CROSSTOOL_TOP, + "//command_line_option:extra_execution_platforms": [LINUX_X86_64], "//command_line_option:extra_toolchains": CC_TOOLCHAIN, "//command_line_option:platforms": [LINUX_X86_64], }, From 1492ae4b53c6ace19cfc67f542b574b2ccd7e40b Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 5 May 2025 18:29:59 +0200 Subject: [PATCH 13/23] fix: configure coverage helpers for test exec group (#2857) They are run on the test action's execution platform, which is resolved for the `test` exec group, not the default one. --- python/private/attributes.bzl | 4 ++-- python/private/py_executable.bzl | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index 8543caba..98aba4eb 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -397,14 +397,14 @@ COVERAGE_ATTRS = { "_collect_cc_coverage": lambda: attrb.Label( default = "@bazel_tools//tools/test:collect_cc_coverage", executable = True, - cfg = "exec", + cfg = config.exec(exec_group = "test"), ), # Magic attribute to make coverage work. There's no # docs about this; see TestActionBuilder.java "_lcov_merger": lambda: attrb.Label( default = configuration_field(fragment = "coverage", name = "output_generator"), executable = True, - cfg = "exec", + cfg = config.exec(exec_group = "test"), ), } diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index a8c669af..24be8dd2 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -78,7 +78,6 @@ EXECUTABLE_ATTRS = dicts.add( AGNOSTIC_EXECUTABLE_ATTRS, PY_SRCS_ATTRS, IMPORTS_ATTRS, - COVERAGE_ATTRS, { "interpreter_args": lambda: attrb.StringList( doc = """ @@ -1903,7 +1902,7 @@ def create_executable_rule_builder(implementation, **kwargs): """ builder = ruleb.Rule( implementation = implementation, - attrs = EXECUTABLE_ATTRS, + attrs = EXECUTABLE_ATTRS | (COVERAGE_ATTRS if kwargs.get("test") else {}), exec_groups = dict(REQUIRED_EXEC_GROUP_BUILDERS), # Mutable copy fragments = ["py", "bazel_py"], provides = [PyExecutableInfo, PyInfo] + _MaybeBuiltinPyInfo, From 63555e1fdf708b6a44f166aa5a3dfa344325e0d0 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Tue, 6 May 2025 10:34:20 +0200 Subject: [PATCH 14/23] fix: fix test analysis error on macOS arm64 (#2860) Fixes: ``` ERROR: /Users/fmeum/git/rules_python/tests/pypi/env_marker_setting/BUILD.bazel:3:30: Illegal ambiguous match on configurable attribute "platform_machine" in //tests/pypi/env_marker_setting:test_expr_python_full_version_lt_negative_subject: @@platforms//cpu:aarch64 @@platforms//cpu:arm64 Multiple matches are not allowed unless one is unambiguously more specialized or they resolve to the same value. See https://bazel.build/reference/be/functions#select. ``` Work towards #2850. Work towards #2826. --- python/private/pypi/pep508_env.bzl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index 3708c46f..d6185356 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -29,11 +29,13 @@ platform_machine_aliases = { # NOTE: There are many cpus, and unfortunately, the value isn't directly # accessible to Starlark. Using CcToolchain.cpu might work, though. +# Some targets are aliases and are omitted below as their value is implied +# by the target they resolve to. platform_machine_select_map = { "@platforms//cpu:aarch32": "aarch32", "@platforms//cpu:aarch64": "aarch64", - "@platforms//cpu:arm": "arm", - "@platforms//cpu:arm64": "arm64", + # @platforms//cpu:arm is an alias for @platforms//cpu:aarch32 + # @platforms//cpu:arm64 is an alias for @platforms//cpu:aarch64 "@platforms//cpu:arm64_32": "arm64_32", "@platforms//cpu:arm64e": "arm64e", "@platforms//cpu:armv6-m": "armv6-m", From 0b3d845ed1803ed27083f850c7c542bd2d3fc52c Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 6 May 2025 01:38:02 -0700 Subject: [PATCH 15/23] refactor: make env marker config available through target and flag (#2853) This factors creation of (most of) the env marker dict into a separate target and provides a label flag to allow customizing the target that provides it. This makes it easier for users to override how env marker values are computed. The `env_marker_setting` rule will still, if necessary, compute values from the toolchain, but existing keys (computed from the env marker config target) have precedence. The `EnvMarkerInfo` provider is the interface for implementing a custom env marker config target; it will be publically exposed in a subsequent PR. Along the way, unify how the env dict and defaults are set. Work towards https://github.com/bazel-contrib/rules_python/issues/2826 --- .../python/config_settings/index.md | 12 ++ docs/pypi-dependencies.md | 32 ++++- python/config_settings/BUILD.bazel | 7 ++ python/private/pypi/BUILD.bazel | 19 +++ python/private/pypi/env_marker_info.bzl | 26 ++++ python/private/pypi/env_marker_setting.bzl | 104 +++++----------- python/private/pypi/flags.bzl | 68 ++++++++++ python/private/pypi/pep508_env.bzl | 117 +++++++++++------- .../env_marker_setting_tests.bzl | 37 +++++- tests/support/support.bzl | 1 + 10 files changed, 300 insertions(+), 123 deletions(-) create mode 100644 python/private/pypi/env_marker_info.bzl diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index ed644429..f4618ff9 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -159,6 +159,18 @@ Values: ::: :::: +::::{bzl:flag} pip_env_marker_config +The target that provides the values for pip env marker evaluation. + +Default: `//python/config_settings:_pip_env_marker_default_config` + +This flag points to a target providing {obj}`EnvMarkerInfo`, which determines +the values used when environment markers are resolved at build time. + +:::{versionadded} VERSION_NEXT_FEATURE +::: +:::: + ::::{bzl:flag} pip_whl Set what distributions are used in the `pip` integration. diff --git a/docs/pypi-dependencies.md b/docs/pypi-dependencies.md index 4ec40bc8..b3ae7fe5 100644 --- a/docs/pypi-dependencies.md +++ b/docs/pypi-dependencies.md @@ -338,7 +338,6 @@ leg of the dependency manually. For instance by making perhaps `apache-airflow-providers-common-sql`. -(bazel-downloader)= ### Multi-platform support Multi-platform support of cross-building the wheels can be done in two ways - either @@ -391,6 +390,31 @@ compatible indexes. This is only supported on `bzlmd`. ``` + + (bazel-downloader)= ### Bazel downloader and multi-platform wheel hub repository. @@ -487,3 +511,9 @@ Bazel will call this file like `cred_helper.sh get` and use the returned JSON to into whatever HTTP(S) request it performs against `example.com`. [rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 + + diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 872d7d1b..24bbe665 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -220,3 +220,10 @@ string_flag( define_pypi_internal_flags( name = "define_pypi_internal_flags", ) + +label_flag( + name = "pip_env_marker_config", + build_setting_default = ":_pip_env_marker_default_config", + # NOTE: Only public because it is used in pip hub repos. + visibility = ["//visibility:public"], +) diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 92161348..d5d897ef 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -71,6 +71,23 @@ bzl_library( ], ) +bzl_library( + name = "env_marker_info_bzl", + srcs = ["env_marker_info.bzl"], +) + +bzl_library( + name = "env_marker_setting_bzl", + srcs = ["env_marker_setting.bzl"], + deps = [ + ":env_marker_info_bzl", + ":pep508_env_bzl", + ":pep508_evaluate_bzl", + "//python/private:toolchain_types_bzl", + "@bazel_skylib//rules:common_settings", + ], +) + bzl_library( name = "evaluate_markers_bzl", srcs = ["evaluate_markers.bzl"], @@ -111,6 +128,8 @@ bzl_library( name = "flags_bzl", srcs = ["flags.bzl"], deps = [ + ":env_marker_info.bzl", + ":pep508_env_bzl", "//python/private:enum_bzl", "@bazel_skylib//rules:common_settings", ], diff --git a/python/private/pypi/env_marker_info.bzl b/python/private/pypi/env_marker_info.bzl new file mode 100644 index 00000000..b483436d --- /dev/null +++ b/python/private/pypi/env_marker_info.bzl @@ -0,0 +1,26 @@ +"""Provider for implementing environment marker values.""" + +EnvMarkerInfo = provider( + doc = """ +The values to use during environment marker evaluation. + +:::{seealso} +The {obj}`--//python/config_settings:pip_env_marker_config` flag. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +""", + fields = { + "env": """ +:type: dict[str, str] + +The values to use for environment markers when evaluating an expression. + +The keys and values should be compatible with the [PyPA dependency specifiers +specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/) + +Missing values will be set to the specification's defaults or computed using +available toolchain information. +""", + }, +) diff --git a/python/private/pypi/env_marker_setting.bzl b/python/private/pypi/env_marker_setting.bzl index bbc59ab1..2bfdf42e 100644 --- a/python/private/pypi/env_marker_setting.bzl +++ b/python/private/pypi/env_marker_setting.bzl @@ -2,14 +2,8 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") -load( - ":pep508_env.bzl", - "env_aliases", - "os_name_select_map", - "platform_machine_select_map", - "platform_system_select_map", - "sys_platform_select_map", -) +load(":env_marker_info.bzl", "EnvMarkerInfo") +load(":pep508_env.bzl", "create_env", "set_missing_env_defaults") load(":pep508_evaluate.bzl", "evaluate") # Use capitals to hint its not an actual boolean type. @@ -39,72 +33,37 @@ def env_marker_setting(*, name, expression, **kwargs): _env_marker_setting( name = name, expression = expression, - os_name = select(os_name_select_map), - sys_platform = select(sys_platform_select_map), - platform_machine = select(platform_machine_select_map), - platform_system = select(platform_system_select_map), - platform_release = select({ - "@platforms//os:osx": "USE_OSX_VERSION_FLAG", - "//conditions:default": "", - }), **kwargs ) def _env_marker_setting_impl(ctx): - env = {} + env = create_env() + env.update( + ctx.attr._env_marker_config_flag[EnvMarkerInfo].env, + ) runtime = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime - if runtime.interpreter_version_info: - version_info = runtime.interpreter_version_info - env["python_version"] = "{major}.{minor}".format( - major = version_info.major, - minor = version_info.minor, - ) - full_version = _format_full_version(version_info) - env["python_full_version"] = full_version - env["implementation_version"] = full_version - else: - env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag) - full_version = _get_flag(ctx.attr._python_full_version_flag) - env["python_full_version"] = full_version - env["implementation_version"] = full_version - - # We assume cpython if the toolchain doesn't specify because it's most - # likely to be true. - env["implementation_name"] = runtime.implementation_name or "cpython" - env["os_name"] = ctx.attr.os_name - env["sys_platform"] = ctx.attr.sys_platform - env["platform_machine"] = ctx.attr.platform_machine - - # The `platform_python_implementation` marker value is supposed to come - # from `platform.python_implementation()`, however, PEP 421 introduced - # `sys.implementation.name` and the `implementation_name` env marker to - # replace it. Per the platform.python_implementation docs, there's now - # essentially just two possible "registered" values: CPython or PyPy. - # Rather than add a field to the toolchain, we just special case the value - # from `sys.implementation.name` to handle the two documented values. - platform_python_impl = runtime.implementation_name - if platform_python_impl == "cpython": - platform_python_impl = "CPython" - elif platform_python_impl == "pypy": - platform_python_impl = "PyPy" - env["platform_python_implementation"] = platform_python_impl - - # NOTE: Platform release for Android will be Android version: - # https://peps.python.org/pep-0738/#platform - # Similar for iOS: - # https://peps.python.org/pep-0730/#platform - platform_release = ctx.attr.platform_release - if platform_release == "USE_OSX_VERSION_FLAG": - platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag) - env["platform_release"] = platform_release - env["platform_system"] = ctx.attr.platform_system - - # For lack of a better option, just use an empty string for now. - env["platform_version"] = "" - - env.update(env_aliases()) + if "python_version" not in env: + if runtime.interpreter_version_info: + version_info = runtime.interpreter_version_info + env["python_version"] = "{major}.{minor}".format( + major = version_info.major, + minor = version_info.minor, + ) + full_version = _format_full_version(version_info) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + else: + env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag) + full_version = _get_flag(ctx.attr._python_full_version_flag) + env["python_full_version"] = full_version + env["implementation_version"] = full_version + + if "implementation_name" not in env and runtime.implementation_name: + env["implementation_name"] = runtime.implementation_name + + set_missing_env_defaults(env) if evaluate(ctx.attr.expression, env = env): value = _ENV_MARKER_TRUE else: @@ -125,14 +84,9 @@ for the specification of behavior. mandatory = True, doc = "Environment marker expression to evaluate.", ), - "os_name": attr.string(), - "platform_machine": attr.string(), - "platform_release": attr.string(), - "platform_system": attr.string(), - "sys_platform": attr.string(), - "_pip_whl_osx_version_flag": attr.label( - default = "//python/config_settings:pip_whl_osx_version", - providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]], + "_env_marker_config_flag": attr.label( + default = "//python/config_settings:pip_env_marker_config", + providers = [EnvMarkerInfo], ), "_python_full_version_flag": attr.label( default = "//python/config_settings:python_version", diff --git a/python/private/pypi/flags.bzl b/python/private/pypi/flags.bzl index a25579a2..03738391 100644 --- a/python/private/pypi/flags.bzl +++ b/python/private/pypi/flags.bzl @@ -20,6 +20,15 @@ unnecessary files when all that are needed are flag definitions. load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo", "string_flag") load("//python/private:enum.bzl", "enum") +load(":env_marker_info.bzl", "EnvMarkerInfo") +load( + ":pep508_env.bzl", + "create_env", + "os_name_select_map", + "platform_machine_select_map", + "platform_system_select_map", + "sys_platform_select_map", +) # Determines if we should use whls for third party # @@ -82,6 +91,10 @@ def define_pypi_internal_flags(name): visibility = ["//visibility:public"], ) + _default_env_marker_config( + name = "_pip_env_marker_default_config", + ) + def _allow_wheels_flag_impl(ctx): input = ctx.attr._setting[BuildSettingInfo].value value = "yes" if input in ["auto", "only"] else "no" @@ -97,3 +110,58 @@ This rule allows us to greatly reduce the number of config setting targets at no if we are duplicating some of the functionality of the `native.config_setting`. """, ) + +def _default_env_marker_config(**kwargs): + _env_marker_config( + os_name = select(os_name_select_map), + sys_platform = select(sys_platform_select_map), + platform_machine = select(platform_machine_select_map), + platform_system = select(platform_system_select_map), + platform_release = select({ + "@platforms//os:osx": "USE_OSX_VERSION_FLAG", + "//conditions:default": "", + }), + **kwargs + ) + +def _env_marker_config_impl(ctx): + env = create_env() + env["os_name"] = ctx.attr.os_name + env["sys_platform"] = ctx.attr.sys_platform + env["platform_machine"] = ctx.attr.platform_machine + + # NOTE: Platform release for Android will be Android version: + # https://peps.python.org/pep-0738/#platform + # Similar for iOS: + # https://peps.python.org/pep-0730/#platform + platform_release = ctx.attr.platform_release + if platform_release == "USE_OSX_VERSION_FLAG": + platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag) + env["platform_release"] = platform_release + env["platform_system"] = ctx.attr.platform_system + + # NOTE: We intentionally do not call set_missing_env_defaults() here because + # `env_marker_setting()` computes missing values using the toolchain. + return [EnvMarkerInfo(env = env)] + +_env_marker_config = rule( + implementation = _env_marker_config_impl, + attrs = { + "os_name": attr.string(), + "platform_machine": attr.string(), + "platform_release": attr.string(), + "platform_system": attr.string(), + "sys_platform": attr.string(), + "_pip_whl_osx_version_flag": attr.label( + default = "//python/config_settings:pip_whl_osx_version", + providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]], + ), + }, +) + +def _get_flag(t): + if config_common.FeatureFlagInfo in t: + return t[config_common.FeatureFlagInfo].value + if BuildSettingInfo in t: + return t[BuildSettingInfo].value + fail("Should not occur: {} does not have necessary providers") diff --git a/python/private/pypi/pep508_env.bzl b/python/private/pypi/pep508_env.bzl index d6185356..a6efb3c5 100644 --- a/python/private/pypi/pep508_env.bzl +++ b/python/private/pypi/pep508_env.bzl @@ -66,23 +66,23 @@ platform_machine_select_map = { # Platform system returns results from the `uname` call. _platform_system_values = { + # See https://peps.python.org/pep-0738/#platform + "android": "Android", + "freebsd": "FreeBSD", + # See https://peps.python.org/pep-0730/#platform + # NOTE: Per Pep 730, "iPadOS" is also an acceptable value + "ios": "iOS", "linux": "Linux", + "netbsd": "NetBSD", + "openbsd": "OpenBSD", "osx": "Darwin", "windows": "Windows", } platform_system_select_map = { - # See https://peps.python.org/pep-0738/#platform - "@platforms//os:android": "Android", - "@platforms//os:freebsd": "FreeBSD", - # See https://peps.python.org/pep-0730/#platform - # NOTE: Per Pep 730, "iPadOS" is also an acceptable value - "@platforms//os:ios": "iOS", - "@platforms//os:linux": "Linux", - "@platforms//os:netbsd": "NetBSD", - "@platforms//os:openbsd": "OpenBSD", - "@platforms//os:osx": "Darwin", - "@platforms//os:windows": "Windows", + "@platforms//os:{}".format(bazel_os): py_system + for bazel_os, py_system in _platform_system_values.items() +} | { # The value is empty string if it cannot be determined: # https://docs.python.org/3/library/platform.html#platform.machine "//conditions:default": "", @@ -114,33 +114,36 @@ platform_system_select_map = { # # We are using only the subset that we actually support. _sys_platform_values = { + # These values are decided by the sys.platform docs. + "android": "android", + "emscripten": "emscripten", + # NOTE: The below values are approximations. The sys.platform() docs + # don't have documented values for these OSes. Per docs, the + # sys.platform() value reflects the OS at the time Python was *built* + # instead of the runtime (target) OS value. + "freebsd": "freebsd", + "ios": "ios", "linux": "linux", + "openbsd": "openbsd", "osx": "darwin", + "wasi": "wasi", "windows": "win32", } -# Taken from -# https://docs.python.org/3/library/sys.html#sys.platform sys_platform_select_map = { - # These values are decided by the sys.platform docs. - "@platforms//os:android": "android", - "@platforms//os:emscripten": "emscripten", - # NOTE: The below values are approximations. The sys.platform() docs - # don't have documented values for these OSes. Per docs, the - # sys.platform() value reflects the OS at the time Python was *built* - # instead of the runtime (target) OS value. - "@platforms//os:freebsd": "freebsd", - "@platforms//os:ios": "ios", - "@platforms//os:linux": "linux", - "@platforms//os:openbsd": "openbsd", - "@platforms//os:osx": "darwin", - "@platforms//os:wasi": "wasi", - "@platforms//os:windows": "win32", + "@platforms//os:{}".format(bazel_os): py_platform + for bazel_os, py_platform in _sys_platform_values.items() +} | { # For lack of a better option, use empty string. No standard doc/spec # about sys_platform value. "//conditions:default": "", } +# The "java" value is documented, but with Jython defunct, +# shouldn't occur in practice. +# The os.name value is technically a property of the runtime, not the +# targetted runtime OS, but the distinction shouldn't matter if +# things are properly configured. _os_name_values = { "linux": "posix", "osx": "posix", @@ -148,18 +151,18 @@ _os_name_values = { } os_name_select_map = { - # The "java" value is documented, but with Jython defunct, - # shouldn't occur in practice. - # The os.name value is technically a property of the runtime, not the - # targetted runtime OS, but the distinction shouldn't matter if - # things are properly configured. - "@platforms//os:windows": "nt", + "@platforms//os:{}".format(bazel_os): py_os + for bazel_os, py_os in _os_name_values.items() +} | { "//conditions:default": "posix", } def env(target_platform, *, extra = None): """Return an env target platform + NOTE: This is for use during the loading phase. For the analysis phase, + `env_marker_setting()` constructs the env dict. + Args: target_platform: {type}`str` the target platform identifier, e.g. `cp33_linux_aarch64` @@ -168,16 +171,9 @@ def env(target_platform, *, extra = None): Returns: A dict that can be used as `env` in the marker evaluation. """ - - # TODO @aignas 2025-02-13: consider moving this into config settings. - - env = {"extra": extra} if extra != None else {} - env = env | { - "implementation_name": "cpython", - "platform_python_implementation": "CPython", - "platform_release": "", - "platform_version": "", - } + env = create_env() + if extra != None: + env["extra"] = extra if type(target_platform) == type(""): target_platform = platform_from_str(target_platform, python_version = "") @@ -198,13 +194,42 @@ def env(target_platform, *, extra = None): "platform_system": _platform_system_values.get(os, ""), "sys_platform": _sys_platform_values.get(os, ""), } + set_missing_env_defaults(env) - # This is split by topic - return env | env_aliases() + return env -def env_aliases(): +def create_env(): return { + # This is split by topic "_aliases": { "platform_machine": platform_machine_aliases, }, } + +def set_missing_env_defaults(env): + """Sets defaults based on existing values. + + Args: + env: dict; NOTE: modified in-place + """ + if "implementation_name" not in env: + # Use cpython as the default because it's likely the correct value. + env["implementation_name"] = "cpython" + if "platform_python_implementation" not in env: + # The `platform_python_implementation` marker value is supposed to come + # from `platform.python_implementation()`, however, PEP 421 introduced + # `sys.implementation.name` and the `implementation_name` env marker to + # replace it. Per the platform.python_implementation docs, there's now + # essentially just two possible "registered" values: CPython or PyPy. + # Rather than add a field to the toolchain, we just special case the value + # from `sys.implementation.name` to handle the two documented values. + platform_python_impl = env["implementation_name"] + if platform_python_impl == "cpython": + platform_python_impl = "CPython" + elif platform_python_impl == "pypy": + platform_python_impl = "PyPy" + env["platform_python_implementation"] = platform_python_impl + if "platform_release" not in env: + env["platform_release"] = "" + if "platform_version" not in env: + env["platform_version"] = "0" diff --git a/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl index 549c15c2..e16f2c8e 100644 --- a/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl +++ b/tests/pypi/env_marker_setting/env_marker_setting_tests.bzl @@ -3,11 +3,46 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", "TestingAspectInfo") +load("//python/private/pypi:env_marker_info.bzl", "EnvMarkerInfo") # buildifier: disable=bzl-visibility load("//python/private/pypi:env_marker_setting.bzl", "env_marker_setting") # buildifier: disable=bzl-visibility -load("//tests/support:support.bzl", "PYTHON_VERSION") +load("//tests/support:support.bzl", "PIP_ENV_MARKER_CONFIG", "PYTHON_VERSION") + +def _custom_env_markers_impl(ctx): + _ = ctx # @unused + return [EnvMarkerInfo(env = { + "os_name": "testos", + })] + +_custom_env_markers = rule( + implementation = _custom_env_markers_impl, +) _tests = [] +def _test_custom_env_markers(name): + def _impl(env, target): + env.expect.where( + expression = target[TestingAspectInfo].attrs.expression, + ).that_str( + target[config_common.FeatureFlagInfo].value, + ).equals("TRUE") + + env_marker_setting( + name = name + "_subject", + expression = "os_name == 'testos'", + ) + _custom_env_markers(name = name + "_env") + analysis_test( + name = name, + impl = _impl, + target = name + "_subject", + config_settings = { + PIP_ENV_MARKER_CONFIG: str(Label(name + "_env")), + }, + ) + +_tests.append(_test_custom_env_markers) + def _test_expr(name): def impl(env, target): env.expect.where( diff --git a/tests/support/support.bzl b/tests/support/support.bzl index 6330155d..7bab263c 100644 --- a/tests/support/support.bzl +++ b/tests/support/support.bzl @@ -37,6 +37,7 @@ CROSSTOOL_TOP = Label("//tests/support/cc_toolchains:cc_toolchain_suite") ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles")) BOOTSTRAP_IMPL = str(Label("//python/config_settings:bootstrap_impl")) EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain")) +PIP_ENV_MARKER_CONFIG = str(Label("//python/config_settings:pip_env_marker_config")) PRECOMPILE = str(Label("//python/config_settings:precompile")) PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention")) PYC_COLLECTION = str(Label("//python/config_settings:pyc_collection")) From 201e2d4314394e484be7797283ddb8db33fe0bba Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Sun, 27 Apr 2025 08:41:48 -0700 Subject: [PATCH 16/23] fix: Make local runtime repo work for windows In particular, this fixes two problems: First, windows requires interface libraries to be linked in. Second, windows requires an alternative search path since LIBDIR is not filled in. --- python/private/get_local_runtime_info.py | 2 ++ python/private/local_runtime_repo.bzl | 9 +++++++++ python/private/local_runtime_repo_setup.bzl | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index 19db3a29..ac05af7d 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -36,6 +36,8 @@ # https://stackoverflow.com/questions/47423246/get-pythons-lib-path # For now, it seems LIBDIR has what is needed, so just use that. "LIBDIR", + # A backup alternative for deriving LIBDIR in cases that LIBDIR is not available + "LIBDEST", # The versioned libpythonX.Y.so.N file. Usually? # It might be a static archive (.a) file instead. "INSTSONAME", diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index ec0643e4..fc5197ce 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -112,6 +112,12 @@ def _local_runtime_repo_impl(rctx): info["INSTSONAME"], ] + if repo_utils.get_platforms_os_name(rctx) == 'windows': + shared_lib_names.append("python{major}{minor}.lib".format(**info)) + shared_lib_names.append("python3.lib") + + interpreter_path = interpreter_path.replace('\\', '/') + # In some cases, the value may be empty. Not clear why. shared_lib_names = [v for v in shared_lib_names if v] @@ -119,6 +125,9 @@ def _local_runtime_repo_impl(rctx): shared_lib_names = {v: None for v in shared_lib_names}.keys() shared_lib_dir = info["LIBDIR"] + if shared_lib_dir == None: + shared_lib_dir = info["LIBDEST"] + "/../libs" + # The specific files are symlinked instead of the whole directory # because it can point to a directory that has more than just # the Python runtime shared libraries, e.g. /usr/lib, or a Python diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 37eab595..2fe5b8f7 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -16,6 +16,7 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_import.bzl", "cc_import") load("@rules_python//python:py_runtime.bzl", "py_runtime") load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair") load("@rules_python//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain") @@ -58,6 +59,20 @@ def define_local_runtime_toolchain_impl( major_minor = "{}.{}".format(major, minor) major_minor_micro = "{}.{}".format(major_minor, micro) + version_dict = {'major': major, 'minor': minor} + + cc_import( + name = "interface", + interface_library = "lib/python{major}{minor}.lib".format(**version_dict), + system_provided = True, + ) + + cc_import( + name = "abi3_interface", + interface_library = "lib/python3.lib", + system_provided = True, + ) + cc_library( name = "_python_headers", # NOTE: Keep in sync with watch_tree() called in local_runtime_repo @@ -67,6 +82,10 @@ def define_local_runtime_toolchain_impl( allow_empty = True, ), includes = ["include"], + deps = select({ + "@bazel_tools//src/conditions:windows": [":interface", ":abi3_interface"], + "//conditions:default": None, + }), ) cc_library( From 43a4f5992243ec1ecd14f219c81e9f4e81c6ecea Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Tue, 6 May 2025 07:52:25 -0700 Subject: [PATCH 17/23] Update get_local_runtime_info.py --- python/private/get_local_runtime_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index ac05af7d..ea3f0752 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -36,7 +36,7 @@ # https://stackoverflow.com/questions/47423246/get-pythons-lib-path # For now, it seems LIBDIR has what is needed, so just use that. "LIBDIR", - # A backup alternative for deriving LIBDIR in cases that LIBDIR is not available + # A backup alternative for deriving LIBDIR when LIBDIR is not available "LIBDEST", # The versioned libpythonX.Y.so.N file. Usually? # It might be a static archive (.a) file instead. From 4c440a7788198840b2725fe3302f09172fc9ce98 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 6 May 2025 18:57:22 -0700 Subject: [PATCH 18/23] Apply suggestions from code review --- python/private/get_local_runtime_info.py | 3 ++- python/private/local_runtime_repo_setup.bzl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index ea3f0752..eeca34c3 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -36,7 +36,8 @@ # https://stackoverflow.com/questions/47423246/get-pythons-lib-path # For now, it seems LIBDIR has what is needed, so just use that. "LIBDIR", - # A backup alternative for deriving LIBDIR when LIBDIR is not available + # LIBDEST is the directory for platform-specific files; it's usually + # a subdirectory of LIBDIR. We use it in case LIBDIR isn't set. "LIBDEST", # The versioned libpythonX.Y.so.N file. Usually? # It might be a static archive (.a) file instead. diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 2fe5b8f7..f7d09ef1 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -83,7 +83,7 @@ def define_local_runtime_toolchain_impl( ), includes = ["include"], deps = select({ - "@bazel_tools//src/conditions:windows": [":interface", ":abi3_interface"], + "@platforms//os:windows": [":interface", ":abi3_interface"], "//conditions:default": None, }), ) From 0bab65585d365aff541a6a3b4fb1abbf11b01d39 Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Tue, 6 May 2025 20:54:23 -0700 Subject: [PATCH 19/23] A variety of improvements --- CHANGELOG.md | 1 + python/private/get_local_runtime_info.py | 3 +++ python/private/local_runtime_repo.bzl | 16 +++++++++++----- python/private/local_runtime_repo_setup.bzl | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d73613a..15b3a3ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ END_UNRELEASED_TEMPLATE multiple times. * (tools/wheelmaker.py) Extras are now preserved in Requires-Dist metadata when using requires_file to specify the requirements. +* (toolchains) Local toolchains now supports Windows. {#v0-0-0-added} ### Added diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index eeca34c3..57a5a4d3 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -48,6 +48,9 @@ # The platform-specific filename suffix for library files. # Includes the dot, e.g. `.so` "SHLIB_SUFFIX", + # A flag for whether this is a free threaded implementation. + # Set to t when free threading is enabled, '' or None otherwise. + "abi_thread", ] data.update(zip(config_vars, sysconfig.get_config_vars(*config_vars))) print(json.dumps(data)) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index fc5197ce..2eb2d8c8 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -14,6 +14,7 @@ """Create a repository for a locally installed Python runtime.""" +load("@bazel_skylib//lib:paths.bzl", "paths") load(":enum.bzl", "enum") load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils") @@ -112,11 +113,16 @@ def _local_runtime_repo_impl(rctx): info["INSTSONAME"], ] - if repo_utils.get_platforms_os_name(rctx) == 'windows': - shared_lib_names.append("python{major}{minor}.lib".format(**info)) - shared_lib_names.append("python3.lib") + if repo_utils.get_platforms_os_name(rctx) == "windows": + # Fall back to assuming no threading when no information is available + if info.get("abi_thread") == None: + info["abi_thread"] = "" - interpreter_path = interpreter_path.replace('\\', '/') + # Windows requires these library definitions to be linked in with the headers + shared_lib_names.append("python{major}{minor}{abi_thread}.lib".format(**info)) + shared_lib_names.append("python{major}{abi_thread}.lib".format(**info)) + + interpreter_path = interpreter_path.replace("\\", "/") # In some cases, the value may be empty. Not clear why. shared_lib_names = [v for v in shared_lib_names if v] @@ -126,7 +132,7 @@ def _local_runtime_repo_impl(rctx): shared_lib_dir = info["LIBDIR"] if shared_lib_dir == None: - shared_lib_dir = info["LIBDEST"] + "/../libs" + shared_lib_dir = paths.dirname(info["LIBDEST"]) + "/libs" # The specific files are symlinked instead of the whole directory # because it can point to a directory that has more than just diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index f7d09ef1..c9798fa3 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -59,7 +59,7 @@ def define_local_runtime_toolchain_impl( major_minor = "{}.{}".format(major, minor) major_minor_micro = "{}.{}".format(major_minor, micro) - version_dict = {'major': major, 'minor': minor} + version_dict = {"major": major, "minor": minor} cc_import( name = "interface", From 23701d72e6fc8e878ff663cc51fd6f8da9ade729 Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Tue, 6 May 2025 20:56:52 -0700 Subject: [PATCH 20/23] Oops, missing dependency --- .gitignore | 12 ++++---- python/private/BUILD.bazel | 1 + python/private/local_runtime_repo.bzl | 30 +++++++++---------- .../integration/local_toolchains/MODULE.bazel | 2 +- tests/integration/local_toolchains/test.py | 2 +- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 863b0e9c..5a34cd45 100644 --- a/.gitignore +++ b/.gitignore @@ -32,11 +32,11 @@ *~ # Bazel directories -/bazel-* -/bazel-bin -/bazel-genfiles -/bazel-out -/bazel-testlogs +bazel-* +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs user.bazelrc # vim swap files @@ -51,4 +51,4 @@ user.bazelrc # MODULE.bazel.lock is ignored for now as per recommendation from upstream. # See https://github.com/bazelbuild/bazel/issues/20369 -MODULE.bazel.lock +MODULE.bazel.lock \ No newline at end of file diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 9cc8ffc6..34273898 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -212,6 +212,7 @@ bzl_library( deps = [ ":enum_bzl", ":repo_utils.bzl", + "@bazel_skylib//lib:paths", ], ) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 2eb2d8c8..41f0c3b7 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -25,21 +25,21 @@ _OnFailure = enum( FAIL = "fail", ) -_TOOLCHAIN_IMPL_TEMPLATE = """\ -# Generated by python/private/local_runtime_repo.bzl - -load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl") - -define_local_runtime_toolchain_impl( - name = "local_runtime", - lib_ext = "{lib_ext}", - major = "{major}", - minor = "{minor}", - micro = "{micro}", - interpreter_path = "{interpreter_path}", - implementation_name = "{implementation_name}", - os = "{os}", -) +_TOOLCHAIN_IMPL_TEMPLATE = """\\\r +# Generated by python/private/local_runtime_repo.bzl\r +\r +load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl")\r +\r +define_local_runtime_toolchain_impl(\r + name = "local_runtime",\r + lib_ext = "{lib_ext}",\r + major = "{major}",\r + minor = "{minor}",\r + micro = "{micro}",\r + interpreter_path = "{interpreter_path}",\r + implementation_name = "{implementation_name}",\r + os = "{os}",\r +)\r """ def _local_runtime_repo_impl(rctx): diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index 6c06909c..c39a7bfe 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -28,7 +28,7 @@ local_runtime_toolchains_repo = use_repo_rule("@rules_python//python/local_toolc local_runtime_repo( name = "local_python3", - interpreter_path = "python3", + interpreter_path = "python", on_failure = "fail", ) diff --git a/tests/integration/local_toolchains/test.py b/tests/integration/local_toolchains/test.py index 8e37fff6..7f70eade 100644 --- a/tests/integration/local_toolchains/test.py +++ b/tests/integration/local_toolchains/test.py @@ -14,7 +14,7 @@ def test_python_from_path_used(self): # repo-phase and when the test is run are roughly the same. It's # easy to violate this condition if there are shell-local changes # that wouldn't be reflected when sub-shells are run later. - shell_path = shutil.which("python3") + shell_path = shutil.which("python") # We call the interpreter and print its executable because of # things like pyenv: they install a shim that re-execs python. From 009e3de7cf261321bea9ab1ac5d7adc8d6a332f6 Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Tue, 6 May 2025 21:00:15 -0700 Subject: [PATCH 21/23] Revert bad changes --- python/private/local_runtime_repo.bzl | 30 +++++++++---------- .../integration/local_toolchains/MODULE.bazel | 2 +- tests/integration/local_toolchains/test.py | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 41f0c3b7..7eb4d5f0 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -25,21 +25,21 @@ _OnFailure = enum( FAIL = "fail", ) -_TOOLCHAIN_IMPL_TEMPLATE = """\\\r -# Generated by python/private/local_runtime_repo.bzl\r -\r -load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl")\r -\r -define_local_runtime_toolchain_impl(\r - name = "local_runtime",\r - lib_ext = "{lib_ext}",\r - major = "{major}",\r - minor = "{minor}",\r - micro = "{micro}",\r - interpreter_path = "{interpreter_path}",\r - implementation_name = "{implementation_name}",\r - os = "{os}",\r -)\r +_TOOLCHAIN_IMPL_TEMPLATE = """\\ +# Generated by python/private/local_runtime_repo.bzl + +load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl") + +define_local_runtime_toolchain_impl( + name = "local_runtime", + lib_ext = "{lib_ext}", + major = "{major}", + minor = "{minor}", + micro = "{micro}", + interpreter_path = "{interpreter_path}", + implementation_name = "{implementation_name}", + os = "{os}", +) """ def _local_runtime_repo_impl(rctx): diff --git a/tests/integration/local_toolchains/MODULE.bazel b/tests/integration/local_toolchains/MODULE.bazel index c39a7bfe..6c06909c 100644 --- a/tests/integration/local_toolchains/MODULE.bazel +++ b/tests/integration/local_toolchains/MODULE.bazel @@ -28,7 +28,7 @@ local_runtime_toolchains_repo = use_repo_rule("@rules_python//python/local_toolc local_runtime_repo( name = "local_python3", - interpreter_path = "python", + interpreter_path = "python3", on_failure = "fail", ) diff --git a/tests/integration/local_toolchains/test.py b/tests/integration/local_toolchains/test.py index 7f70eade..8e37fff6 100644 --- a/tests/integration/local_toolchains/test.py +++ b/tests/integration/local_toolchains/test.py @@ -14,7 +14,7 @@ def test_python_from_path_used(self): # repo-phase and when the test is run are roughly the same. It's # easy to violate this condition if there are shell-local changes # that wouldn't be reflected when sub-shells are run later. - shell_path = shutil.which("python") + shell_path = shutil.which("python3") # We call the interpreter and print its executable because of # things like pyenv: they install a shim that re-execs python. From 1b1960552857bf5763e5afef5adbeb04f86bb95e Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Tue, 6 May 2025 21:00:58 -0700 Subject: [PATCH 22/23] One final bad change --- python/private/local_runtime_repo.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 7eb4d5f0..2eb2d8c8 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -25,7 +25,7 @@ _OnFailure = enum( FAIL = "fail", ) -_TOOLCHAIN_IMPL_TEMPLATE = """\\ +_TOOLCHAIN_IMPL_TEMPLATE = """\ # Generated by python/private/local_runtime_repo.bzl load("@rules_python//python/private:local_runtime_repo_setup.bzl", "define_local_runtime_toolchain_impl") From a4430d70d743d3a46c15a0b65f420b306dddc646 Mon Sep 17 00:00:00 2001 From: Ethan Steinberg Date: Tue, 6 May 2025 21:07:44 -0700 Subject: [PATCH 23/23] Fix order --- python/private/local_runtime_repo_setup.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index c9798fa3..96f8571d 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -15,8 +15,8 @@ """Setup code called by the code generated by `local_runtime_repo`.""" load("@bazel_skylib//lib:selects.bzl", "selects") -load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_cc//cc:cc_import.bzl", "cc_import") +load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_python//python:py_runtime.bzl", "py_runtime") load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair") load("@rules_python//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")