go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/vpython/spec/spec.go (about)

     1  // Copyright 2017 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 spec contains implementation for search, parse, normalize vpython
    16  // spec.
    17  package spec
    18  
    19  import (
    20  	"sort"
    21  
    22  	"go.chromium.org/luci/common/data/sortby"
    23  	"go.chromium.org/luci/common/errors"
    24  
    25  	"go.chromium.org/luci/vpython/api/vpython"
    26  )
    27  
    28  // NormalizeSpec normalizes the specification Message such that two messages
    29  // with identical meaning will have identical representation.
    30  //
    31  // If multiple wheel entries exist for the same package name, they must also
    32  // share a version. If they don't, an error will be returned. Otherwise, they
    33  // will be merged into a single wheel entry.
    34  //
    35  // NormalizeSpec will prune any Wheel entries that don't match the specified
    36  // tags, and will remove the match entries from any remaining Wheel entries.
    37  func NormalizeSpec(spec *vpython.Spec, tags []*vpython.PEP425Tag) error {
    38  	// Apply match filters, prune any entries that don't match, and clear the
    39  	// MatchTag entries for those that do.
    40  	//
    41  	// Make sure the VirtualEnv package isn't listed in the wheels list.
    42  	//
    43  	// Track the versions for each package and assert that any duplicate packages
    44  	// don't share a version.
    45  	pos := 0
    46  	packageVersions := make(map[string]string, len(spec.Wheel))
    47  	for _, w := range spec.Wheel {
    48  		// If this package doesn't match the tag set, skip it.
    49  		if !PackageMatches(w, tags) {
    50  			continue
    51  		}
    52  
    53  		// If this package has already been included, assert version consistency.
    54  		if v, ok := packageVersions[w.Name]; ok {
    55  			if v != w.Version {
    56  				return errors.Reason("multiple versions for package %q: %q != %q", w.Name, w.Version, v).Err()
    57  			}
    58  
    59  			// This package has already been included, so we can ignore it.
    60  			continue
    61  		}
    62  
    63  		// Mark that this package was included, so we can assert version consistency
    64  		// and avoid duplicates.
    65  		packageVersions[w.Name] = w.Version
    66  		w.MatchTag = nil
    67  		spec.Wheel[pos] = w
    68  		pos++
    69  	}
    70  	spec.Wheel = spec.Wheel[:pos]
    71  	sort.Sort(specPackageSlice(spec.Wheel))
    72  	return nil
    73  }
    74  
    75  type specPackageSlice []*vpython.Spec_Package
    76  
    77  func (s specPackageSlice) Len() int      { return len(s) }
    78  func (s specPackageSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    79  
    80  func (s specPackageSlice) Less(i, j int) bool {
    81  	return sortby.Chain{
    82  		func(i, j int) bool { return s[i].Name < s[j].Name },
    83  		func(i, j int) bool { return s[i].Version < s[j].Version },
    84  	}.Use(i, j)
    85  }