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  }