blob: 6ffd594b53e68dd3dab00232daaa974c36ae92c2 [file] [log] [blame]
Serge Bazanskif369cfa2020-05-22 18:36:42 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
Serge Bazanski4b1e37c2021-09-28 12:49:15 +020017package fietsje
Serge Bazanskif369cfa2020-05-22 18:36:42 +020018
19import (
20 "fmt"
Serge Bazanskif369cfa2020-05-22 18:36:42 +020021 "log"
22 "os"
23 "strings"
24
25 "golang.org/x/mod/modfile"
26)
27
Serge Bazanski216fe7b2021-05-21 18:36:16 +020028// getTransitiveDeps is a hairy ball of heuristic used to find all recursively
29// transitive dependencies of a given dependency. It downloads a given dependency
30// using `go get`, and performs analysis of standard (go.mod/go.sum) and project-
31// specific dependency management configuration/lock files in order to build a full
32// view of all known, versioned transitive dependencies.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020033func (d *dependency) getTransitiveDeps() (map[string]*dependency, error) {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020034 // First, lock the dependency. Downloading it later will also return a sum, and we
35 // want to ensure both are the same.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020036 err := d.lock()
37 if err != nil {
38 return nil, fmt.Errorf("could not lock: %v", err)
39 }
40
41 _, path, sum, err := d.download()
42 if err != nil {
43 return nil, fmt.Errorf("could not download: %v", err)
44 }
45
46 if sum != d.locked.sum {
47 return nil, fmt.Errorf("inconsistent sum: %q downloaded, %q in shelf/lock", sum, d.locked.sum)
48 }
49
50 exists := func(p string) bool {
51 full := fmt.Sprintf("%s/%s", path, p)
52 if _, err := os.Stat(full); err == nil {
53 return true
54 }
55 if err != nil && !os.IsExist(err) {
56 panic(fmt.Sprintf("checking file %q: %v", full, err))
57 }
58 return false
59 }
60
61 read := func(p string) []byte {
62 full := fmt.Sprintf("%s/%s", path, p)
Lorenz Brun764a2de2021-11-22 16:26:36 +010063 data, err := os.ReadFile(full)
Serge Bazanskif369cfa2020-05-22 18:36:42 +020064 if err != nil {
65 panic(fmt.Sprintf("reading file %q: %v", full, err))
66 }
67 return data
68 }
69
70 requirements := make(map[string]*dependency)
71
72 // Read & parse go.mod if present.
73 var mf *modfile.File
74 if exists("go.mod") {
75 log.Printf("%q: parsing go.mod\n", d.importpath)
76 data := read("go.mod")
77 mf, err = modfile.Parse("go.mod", data, nil)
78 if err != nil {
79 return nil, fmt.Errorf("parsing go.mod in %s: %v", d.importpath, err)
80 }
81 }
82
83 // If a go.mod file was present, interpret it to populate dependencies.
84 if mf != nil {
85 for _, req := range mf.Require {
86 requirements[req.Mod.Path] = d.child(req.Mod.Path, req.Mod.Version)
87 }
88 for _, rep := range mf.Replace {
89 // skip filesystem rewrites
90 if rep.New.Version == "" {
91 continue
92 }
93
94 requirements[rep.New.Path] = d.child(rep.New.Path, rep.New.Version)
95 }
96 }
97
98 // Read parse, and interpret. go.sum if present.
99 // This should bring into view all recursively transitive dependencies.
100 if exists("go.sum") {
101 log.Printf("%q: parsing go.sum", d.importpath)
102 data := read("go.sum")
103 for _, line := range strings.Split(string(data), "\n") {
104 line = strings.TrimSpace(line)
105 if line == "" {
106 continue
107 }
108
109 parts := strings.Fields(line)
110 if len(parts) != 3 {
111 return nil, fmt.Errorf("parsing go.sum: unparseable line %q", line)
112 }
113
114 importpath, version := parts[0], parts[1]
115
116 // Skip if already created from go.mod.
117 // TODO(q3k): error if go.sum and go.mod disagree?
118 if _, ok := requirements[importpath]; ok {
119 continue
120 }
121
122 if strings.HasSuffix(version, "/go.mod") {
123 version = strings.TrimSuffix(version, "/go.mod")
124 }
125 requirements[importpath] = d.child(importpath, version)
126 }
127 }
128
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200129 // Special case: root Kubernetes repo - rewrite staging/ deps to k8s.io/ at correct
130 // versions, quit early. Kubernetes vendors all dependencies into vendor/, and also
131 // contains sub-projects (components) in staging/. This converts all staging
132 // dependencies into appropriately versioned k8s.io/<dep> paths.
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200133 if d.importpath == "k8s.io/kubernetes" {
134 log.Printf("%q: special case for Kubernetes main repository", d.importpath)
135 if mf == nil {
136 return nil, fmt.Errorf("k8s.io/kubernetes needs a go.mod")
137 }
138 // extract the version, turn into component version
139 version := d.version
140 if !strings.HasPrefix(version, "v") {
141 return nil, fmt.Errorf("invalid version format for k8s: %q", version)
142 }
143 version = version[1:]
144 componentVersion := fmt.Sprintf("kubernetes-%s", version)
145
146 // find all k8s.io 'components'
147 components := make(map[string]bool)
148 for _, rep := range mf.Replace {
149 if !strings.HasPrefix(rep.Old.Path, "k8s.io/") || !strings.HasPrefix(rep.New.Path, "./staging/src/") {
150 continue
151 }
152 components[rep.Old.Path] = true
153 }
154
155 // add them to planner at the 'kubernetes-$ver' tag
156 for component, _ := range components {
157 requirements[component] = d.child(component, componentVersion)
158 }
159 return requirements, nil
160 }
161
162 // Special case: github.com/containerd/containerd: read vendor.conf.
163 if d.importpath == "github.com/containerd/containerd" {
164 log.Printf("%q: special case for containerd", d.importpath)
165 if !exists("vendor.conf") {
166 panic("containerd needs vendor.conf")
167 }
168 data := read("vendor.conf")
169 for _, line := range strings.Split(string(data), "\n") {
170 // strip comments
171 parts := strings.SplitN(line, "#", 2)
172 line = parts[0]
173
174 // skip empty contents
175 line = strings.TrimSpace(line)
176 if line == "" {
177 continue
178 }
179
180 // read dep/version pairs
181 parts = strings.Fields(line)
182 if len(parts) < 2 {
183 return nil, fmt.Errorf("unparseable line in containerd vendor.conf: %q", line)
184 }
185 importpath, version := parts[0], parts[1]
186 requirements[importpath] = d.child(importpath, version)
187 }
188 return requirements, nil
189 }
190
191 return requirements, nil
192}