Skip to content

Commit bf70429

Browse files
authored
refactor(pypi): split out code for env marker evaluation for reuse (#2068)
This is just a small PR to reduce the scope of #2059. This just moves some code from one python file to a separate one. Work towards #260, #1105, #1868.
1 parent bb3615f commit bf70429

File tree

7 files changed

+509
-483
lines changed

7 files changed

+509
-483
lines changed

python/private/pypi/whl_installer/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ py_library(
55
srcs = [
66
"arguments.py",
77
"namespace_pkgs.py",
8+
"platform.py",
89
"wheel.py",
910
"wheel_installer.py",
1011
],

python/private/pypi/whl_installer/arguments.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import pathlib
1818
from typing import Any, Dict, Set
1919

20-
from python.private.pypi.whl_installer import wheel
20+
from python.private.pypi.whl_installer.platform import Platform
2121

2222

2323
def parser(**kwargs: Any) -> argparse.ArgumentParser:
@@ -44,7 +44,7 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser:
4444
parser.add_argument(
4545
"--platform",
4646
action="extend",
47-
type=wheel.Platform.from_string,
47+
type=Platform.from_string,
4848
help="Platforms to target dependencies. Can be used multiple times.",
4949
)
5050
parser.add_argument(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Utility class to inspect an extracted wheel directory"""
16+
17+
import platform
18+
import sys
19+
from dataclasses import dataclass
20+
from enum import Enum
21+
from typing import Any, Dict, Iterator, List, Optional, Union
22+
23+
24+
class OS(Enum):
25+
linux = 1
26+
osx = 2
27+
windows = 3
28+
darwin = osx
29+
win32 = windows
30+
31+
@classmethod
32+
def interpreter(cls) -> "OS":
33+
"Return the interpreter operating system."
34+
return cls[sys.platform.lower()]
35+
36+
def __str__(self) -> str:
37+
return self.name.lower()
38+
39+
40+
class Arch(Enum):
41+
x86_64 = 1
42+
x86_32 = 2
43+
aarch64 = 3
44+
ppc = 4
45+
s390x = 5
46+
arm = 6
47+
amd64 = x86_64
48+
arm64 = aarch64
49+
i386 = x86_32
50+
i686 = x86_32
51+
x86 = x86_32
52+
ppc64le = ppc
53+
54+
@classmethod
55+
def interpreter(cls) -> "Arch":
56+
"Return the currently running interpreter architecture."
57+
# FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6
58+
# is returning an empty string here, so lets default to x86_64
59+
return cls[platform.machine().lower() or "x86_64"]
60+
61+
def __str__(self) -> str:
62+
return self.name.lower()
63+
64+
65+
def _as_int(value: Optional[Union[OS, Arch]]) -> int:
66+
"""Convert one of the enums above to an int for easier sorting algorithms.
67+
68+
Args:
69+
value: The value of an enum or None.
70+
71+
Returns:
72+
-1 if we get None, otherwise, the numeric value of the given enum.
73+
"""
74+
if value is None:
75+
return -1
76+
77+
return int(value.value)
78+
79+
80+
def host_interpreter_minor_version() -> int:
81+
return sys.version_info.minor
82+
83+
84+
@dataclass(frozen=True)
85+
class Platform:
86+
os: Optional[OS] = None
87+
arch: Optional[Arch] = None
88+
minor_version: Optional[int] = None
89+
90+
@classmethod
91+
def all(
92+
cls,
93+
want_os: Optional[OS] = None,
94+
minor_version: Optional[int] = None,
95+
) -> List["Platform"]:
96+
return sorted(
97+
[
98+
cls(os=os, arch=arch, minor_version=minor_version)
99+
for os in OS
100+
for arch in Arch
101+
if not want_os or want_os == os
102+
]
103+
)
104+
105+
@classmethod
106+
def host(cls) -> List["Platform"]:
107+
"""Use the Python interpreter to detect the platform.
108+
109+
We extract `os` from sys.platform and `arch` from platform.machine
110+
111+
Returns:
112+
A list of parsed values which makes the signature the same as
113+
`Platform.all` and `Platform.from_string`.
114+
"""
115+
return [
116+
Platform(
117+
os=OS.interpreter(),
118+
arch=Arch.interpreter(),
119+
minor_version=host_interpreter_minor_version(),
120+
)
121+
]
122+
123+
def all_specializations(self) -> Iterator["Platform"]:
124+
"""Return the platform itself and all its unambiguous specializations.
125+
126+
For more info about specializations see
127+
https://bazel.build/docs/configurable-attributes
128+
"""
129+
yield self
130+
if self.arch is None:
131+
for arch in Arch:
132+
yield Platform(os=self.os, arch=arch, minor_version=self.minor_version)
133+
if self.os is None:
134+
for os in OS:
135+
yield Platform(os=os, arch=self.arch, minor_version=self.minor_version)
136+
if self.arch is None and self.os is None:
137+
for os in OS:
138+
for arch in Arch:
139+
yield Platform(os=os, arch=arch, minor_version=self.minor_version)
140+
141+
def __lt__(self, other: Any) -> bool:
142+
"""Add a comparison method, so that `sorted` returns the most specialized platforms first."""
143+
if not isinstance(other, Platform) or other is None:
144+
raise ValueError(f"cannot compare {other} with Platform")
145+
146+
self_arch, self_os = _as_int(self.arch), _as_int(self.os)
147+
other_arch, other_os = _as_int(other.arch), _as_int(other.os)
148+
149+
if self_os == other_os:
150+
return self_arch < other_arch
151+
else:
152+
return self_os < other_os
153+
154+
def __str__(self) -> str:
155+
if self.minor_version is None:
156+
if self.os is None and self.arch is None:
157+
return "//conditions:default"
158+
159+
if self.arch is None:
160+
return f"@platforms//os:{self.os}"
161+
else:
162+
return f"{self.os}_{self.arch}"
163+
164+
if self.arch is None and self.os is None:
165+
return f"@//python/config_settings:is_python_3.{self.minor_version}"
166+
167+
if self.arch is None:
168+
return f"cp3{self.minor_version}_{self.os}_anyarch"
169+
170+
if self.os is None:
171+
return f"cp3{self.minor_version}_anyos_{self.arch}"
172+
173+
return f"cp3{self.minor_version}_{self.os}_{self.arch}"
174+
175+
@classmethod
176+
def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
177+
"""Parse a string and return a list of platforms"""
178+
platform = [platform] if isinstance(platform, str) else list(platform)
179+
ret = set()
180+
for p in platform:
181+
if p == "host":
182+
ret.update(cls.host())
183+
continue
184+
185+
abi, _, tail = p.partition("_")
186+
if not abi.startswith("cp"):
187+
# The first item is not an abi
188+
tail = p
189+
abi = ""
190+
os, _, arch = tail.partition("_")
191+
arch = arch or "*"
192+
193+
minor_version = int(abi[len("cp3") :]) if abi else None
194+
195+
if arch != "*":
196+
ret.add(
197+
cls(
198+
os=OS[os] if os != "*" else None,
199+
arch=Arch[arch],
200+
minor_version=minor_version,
201+
)
202+
)
203+
204+
else:
205+
ret.update(
206+
cls.all(
207+
want_os=OS[os] if os != "*" else None,
208+
minor_version=minor_version,
209+
)
210+
)
211+
212+
return sorted(ret)
213+
214+
# NOTE @aignas 2023-12-05: below is the minimum number of accessors that are defined in
215+
# https://peps.python.org/pep-0496/ to make rules_python generate dependencies.
216+
#
217+
# WARNING: It may not work in cases where the python implementation is different between
218+
# different platforms.
219+
220+
# derived from OS
221+
@property
222+
def os_name(self) -> str:
223+
if self.os == OS.linux or self.os == OS.osx:
224+
return "posix"
225+
elif self.os == OS.windows:
226+
return "nt"
227+
else:
228+
return ""
229+
230+
@property
231+
def sys_platform(self) -> str:
232+
if self.os == OS.linux:
233+
return "linux"
234+
elif self.os == OS.osx:
235+
return "darwin"
236+
elif self.os == OS.windows:
237+
return "win32"
238+
else:
239+
return ""
240+
241+
@property
242+
def platform_system(self) -> str:
243+
if self.os == OS.linux:
244+
return "Linux"
245+
elif self.os == OS.osx:
246+
return "Darwin"
247+
elif self.os == OS.windows:
248+
return "Windows"
249+
else:
250+
return ""
251+
252+
# derived from OS and Arch
253+
@property
254+
def platform_machine(self) -> str:
255+
"""Guess the target 'platform_machine' marker.
256+
257+
NOTE @aignas 2023-12-05: this may not work on really new systems, like
258+
Windows if they define the platform markers in a different way.
259+
"""
260+
if self.arch == Arch.x86_64:
261+
return "x86_64"
262+
elif self.arch == Arch.x86_32 and self.os != OS.osx:
263+
return "i386"
264+
elif self.arch == Arch.x86_32:
265+
return ""
266+
elif self.arch == Arch.aarch64 and self.os == OS.linux:
267+
return "aarch64"
268+
elif self.arch == Arch.aarch64:
269+
# Assuming that OSX and Windows use this one since the precedent is set here:
270+
# https://github.com/cgohlke/win_arm64-wheels
271+
return "arm64"
272+
elif self.os != OS.linux:
273+
return ""
274+
elif self.arch == Arch.ppc64le:
275+
return "ppc64le"
276+
elif self.arch == Arch.s390x:
277+
return "s390x"
278+
else:
279+
return ""
280+
281+
def env_markers(self, extra: str) -> Dict[str, str]:
282+
# If it is None, use the host version
283+
minor_version = self.minor_version or host_interpreter_minor_version()
284+
285+
return {
286+
"extra": extra,
287+
"os_name": self.os_name,
288+
"sys_platform": self.sys_platform,
289+
"platform_machine": self.platform_machine,
290+
"platform_system": self.platform_system,
291+
"platform_release": "", # unset
292+
"platform_version": "", # unset
293+
"python_version": f"3.{minor_version}",
294+
# FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should
295+
# use `20` or something else to avoid having weird issues where the full version is used for
296+
# matching and the author decides to only support 3.y.5 upwards.
297+
"implementation_version": f"3.{minor_version}.0",
298+
"python_full_version": f"3.{minor_version}.0",
299+
# we assume that the following are the same as the interpreter used to setup the deps:
300+
# "implementation_name": "cpython"
301+
# "platform_python_implementation: "CPython",
302+
}

0 commit comments

Comments
 (0)