xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/pypi/whl_installer/platform.py (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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
17import platform
18import sys
19from dataclasses import dataclass
20from enum import Enum
21from typing import Any, Dict, Iterator, List, Optional, Union
22
23
24class 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
40class 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
65def _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
80def host_interpreter_minor_version() -> int:
81    return sys.version_info.minor
82
83
84@dataclass(frozen=True)
85class 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        }
303