aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/lockfile/lockfile.go
blob: bb36eda93f419419645142acca7625f2313d3691 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Package lockfile provides the lockfile interface and implementations for the various package managers
package lockfile

import (
	"fmt"
	"io"
	"reflect"
	"sort"

	mapset "github.com/deckarep/golang-set"
	"github.com/vercel/turbo/cli/internal/turbopath"
	"golang.org/x/sync/errgroup"
)

// Lockfile Interface for general operations that work across all lockfiles
type Lockfile interface {
	// ResolvePackage Given a workspace, a package it imports and version returns the key, resolved version, and if it was found
	ResolvePackage(workspacePath turbopath.AnchoredUnixPath, name string, version string) (Package, error)
	// AllDependencies Given a lockfile key return all (dev/optional/peer) dependencies of that package
	AllDependencies(key string) (map[string]string, bool)
	// Subgraph Given a list of lockfile keys returns a Lockfile based off the original one that only contains the packages given
	Subgraph(workspacePackages []turbopath.AnchoredSystemPath, packages []string) (Lockfile, error)
	// Encode encode the lockfile representation and write it to the given writer
	Encode(w io.Writer) error
	// Patches return a list of patches used in the lockfile
	Patches() []turbopath.AnchoredUnixPath
	// GlobalChange checks if there are any differences between lockfiles that would completely invalidate
	// the cache.
	GlobalChange(other Lockfile) bool
}

// IsNil checks if lockfile is nil
func IsNil(l Lockfile) bool {
	return l == nil || reflect.ValueOf(l).IsNil()
}

// Package Structure representing a possible Pack
type Package struct {
	// Key used to lookup a package in the lockfile
	Key string
	// The resolved version of a package as it appears in the lockfile
	Version string
	// Set to true iff Key and Version are set
	Found bool
}

// ByKey sort package structures by key
type ByKey []Package

func (p ByKey) Len() int {
	return len(p)
}

func (p ByKey) Swap(i, j int) {
	p[i], p[j] = p[j], p[i]
}

func (p ByKey) Less(i, j int) bool {
	return p[i].Key+p[i].Version < p[j].Key+p[j].Version
}

var _ (sort.Interface) = (*ByKey)(nil)

// TransitiveClosure the set of all lockfile keys that pkg depends on
func TransitiveClosure(
	workspaceDir turbopath.AnchoredUnixPath,
	unresolvedDeps map[string]string,
	lockFile Lockfile,
) (mapset.Set, error) {
	if lf, ok := lockFile.(*NpmLockfile); ok {
		// We special case as Rust implementations have their own dep crawl
		return npmTransitiveDeps(lf, workspaceDir, unresolvedDeps)
	}
	return transitiveClosure(workspaceDir, unresolvedDeps, lockFile)
}

func transitiveClosure(
	workspaceDir turbopath.AnchoredUnixPath,
	unresolvedDeps map[string]string,
	lockFile Lockfile,
) (mapset.Set, error) {
	if IsNil(lockFile) {
		return nil, fmt.Errorf("No lockfile available to do analysis on")
	}

	resolvedPkgs := mapset.NewSet()
	lockfileEg := &errgroup.Group{}

	transitiveClosureHelper(lockfileEg, workspaceDir, lockFile, unresolvedDeps, resolvedPkgs)

	if err := lockfileEg.Wait(); err != nil {
		return nil, err
	}

	return resolvedPkgs, nil
}

func transitiveClosureHelper(
	wg *errgroup.Group,
	workspacePath turbopath.AnchoredUnixPath,
	lockfile Lockfile,
	unresolvedDirectDeps map[string]string,
	resolvedDeps mapset.Set,
) {
	for directDepName, unresolvedVersion := range unresolvedDirectDeps {
		directDepName := directDepName
		unresolvedVersion := unresolvedVersion
		wg.Go(func() error {

			lockfilePkg, err := lockfile.ResolvePackage(workspacePath, directDepName, unresolvedVersion)

			if err != nil {
				return err
			}

			if !lockfilePkg.Found || resolvedDeps.Contains(lockfilePkg) {
				return nil
			}

			resolvedDeps.Add(lockfilePkg)

			allDeps, ok := lockfile.AllDependencies(lockfilePkg.Key)

			if !ok {
				panic(fmt.Sprintf("Unable to find entry for %s", lockfilePkg.Key))
			}

			if len(allDeps) > 0 {
				transitiveClosureHelper(wg, workspacePath, lockfile, allDeps, resolvedDeps)
			}

			return nil
		})
	}
}