|  | 
|  | 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 | +    ppc64le = 5 | 
|  | 46 | +    s390x = 6 | 
|  | 47 | +    arm = 7 | 
|  | 48 | +    amd64 = x86_64 | 
|  | 49 | +    arm64 = aarch64 | 
|  | 50 | +    i386 = x86_32 | 
|  | 51 | +    i686 = x86_32 | 
|  | 52 | +    x86 = x86_32 | 
|  | 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.ppc: | 
|  | 275 | +            return "ppc" | 
|  | 276 | +        elif self.arch == Arch.ppc64le: | 
|  | 277 | +            return "ppc64le" | 
|  | 278 | +        elif self.arch == Arch.s390x: | 
|  | 279 | +            return "s390x" | 
|  | 280 | +        else: | 
|  | 281 | +            return "" | 
|  | 282 | + | 
|  | 283 | +    def env_markers(self, extra: str) -> Dict[str, str]: | 
|  | 284 | +        # If it is None, use the host version | 
|  | 285 | +        minor_version = self.minor_version or host_interpreter_minor_version() | 
|  | 286 | + | 
|  | 287 | +        return { | 
|  | 288 | +            "extra": extra, | 
|  | 289 | +            "os_name": self.os_name, | 
|  | 290 | +            "sys_platform": self.sys_platform, | 
|  | 291 | +            "platform_machine": self.platform_machine, | 
|  | 292 | +            "platform_system": self.platform_system, | 
|  | 293 | +            "platform_release": "",  # unset | 
|  | 294 | +            "platform_version": "",  # unset | 
|  | 295 | +            "python_version": f"3.{minor_version}", | 
|  | 296 | +            # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should | 
|  | 297 | +            # use `20` or something else to avoid having weird issues where the full version is used for | 
|  | 298 | +            # matching and the author decides to only support 3.y.5 upwards. | 
|  | 299 | +            "implementation_version": f"3.{minor_version}.0", | 
|  | 300 | +            "python_full_version": f"3.{minor_version}.0", | 
|  | 301 | +            # we assume that the following are the same as the interpreter used to setup the deps: | 
|  | 302 | +            # "implementation_name": "cpython" | 
|  | 303 | +            # "platform_python_implementation: "CPython", | 
|  | 304 | +        } | 
0 commit comments