go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/vpython/wheels/pep425.go (about) 1 // Copyright 2022 The LUCI Authors. 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 package wheels 16 17 import ( 18 "fmt" 19 "strconv" 20 "strings" 21 22 "google.golang.org/protobuf/proto" 23 24 "go.chromium.org/luci/cipd/client/cipd/template" 25 "go.chromium.org/luci/common/errors" 26 27 "go.chromium.org/luci/vpython/api/vpython" 28 ) 29 30 // pep425MacPlatform is a parsed PEP425 Mac platform string. 31 // 32 // The string is formatted: 33 // macosx_<maj>_<min>_<cpu-arch> 34 // 35 // For example: 36 // - macosx_10_6_intel 37 // - macosx_10_0_fat 38 // - macosx_10_2_x86_64 39 type pep425MacPlatform struct { 40 major int 41 minor int 42 arch string 43 } 44 45 // parsePEP425MacPlatform parses a pep425MacPlatform from the supplied 46 // platform string. If the string does not contain a recognizable Mac 47 // platform, this function returns nil. 48 func parsePEP425MacPlatform(v string) *pep425MacPlatform { 49 parts := strings.SplitN(v, "_", 4) 50 if len(parts) != 4 { 51 return nil 52 } 53 if parts[0] != "macosx" { 54 return nil 55 } 56 57 var ma pep425MacPlatform 58 var err error 59 if ma.major, err = strconv.Atoi(parts[1]); err != nil { 60 return nil 61 } 62 if ma.minor, err = strconv.Atoi(parts[2]); err != nil { 63 return nil 64 } 65 66 ma.arch = parts[3] 67 return &ma 68 } 69 70 // less returns true if "ma" represents a Mac version before "other". 71 func (ma *pep425MacPlatform) less(other *pep425MacPlatform) bool { 72 switch { 73 case ma.major < other.major: 74 return true 75 case ma.major > other.major: 76 return false 77 case ma.minor < other.minor: 78 return true 79 default: 80 return false 81 } 82 } 83 84 // pep425IsBetterMacPlatform processes two PEP425 platform strings and 85 // returns true if "candidate" is a superior PEP425 tag candidate than "cur". 86 // 87 // This function favors, in order: 88 // - Mac platforms over non-Mac platforms, 89 // - arm64 > intel > others 90 // - Older Mac versions over newer ones 91 func pep425IsBetterMacPlatform(cur, candidate string) bool { 92 // Parse a Mac platform string 93 curPlatform := parsePEP425MacPlatform(cur) 94 candidatePlatform := parsePEP425MacPlatform(candidate) 95 96 archScore := func(c *pep425MacPlatform) int { 97 // Smaller is better 98 switch c.arch { 99 case "arm64": 100 return 0 101 case "intel": 102 return 1 103 default: 104 return 2 105 } 106 } 107 108 switch { 109 case curPlatform == nil: 110 return candidatePlatform != nil 111 case candidatePlatform == nil: 112 return false 113 case archScore(candidatePlatform) != archScore(curPlatform): 114 return archScore(candidatePlatform) < archScore(curPlatform) 115 case candidatePlatform.less(curPlatform): 116 // We prefer the lowest Mac architecture available. 117 return true 118 default: 119 return false 120 } 121 } 122 123 // Determies if the specified platform is a Linux platform and, if so, if it 124 // is a "manylinux1_" Linux platform. 125 func isLinuxPlatform(plat string) (is bool, many bool) { 126 switch { 127 case strings.HasPrefix(plat, "linux_"): 128 is = true 129 case strings.HasPrefix(plat, "manylinux1_"): 130 is, many = true, true 131 } 132 return 133 } 134 135 // pep425IsBetterLinuxPlatform processes two PEP425 platform strings and 136 // returns true if "candidate" is a superior PEP425 tag candidate than "cur". 137 // 138 // This function favors, in order: 139 // - Linux platforms over non-Linux platforms. 140 // - "manylinux1_" over non-"manylinux1_". 141 // 142 // Examples of expected Linux platform strings are: 143 // - linux1_x86_64 144 // - linux1_i686 145 // - manylinux1_i686 146 func pep425IsBetterLinuxPlatform(cur, candidate string) bool { 147 // We prefer "manylinux1_" platforms over "linux_" platforms. 148 curIs, curMany := isLinuxPlatform(cur) 149 candidateIs, candidateMany := isLinuxPlatform(candidate) 150 switch { 151 case !curIs: 152 return candidateIs 153 case !candidateIs: 154 return false 155 case curMany: 156 return false 157 default: 158 return candidateMany 159 } 160 } 161 162 // preferredPlatformFuncForTagSet examines a tag set and returns a function 163 // that compares two "platform" tags. 164 // 165 // The comparison function is chosen based on the operating system represented 166 // by the tag set. This choice is made with the assumption that the tag set 167 // represents a realistic platform (e.g., no mixed Mac and Linux tags). 168 func preferredPlatformFuncForTagSet(tags []*vpython.PEP425Tag) func(cur, candidate string) bool { 169 // Identify the operating system from the tag set. Iterate through tags until 170 // we see an indicator. 171 for _, tag := range tags { 172 // Linux? 173 if is, _ := isLinuxPlatform(tag.Platform); is { 174 return pep425IsBetterLinuxPlatform 175 } 176 177 // Mac 178 if plat := parsePEP425MacPlatform(tag.Platform); plat != nil { 179 return pep425IsBetterMacPlatform 180 } 181 } 182 183 // No opinion. 184 return func(cur, candidate string) bool { return false } 185 } 186 187 // isNewerPy3Abi returns true if the candidate string identifies a new, unstable 188 // ABI that should be preferred over the long-term stable "abi3", which we don't 189 // build wheels against. 190 func isNewerPy3Abi(cur, candidate string) bool { 191 // We don't bother finding the latest ABI (e.g. preferring "cp39" over 192 // "cp38"). Each release only has one supported unstable ABI, so we should 193 // never encounter more than one anyway. 194 return (cur == "abi3" || cur == "none") && strings.HasPrefix(candidate, "cp3") 195 } 196 197 // Prefer specific Python (e.g., cp27) over generic (e.g., py27). 198 func isSpecificImplAbi(python string) bool { 199 return !strings.HasPrefix(python, "py") 200 } 201 202 // pep425TagSelector chooses the "best" PEP425 tag from a set of potential tags. 203 // This "best" tag will be used to resolve our CIPD templates and allow for 204 // Python implementation-specific CIPD template parameters. 205 func pep425TagSelector(tags []*vpython.PEP425Tag) *vpython.PEP425Tag { 206 var best *vpython.PEP425Tag 207 208 // isPreferredOSPlatform is an OS-specific platform preference function. 209 isPreferredOSPlatform := preferredPlatformFuncForTagSet(tags) 210 211 isBetter := func(t *vpython.PEP425Tag) bool { 212 switch { 213 case best == nil: 214 return true 215 case t.Count() > best.Count(): 216 // More populated fields is more specificity. 217 return true 218 case best.AnyPlatform() && !t.AnyPlatform(): 219 // More specific platform is preferred. 220 return true 221 case !best.HasABI() && t.HasABI(): 222 // More specific ABI is preferred. 223 return true 224 case isNewerPy3Abi(best.Abi, t.Abi): 225 // Prefer the newest supported ABI tag. In theory this can break if 226 // we have wheels built against a long-term stable ABI like abi3, as 227 // we'll only look for packages built against the newest, unstable 228 // ABI. But in practice that doesn't happen, as dockerbuild 229 // produces packages tagged with the unstable ABIs. 230 return true 231 case isPreferredOSPlatform(best.Platform, t.Platform) && (isSpecificImplAbi(t.Python) || !isSpecificImplAbi(best.Python)): 232 // Prefer a better platform, but not if it means moving 233 // to a less-specific ABI. 234 return true 235 case isSpecificImplAbi(t.Python) && !isSpecificImplAbi(best.Python): 236 return true 237 238 default: 239 return false 240 } 241 } 242 243 for _, t := range tags { 244 tag := proto.Clone(t).(*vpython.PEP425Tag) 245 if isBetter(tag) { 246 best = tag 247 } 248 } 249 return best 250 } 251 252 // getPEP425CIPDTemplates returns the set of CIPD template strings for a 253 // given PEP425 tag. 254 // 255 // Template parameters are derived from the most representative PEP425 tag. 256 // Any missing tag parameters will result in their associated template 257 // parameters not getting exported. 258 // 259 // The full set of exported tag parameters is: 260 // - py_python: The PEP425 "python" tag value (e.g., "cp27"). 261 // - py_abi: The PEP425 Python ABI (e.g., "cp27mu"). 262 // - py_platform: The PEP425 Python platform (e.g., "manylinux1_x86_64"). 263 // - py_tag: The full PEP425 tag (e.g., "cp27-cp27mu-manylinux1_x86_64"). 264 // 265 // This function also backports the Python platform into the CIPD "platform" 266 // field, ensuring that regardless of the host platform, the Python CIPD 267 // wheel is chosen based solely on that host's Python interpreter. 268 // 269 // Infra CIPD packages tend to use "${platform}" (generic) combined with 270 // "${py_abi}" and "${py_platform}" to identify its packages. 271 func addPEP425CIPDTemplateForTag(expander template.Expander, tag *vpython.PEP425Tag) error { 272 if tag == nil { 273 return errors.New("no PEP425 tag") 274 } 275 276 if tag.Python != "" { 277 expander["py_python"] = tag.Python 278 } 279 if tag.Abi != "" { 280 expander["py_abi"] = tag.Abi 281 } 282 if tag.Platform != "" { 283 expander["py_platform"] = tag.Platform 284 } 285 if tag.Python != "" && tag.Abi != "" && tag.Platform != "" { 286 expander["py_tag"] = tag.TagString() 287 } 288 289 // Override the CIPD "platform" based on the PEP425 tag. This allows selection 290 // of Python wheels based on the platform of the Python executable rather 291 // than the platform of the underlying operating system. 292 // 293 // For example, a 64-bit Windows version can run 32-bit Python, and we'll 294 // want to use 32-bit Python wheels. 295 platform := PlatformForPEP425Tag(tag) 296 if platform.String() == "-" { 297 return errors.Reason("failed to infer CIPD platform for tag [%s]", tag).Err() 298 } 299 expander["platform"] = platform.String() 300 301 // Build the sum tag, "vpython_platform", 302 // "${platform}_${py_python}_${py_abi}" 303 if tag.Python != "" && tag.Abi != "" { 304 expander["vpython_platform"] = fmt.Sprintf("%s_%s_%s", platform, tag.Python, tag.Abi) 305 } 306 307 return nil 308 } 309 310 // PlatformForPEP425Tag returns the CIPD platform inferred from a given Python 311 // PEP425 tag. 312 // 313 // If the platform could not be determined, an empty string will be returned. 314 func PlatformForPEP425Tag(t *vpython.PEP425Tag) template.Platform { 315 switch platSplit := strings.SplitN(t.Platform, "_", 2); platSplit[0] { 316 case "linux", "manylinux1": 317 // Grab the remainder. 318 // 319 // Examples: 320 // - linux_i686 321 // - manylinux1_x86_64 322 // - linux_arm64 323 cpu := "" 324 if len(platSplit) > 1 { 325 cpu = platSplit[1] 326 } 327 switch cpu { 328 case "i686": 329 return template.Platform{OS: "linux", Arch: "386"} 330 case "x86_64": 331 return template.Platform{OS: "linux", Arch: "amd64"} 332 case "arm64", "aarch64": 333 return template.Platform{OS: "linux", Arch: "arm64"} 334 case "mipsel", "mips": 335 return template.Platform{OS: "linux", Arch: "mips32"} 336 case "mips64": 337 return template.Platform{OS: "linux", Arch: "mips64"} 338 default: 339 // All remaining "arm*" get the "armv6l" CIPD platform. 340 if strings.HasPrefix(cpu, "arm") { 341 return template.Platform{OS: "linux", Arch: "armv6l"} 342 } 343 return template.Platform{} 344 } 345 346 case "macosx": 347 // Grab the last token. 348 // 349 // Examples: 350 // - macosx_10_10_intel 351 // - macosx_10_10_i386 352 if len(platSplit) == 1 { 353 return template.Platform{} 354 } 355 suffixSplit := strings.SplitN(platSplit[1], "_", -1) 356 switch suffixSplit[len(suffixSplit)-1] { 357 case "intel", "x86_64", "fat64", "universal": 358 return template.Platform{OS: "mac", Arch: "amd64"} 359 case "arm64": 360 return template.Platform{OS: "mac", Arch: "arm64"} 361 case "i386", "fat32": 362 return template.Platform{OS: "mac", Arch: "386"} 363 default: 364 return template.Platform{} 365 } 366 367 case "win32": 368 // win32 369 return template.Platform{OS: "windows", Arch: "386"} 370 case "win": 371 // Examples: 372 // - win_amd64 373 if len(platSplit) == 1 { 374 return template.Platform{} 375 } 376 switch platSplit[1] { 377 case "amd64": 378 return template.Platform{OS: "windows", Arch: "amd64"} 379 default: 380 return template.Platform{} 381 } 382 383 default: 384 return template.Platform{} 385 } 386 }