| 
 | 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