blob: e42199b1854f6b7aef998145d5b872de8f54919c [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
Serge Bazanski216fe7b2021-05-21 18:36:16 +020029// getTransitiveDeps is a hairy ball of heuristic used to find all recursively
30// transitive dependencies of a given dependency. It downloads a given dependency
31// 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
33// view of all known, versioned transitive dependencies.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020034func (d *dependency) getTransitiveDeps() (map[string]*dependency, error) {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020035 // First, lock the dependency. Downloading it later will also return a sum, and we
36 // want to ensure both are the same.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020037 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
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200130 // Special case: root Kubernetes repo - rewrite staging/ deps to k8s.io/ at correct
131 // versions, quit early. Kubernetes vendors all dependencies into vendor/, and also
132 // contains sub-projects (components) in staging/. This converts all staging
133 // dependencies into appropriately versioned k8s.io/<dep> paths.
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200134 if d.importpath == "k8s.io/kubernetes" {
135 log.Printf("%q: special case for Kubernetes main repository", d.importpath)
136 if mf == nil {
137 return nil, fmt.Errorf("k8s.io/kubernetes needs a go.mod")
138 }
139 // extract the version, turn into component version
140 version := d.version
141 if !strings.HasPrefix(version, "v") {
142 return nil, fmt.Errorf("invalid version format for k8s: %q", version)
143 }
144 version = version[1:]
145 componentVersion := fmt.Sprintf("kubernetes-%s", version)
146
147 // find all k8s.io 'components'
148 components := make(map[string]bool)
149 for _, rep := range mf.Replace {
150 if !strings.HasPrefix(rep.Old.Path, "k8s.io/") || !strings.HasPrefix(rep.New.Path, "./staging/src/") {
151 continue
152 }
153 components[rep.Old.Path] = true
154 }
155
156 // add them to planner at the 'kubernetes-$ver' tag
157 for component, _ := range components {
158 requirements[component] = d.child(component, componentVersion)
159 }
160 return requirements, nil
161 }
162
163 // Special case: github.com/containerd/containerd: read vendor.conf.
164 if d.importpath == "github.com/containerd/containerd" {
165 log.Printf("%q: special case for containerd", d.importpath)
166 if !exists("vendor.conf") {
167 panic("containerd needs vendor.conf")
168 }
169 data := read("vendor.conf")
170 for _, line := range strings.Split(string(data), "\n") {
171 // strip comments
172 parts := strings.SplitN(line, "#", 2)
173 line = parts[0]
174
175 // skip empty contents
176 line = strings.TrimSpace(line)
177 if line == "" {
178 continue
179 }
180
181 // read dep/version pairs
182 parts = strings.Fields(line)
183 if len(parts) < 2 {
184 return nil, fmt.Errorf("unparseable line in containerd vendor.conf: %q", line)
185 }
186 importpath, version := parts[0], parts[1]
187 requirements[importpath] = d.child(importpath, version)
188 }
189 return requirements, nil
190 }
191
192 return requirements, nil
193}