blob: f4a6e1d0a3ccfed30d2768f1e50556f6bebee070 [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
17package main
18
19import (
20 "fmt"
21 "io/ioutil"
22 "log"
23 "os"
24 "strings"
25
26 "golang.org/x/mod/modfile"
27)
28
29// getTransitiveDeps is a hairy ball of heuristic used to find all recursively transitive dependencies of a given
30// dependency.
31// It downloads a given dependency using `go get`, and performs analysis of standard (go.mod/go.sum) and project-
32// specific dependency management configuration/lock files in order to build a full view of all known, versioned
33// transitive dependencies.
34func (d *dependency) getTransitiveDeps() (map[string]*dependency, error) {
35 // First, lock the dependency. Downloading it later will also return a sum, and we want to ensure both are the
36 // same.
37 err := d.lock()
38 if err != nil {
39 return nil, fmt.Errorf("could not lock: %v", err)
40 }
41
42 _, path, sum, err := d.download()
43 if err != nil {
44 return nil, fmt.Errorf("could not download: %v", err)
45 }
46
47 if sum != d.locked.sum {
48 return nil, fmt.Errorf("inconsistent sum: %q downloaded, %q in shelf/lock", sum, d.locked.sum)
49 }
50
51 exists := func(p string) bool {
52 full := fmt.Sprintf("%s/%s", path, p)
53 if _, err := os.Stat(full); err == nil {
54 return true
55 }
56 if err != nil && !os.IsExist(err) {
57 panic(fmt.Sprintf("checking file %q: %v", full, err))
58 }
59 return false
60 }
61
62 read := func(p string) []byte {
63 full := fmt.Sprintf("%s/%s", path, p)
64 data, err := ioutil.ReadFile(full)
65 if err != nil {
66 panic(fmt.Sprintf("reading file %q: %v", full, err))
67 }
68 return data
69 }
70
71 requirements := make(map[string]*dependency)
72
73 // Read & parse go.mod if present.
74 var mf *modfile.File
75 if exists("go.mod") {
76 log.Printf("%q: parsing go.mod\n", d.importpath)
77 data := read("go.mod")
78 mf, err = modfile.Parse("go.mod", data, nil)
79 if err != nil {
80 return nil, fmt.Errorf("parsing go.mod in %s: %v", d.importpath, err)
81 }
82 }
83
84 // If a go.mod file was present, interpret it to populate dependencies.
85 if mf != nil {
86 for _, req := range mf.Require {
87 requirements[req.Mod.Path] = d.child(req.Mod.Path, req.Mod.Version)
88 }
89 for _, rep := range mf.Replace {
90 // skip filesystem rewrites
91 if rep.New.Version == "" {
92 continue
93 }
94
95 requirements[rep.New.Path] = d.child(rep.New.Path, rep.New.Version)
96 }
97 }
98
99 // Read parse, and interpret. go.sum if present.
100 // This should bring into view all recursively transitive dependencies.
101 if exists("go.sum") {
102 log.Printf("%q: parsing go.sum", d.importpath)
103 data := read("go.sum")
104 for _, line := range strings.Split(string(data), "\n") {
105 line = strings.TrimSpace(line)
106 if line == "" {
107 continue
108 }
109
110 parts := strings.Fields(line)
111 if len(parts) != 3 {
112 return nil, fmt.Errorf("parsing go.sum: unparseable line %q", line)
113 }
114
115 importpath, version := parts[0], parts[1]
116
117 // Skip if already created from go.mod.
118 // TODO(q3k): error if go.sum and go.mod disagree?
119 if _, ok := requirements[importpath]; ok {
120 continue
121 }
122
123 if strings.HasSuffix(version, "/go.mod") {
124 version = strings.TrimSuffix(version, "/go.mod")
125 }
126 requirements[importpath] = d.child(importpath, version)
127 }
128 }
129
130 // Special case: root Kubernetes repo - rewrite staging/ deps to k8s.io/ at correct versions, quit early.
131 // Kubernetes vendors all dependencies into vendor/, and also contains sub-projects (components) in staging/.
132 // This converts all staging dependencies into appropriately versioned k8s.io/<dep> paths.
133 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}