diff options
Diffstat (limited to 'src/cmd/go/internal/modcmd/vendor.go')
-rw-r--r-- | src/cmd/go/internal/modcmd/vendor.go | 481 |
1 files changed, 481 insertions, 0 deletions
diff --git a/src/cmd/go/internal/modcmd/vendor.go b/src/cmd/go/internal/modcmd/vendor.go new file mode 100644 index 0000000..3db85bd --- /dev/null +++ b/src/cmd/go/internal/modcmd/vendor.go @@ -0,0 +1,481 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modcmd + +import ( + "bytes" + "context" + "errors" + "fmt" + "go/build" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "cmd/go/internal/base" + "cmd/go/internal/cfg" + "cmd/go/internal/fsys" + "cmd/go/internal/gover" + "cmd/go/internal/imports" + "cmd/go/internal/load" + "cmd/go/internal/modload" + "cmd/go/internal/str" + + "golang.org/x/mod/module" +) + +var cmdVendor = &base.Command{ + UsageLine: "go mod vendor [-e] [-v] [-o outdir]", + Short: "make vendored copy of dependencies", + Long: ` +Vendor resets the main module's vendor directory to include all packages +needed to build and test all the main module's packages. +It does not include test code for vendored packages. + +The -v flag causes vendor to print the names of vendored +modules and packages to standard error. + +The -e flag causes vendor to attempt to proceed despite errors +encountered while loading packages. + +The -o flag causes vendor to create the vendor directory at the given +path instead of "vendor". The go command can only use a vendor directory +named "vendor" within the module root directory, so this flag is +primarily useful for other tools. + +See https://golang.org/ref/mod#go-mod-vendor for more about 'go mod vendor'. + `, + Run: runVendor, +} + +var vendorE bool // if true, report errors but proceed anyway +var vendorO string // if set, overrides the default output directory + +func init() { + cmdVendor.Flag.BoolVar(&cfg.BuildV, "v", false, "") + cmdVendor.Flag.BoolVar(&vendorE, "e", false, "") + cmdVendor.Flag.StringVar(&vendorO, "o", "", "") + base.AddChdirFlag(&cmdVendor.Flag) + base.AddModCommonFlags(&cmdVendor.Flag) +} + +func runVendor(ctx context.Context, cmd *base.Command, args []string) { + modload.InitWorkfile() + if modload.WorkFilePath() != "" { + base.Fatalf("go: 'go mod vendor' cannot be run in workspace mode. Run 'go work vendor' to vendor the workspace or set 'GOWORK=off' to exit workspace mode.") + } + RunVendor(ctx, vendorE, vendorO, args) +} + +func RunVendor(ctx context.Context, vendorE bool, vendorO string, args []string) { + if len(args) != 0 { + base.Fatalf("go: 'go mod vendor' accepts no arguments") + } + modload.ForceUseModules = true + modload.RootMode = modload.NeedRoot + + loadOpts := modload.PackageOpts{ + Tags: imports.AnyTags(), + VendorModulesInGOROOTSrc: true, + ResolveMissingImports: true, + UseVendorAll: true, + AllowErrors: vendorE, + SilenceMissingStdImports: true, + } + _, pkgs := modload.LoadPackages(ctx, loadOpts, "all") + + var vdir string + switch { + case filepath.IsAbs(vendorO): + vdir = vendorO + case vendorO != "": + vdir = filepath.Join(base.Cwd(), vendorO) + default: + vdir = filepath.Join(modload.VendorDir()) + } + if err := os.RemoveAll(vdir); err != nil { + base.Fatal(err) + } + + modpkgs := make(map[module.Version][]string) + for _, pkg := range pkgs { + m := modload.PackageModule(pkg) + if m.Path == "" || modload.MainModules.Contains(m.Path) { + continue + } + modpkgs[m] = append(modpkgs[m], pkg) + } + + includeAllReplacements := false + includeGoVersions := false + isExplicit := map[module.Version]bool{} + gv := modload.MainModules.GoVersion() + if gover.Compare(gv, "1.14") >= 0 && (modload.FindGoWork(base.Cwd()) != "" || modload.ModFile().Go != nil) { + // If the Go version is at least 1.14, annotate all explicit 'require' and + // 'replace' targets found in the go.mod file so that we can perform a + // stronger consistency check when -mod=vendor is set. + for _, m := range modload.MainModules.Versions() { + if modFile := modload.MainModules.ModFile(m); modFile != nil { + for _, r := range modFile.Require { + isExplicit[r.Mod] = true + } + } + + } + includeAllReplacements = true + } + if gover.Compare(gv, "1.17") >= 0 { + // If the Go version is at least 1.17, annotate all modules with their + // 'go' version directives. + includeGoVersions = true + } + + var vendorMods []module.Version + for m := range isExplicit { + vendorMods = append(vendorMods, m) + } + for m := range modpkgs { + if !isExplicit[m] { + vendorMods = append(vendorMods, m) + } + } + gover.ModSort(vendorMods) + + var ( + buf bytes.Buffer + w io.Writer = &buf + ) + if cfg.BuildV { + w = io.MultiWriter(&buf, os.Stderr) + } + + if modload.MainModules.WorkFile() != nil { + fmt.Fprintf(w, "## workspace\n") + } + + replacementWritten := make(map[module.Version]bool) + for _, m := range vendorMods { + replacement := modload.Replacement(m) + line := moduleLine(m, replacement) + replacementWritten[m] = true + io.WriteString(w, line) + + goVersion := "" + if includeGoVersions { + goVersion = modload.ModuleInfo(ctx, m.Path).GoVersion + } + switch { + case isExplicit[m] && goVersion != "": + fmt.Fprintf(w, "## explicit; go %s\n", goVersion) + case isExplicit[m]: + io.WriteString(w, "## explicit\n") + case goVersion != "": + fmt.Fprintf(w, "## go %s\n", goVersion) + } + + pkgs := modpkgs[m] + sort.Strings(pkgs) + for _, pkg := range pkgs { + fmt.Fprintf(w, "%s\n", pkg) + vendorPkg(vdir, pkg) + } + } + + if includeAllReplacements { + // Record unused and wildcard replacements at the end of the modules.txt file: + // without access to the complete build list, the consumer of the vendor + // directory can't otherwise determine that those replacements had no effect. + for _, m := range modload.MainModules.Versions() { + if workFile := modload.MainModules.WorkFile(); workFile != nil { + for _, r := range workFile.Replace { + if replacementWritten[r.Old] { + // We already recorded this replacement. + continue + } + replacementWritten[r.Old] = true + + line := moduleLine(r.Old, r.New) + buf.WriteString(line) + if cfg.BuildV { + os.Stderr.WriteString(line) + } + } + } + if modFile := modload.MainModules.ModFile(m); modFile != nil { + for _, r := range modFile.Replace { + if replacementWritten[r.Old] { + // We already recorded this replacement. + continue + } + replacementWritten[r.Old] = true + rNew := modload.Replacement(r.Old) + if rNew == (module.Version{}) { + // There is no replacement. Don't try to write it. + continue + } + + line := moduleLine(r.Old, rNew) + buf.WriteString(line) + if cfg.BuildV { + os.Stderr.WriteString(line) + } + } + } + } + } + + if buf.Len() == 0 { + fmt.Fprintf(os.Stderr, "go: no dependencies to vendor\n") + return + } + + if err := os.MkdirAll(vdir, 0777); err != nil { + base.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(vdir, "modules.txt"), buf.Bytes(), 0666); err != nil { + base.Fatal(err) + } +} + +func moduleLine(m, r module.Version) string { + b := new(strings.Builder) + b.WriteString("# ") + b.WriteString(m.Path) + if m.Version != "" { + b.WriteString(" ") + b.WriteString(m.Version) + } + if r.Path != "" { + if str.HasFilePathPrefix(filepath.Clean(r.Path), "vendor") { + base.Fatalf("go: replacement path %s inside vendor directory", r.Path) + } + b.WriteString(" => ") + b.WriteString(r.Path) + if r.Version != "" { + b.WriteString(" ") + b.WriteString(r.Version) + } + } + b.WriteString("\n") + return b.String() +} + +func vendorPkg(vdir, pkg string) { + src, realPath, _ := modload.Lookup("", false, pkg) + if src == "" { + base.Errorf("internal error: no pkg for %s\n", pkg) + return + } + if realPath != pkg { + // TODO(#26904): Revisit whether this behavior still makes sense. + // This should actually be impossible today, because the import map is the + // identity function for packages outside of the standard library. + // + // Part of the purpose of the vendor directory is to allow the packages in + // the module to continue to build in GOPATH mode, and GOPATH-mode users + // won't know about replacement aliasing. How important is it to maintain + // compatibility? + fmt.Fprintf(os.Stderr, "warning: %s imported as both %s and %s; making two copies.\n", realPath, realPath, pkg) + } + + copiedFiles := make(map[string]bool) + dst := filepath.Join(vdir, pkg) + copyDir(dst, src, matchPotentialSourceFile, copiedFiles) + if m := modload.PackageModule(realPath); m.Path != "" { + copyMetadata(m.Path, realPath, dst, src, copiedFiles) + } + + ctx := build.Default + ctx.UseAllFiles = true + bp, err := ctx.ImportDir(src, build.IgnoreVendor) + // Because UseAllFiles is set on the build.Context, it's possible ta get + // a MultiplePackageError on an otherwise valid package: the package could + // have different names for GOOS=windows and GOOS=mac for example. On the + // other hand if there's a NoGoError, the package might have source files + // specifying "//go:build ignore" those packages should be skipped because + // embeds from ignored files can't be used. + // TODO(#42504): Find a better way to avoid errors from ImportDir. We'll + // need to figure this out when we switch to PackagesAndErrors as per the + // TODO above. + var multiplePackageError *build.MultiplePackageError + var noGoError *build.NoGoError + if err != nil { + if errors.As(err, &noGoError) { + return // No source files in this package are built. Skip embeds in ignored files. + } else if !errors.As(err, &multiplePackageError) { // multiplePackageErrors are OK, but others are not. + base.Fatalf("internal error: failed to find embedded files of %s: %v\n", pkg, err) + } + } + var embedPatterns []string + if gover.Compare(modload.MainModules.GoVersion(), "1.22") >= 0 { + embedPatterns = bp.EmbedPatterns + } else { + // Maintain the behavior of https://github.com/golang/go/issues/63473 + // so that we continue to agree with older versions of the go command + // about the contents of vendor directories in existing modules + embedPatterns = str.StringList(bp.EmbedPatterns, bp.TestEmbedPatterns, bp.XTestEmbedPatterns) + } + embeds, err := load.ResolveEmbed(bp.Dir, embedPatterns) + if err != nil { + base.Fatal(err) + } + for _, embed := range embeds { + embedDst := filepath.Join(dst, embed) + if copiedFiles[embedDst] { + continue + } + + // Copy the file as is done by copyDir below. + r, err := os.Open(filepath.Join(src, embed)) + if err != nil { + base.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(embedDst), 0777); err != nil { + base.Fatal(err) + } + w, err := os.Create(embedDst) + if err != nil { + base.Fatal(err) + } + if _, err := io.Copy(w, r); err != nil { + base.Fatal(err) + } + r.Close() + if err := w.Close(); err != nil { + base.Fatal(err) + } + } +} + +type metakey struct { + modPath string + dst string +} + +var copiedMetadata = make(map[metakey]bool) + +// copyMetadata copies metadata files from parents of src to parents of dst, +// stopping after processing the src parent for modPath. +func copyMetadata(modPath, pkg, dst, src string, copiedFiles map[string]bool) { + for parent := 0; ; parent++ { + if copiedMetadata[metakey{modPath, dst}] { + break + } + copiedMetadata[metakey{modPath, dst}] = true + if parent > 0 { + copyDir(dst, src, matchMetadata, copiedFiles) + } + if modPath == pkg { + break + } + pkg = path.Dir(pkg) + dst = filepath.Dir(dst) + src = filepath.Dir(src) + } +} + +// metaPrefixes is the list of metadata file prefixes. +// Vendoring copies metadata files from parents of copied directories. +// Note that this list could be arbitrarily extended, and it is longer +// in other tools (such as godep or dep). By using this limited set of +// prefixes and also insisting on capitalized file names, we are trying +// to nudge people toward more agreement on the naming +// and also trying to avoid false positives. +var metaPrefixes = []string{ + "AUTHORS", + "CONTRIBUTORS", + "COPYLEFT", + "COPYING", + "COPYRIGHT", + "LEGAL", + "LICENSE", + "NOTICE", + "PATENTS", +} + +// matchMetadata reports whether info is a metadata file. +func matchMetadata(dir string, info fs.DirEntry) bool { + name := info.Name() + for _, p := range metaPrefixes { + if strings.HasPrefix(name, p) { + return true + } + } + return false +} + +// matchPotentialSourceFile reports whether info may be relevant to a build operation. +func matchPotentialSourceFile(dir string, info fs.DirEntry) bool { + if strings.HasSuffix(info.Name(), "_test.go") { + return false + } + if info.Name() == "go.mod" || info.Name() == "go.sum" { + if gv := modload.MainModules.GoVersion(); gover.Compare(gv, "1.17") >= 0 { + // As of Go 1.17, we strip go.mod and go.sum files from dependency modules. + // Otherwise, 'go' commands invoked within the vendor subtree may misidentify + // an arbitrary directory within the vendor tree as a module root. + // (See https://golang.org/issue/42970.) + return false + } + } + if strings.HasSuffix(info.Name(), ".go") { + f, err := fsys.Open(filepath.Join(dir, info.Name())) + if err != nil { + base.Fatal(err) + } + defer f.Close() + + content, err := imports.ReadImports(f, false, nil) + if err == nil && !imports.ShouldBuild(content, imports.AnyTags()) { + // The file is explicitly tagged "ignore", so it can't affect the build. + // Leave it out. + return false + } + return true + } + + // We don't know anything about this file, so optimistically assume that it is + // needed. + return true +} + +// copyDir copies all regular files satisfying match(info) from src to dst. +func copyDir(dst, src string, match func(dir string, info fs.DirEntry) bool, copiedFiles map[string]bool) { + files, err := os.ReadDir(src) + if err != nil { + base.Fatal(err) + } + if err := os.MkdirAll(dst, 0777); err != nil { + base.Fatal(err) + } + for _, file := range files { + if file.IsDir() || !file.Type().IsRegular() || !match(src, file) { + continue + } + copiedFiles[file.Name()] = true + r, err := os.Open(filepath.Join(src, file.Name())) + if err != nil { + base.Fatal(err) + } + dstPath := filepath.Join(dst, file.Name()) + copiedFiles[dstPath] = true + w, err := os.Create(dstPath) + if err != nil { + base.Fatal(err) + } + if _, err := io.Copy(w, r); err != nil { + base.Fatal(err) + } + r.Close() + if err := w.Close(); err != nil { + base.Fatal(err) + } + } +} |