github.com/sdboyer/gps@v0.16.3/lock.go (about)

     1  package gps
     2  
     3  import (
     4  	"bytes"
     5  	"sort"
     6  )
     7  
     8  // Lock represents data from a lock file (or however the implementing tool
     9  // chooses to store it) at a particular version that is relevant to the
    10  // satisfiability solving process.
    11  //
    12  // In general, the information produced by gps on finding a successful
    13  // solution is all that would be necessary to constitute a lock file, though
    14  // tools can include whatever other information they want in their storage.
    15  type Lock interface {
    16  	// Indicates the version of the solver used to generate this lock data
    17  	//SolverVersion() string
    18  
    19  	// The hash of inputs to gps that resulted in this lock data
    20  	InputHash() []byte
    21  
    22  	// Projects returns the list of LockedProjects contained in the lock data.
    23  	Projects() []LockedProject
    24  }
    25  
    26  // LocksAreEq checks if two locks are equivalent. This checks that
    27  // all contained LockedProjects are equal, and optionally (if the third
    28  // parameter is true) whether the locks' input hashes are equal.
    29  func LocksAreEq(l1, l2 Lock, checkHash bool) bool {
    30  	// Cheapest ops first
    31  	if checkHash && !bytes.Equal(l1.InputHash(), l2.InputHash()) {
    32  		return false
    33  	}
    34  
    35  	p1, p2 := l1.Projects(), l2.Projects()
    36  	if len(p1) != len(p2) {
    37  		return false
    38  	}
    39  
    40  	// Check if the slices are sorted already. If they are, we can compare
    41  	// without copying. Otherwise, we have to copy to avoid altering the
    42  	// original input.
    43  	sp1, sp2 := lpsorter(p1), lpsorter(p2)
    44  	if len(p1) > 1 && !sort.IsSorted(sp1) {
    45  		p1 = make([]LockedProject, len(p1))
    46  		copy(p1, l1.Projects())
    47  		sort.Sort(lpsorter(p1))
    48  	}
    49  	if len(p2) > 1 && !sort.IsSorted(sp2) {
    50  		p2 = make([]LockedProject, len(p2))
    51  		copy(p2, l2.Projects())
    52  		sort.Sort(lpsorter(p2))
    53  	}
    54  
    55  	for k, lp := range p1 {
    56  		if !lp.Eq(p2[k]) {
    57  			return false
    58  		}
    59  	}
    60  	return true
    61  }
    62  
    63  // LockedProject is a single project entry from a lock file. It expresses the
    64  // project's name, one or both of version and underlying revision, the network
    65  // URI for accessing it, the path at which it should be placed within a vendor
    66  // directory, and the packages that are used in it.
    67  type LockedProject struct {
    68  	pi   ProjectIdentifier
    69  	v    UnpairedVersion
    70  	r    Revision
    71  	pkgs []string
    72  }
    73  
    74  // SimpleLock is a helper for tools to easily describe lock data when they know
    75  // that no hash, or other complex information, is available.
    76  type SimpleLock []LockedProject
    77  
    78  var _ Lock = SimpleLock{}
    79  
    80  // InputHash always returns an empty string for SimpleLock. This makes it useless
    81  // as a stable lock to be written to disk, but still useful for some ephemeral
    82  // purposes.
    83  func (SimpleLock) InputHash() []byte {
    84  	return nil
    85  }
    86  
    87  // Projects returns the entire contents of the SimpleLock.
    88  func (l SimpleLock) Projects() []LockedProject {
    89  	return l
    90  }
    91  
    92  // NewLockedProject creates a new LockedProject struct with a given
    93  // ProjectIdentifier (name and optional upstream source URL), version. and list
    94  // of packages required from the project.
    95  //
    96  // Note that passing a nil version will cause a panic. This is a correctness
    97  // measure to ensure that the solver is never exposed to a version-less lock
    98  // entry. Such a case would be meaningless - the solver would have no choice but
    99  // to simply dismiss that project. By creating a hard failure case via panic
   100  // instead, we are trying to avoid inflicting the resulting pain on the user by
   101  // instead forcing a decision on the Analyzer implementation.
   102  func NewLockedProject(id ProjectIdentifier, v Version, pkgs []string) LockedProject {
   103  	if v == nil {
   104  		panic("must provide a non-nil version to create a LockedProject")
   105  	}
   106  
   107  	lp := LockedProject{
   108  		pi:   id,
   109  		pkgs: pkgs,
   110  	}
   111  
   112  	switch tv := v.(type) {
   113  	case Revision:
   114  		lp.r = tv
   115  	case branchVersion:
   116  		lp.v = tv
   117  	case semVersion:
   118  		lp.v = tv
   119  	case plainVersion:
   120  		lp.v = tv
   121  	case versionPair:
   122  		lp.r = tv.r
   123  		lp.v = tv.v
   124  	}
   125  
   126  	return lp
   127  }
   128  
   129  // Ident returns the identifier describing the project. This includes both the
   130  // local name (the root name by which the project is referenced in import paths)
   131  // and the network name, where the upstream source lives.
   132  func (lp LockedProject) Ident() ProjectIdentifier {
   133  	return lp.pi
   134  }
   135  
   136  // Version assembles together whatever version and/or revision data is
   137  // available into a single Version.
   138  func (lp LockedProject) Version() Version {
   139  	if lp.r == "" {
   140  		return lp.v
   141  	}
   142  
   143  	if lp.v == nil {
   144  		return lp.r
   145  	}
   146  
   147  	return lp.v.Is(lp.r)
   148  }
   149  
   150  // Eq checks if two LockedProject instances are equal.
   151  func (lp LockedProject) Eq(lp2 LockedProject) bool {
   152  	if lp.pi != lp2.pi {
   153  		return false
   154  	}
   155  
   156  	if lp.r != lp2.r {
   157  		return false
   158  	}
   159  
   160  	if len(lp.pkgs) != len(lp2.pkgs) {
   161  		return false
   162  	}
   163  
   164  	for k, v := range lp.pkgs {
   165  		if lp2.pkgs[k] != v {
   166  			return false
   167  		}
   168  	}
   169  
   170  	v1n := lp.v == nil
   171  	v2n := lp2.v == nil
   172  
   173  	if v1n != v2n {
   174  		return false
   175  	}
   176  
   177  	if !v1n && !lp.v.Matches(lp2.v) {
   178  		return false
   179  	}
   180  
   181  	return true
   182  }
   183  
   184  // Packages returns the list of packages from within the LockedProject that are
   185  // actually used in the import graph. Some caveats:
   186  //
   187  //  * The names given are relative to the root import path for the project. If
   188  //    the root package itself is imported, it's represented as ".".
   189  //  * Just because a package path isn't included in this list doesn't mean it's
   190  //    safe to remove - it could contain C files, or other assets, that can't be
   191  //    safely removed.
   192  //  * The slice is not a copy. If you need to modify it, copy it first.
   193  func (lp LockedProject) Packages() []string {
   194  	return lp.pkgs
   195  }
   196  
   197  type safeLock struct {
   198  	h []byte
   199  	p []LockedProject
   200  }
   201  
   202  func (sl safeLock) InputHash() []byte {
   203  	return sl.h
   204  }
   205  
   206  func (sl safeLock) Projects() []LockedProject {
   207  	return sl.p
   208  }
   209  
   210  // prepLock ensures a lock is prepared and safe for use by the solver. This is
   211  // mostly about defensively ensuring that no outside routine can modify the lock
   212  // while the solver is in-flight.
   213  //
   214  // This is achieved by copying the lock's data into a new safeLock.
   215  func prepLock(l Lock) safeLock {
   216  	pl := l.Projects()
   217  
   218  	rl := safeLock{
   219  		h: l.InputHash(),
   220  		p: make([]LockedProject, len(pl)),
   221  	}
   222  	copy(rl.p, pl)
   223  
   224  	return rl
   225  }
   226  
   227  // SortLockedProjects sorts a slice of LockedProject in alphabetical order by
   228  // ProjectRoot.
   229  func SortLockedProjects(lps []LockedProject) {
   230  	sort.Stable(lpsorter(lps))
   231  }
   232  
   233  type lpsorter []LockedProject
   234  
   235  func (lps lpsorter) Swap(i, j int) {
   236  	lps[i], lps[j] = lps[j], lps[i]
   237  }
   238  
   239  func (lps lpsorter) Len() int {
   240  	return len(lps)
   241  }
   242  
   243  func (lps lpsorter) Less(i, j int) bool {
   244  	return lps[i].pi.ProjectRoot < lps[j].pi.ProjectRoot
   245  }