mirror of https://github.com/k3s-io/k3s
613 lines
15 KiB
Go
613 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"go/build"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/kr/fs"
|
|
)
|
|
|
|
var cmdSave = &Command{
|
|
Name: "save",
|
|
Args: "[-r] [-t] [packages]",
|
|
Short: "list and copy dependencies into Godeps",
|
|
Long: `
|
|
|
|
Save writes a list of the named packages and their dependencies along
|
|
with the exact source control revision of each package, and copies
|
|
their source code into a subdirectory. Packages inside "." are excluded
|
|
from the list to be copied.
|
|
|
|
The list is written to Godeps/Godeps.json, and source code for all
|
|
dependencies is copied into either Godeps/_workspace or, if the vendor
|
|
experiment is turned on, vendor/.
|
|
|
|
The dependency list is a JSON document with the following structure:
|
|
|
|
type Godeps struct {
|
|
ImportPath string
|
|
GoVersion string // Abridged output of 'go version'.
|
|
Packages []string // Arguments to godep save, if any.
|
|
Deps []struct {
|
|
ImportPath string
|
|
Comment string // Tag or description of commit.
|
|
Rev string // VCS-specific commit ID.
|
|
}
|
|
}
|
|
|
|
Any packages already present in the list will be left unchanged.
|
|
To update a dependency to a newer revision, use 'godep update'.
|
|
|
|
If -r is given, import statements will be rewritten to refer directly
|
|
to the copied source code. This is not compatible with the vendor
|
|
experiment. Note that this will not rewrite the statements in the
|
|
files outside the project.
|
|
|
|
If -t is given, test files (*_test.go files + testdata directories) are
|
|
also saved.
|
|
|
|
For more about specifying packages, see 'go help packages'.
|
|
`,
|
|
Run: runSave,
|
|
OnlyInGOPATH: true,
|
|
}
|
|
|
|
var (
|
|
saveR, saveT bool
|
|
)
|
|
|
|
func init() {
|
|
cmdSave.Flag.BoolVar(&saveR, "r", false, "rewrite import paths")
|
|
cmdSave.Flag.BoolVar(&saveT, "t", false, "save test files")
|
|
|
|
}
|
|
|
|
func runSave(cmd *Command, args []string) {
|
|
if VendorExperiment && saveR {
|
|
log.Println("flag -r is incompatible with the vendoring experiment")
|
|
cmd.UsageExit()
|
|
}
|
|
err := save(args)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
}
|
|
|
|
func dotPackage() (*build.Package, error) {
|
|
dir, err := filepath.Abs(".")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return build.ImportDir(dir, build.FindOnly)
|
|
}
|
|
|
|
func projectPackages(dDir string, a []*Package) []*Package {
|
|
var projPkgs []*Package
|
|
dotDir := fmt.Sprintf("%s%c", dDir, filepath.Separator)
|
|
for _, p := range a {
|
|
pkgDir := fmt.Sprintf("%s%c", p.Dir, filepath.Separator)
|
|
if strings.HasPrefix(pkgDir, dotDir) {
|
|
projPkgs = append(projPkgs, p)
|
|
}
|
|
}
|
|
return projPkgs
|
|
}
|
|
|
|
func save(pkgs []string) error {
|
|
var err error
|
|
dp, err := dotPackage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
debugln("dotPackageImportPath:", dp.ImportPath)
|
|
debugln("dotPackageDir:", dp.Dir)
|
|
|
|
cv, err := goVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
verboseln("Go Version:", cv)
|
|
|
|
gold, err := loadDefaultGodepsFile()
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
verboseln("No old Godeps.json found.")
|
|
gold.GoVersion = cv
|
|
}
|
|
|
|
printVersionWarnings(gold.GoVersion)
|
|
if len(gold.GoVersion) == 0 {
|
|
gold.GoVersion = majorGoVersion
|
|
} else {
|
|
majorGoVersion, err = trimGoVersion(gold.GoVersion)
|
|
if err != nil {
|
|
log.Fatalf("Unable to determine go major version from value specified in %s: %s\n", gold.file(), gold.GoVersion)
|
|
}
|
|
}
|
|
|
|
gnew := &Godeps{
|
|
ImportPath: dp.ImportPath,
|
|
GoVersion: gold.GoVersion,
|
|
}
|
|
|
|
switch len(pkgs) {
|
|
case 0:
|
|
pkgs = []string{"."}
|
|
default:
|
|
gnew.Packages = pkgs
|
|
}
|
|
|
|
verboseln("Finding dependencies for", pkgs)
|
|
a, err := LoadPackages(pkgs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, p := range a {
|
|
verboseln("Found package:", p.ImportPath)
|
|
verboseln("\tDeps:", strings.Join(p.Deps, " "))
|
|
}
|
|
ppln(a)
|
|
|
|
projA := projectPackages(dp.Dir, a)
|
|
debugln("Filtered projectPackages")
|
|
ppln(projA)
|
|
|
|
verboseln("Computing new Godeps.json file")
|
|
err = gnew.fill(a, dp.ImportPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
debugln("New Godeps Filled")
|
|
ppln(gnew)
|
|
|
|
if gnew.Deps == nil {
|
|
gnew.Deps = make([]Dependency, 0) // produce json [], not null
|
|
}
|
|
gdisk := gnew.copy()
|
|
err = carryVersions(&gold, gnew)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if gold.isOldFile {
|
|
// If we are migrating from an old format file,
|
|
// we require that the listed version of every
|
|
// dependency must be installed in GOPATH, so it's
|
|
// available to copy.
|
|
if !eqDeps(gnew.Deps, gdisk.Deps) {
|
|
return errors.New(strings.TrimSpace(needRestore))
|
|
}
|
|
gold = Godeps{}
|
|
}
|
|
os.Remove("Godeps") // remove regular file if present; ignore error
|
|
readme := filepath.Join("Godeps", "Readme")
|
|
err = writeFile(readme, strings.TrimSpace(Readme)+"\n")
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
_, err = gnew.save()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
verboseln("Computing diff between old and new deps")
|
|
// We use a name starting with "_" so the go tool
|
|
// ignores this directory when traversing packages
|
|
// starting at the project's root. For example,
|
|
// godep go list ./...
|
|
srcdir := filepath.FromSlash(strings.Trim(sep, "/"))
|
|
rem := subDeps(gold.Deps, gnew.Deps)
|
|
ppln(rem)
|
|
add := subDeps(gnew.Deps, gold.Deps)
|
|
ppln(add)
|
|
if len(rem) > 0 {
|
|
verboseln("Deps to remove:")
|
|
for _, r := range rem {
|
|
verboseln("\t", r.ImportPath)
|
|
}
|
|
verboseln("Removing unused dependencies")
|
|
err = removeSrc(srcdir, rem)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(add) > 0 {
|
|
verboseln("Deps to add:")
|
|
for _, a := range add {
|
|
verboseln("\t", a.ImportPath)
|
|
}
|
|
verboseln("Adding new dependencies")
|
|
err = copySrc(srcdir, add)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !VendorExperiment {
|
|
f, _ := filepath.Split(srcdir)
|
|
writeVCSIgnore(f)
|
|
}
|
|
var rewritePaths []string
|
|
if saveR {
|
|
for _, dep := range gnew.Deps {
|
|
rewritePaths = append(rewritePaths, dep.ImportPath)
|
|
}
|
|
}
|
|
verboseln("Rewriting paths (if necessary)")
|
|
ppln(rewritePaths)
|
|
return rewrite(projA, dp.ImportPath, rewritePaths)
|
|
}
|
|
|
|
func printVersionWarnings(ov string) {
|
|
var warning bool
|
|
cv, err := goVersion()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Trim the old version because we may have saved it w/o trimming it
|
|
// cv is already trimmed by goVersion()
|
|
tov, err := trimGoVersion(ov)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if tov != ov {
|
|
log.Printf("WARNING: Recorded go version (%s) with minor version string found.\n", ov)
|
|
warning = true
|
|
}
|
|
if cv != tov {
|
|
log.Printf("WARNING: Recorded major go version (%s) and in-use major go version (%s) differ.\n", tov, cv)
|
|
warning = true
|
|
}
|
|
if warning {
|
|
log.Println("To record current major go version run `godep update -goversion`.")
|
|
}
|
|
}
|
|
|
|
type revError struct {
|
|
ImportPath string
|
|
WantRev string
|
|
HavePath string
|
|
HaveRev string
|
|
}
|
|
|
|
func (v *revError) Error() string {
|
|
return fmt.Sprintf("cannot save %s at revision %s: already have %s at revision %s.\n"+
|
|
"Run `godep update %s' first.", v.ImportPath, v.WantRev, v.HavePath, v.HaveRev, v.HavePath)
|
|
}
|
|
|
|
// carryVersions copies Rev and Comment from a to b for
|
|
// each dependency with an identical ImportPath. For any
|
|
// dependency in b that appears to be from the same repo
|
|
// as one in a (for example, a parent or child directory),
|
|
// the Rev must already match - otherwise it is an error.
|
|
func carryVersions(a, b *Godeps) error {
|
|
for i := range b.Deps {
|
|
err := carryVersion(a, &b.Deps[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func carryVersion(a *Godeps, db *Dependency) error {
|
|
// First see if this exact package is already in the list.
|
|
for _, da := range a.Deps {
|
|
if db.ImportPath == da.ImportPath {
|
|
db.Rev = da.Rev
|
|
db.Comment = da.Comment
|
|
return nil
|
|
}
|
|
}
|
|
// No exact match, check for child or sibling package.
|
|
// We can't handle mismatched versions for packages in
|
|
// the same repo, so report that as an error.
|
|
for _, da := range a.Deps {
|
|
if strings.HasPrefix(db.ImportPath, da.ImportPath+"/") ||
|
|
strings.HasPrefix(da.ImportPath, db.root+"/") {
|
|
if da.Rev != db.Rev {
|
|
return &revError{
|
|
ImportPath: db.ImportPath,
|
|
WantRev: db.Rev,
|
|
HavePath: da.ImportPath,
|
|
HaveRev: da.Rev,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// No related package in the list, must be a new repo.
|
|
return nil
|
|
}
|
|
|
|
// subDeps returns a - b, using ImportPath for equality.
|
|
func subDeps(a, b []Dependency) (diff []Dependency) {
|
|
Diff:
|
|
for _, da := range a {
|
|
for _, db := range b {
|
|
if da.ImportPath == db.ImportPath {
|
|
continue Diff
|
|
}
|
|
}
|
|
diff = append(diff, da)
|
|
}
|
|
return diff
|
|
}
|
|
|
|
func removeSrc(srcdir string, deps []Dependency) error {
|
|
for _, dep := range deps {
|
|
path := filepath.FromSlash(dep.ImportPath)
|
|
err := os.RemoveAll(filepath.Join(srcdir, path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func copySrc(dir string, deps []Dependency) error {
|
|
// mapping to see if we visited a parent directory already
|
|
visited := make(map[string]bool)
|
|
ok := true
|
|
for _, dep := range deps {
|
|
debugln("copySrc for", dep.ImportPath)
|
|
srcdir := filepath.Join(dep.ws, "src")
|
|
rel, err := filepath.Rel(srcdir, dep.dir)
|
|
debugln("srcdir", srcdir)
|
|
debugln("rel", rel)
|
|
debugln("err", err)
|
|
if err != nil { // this should never happen
|
|
return err
|
|
}
|
|
dstpkgroot := filepath.Join(dir, rel)
|
|
err = os.RemoveAll(dstpkgroot)
|
|
if err != nil {
|
|
log.Println(err)
|
|
ok = false
|
|
}
|
|
|
|
// copy actual dependency
|
|
vf := dep.vcs.listFiles(dep.dir)
|
|
debugln("vf", vf)
|
|
w := fs.Walk(dep.dir)
|
|
for w.Step() {
|
|
err = copyPkgFile(vf, dir, srcdir, w)
|
|
if err != nil {
|
|
log.Println(err)
|
|
ok = false
|
|
}
|
|
}
|
|
|
|
// Look for legal files in root
|
|
// some packages are imports as a sub-package but license info
|
|
// is at root: exampleorg/common has license file in exampleorg
|
|
//
|
|
if dep.ImportPath == dep.root {
|
|
// we are already at root
|
|
continue
|
|
}
|
|
|
|
// prevent copying twice This could happen if we have
|
|
// two subpackages listed someorg/common and
|
|
// someorg/anotherpack which has their license in
|
|
// the parent dir of someorg
|
|
rootdir := filepath.Join(srcdir, filepath.FromSlash(dep.root))
|
|
if visited[rootdir] {
|
|
continue
|
|
}
|
|
visited[rootdir] = true
|
|
vf = dep.vcs.listFiles(rootdir)
|
|
w = fs.Walk(rootdir)
|
|
for w.Step() {
|
|
fname := filepath.Base(w.Path())
|
|
if IsLegalFile(fname) && !strings.Contains(w.Path(), sep) {
|
|
err = copyPkgFile(vf, dir, srcdir, w)
|
|
if err != nil {
|
|
log.Println(err)
|
|
ok = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !ok {
|
|
return errorCopyingSourceCode
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyPkgFile(vf vcsFiles, dstroot, srcroot string, w *fs.Walker) error {
|
|
if w.Err() != nil {
|
|
return w.Err()
|
|
}
|
|
name := w.Stat().Name()
|
|
if w.Stat().IsDir() {
|
|
if name[0] == '.' || name[0] == '_' || (!saveT && name == "testdata") {
|
|
// Skip directories starting with '.' or '_' or
|
|
// 'testdata' (last is only skipped if saveT is false)
|
|
w.SkipDir()
|
|
}
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(srcroot, w.Path())
|
|
if err != nil { // this should never happen
|
|
return err
|
|
}
|
|
if !saveT && strings.HasSuffix(name, "_test.go") {
|
|
if verbose {
|
|
log.Printf("save: skipping test file: %s", w.Path())
|
|
}
|
|
return nil
|
|
}
|
|
if !vf.Contains(w.Path()) {
|
|
if verbose {
|
|
log.Printf("save: skipping untracked file: %s", w.Path())
|
|
}
|
|
return nil
|
|
}
|
|
return copyFile(filepath.Join(dstroot, rel), w.Path())
|
|
}
|
|
|
|
// copyFile copies a regular file from src to dst.
|
|
// dst is opened with os.Create.
|
|
// If the file name ends with .go,
|
|
// copyFile strips canonical import path annotations.
|
|
// These are comments of the form:
|
|
// package foo // import "bar/foo"
|
|
// package foo /* import "bar/foo" */
|
|
func copyFile(dst, src string) error {
|
|
err := os.MkdirAll(filepath.Dir(dst), 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
linkDst, err := os.Readlink(src)
|
|
if err == nil {
|
|
return os.Symlink(linkDst, dst)
|
|
}
|
|
|
|
si, err := stat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
w, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(dst, si.Mode()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.HasSuffix(dst, ".go") {
|
|
debugln("Copy Without Import Comment", w, r)
|
|
err = copyWithoutImportComment(w, r)
|
|
} else {
|
|
debugln("Copy (plain)", w, r)
|
|
_, err = io.Copy(w, r)
|
|
}
|
|
err1 := w.Close()
|
|
if err == nil {
|
|
err = err1
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func copyWithoutImportComment(w io.Writer, r io.Reader) error {
|
|
b := bufio.NewReader(r)
|
|
for {
|
|
l, err := b.ReadBytes('\n')
|
|
eof := err == io.EOF
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
|
|
// If we have data then write it out...
|
|
if len(l) > 0 {
|
|
// Strip off \n if it exists because stripImportComment
|
|
_, err := w.Write(append(stripImportComment(bytes.TrimRight(l, "\n")), '\n'))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if eof {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
importAnnotation = `import\s+(?:"[^"]*"|` + "`[^`]*`" + `)`
|
|
importComment = `(?://\s*` + importAnnotation + `\s*$|/\*\s*` + importAnnotation + `\s*\*/)`
|
|
)
|
|
|
|
var (
|
|
importCommentRE = regexp.MustCompile(`^\s*(package\s+\w+)\s+` + importComment + `(.*)`)
|
|
pkgPrefix = []byte("package ")
|
|
)
|
|
|
|
// stripImportComment returns line with its import comment removed.
|
|
// If s is not a package statement containing an import comment,
|
|
// it is returned unaltered.
|
|
// FIXME: expects lines w/o a \n at the end
|
|
// See also http://golang.org/s/go14customimport.
|
|
func stripImportComment(line []byte) []byte {
|
|
if !bytes.HasPrefix(line, pkgPrefix) {
|
|
// Fast path; this will skip all but one line in the file.
|
|
// This assumes there is no whitespace before the keyword.
|
|
return line
|
|
}
|
|
if m := importCommentRE.FindSubmatch(line); m != nil {
|
|
return append(m[1], m[2]...)
|
|
}
|
|
return line
|
|
}
|
|
|
|
// Func writeVCSIgnore writes "ignore" files inside dir for known VCSs,
|
|
// so that dir/pkg and dir/bin don't accidentally get committed.
|
|
// It logs any errors it encounters.
|
|
func writeVCSIgnore(dir string) {
|
|
// Currently git is the only VCS for which we know how to do this.
|
|
// Mercurial and Bazaar have similar mechanisms, but they apparently
|
|
// require writing files outside of dir.
|
|
const ignore = "/pkg\n/bin\n"
|
|
name := filepath.Join(dir, ".gitignore")
|
|
err := writeFile(name, ignore)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
|
|
// writeFile is like ioutil.WriteFile but it creates
|
|
// intermediate directories with os.MkdirAll.
|
|
func writeFile(name, body string) error {
|
|
err := os.MkdirAll(filepath.Dir(name), 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(name, []byte(body), 0666)
|
|
}
|
|
|
|
const (
|
|
// Readme contains the README text.
|
|
Readme = `
|
|
This directory tree is generated automatically by godep.
|
|
|
|
Please do not edit.
|
|
|
|
See https://github.com/tools/godep for more information.
|
|
`
|
|
needRestore = `
|
|
mismatched versions while migrating
|
|
|
|
It looks like you are switching from the old Godeps format
|
|
(from flag -copy=false). The old format is just a file; it
|
|
doesn't contain source code. For this migration, godep needs
|
|
the appropriate version of each dependency to be installed in
|
|
GOPATH, so that the source code is available to copy.
|
|
|
|
To fix this, run 'godep restore'.
|
|
`
|
|
)
|