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