| // Copyright 2020 The Monogon Project Authors. | 
 | // | 
 | // SPDX-License-Identifier: Apache-2.0 | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //     http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | package main | 
 |  | 
 | import ( | 
 | 	"fmt" | 
 | 	"io/ioutil" | 
 | 	"log" | 
 | 	"os" | 
 | 	"strings" | 
 |  | 
 | 	"golang.org/x/mod/modfile" | 
 | ) | 
 |  | 
 | // getTransitiveDeps is a hairy ball of heuristic used to find all recursively transitive dependencies of a given | 
 | // dependency. | 
 | // It downloads a given dependency using `go get`, and performs analysis of standard (go.mod/go.sum) and project- | 
 | // specific dependency management configuration/lock files in order to build a full view of all known, versioned | 
 | // transitive dependencies. | 
 | func (d *dependency) getTransitiveDeps() (map[string]*dependency, error) { | 
 | 	// First, lock the dependency. Downloading it later will also return a sum, and we want to ensure both are the | 
 | 	// same. | 
 | 	err := d.lock() | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("could not lock: %v", err) | 
 | 	} | 
 |  | 
 | 	_, path, sum, err := d.download() | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("could not download: %v", err) | 
 | 	} | 
 |  | 
 | 	if sum != d.locked.sum { | 
 | 		return nil, fmt.Errorf("inconsistent sum: %q downloaded, %q in shelf/lock", sum, d.locked.sum) | 
 | 	} | 
 |  | 
 | 	exists := func(p string) bool { | 
 | 		full := fmt.Sprintf("%s/%s", path, p) | 
 | 		if _, err := os.Stat(full); err == nil { | 
 | 			return true | 
 | 		} | 
 | 		if err != nil && !os.IsExist(err) { | 
 | 			panic(fmt.Sprintf("checking file %q: %v", full, err)) | 
 | 		} | 
 | 		return false | 
 | 	} | 
 |  | 
 | 	read := func(p string) []byte { | 
 | 		full := fmt.Sprintf("%s/%s", path, p) | 
 | 		data, err := ioutil.ReadFile(full) | 
 | 		if err != nil { | 
 | 			panic(fmt.Sprintf("reading file %q: %v", full, err)) | 
 | 		} | 
 | 		return data | 
 | 	} | 
 |  | 
 | 	requirements := make(map[string]*dependency) | 
 |  | 
 | 	// Read & parse go.mod if present. | 
 | 	var mf *modfile.File | 
 | 	if exists("go.mod") { | 
 | 		log.Printf("%q: parsing go.mod\n", d.importpath) | 
 | 		data := read("go.mod") | 
 | 		mf, err = modfile.Parse("go.mod", data, nil) | 
 | 		if err != nil { | 
 | 			return nil, fmt.Errorf("parsing go.mod in %s: %v", d.importpath, err) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// If a go.mod file was present, interpret it to populate dependencies. | 
 | 	if mf != nil { | 
 | 		for _, req := range mf.Require { | 
 | 			requirements[req.Mod.Path] = d.child(req.Mod.Path, req.Mod.Version) | 
 | 		} | 
 | 		for _, rep := range mf.Replace { | 
 | 			// skip filesystem rewrites | 
 | 			if rep.New.Version == "" { | 
 | 				continue | 
 | 			} | 
 |  | 
 | 			requirements[rep.New.Path] = d.child(rep.New.Path, rep.New.Version) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Read parse, and interpret. go.sum if present. | 
 | 	// This should bring into view all recursively transitive dependencies. | 
 | 	if exists("go.sum") { | 
 | 		log.Printf("%q: parsing go.sum", d.importpath) | 
 | 		data := read("go.sum") | 
 | 		for _, line := range strings.Split(string(data), "\n") { | 
 | 			line = strings.TrimSpace(line) | 
 | 			if line == "" { | 
 | 				continue | 
 | 			} | 
 |  | 
 | 			parts := strings.Fields(line) | 
 | 			if len(parts) != 3 { | 
 | 				return nil, fmt.Errorf("parsing go.sum: unparseable line %q", line) | 
 | 			} | 
 |  | 
 | 			importpath, version := parts[0], parts[1] | 
 |  | 
 | 			// Skip if already created from go.mod. | 
 | 			// TODO(q3k): error if go.sum and go.mod disagree? | 
 | 			if _, ok := requirements[importpath]; ok { | 
 | 				continue | 
 | 			} | 
 |  | 
 | 			if strings.HasSuffix(version, "/go.mod") { | 
 | 				version = strings.TrimSuffix(version, "/go.mod") | 
 | 			} | 
 | 			requirements[importpath] = d.child(importpath, version) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Special case: root Kubernetes repo - rewrite staging/ deps to k8s.io/ at correct versions, quit early. | 
 | 	// Kubernetes vendors all dependencies into vendor/, and also contains sub-projects (components) in staging/. | 
 | 	// This converts all staging dependencies into appropriately versioned k8s.io/<dep> paths. | 
 | 	if d.importpath == "k8s.io/kubernetes" { | 
 | 		log.Printf("%q: special case for Kubernetes main repository", d.importpath) | 
 | 		if mf == nil { | 
 | 			return nil, fmt.Errorf("k8s.io/kubernetes needs a go.mod") | 
 | 		} | 
 | 		// extract the version, turn into component version | 
 | 		version := d.version | 
 | 		if !strings.HasPrefix(version, "v") { | 
 | 			return nil, fmt.Errorf("invalid version format for k8s: %q", version) | 
 | 		} | 
 | 		version = version[1:] | 
 | 		componentVersion := fmt.Sprintf("kubernetes-%s", version) | 
 |  | 
 | 		// find all k8s.io 'components' | 
 | 		components := make(map[string]bool) | 
 | 		for _, rep := range mf.Replace { | 
 | 			if !strings.HasPrefix(rep.Old.Path, "k8s.io/") || !strings.HasPrefix(rep.New.Path, "./staging/src/") { | 
 | 				continue | 
 | 			} | 
 | 			components[rep.Old.Path] = true | 
 | 		} | 
 |  | 
 | 		// add them to planner at the 'kubernetes-$ver' tag | 
 | 		for component, _ := range components { | 
 | 			requirements[component] = d.child(component, componentVersion) | 
 | 		} | 
 | 		return requirements, nil | 
 | 	} | 
 |  | 
 | 	// Special case: github.com/containerd/containerd: read vendor.conf. | 
 | 	if d.importpath == "github.com/containerd/containerd" { | 
 | 		log.Printf("%q: special case for containerd", d.importpath) | 
 | 		if !exists("vendor.conf") { | 
 | 			panic("containerd needs vendor.conf") | 
 | 		} | 
 | 		data := read("vendor.conf") | 
 | 		for _, line := range strings.Split(string(data), "\n") { | 
 | 			// strip comments | 
 | 			parts := strings.SplitN(line, "#", 2) | 
 | 			line = parts[0] | 
 |  | 
 | 			// skip empty contents | 
 | 			line = strings.TrimSpace(line) | 
 | 			if line == "" { | 
 | 				continue | 
 | 			} | 
 |  | 
 | 			// read dep/version pairs | 
 | 			parts = strings.Fields(line) | 
 | 			if len(parts) < 2 { | 
 | 				return nil, fmt.Errorf("unparseable line in containerd vendor.conf: %q", line) | 
 | 			} | 
 | 			importpath, version := parts[0], parts[1] | 
 | 			requirements[importpath] = d.child(importpath, version) | 
 | 		} | 
 | 		return requirements, nil | 
 | 	} | 
 |  | 
 | 	return requirements, nil | 
 | } |