summaryrefslogtreecommitdiff
path: root/src/cmd/go/internal/modload
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/modload')
-rw-r--r--src/cmd/go/internal/modload/build.go4
-rw-r--r--src/cmd/go/internal/modload/buildlist.go21
-rw-r--r--src/cmd/go/internal/modload/import.go268
-rw-r--r--src/cmd/go/internal/modload/init.go251
-rw-r--r--src/cmd/go/internal/modload/load.go59
-rw-r--r--src/cmd/go/internal/modload/modfile.go70
-rw-r--r--src/cmd/go/internal/modload/mvs.go48
-rw-r--r--src/cmd/go/internal/modload/query.go587
-rw-r--r--src/cmd/go/internal/modload/query_test.go22
-rw-r--r--src/cmd/go/internal/modload/search.go11
-rw-r--r--src/cmd/go/internal/modload/stat_openfile.go5
-rw-r--r--src/cmd/go/internal/modload/stat_unix.go5
-rw-r--r--src/cmd/go/internal/modload/stat_windows.go6
-rw-r--r--src/cmd/go/internal/modload/testgo.go11
-rw-r--r--src/cmd/go/internal/modload/vendor.go4
15 files changed, 863 insertions, 509 deletions
diff --git a/src/cmd/go/internal/modload/build.go b/src/cmd/go/internal/modload/build.go
index f49b52df56..b9abb0b93c 100644
--- a/src/cmd/go/internal/modload/build.go
+++ b/src/cmd/go/internal/modload/build.go
@@ -13,7 +13,6 @@ import (
"internal/goroot"
"os"
"path/filepath"
- "runtime/debug"
"strings"
"cmd/go/internal/base"
@@ -312,9 +311,6 @@ func mustFindModule(target, path string) module.Version {
return Target
}
- if printStackInDie {
- debug.PrintStack()
- }
base.Fatalf("build %v: cannot find module for path %v", target, path)
panic("unreachable")
}
diff --git a/src/cmd/go/internal/modload/buildlist.go b/src/cmd/go/internal/modload/buildlist.go
index 059b020420..4a183d6881 100644
--- a/src/cmd/go/internal/modload/buildlist.go
+++ b/src/cmd/go/internal/modload/buildlist.go
@@ -37,7 +37,7 @@ var buildList []module.Version
//
// The caller must not modify the returned list.
func LoadAllModules(ctx context.Context) []module.Version {
- InitMod(ctx)
+ LoadModFile(ctx)
ReloadBuildList()
WriteGoMod()
return buildList
@@ -52,6 +52,21 @@ func LoadedModules() []module.Version {
return buildList
}
+// Selected returns the selected version of the module with the given path, or
+// the empty string if the given module has no selected version
+// (either because it is not required or because it is the Target module).
+func Selected(path string) (version string) {
+ if path == Target.Path {
+ return ""
+ }
+ for _, m := range buildList {
+ if m.Path == path {
+ return m.Version
+ }
+ }
+ return ""
+}
+
// SetBuildList sets the module build list.
// The caller is responsible for ensuring that the list is valid.
// SetBuildList does not retain a reference to the original list.
@@ -63,7 +78,9 @@ func SetBuildList(list []module.Version) {
// the build list set in SetBuildList.
func ReloadBuildList() []module.Version {
loaded = loadFromRoots(loaderParams{
- tags: imports.Tags(),
+ PackageOpts: PackageOpts{
+ Tags: imports.Tags(),
+ },
listRoots: func() []string { return nil },
allClosesOverTests: index.allPatternClosesOverTests(), // but doesn't matter because the root list is empty.
})
diff --git a/src/cmd/go/internal/modload/import.go b/src/cmd/go/internal/modload/import.go
index c36c8bd29b..e959347020 100644
--- a/src/cmd/go/internal/modload/import.go
+++ b/src/cmd/go/internal/modload/import.go
@@ -10,13 +10,14 @@ import (
"fmt"
"go/build"
"internal/goroot"
+ "io/fs"
"os"
"path/filepath"
"sort"
"strings"
- "time"
"cmd/go/internal/cfg"
+ "cmd/go/internal/fsys"
"cmd/go/internal/modfetch"
"cmd/go/internal/par"
"cmd/go/internal/search"
@@ -25,13 +26,25 @@ import (
"golang.org/x/mod/semver"
)
-var errImportMissing = errors.New("import missing")
-
type ImportMissingError struct {
Path string
Module module.Version
QueryErr error
+ // inAll indicates whether Path is in the "all" package pattern,
+ // and thus would be added by 'go mod tidy'.
+ inAll bool
+
+ // isStd indicates whether we would expect to find the package in the standard
+ // library. This is normally true for all dotless import paths, but replace
+ // directives can cause us to treat the replaced paths as also being in
+ // modules.
+ isStd bool
+
+ // replaced the highest replaced version of the module where the replacement
+ // contains the package. replaced is only set if the replacement is unused.
+ replaced module.Version
+
// newMissingVersion is set to a newer version of Module if one is present
// in the build list. When set, we can't automatically upgrade.
newMissingVersion string
@@ -39,13 +52,33 @@ type ImportMissingError struct {
func (e *ImportMissingError) Error() string {
if e.Module.Path == "" {
- if search.IsStandardImportPath(e.Path) {
+ if e.isStd {
return fmt.Sprintf("package %s is not in GOROOT (%s)", e.Path, filepath.Join(cfg.GOROOT, "src", e.Path))
}
if e.QueryErr != nil {
return fmt.Sprintf("cannot find module providing package %s: %v", e.Path, e.QueryErr)
}
- return "cannot find module providing package " + e.Path
+ if cfg.BuildMod == "mod" {
+ return "cannot find module providing package " + e.Path
+ }
+
+ if e.replaced.Path != "" {
+ suggestArg := e.replaced.Path
+ if !modfetch.IsZeroPseudoVersion(e.replaced.Version) {
+ suggestArg = e.replaced.String()
+ }
+ return fmt.Sprintf("module %s provides package %s and is replaced but not required; try 'go get -d %s' to add it", e.replaced.Path, e.Path, suggestArg)
+ }
+
+ suggestion := ""
+ if !HasModRoot() {
+ suggestion = ": working directory is not part of a module"
+ } else if e.inAll {
+ suggestion = "; try 'go mod tidy' to add it"
+ } else {
+ suggestion = fmt.Sprintf("; try 'go get -d %s' to add it", e.Path)
+ }
+ return fmt.Sprintf("no required module provides package %s%s", e.Path, suggestion)
}
if e.newMissingVersion != "" {
@@ -102,6 +135,31 @@ func (e *AmbiguousImportError) Error() string {
return buf.String()
}
+// ImportMissingSumError is reported in readonly mode when we need to check
+// if a module in the build list contains a package, but we don't have a sum
+// for its .zip file.
+type ImportMissingSumError struct {
+ importPath string
+ found, inAll bool
+}
+
+func (e *ImportMissingSumError) Error() string {
+ var message string
+ if e.found {
+ message = fmt.Sprintf("missing go.sum entry needed to verify package %s is provided by exactly one module", e.importPath)
+ } else {
+ message = fmt.Sprintf("missing go.sum entry for module providing package %s", e.importPath)
+ }
+ if e.inAll {
+ return message + "; try 'go mod tidy' to add it"
+ }
+ return message
+}
+
+func (e *ImportMissingSumError) ImportPath() string {
+ return e.importPath
+}
+
type invalidImportError struct {
importPath string
err error
@@ -131,7 +189,7 @@ func (e *invalidImportError) Unwrap() error {
// like "C" and "unsafe".
//
// If the package cannot be found in the current build list,
-// importFromBuildList returns errImportMissing as the error.
+// importFromBuildList returns an *ImportMissingError.
func importFromBuildList(ctx context.Context, path string) (m module.Version, dir string, err error) {
if strings.Contains(path, "@") {
return module.Version{}, "", fmt.Errorf("import path should not have @version")
@@ -143,6 +201,10 @@ func importFromBuildList(ctx context.Context, path string) (m module.Version, di
// There's no directory for import "C" or import "unsafe".
return module.Version{}, "", nil
}
+ // Before any further lookup, check that the path is valid.
+ if err := module.CheckImportPath(path); err != nil {
+ return module.Version{}, "", &invalidImportError{importPath: path, err: err}
+ }
// Is the package in the standard library?
pathIsStd := search.IsStandardImportPath(path)
@@ -182,13 +244,23 @@ func importFromBuildList(ctx context.Context, path string) (m module.Version, di
// Check each module on the build list.
var dirs []string
var mods []module.Version
+ haveSumErr := false
for _, m := range buildList {
if !maybeInModule(path, m.Path) {
// Avoid possibly downloading irrelevant modules.
continue
}
- root, isLocal, err := fetch(ctx, m)
+ needSum := true
+ root, isLocal, err := fetch(ctx, m, needSum)
if err != nil {
+ if sumErr := (*sumMissingError)(nil); errors.As(err, &sumErr) {
+ // We are missing a sum needed to fetch a module in the build list.
+ // We can't verify that the package is unique, and we may not find
+ // the package at all. Keep checking other modules to decide which
+ // error to report.
+ haveSumErr = true
+ continue
+ }
// Report fetch error.
// Note that we don't know for sure this module is necessary,
// but it certainly _could_ provide the package, and even if we
@@ -204,60 +276,46 @@ func importFromBuildList(ctx context.Context, path string) (m module.Version, di
dirs = append(dirs, dir)
}
}
+ if len(mods) > 1 {
+ return module.Version{}, "", &AmbiguousImportError{importPath: path, Dirs: dirs, Modules: mods}
+ }
+ if haveSumErr {
+ return module.Version{}, "", &ImportMissingSumError{importPath: path, found: len(mods) > 0}
+ }
if len(mods) == 1 {
return mods[0], dirs[0], nil
}
- if len(mods) > 0 {
- return module.Version{}, "", &AmbiguousImportError{importPath: path, Dirs: dirs, Modules: mods}
- }
- return module.Version{}, "", errImportMissing
+ return module.Version{}, "", &ImportMissingError{Path: path, isStd: pathIsStd}
}
// queryImport attempts to locate a module that can be added to the current
// build list to provide the package with the given import path.
+//
+// Unlike QueryPattern, queryImport prefers to add a replaced version of a
+// module *before* checking the proxies for a version to add.
func queryImport(ctx context.Context, path string) (module.Version, error) {
- pathIsStd := search.IsStandardImportPath(path)
-
- if modRoot == "" && !allowMissingModuleImports {
- return module.Version{}, &ImportMissingError{
- Path: path,
- QueryErr: errors.New("working directory is not part of a module"),
- }
- }
-
- // Not on build list.
- // To avoid spurious remote fetches, next try the latest replacement for each
- // module (golang.org/issue/26241). This should give a useful message
- // in -mod=readonly, and it will allow us to add a requirement with -mod=mod.
- if modFile != nil {
- latest := map[string]string{} // path -> version
- for _, r := range modFile.Replace {
- if maybeInModule(path, r.Old.Path) {
- // Don't use semver.Max here; need to preserve +incompatible suffix.
- v := latest[r.Old.Path]
- if semver.Compare(r.Old.Version, v) > 0 {
- v = r.Old.Version
- }
- latest[r.Old.Path] = v
+ // To avoid spurious remote fetches, try the latest replacement for each
+ // module (golang.org/issue/26241).
+ if index != nil {
+ var mods []module.Version
+ for mp, mv := range index.highestReplaced {
+ if !maybeInModule(path, mp) {
+ continue
}
- }
-
- mods := make([]module.Version, 0, len(latest))
- for p, v := range latest {
- // If the replacement didn't specify a version, synthesize a
- // pseudo-version with an appropriate major version and a timestamp below
- // any real timestamp. That way, if the main module is used from within
- // some other module, the user will be able to upgrade the requirement to
- // any real version they choose.
- if v == "" {
- if _, pathMajor, ok := module.SplitPathVersion(p); ok && len(pathMajor) > 0 {
- v = modfetch.PseudoVersion(pathMajor[1:], "", time.Time{}, "000000000000")
+ if mv == "" {
+ // The only replacement is a wildcard that doesn't specify a version, so
+ // synthesize a pseudo-version with an appropriate major version and a
+ // timestamp below any real timestamp. That way, if the main module is
+ // used from within some other module, the user will be able to upgrade
+ // the requirement to any real version they choose.
+ if _, pathMajor, ok := module.SplitPathVersion(mp); ok && len(pathMajor) > 0 {
+ mv = modfetch.ZeroPseudoVersion(pathMajor[1:])
} else {
- v = modfetch.PseudoVersion("v0", "", time.Time{}, "000000000000")
+ mv = modfetch.ZeroPseudoVersion("v0")
}
}
- mods = append(mods, module.Version{Path: p, Version: v})
+ mods = append(mods, module.Version{Path: mp, Version: mv})
}
// Every module path in mods is a prefix of the import path.
@@ -266,20 +324,26 @@ func queryImport(ctx context.Context, path string) (module.Version, error) {
return len(mods[i].Path) > len(mods[j].Path)
})
for _, m := range mods {
- root, isLocal, err := fetch(ctx, m)
+ needSum := true
+ root, isLocal, err := fetch(ctx, m, needSum)
if err != nil {
- // Report fetch error as above.
+ if sumErr := (*sumMissingError)(nil); errors.As(err, &sumErr) {
+ return module.Version{}, &ImportMissingSumError{importPath: path}
+ }
return module.Version{}, err
}
if _, ok, err := dirInModule(path, m.Path, root, isLocal); err != nil {
return m, err
} else if ok {
+ if cfg.BuildMod == "readonly" {
+ return module.Version{}, &ImportMissingError{Path: path, replaced: m}
+ }
return m, nil
}
}
if len(mods) > 0 && module.CheckPath(path) != nil {
// The package path is not valid to fetch remotely,
- // so it can only exist if in a replaced module,
+ // so it can only exist in a replaced module,
// and we know from the above loop that it is not.
return module.Version{}, &PackageNotInModuleError{
Mod: mods[0],
@@ -290,12 +354,7 @@ func queryImport(ctx context.Context, path string) (module.Version, error) {
}
}
- // Before any further lookup, check that the path is valid.
- if err := module.CheckImportPath(path); err != nil {
- return module.Version{}, &invalidImportError{importPath: path, err: err}
- }
-
- if pathIsStd {
+ if search.IsStandardImportPath(path) {
// This package isn't in the standard library, isn't in any module already
// in the build list, and isn't in any other module that the user has
// shimmed in via a "replace" directive.
@@ -303,10 +362,13 @@ func queryImport(ctx context.Context, path string) (module.Version, error) {
// QueryPattern cannot possibly find a module containing this package.
//
// Instead of trying QueryPattern, report an ImportMissingError immediately.
- return module.Version{}, &ImportMissingError{Path: path}
+ return module.Version{}, &ImportMissingError{Path: path, isStd: true}
}
if cfg.BuildMod == "readonly" {
+ // In readonly mode, we can't write go.mod, so we shouldn't try to look up
+ // the module. If readonly mode was enabled explicitly, include that in
+ // the error message.
var queryErr error
if cfg.BuildModExplicit {
queryErr = fmt.Errorf("import lookup disabled by -mod=%s", cfg.BuildMod)
@@ -321,9 +383,9 @@ func queryImport(ctx context.Context, path string) (module.Version, error) {
// and return m, dir, ImpportMissingError.
fmt.Fprintf(os.Stderr, "go: finding module for package %s\n", path)
- candidates, err := QueryPattern(ctx, path, "latest", CheckAllowed)
+ candidates, err := QueryPattern(ctx, path, "latest", Selected, CheckAllowed)
if err != nil {
- if errors.Is(err, os.ErrNotExist) {
+ if errors.Is(err, fs.ErrNotExist) {
// Return "cannot find module providing package […]" instead of whatever
// low-level error QueryPattern produced.
return module.Version{}, &ImportMissingError{Path: path, QueryErr: err}
@@ -438,57 +500,65 @@ func dirInModule(path, mpath, mdir string, isLocal bool) (dir string, haveGoFile
// We don't care about build tags, not even "+build ignore".
// We're just looking for a plausible directory.
res := haveGoFilesCache.Do(dir, func() interface{} {
- ok, err := isDirWithGoFiles(dir)
+ ok, err := fsys.IsDirWithGoFiles(dir)
return goFilesEntry{haveGoFiles: ok, err: err}
}).(goFilesEntry)
return dir, res.haveGoFiles, res.err
}
-func isDirWithGoFiles(dir string) (bool, error) {
- f, err := os.Open(dir)
- if err != nil {
- if os.IsNotExist(err) {
- return false, nil
- }
- return false, err
- }
- defer f.Close()
-
- names, firstErr := f.Readdirnames(-1)
- if firstErr != nil {
- if fi, err := f.Stat(); err == nil && !fi.IsDir() {
- return false, nil
- }
-
- // Rewrite the error from ReadDirNames to include the path if not present.
- // See https://golang.org/issue/38923.
- var pe *os.PathError
- if !errors.As(firstErr, &pe) {
- firstErr = &os.PathError{Op: "readdir", Path: dir, Err: firstErr}
- }
+// fetch downloads the given module (or its replacement)
+// and returns its location.
+//
+// needSum indicates whether the module may be downloaded in readonly mode
+// without a go.sum entry. It should only be false for modules fetched
+// speculatively (for example, for incompatible version filtering). The sum
+// will still be verified normally.
+//
+// The isLocal return value reports whether the replacement,
+// if any, is local to the filesystem.
+func fetch(ctx context.Context, mod module.Version, needSum bool) (dir string, isLocal bool, err error) {
+ if mod == Target {
+ return ModRoot(), true, nil
}
-
- for _, name := range names {
- if strings.HasSuffix(name, ".go") {
- info, err := os.Stat(filepath.Join(dir, name))
- if err == nil && info.Mode().IsRegular() {
- // If any .go source file exists, the package exists regardless of
- // errors for other source files. Leave further error reporting for
- // later.
- return true, nil
+ if r := Replacement(mod); r.Path != "" {
+ if r.Version == "" {
+ dir = r.Path
+ if !filepath.IsAbs(dir) {
+ dir = filepath.Join(ModRoot(), dir)
}
- if firstErr == nil {
+ // Ensure that the replacement directory actually exists:
+ // dirInModule does not report errors for missing modules,
+ // so if we don't report the error now, later failures will be
+ // very mysterious.
+ if _, err := os.Stat(dir); err != nil {
if os.IsNotExist(err) {
- // If the file was concurrently deleted, or was a broken symlink,
- // convert the error to an opaque error instead of one matching
- // os.IsNotExist.
- err = errors.New(err.Error())
+ // Semantically the module version itself “exists” — we just don't
+ // have its source code. Remove the equivalence to os.ErrNotExist,
+ // and make the message more concise while we're at it.
+ err = fmt.Errorf("replacement directory %s does not exist", r.Path)
+ } else {
+ err = fmt.Errorf("replacement directory %s: %w", r.Path, err)
}
- firstErr = err
+ return dir, true, module.VersionError(mod, err)
}
+ return dir, true, nil
}
+ mod = r
+ }
+
+ if cfg.BuildMod == "readonly" && needSum && !modfetch.HaveSum(mod) {
+ return "", false, module.VersionError(mod, &sumMissingError{})
}
- return false, firstErr
+ dir, err = modfetch.Download(ctx, mod)
+ return dir, false, err
+}
+
+type sumMissingError struct {
+ suggestion string
+}
+
+func (e *sumMissingError) Error() string {
+ return "missing go.sum entry" + e.suggestion
}
diff --git a/src/cmd/go/internal/modload/init.go b/src/cmd/go/internal/modload/init.go
index 3344242489..8fe71a2448 100644
--- a/src/cmd/go/internal/modload/init.go
+++ b/src/cmd/go/internal/modload/init.go
@@ -16,12 +16,13 @@ import (
"os"
"path"
"path/filepath"
- "runtime/debug"
"strconv"
"strings"
+ "sync"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
+ "cmd/go/internal/fsys"
"cmd/go/internal/lockedfile"
"cmd/go/internal/modconv"
"cmd/go/internal/modfetch"
@@ -50,9 +51,6 @@ var (
gopath string
- CmdModInit bool // running 'go mod init'
- CmdModModule string // module argument for 'go mod init'
-
// RootMode determines whether a module root is needed.
RootMode Root
@@ -132,6 +130,10 @@ func Init() {
return
}
+ if err := fsys.Init(base.Cwd); err != nil {
+ base.Fatalf("go: %v", err)
+ }
+
// Disable any prompting for passwords by Git.
// Only has an effect for 2.3.0 or later, but avoiding
// the prompt in earlier versions is just too hard.
@@ -159,9 +161,9 @@ func Init() {
os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
}
- if CmdModInit {
- // Running 'go mod init': go.mod will be created in current directory.
- modRoot = base.Cwd
+ if modRoot != "" {
+ // modRoot set before Init was called ("go mod init" does this).
+ // No need to search for go.mod.
} else if RootMode == NoRoot {
if cfg.ModFile != "" && !base.InGOFLAGS("-modfile") {
base.Fatalf("go: -modfile cannot be used with commands that ignore the current module")
@@ -198,8 +200,7 @@ func Init() {
base.Fatalf("go: -modfile=%s: file does not have .mod extension", cfg.ModFile)
}
- // We're in module mode. Install the hooks to make it work.
-
+ // We're in module mode. Set any global variables that need to be set.
list := filepath.SplitList(cfg.BuildContext.GOPATH)
if len(list) == 0 || list[0] == "" {
base.Fatalf("missing $GOPATH")
@@ -266,10 +267,6 @@ func WillBeEnabled() bool {
return false
}
- if CmdModInit {
- // Running 'go mod init': go.mod will be created in current directory.
- return true
- }
if modRoot := findModuleRoot(base.Cwd); modRoot == "" {
// GO111MODULE is 'auto', and we can't find a module root.
// Stay in GOPATH mode.
@@ -325,16 +322,7 @@ func ModFilePath() string {
return filepath.Join(modRoot, "go.mod")
}
-// printStackInDie causes die to print a stack trace.
-//
-// It is enabled by the testgo tag, and helps to diagnose paths that
-// unexpectedly require a main module.
-var printStackInDie = false
-
func die() {
- if printStackInDie {
- debug.PrintStack()
- }
if cfg.Getenv("GO111MODULE") == "off" {
base.Fatalf("go: modules disabled by GO111MODULE=off; see 'go help modules'")
}
@@ -352,16 +340,16 @@ func die() {
base.Fatalf("go: cannot find main module; see 'go help modules'")
}
-// InitMod sets Target and, if there is a main module, parses the initial build
-// list from its go.mod file. If InitMod is called by 'go mod init', InitMod
-// will populate go.mod in memory, possibly importing dependencies from a
-// legacy configuration file. For other commands, InitMod may make other
-// adjustments in memory, like adding a go directive. WriteGoMod should be
-// called later to write changes out to disk.
+// LoadModFile sets Target and, if there is a main module, parses the initial
+// build list from its go.mod file.
//
-// As a side-effect, InitMod sets a default for cfg.BuildMod if it does not
+// LoadModFile may make changes in memory, like adding a go directive and
+// ensuring requirements are consistent. WriteGoMod should be called later to
+// write changes out to disk or report errors in readonly mode.
+//
+// As a side-effect, LoadModFile sets a default for cfg.BuildMod if it does not
// already have an explicit value.
-func InitMod(ctx context.Context) {
+func LoadModFile(ctx context.Context) {
if len(buildList) > 0 {
return
}
@@ -374,13 +362,6 @@ func InitMod(ctx context.Context) {
return
}
- if CmdModInit {
- // Running go mod init: do legacy module conversion
- legacyModInit()
- modFileToBuildList()
- return
- }
-
gomod := ModFilePath()
data, err := lockedfile.Read(gomod)
if err != nil {
@@ -401,12 +382,6 @@ func InitMod(ctx context.Context) {
base.Fatalf("go: no module declaration in go.mod.\n\tRun 'go mod edit -module=example.com/mod' to specify the module path.")
}
- if len(f.Syntax.Stmt) == 1 && f.Module != nil {
- // Entire file is just a module statement.
- // Populate require if possible.
- legacyModInit()
- }
-
if err := checkModulePathLax(f.Module.Mod.Path); err != nil {
base.Fatalf("go: %v", err)
}
@@ -419,6 +394,73 @@ func InitMod(ctx context.Context) {
}
}
+// CreateModFile initializes a new module by creating a go.mod file.
+//
+// If modPath is empty, CreateModFile will attempt to infer the path from the
+// directory location within GOPATH.
+//
+// If a vendoring configuration file is present, CreateModFile will attempt to
+// translate it to go.mod directives. The resulting build list may not be
+// exactly the same as in the legacy configuration (for example, we can't get
+// packages at multiple versions from the same module).
+func CreateModFile(ctx context.Context, modPath string) {
+ modRoot = base.Cwd
+ Init()
+ modFilePath := ModFilePath()
+ if _, err := os.Stat(modFilePath); err == nil {
+ base.Fatalf("go: %s already exists", modFilePath)
+ }
+
+ if modPath == "" {
+ var err error
+ modPath, err = findModulePath(modRoot)
+ if err != nil {
+ base.Fatalf("go: %v", err)
+ }
+ } else if err := checkModulePathLax(modPath); err != nil {
+ base.Fatalf("go: %v", err)
+ }
+
+ fmt.Fprintf(os.Stderr, "go: creating new go.mod: module %s\n", modPath)
+ modFile = new(modfile.File)
+ modFile.AddModuleStmt(modPath)
+ addGoStmt() // Add the go directive before converted module requirements.
+
+ convertedFrom, err := convertLegacyConfig(modPath)
+ if convertedFrom != "" {
+ fmt.Fprintf(os.Stderr, "go: copying requirements from %s\n", base.ShortPath(convertedFrom))
+ }
+ if err != nil {
+ base.Fatalf("go: %v", err)
+ }
+
+ modFileToBuildList()
+ WriteGoMod()
+
+ // Suggest running 'go mod tidy' unless the project is empty. Even if we
+ // imported all the correct requirements above, we're probably missing
+ // some sums, so the next build command in -mod=readonly will likely fail.
+ //
+ // We look for non-hidden .go files or subdirectories to determine whether
+ // this is an existing project. Walking the tree for packages would be more
+ // accurate, but could take much longer.
+ empty := true
+ fis, _ := ioutil.ReadDir(modRoot)
+ for _, fi := range fis {
+ name := fi.Name()
+ if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
+ continue
+ }
+ if strings.HasSuffix(name, ".go") || fi.IsDir() {
+ empty = false
+ break
+ }
+ }
+ if !empty {
+ fmt.Fprintf(os.Stderr, "go: run 'go mod tidy' to add module requirements and sums\n")
+ }
+}
+
// checkModulePathLax checks that the path meets some minimum requirements
// to avoid confusing users or the module cache. The requirements are weaker
// than those of module.CheckPath to allow room for weakening module path
@@ -585,38 +627,23 @@ func setDefaultBuildMod() {
cfg.BuildMod = "readonly"
}
-func legacyModInit() {
- if modFile == nil {
- path, err := findModulePath(modRoot)
- if err != nil {
- base.Fatalf("go: %v", err)
- }
- fmt.Fprintf(os.Stderr, "go: creating new go.mod: module %s\n", path)
- modFile = new(modfile.File)
- modFile.AddModuleStmt(path)
- addGoStmt() // Add the go directive before converted module requirements.
- }
-
+// convertLegacyConfig imports module requirements from a legacy vendoring
+// configuration file, if one is present.
+func convertLegacyConfig(modPath string) (from string, err error) {
for _, name := range altConfigs {
cfg := filepath.Join(modRoot, name)
data, err := ioutil.ReadFile(cfg)
if err == nil {
convert := modconv.Converters[name]
if convert == nil {
- return
+ return "", nil
}
- fmt.Fprintf(os.Stderr, "go: copying requirements from %s\n", base.ShortPath(cfg))
cfg = filepath.ToSlash(cfg)
- if err := modconv.ConvertLegacyConfig(modFile, cfg, data); err != nil {
- base.Fatalf("go: %v", err)
- }
- if len(modFile.Syntax.Stmt) == 1 {
- // Add comment to avoid re-converting every time it runs.
- modFile.AddComment("// go: no requirements found in " + name)
- }
- return
+ err := modconv.ConvertLegacyConfig(modFile, cfg, data)
+ return name, err
}
}
+ return "", nil
}
// addGoStmt adds a go directive to the go.mod file if it does not already include one.
@@ -696,14 +723,6 @@ func findAltConfig(dir string) (root, name string) {
}
func findModulePath(dir string) (string, error) {
- if CmdModModule != "" {
- // Running go mod init x/y/z; return x/y/z.
- if err := module.CheckImportPath(CmdModModule); err != nil {
- return "", err
- }
- return CmdModModule, nil
- }
-
// TODO(bcmills): once we have located a plausible module path, we should
// query version control (if available) to verify that it matches the major
// version of the most recent tag.
@@ -893,7 +912,10 @@ func WriteGoMod() {
// The go.mod file has the same semantic content that it had before
// (but not necessarily the same exact bytes).
// Don't write go.mod, but write go.sum in case we added or trimmed sums.
- modfetch.WriteGoSum(keepSums(true))
+ // 'go mod init' shouldn't write go.sum, since it will be incomplete.
+ if cfg.CmdName != "mod init" {
+ modfetch.WriteGoSum(keepSums(true))
+ }
return
}
@@ -906,7 +928,10 @@ func WriteGoMod() {
index = indexModFile(new, modFile, false)
// Update go.sum after releasing the side lock and refreshing the index.
- modfetch.WriteGoSum(keepSums(true))
+ // 'go mod init' shouldn't write go.sum, since it will be incomplete.
+ if cfg.CmdName != "mod init" {
+ modfetch.WriteGoSum(keepSums(true))
+ }
}()
// Make a best-effort attempt to acquire the side lock, only to exclude
@@ -951,41 +976,55 @@ func WriteGoMod() {
// If addDirect is true, the set also includes sums for modules directly
// required by go.mod, as represented by the index, with replacements applied.
func keepSums(addDirect bool) map[module.Version]bool {
- // Walk the module graph and keep sums needed by MVS.
+ // Re-derive the build list using the current list of direct requirements.
+ // Keep the sum for the go.mod of each visited module version (or its
+ // replacement).
modkey := func(m module.Version) module.Version {
return module.Version{Path: m.Path, Version: m.Version + "/go.mod"}
}
keep := make(map[module.Version]bool)
- replaced := make(map[module.Version]bool)
- reqs := Reqs()
- var walk func(module.Version)
- walk = func(m module.Version) {
- // If we build using a replacement module, keep the sum for the replacement,
- // since that's the code we'll actually use during a build.
- r := Replacement(m)
- if r.Path == "" {
- keep[modkey(m)] = true
- } else {
- replaced[m] = true
- keep[modkey(r)] = true
- }
- list, _ := reqs.Required(m)
- for _, r := range list {
- if !keep[modkey(r)] && !replaced[r] {
- walk(r)
+ var mu sync.Mutex
+ reqs := &keepSumReqs{
+ Reqs: Reqs(),
+ visit: func(m module.Version) {
+ // If we build using a replacement module, keep the sum for the replacement,
+ // since that's the code we'll actually use during a build.
+ mu.Lock()
+ r := Replacement(m)
+ if r.Path == "" {
+ keep[modkey(m)] = true
+ } else {
+ keep[modkey(r)] = true
}
- }
+ mu.Unlock()
+ },
+ }
+ buildList, err := mvs.BuildList(Target, reqs)
+ if err != nil {
+ panic(fmt.Sprintf("unexpected error reloading build list: %v", err))
}
- walk(Target)
- // Add entries for modules from which packages were loaded.
+ // Add entries for modules in the build list with paths that are prefixes of
+ // paths of loaded packages. We need to retain sums for modules needed to
+ // report ambiguous import errors. We use our re-derived build list,
+ // since the global build list may have been tidied.
if loaded != nil {
- for _, pkg := range loaded.pkgs {
- m := pkg.mod
+ actualMods := make(map[string]module.Version)
+ for _, m := range buildList[1:] {
if r := Replacement(m); r.Path != "" {
- keep[r] = true
+ actualMods[m.Path] = r
} else {
- keep[m] = true
+ actualMods[m.Path] = m
+ }
+ }
+ for _, pkg := range loaded.pkgs {
+ if pkg.testOf != nil || pkg.inStd {
+ continue
+ }
+ for prefix := pkg.path; prefix != "."; prefix = path.Dir(prefix) {
+ if m, ok := actualMods[prefix]; ok {
+ keep[m] = true
+ }
}
}
}
@@ -1007,6 +1046,18 @@ func keepSums(addDirect bool) map[module.Version]bool {
return keep
}
+// keepSumReqs embeds another Reqs implementation. The Required method
+// calls visit for each version in the module graph.
+type keepSumReqs struct {
+ mvs.Reqs
+ visit func(module.Version)
+}
+
+func (r *keepSumReqs) Required(m module.Version) ([]module.Version, error) {
+ r.visit(m)
+ return r.Reqs.Required(m)
+}
+
func TrimGoSum() {
// Don't retain sums for direct requirements in go.mod. When TrimGoSum is
// called, go.mod has not been updated, and it may contain requirements on
diff --git a/src/cmd/go/internal/modload/load.go b/src/cmd/go/internal/modload/load.go
index 9194f9cc7c..f9c468c8b2 100644
--- a/src/cmd/go/internal/modload/load.go
+++ b/src/cmd/go/internal/modload/load.go
@@ -98,6 +98,7 @@ import (
"errors"
"fmt"
"go/build"
+ "io/fs"
"os"
"path"
pathpkg "path"
@@ -111,6 +112,7 @@ import (
"cmd/go/internal/base"
"cmd/go/internal/cfg"
+ "cmd/go/internal/fsys"
"cmd/go/internal/imports"
"cmd/go/internal/modfetch"
"cmd/go/internal/mvs"
@@ -167,7 +169,7 @@ type PackageOpts struct {
// LoadPackages identifies the set of packages matching the given patterns and
// loads the packages in the import graph rooted at that set.
func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (matches []*search.Match, loadedPackages []string) {
- InitMod(ctx)
+ LoadModFile(ctx)
if opts.Tags == nil {
opts.Tags = imports.Tags()
}
@@ -248,9 +250,7 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
}
loaded = loadFromRoots(loaderParams{
- tags: opts.Tags,
- loadTests: opts.LoadTests,
- resolveMissing: opts.ResolveMissingImports,
+ PackageOpts: opts,
allClosesOverTests: index.allPatternClosesOverTests() && !opts.UseVendorAll,
allPatternIsRoot: allPatternIsRoot,
@@ -270,11 +270,21 @@ func LoadPackages(ctx context.Context, opts PackageOpts, patterns ...string) (ma
// Report errors, if any.
checkMultiplePaths()
for _, pkg := range loaded.pkgs {
- if pkg.err != nil && !opts.SilenceErrors {
- if opts.AllowErrors {
- fmt.Fprintf(os.Stderr, "%s: %v\n", pkg.stackText(), pkg.err)
- } else {
- base.Errorf("%s: %v", pkg.stackText(), pkg.err)
+ if pkg.err != nil {
+ if pkg.flags.has(pkgInAll) {
+ if imErr := (*ImportMissingError)(nil); errors.As(pkg.err, &imErr) {
+ imErr.inAll = true
+ } else if sumErr := (*ImportMissingSumError)(nil); errors.As(pkg.err, &sumErr) {
+ sumErr.inAll = true
+ }
+ }
+
+ if !opts.SilenceErrors {
+ if opts.AllowErrors {
+ fmt.Fprintf(os.Stderr, "%s: %v\n", pkg.stackText(), pkg.err)
+ } else {
+ base.Errorf("%s: %v", pkg.stackText(), pkg.err)
+ }
}
}
if !pkg.isTest() {
@@ -352,11 +362,11 @@ func resolveLocalPackage(dir string) (string, error) {
// If the named directory does not exist or contains no Go files,
// the package does not exist.
// Other errors may affect package loading, but not resolution.
- if _, err := os.Stat(absDir); err != nil {
+ if _, err := fsys.Stat(absDir); err != nil {
if os.IsNotExist(err) {
// Canonicalize OS-specific errors to errDirectoryNotFound so that error
// messages will be easier for users to search for.
- return "", &os.PathError{Op: "stat", Path: absDir, Err: errDirectoryNotFound}
+ return "", &fs.PathError{Op: "stat", Path: absDir, Err: errDirectoryNotFound}
}
return "", err
}
@@ -484,7 +494,7 @@ func pathInModuleCache(dir string) string {
// ImportFromFiles adds modules to the build list as needed
// to satisfy the imports in the named Go source files.
func ImportFromFiles(ctx context.Context, gofiles []string) {
- InitMod(ctx)
+ LoadModFile(ctx)
tags := imports.Tags()
imports, testImports, err := imports.ScanFiles(gofiles, tags)
@@ -493,8 +503,10 @@ func ImportFromFiles(ctx context.Context, gofiles []string) {
}
loaded = loadFromRoots(loaderParams{
- tags: tags,
- resolveMissing: true,
+ PackageOpts: PackageOpts{
+ Tags: tags,
+ ResolveMissingImports: true,
+ },
allClosesOverTests: index.allPatternClosesOverTests(),
listRoots: func() (roots []string) {
roots = append(roots, imports...)
@@ -647,10 +659,10 @@ type loader struct {
direct map[string]bool // imported directly by main module
}
+// loaderParams configure the packages loaded by, and the properties reported
+// by, a loader instance.
type loaderParams struct {
- tags map[string]bool // tags for scanDir
- loadTests bool
- resolveMissing bool
+ PackageOpts
allClosesOverTests bool // Does the "all" pattern include the transitive closure of tests of packages in "all"?
allPatternIsRoot bool // Is the "all" pattern an additional root?
@@ -809,7 +821,7 @@ func loadFromRoots(params loaderParams) *loader {
ld.buildStacks()
- if !ld.resolveMissing {
+ if !ld.ResolveMissingImports || (!HasModRoot() && !allowMissingModuleImports) {
// We've loaded as much as we can without resolving missing imports.
break
}
@@ -852,7 +864,7 @@ func loadFromRoots(params loaderParams) *loader {
// contributes “direct” imports — so we can't safely mark existing
// dependencies as indirect-only.
// Conservatively mark those dependencies as direct.
- if modFile != nil && (!ld.allPatternIsRoot || !reflect.DeepEqual(ld.tags, imports.AnyTags())) {
+ if modFile != nil && (!ld.allPatternIsRoot || !reflect.DeepEqual(ld.Tags, imports.AnyTags())) {
for _, r := range modFile.Require {
if !r.Indirect {
ld.direct[r.Mod.Path] = true
@@ -872,12 +884,15 @@ func loadFromRoots(params loaderParams) *loader {
func (ld *loader) resolveMissingImports(addedModuleFor map[string]bool) (modAddedBy map[module.Version]*loadPkg) {
var needPkgs []*loadPkg
for _, pkg := range ld.pkgs {
+ if pkg.err == nil {
+ continue
+ }
if pkg.isTest() {
// If we are missing a test, we are also missing its non-test version, and
// we should only add the missing import once.
continue
}
- if pkg.err != errImportMissing {
+ if !errors.As(pkg.err, new(*ImportMissingError)) {
// Leave other errors for Import or load.Packages to report.
continue
}
@@ -980,7 +995,7 @@ func (ld *loader) applyPkgFlags(pkg *loadPkg, flags loadPkgFlags) {
// also in "all" (as above).
wantTest = true
- case ld.loadTests && new.has(pkgIsRoot):
+ case ld.LoadTests && new.has(pkgIsRoot):
// LoadTest explicitly requests tests of “the root packages”.
wantTest = true
}
@@ -1043,7 +1058,7 @@ func (ld *loader) load(pkg *loadPkg) {
ld.applyPkgFlags(pkg, pkgInAll)
}
- imports, testImports, err := scanDir(pkg.dir, ld.tags)
+ imports, testImports, err := scanDir(pkg.dir, ld.Tags)
if err != nil {
pkg.err = err
return
diff --git a/src/cmd/go/internal/modload/modfile.go b/src/cmd/go/internal/modload/modfile.go
index 6457a7d968..7a8963246b 100644
--- a/src/cmd/go/internal/modload/modfile.go
+++ b/src/cmd/go/internal/modload/modfile.go
@@ -35,13 +35,14 @@ var modFile *modfile.File
// A modFileIndex is an index of data corresponding to a modFile
// at a specific point in time.
type modFileIndex struct {
- data []byte
- dataNeedsFix bool // true if fixVersion applied a change while parsing data
- module module.Version
- goVersionV string // GoVersion with "v" prefix
- require map[module.Version]requireMeta
- replace map[module.Version]module.Version
- exclude map[module.Version]bool
+ data []byte
+ dataNeedsFix bool // true if fixVersion applied a change while parsing data
+ module module.Version
+ goVersionV string // GoVersion with "v" prefix
+ require map[module.Version]requireMeta
+ replace map[module.Version]module.Version
+ highestReplaced map[string]string // highest replaced version of each module path; empty string for wildcard-only replacements
+ exclude map[module.Version]bool
}
// index is the index of the go.mod file as of when it was last read or written.
@@ -115,9 +116,9 @@ func checkRetractions(ctx context.Context, m module.Version) error {
// Ignore exclusions from the main module's go.mod.
// We may need to account for the current version: for example,
// v2.0.0+incompatible is not "latest" if v1.0.0 is current.
- rev, err := Query(ctx, path, "latest", findCurrentVersion(path), nil)
+ rev, err := Query(ctx, path, "latest", Selected(path), nil)
if err != nil {
- return &entry{err: err}
+ return &entry{nil, err}
}
// Load go.mod for that version.
@@ -138,13 +139,19 @@ func checkRetractions(ctx context.Context, m module.Version) error {
}
summary, err := rawGoModSummary(rm)
if err != nil {
- return &entry{err: err}
+ return &entry{nil, err}
}
- return &entry{retract: summary.retract}
+ return &entry{summary.retract, nil}
}).(*entry)
- if e.err != nil {
- return fmt.Errorf("loading module retractions: %v", e.err)
+ if err := e.err; err != nil {
+ // Attribute the error to the version being checked, not the version from
+ // which the retractions were to be loaded.
+ var mErr *module.ModuleError
+ if errors.As(err, &mErr) {
+ err = mErr.Err
+ }
+ return &retractionLoadingError{m: m, err: err}
}
var rationale []string
@@ -158,7 +165,7 @@ func checkRetractions(ctx context.Context, m module.Version) error {
}
}
if isRetracted {
- return &retractedError{rationale: rationale}
+ return module.VersionError(m, &retractedError{rationale: rationale})
}
return nil
}
@@ -183,6 +190,19 @@ func (e *retractedError) Is(err error) bool {
return err == ErrDisallowed
}
+type retractionLoadingError struct {
+ m module.Version
+ err error
+}
+
+func (e *retractionLoadingError) Error() string {
+ return fmt.Sprintf("loading module retractions for %v: %v", e.m, e.err)
+}
+
+func (e *retractionLoadingError) Unwrap() error {
+ return e.err
+}
+
// ShortRetractionRationale returns a retraction rationale string that is safe
// to print in a terminal. It returns hard-coded strings if the rationale
// is empty, too long, or contains non-printable characters.
@@ -255,6 +275,14 @@ func indexModFile(data []byte, modFile *modfile.File, needsFix bool) *modFileInd
i.replace[r.Old] = r.New
}
+ i.highestReplaced = make(map[string]string)
+ for _, r := range modFile.Replace {
+ v, ok := i.highestReplaced[r.Old.Path]
+ if !ok || semver.Compare(r.Old.Version, v) > 0 {
+ i.highestReplaced[r.Old.Path] = r.Old.Version
+ }
+ }
+
i.exclude = make(map[module.Version]bool, len(modFile.Exclude))
for _, x := range modFile.Exclude {
i.exclude[x.Mod] = true
@@ -378,8 +406,11 @@ type retraction struct {
// taking into account any replacements for m, exclusions of its dependencies,
// and/or vendoring.
//
-// goModSummary cannot be used on the Target module, as its requirements
-// may change.
+// m must be a version in the module graph, reachable from the Target module.
+// In readonly mode, the go.sum file must contain an entry for m's go.mod file
+// (or its replacement). goModSummary must not be called for the Target module
+// itself, as its requirements may change. Use rawGoModSummary for other
+// module versions.
//
// The caller must not modify the returned summary.
func goModSummary(m module.Version) (*modFileSummary, error) {
@@ -414,6 +445,13 @@ func goModSummary(m module.Version) (*modFileSummary, error) {
if actual.Path == "" {
actual = m
}
+ if cfg.BuildMod == "readonly" && actual.Version != "" {
+ key := module.Version{Path: actual.Path, Version: actual.Version + "/go.mod"}
+ if !modfetch.HaveSum(key) {
+ suggestion := fmt.Sprintf("; try 'go mod download %s' to add it", m.Path)
+ return nil, module.VersionError(actual, &sumMissingError{suggestion: suggestion})
+ }
+ }
summary, err := rawGoModSummary(actual)
if err != nil {
return nil, err
diff --git a/src/cmd/go/internal/modload/mvs.go b/src/cmd/go/internal/modload/mvs.go
index 24856260d4..76a1d8a12a 100644
--- a/src/cmd/go/internal/modload/mvs.go
+++ b/src/cmd/go/internal/modload/mvs.go
@@ -7,9 +7,6 @@ package modload
import (
"context"
"errors"
- "fmt"
- "os"
- "path/filepath"
"sort"
"cmd/go/internal/modfetch"
@@ -77,11 +74,7 @@ func versions(ctx context.Context, path string, allowed AllowedFunc) ([]string,
// so there's no need for us to add extra caching here.
var versions []string
err := modfetch.TryProxies(func(proxy string) error {
- repo, err := modfetch.Lookup(proxy, path)
- if err != nil {
- return err
- }
- allVersions, err := repo.Versions("")
+ allVersions, err := modfetch.Lookup(proxy, path).Versions("")
if err != nil {
return err
}
@@ -129,42 +122,3 @@ func (*mvsReqs) next(m module.Version) (module.Version, error) {
}
return module.Version{Path: m.Path, Version: "none"}, nil
}
-
-// fetch downloads the given module (or its replacement)
-// and returns its location.
-//
-// The isLocal return value reports whether the replacement,
-// if any, is local to the filesystem.
-func fetch(ctx context.Context, mod module.Version) (dir string, isLocal bool, err error) {
- if mod == Target {
- return ModRoot(), true, nil
- }
- if r := Replacement(mod); r.Path != "" {
- if r.Version == "" {
- dir = r.Path
- if !filepath.IsAbs(dir) {
- dir = filepath.Join(ModRoot(), dir)
- }
- // Ensure that the replacement directory actually exists:
- // dirInModule does not report errors for missing modules,
- // so if we don't report the error now, later failures will be
- // very mysterious.
- if _, err := os.Stat(dir); err != nil {
- if os.IsNotExist(err) {
- // Semantically the module version itself “exists” — we just don't
- // have its source code. Remove the equivalence to os.ErrNotExist,
- // and make the message more concise while we're at it.
- err = fmt.Errorf("replacement directory %s does not exist", r.Path)
- } else {
- err = fmt.Errorf("replacement directory %s: %w", r.Path, err)
- }
- return dir, true, module.VersionError(mod, err)
- }
- return dir, true, nil
- }
- mod = r
- }
-
- dir, err = modfetch.Download(ctx, mod)
- return dir, false, err
-}
diff --git a/src/cmd/go/internal/modload/query.go b/src/cmd/go/internal/modload/query.go
index e75d901ec6..3927051015 100644
--- a/src/cmd/go/internal/modload/query.go
+++ b/src/cmd/go/internal/modload/query.go
@@ -8,17 +8,19 @@ import (
"context"
"errors"
"fmt"
+ "io/fs"
"os"
pathpkg "path"
"path/filepath"
+ "sort"
"strings"
"sync"
+ "time"
"cmd/go/internal/cfg"
"cmd/go/internal/imports"
"cmd/go/internal/modfetch"
"cmd/go/internal/search"
- "cmd/go/internal/str"
"cmd/go/internal/trace"
"golang.org/x/mod/module"
@@ -106,138 +108,45 @@ func queryProxy(ctx context.Context, proxy, path, query, current string, allowed
allowed = func(context.Context, module.Version) error { return nil }
}
- // Parse query to detect parse errors (and possibly handle query)
- // before any network I/O.
- badVersion := func(v string) (*modfetch.RevInfo, error) {
- return nil, fmt.Errorf("invalid semantic version %q in range %q", v, query)
- }
- matchesMajor := func(v string) bool {
- _, pathMajor, ok := module.SplitPathVersion(path)
- if !ok {
- return false
- }
- return module.CheckPathMajor(v, pathMajor) == nil
- }
- var (
- match = func(m module.Version) bool { return true }
-
- prefix string
- preferOlder bool
- mayUseLatest bool
- preferIncompatible bool = strings.HasSuffix(current, "+incompatible")
- )
- switch {
- case query == "latest":
- mayUseLatest = true
-
- case query == "upgrade":
- mayUseLatest = true
-
- case query == "patch":
- if current == "" {
- mayUseLatest = true
- } else {
- prefix = semver.MajorMinor(current)
- match = func(m module.Version) bool {
- return matchSemverPrefix(prefix, m.Version)
- }
- }
-
- case strings.HasPrefix(query, "<="):
- v := query[len("<="):]
- if !semver.IsValid(v) {
- return badVersion(v)
- }
- if isSemverPrefix(v) {
- // Refuse to say whether <=v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
- return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
- }
- match = func(m module.Version) bool {
- return semver.Compare(m.Version, v) <= 0
- }
- if !matchesMajor(v) {
- preferIncompatible = true
- }
-
- case strings.HasPrefix(query, "<"):
- v := query[len("<"):]
- if !semver.IsValid(v) {
- return badVersion(v)
- }
- match = func(m module.Version) bool {
- return semver.Compare(m.Version, v) < 0
- }
- if !matchesMajor(v) {
- preferIncompatible = true
- }
-
- case strings.HasPrefix(query, ">="):
- v := query[len(">="):]
- if !semver.IsValid(v) {
- return badVersion(v)
- }
- match = func(m module.Version) bool {
- return semver.Compare(m.Version, v) >= 0
+ if path == Target.Path {
+ if query != "latest" {
+ return nil, fmt.Errorf("can't query specific version (%q) for the main module (%s)", query, path)
}
- preferOlder = true
- if !matchesMajor(v) {
- preferIncompatible = true
+ if err := allowed(ctx, Target); err != nil {
+ return nil, fmt.Errorf("internal error: main module version is not allowed: %w", err)
}
+ return &modfetch.RevInfo{Version: Target.Version}, nil
+ }
- case strings.HasPrefix(query, ">"):
- v := query[len(">"):]
- if !semver.IsValid(v) {
- return badVersion(v)
- }
- if isSemverPrefix(v) {
- // Refuse to say whether >v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
- return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
- }
- match = func(m module.Version) bool {
- return semver.Compare(m.Version, v) > 0
- }
- preferOlder = true
- if !matchesMajor(v) {
- preferIncompatible = true
- }
+ if path == "std" || path == "cmd" {
+ return nil, fmt.Errorf("can't query specific version (%q) of standard-library module %q", query, path)
+ }
- case semver.IsValid(query) && isSemverPrefix(query):
- match = func(m module.Version) bool {
- return matchSemverPrefix(query, m.Version)
- }
- prefix = query + "."
- if !matchesMajor(query) {
- preferIncompatible = true
- }
+ repo, err := lookupRepo(proxy, path)
+ if err != nil {
+ return nil, err
+ }
- default:
- // Direct lookup of semantic version or commit identifier.
-
- // If the query is a valid semantic version and that version is replaced,
- // use the replacement module without searching the proxy.
- canonicalQuery := module.CanonicalVersion(query)
- if canonicalQuery != "" {
- m := module.Version{Path: path, Version: query}
- if r := Replacement(m); r.Path != "" {
- if err := allowed(ctx, m); errors.Is(err, ErrDisallowed) {
- return nil, err
- }
- return &modfetch.RevInfo{Version: query}, nil
- }
- }
+ // Parse query to detect parse errors (and possibly handle query)
+ // before any network I/O.
+ qm, err := newQueryMatcher(path, query, current, allowed)
+ if (err == nil && qm.canStat) || err == errRevQuery {
+ // Direct lookup of a commit identifier or complete (non-prefix) semantic
+ // version.
// If the identifier is not a canonical semver tag — including if it's a
// semver tag with a +metadata suffix — then modfetch.Stat will populate
// info.Version with a suitable pseudo-version.
- info, err := modfetch.Stat(proxy, path, query)
+ info, err := repo.Stat(query)
if err != nil {
queryErr := err
// The full query doesn't correspond to a tag. If it is a semantic version
// with a +metadata suffix, see if there is a tag without that suffix:
// semantic versioning defines them to be equivalent.
+ canonicalQuery := module.CanonicalVersion(query)
if canonicalQuery != "" && query != canonicalQuery {
- info, err = modfetch.Stat(proxy, path, canonicalQuery)
- if err != nil && !errors.Is(err, os.ErrNotExist) {
+ info, err = repo.Stat(canonicalQuery)
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
return info, err
}
}
@@ -249,38 +158,16 @@ func queryProxy(ctx context.Context, proxy, path, query, current string, allowed
return nil, err
}
return info, nil
- }
-
- if path == Target.Path {
- if query != "latest" {
- return nil, fmt.Errorf("can't query specific version (%q) for the main module (%s)", query, path)
- }
- if err := allowed(ctx, Target); err != nil {
- return nil, fmt.Errorf("internal error: main module version is not allowed: %w", err)
- }
- return &modfetch.RevInfo{Version: Target.Version}, nil
- }
-
- if str.HasPathPrefix(path, "std") || str.HasPathPrefix(path, "cmd") {
- return nil, fmt.Errorf("explicit requirement on standard-library module %s not allowed", path)
+ } else if err != nil {
+ return nil, err
}
// Load versions and execute query.
- repo, err := modfetch.Lookup(proxy, path)
- if err != nil {
- return nil, err
- }
- versions, err := repo.Versions(prefix)
+ versions, err := repo.Versions(qm.prefix)
if err != nil {
return nil, err
}
- matchAndAllowed := func(ctx context.Context, m module.Version) error {
- if !match(m) {
- return ErrDisallowed
- }
- return allowed(ctx, m)
- }
- releases, prereleases, err := filterVersions(ctx, path, versions, matchAndAllowed, preferIncompatible)
+ releases, prereleases, err := qm.filterVersions(ctx, versions)
if err != nil {
return nil, err
}
@@ -291,11 +178,30 @@ func queryProxy(ctx context.Context, proxy, path, query, current string, allowed
return nil, err
}
- // For "upgrade" and "patch", make sure we don't accidentally downgrade
- // from a newer prerelease or from a chronologically newer pseudoversion.
- if current != "" && (query == "upgrade" || query == "patch") {
+ if (query == "upgrade" || query == "patch") && modfetch.IsPseudoVersion(current) && !rev.Time.IsZero() {
+ // Don't allow "upgrade" or "patch" to move from a pseudo-version
+ // to a chronologically older version or pseudo-version.
+ //
+ // If the current version is a pseudo-version from an untagged branch, it
+ // may be semantically lower than the "latest" release or the latest
+ // pseudo-version on the main branch. A user on such a version is unlikely
+ // to intend to “upgrade” to a version that already existed at that point
+ // in time.
+ //
+ // We do this only if the current version is a pseudo-version: if the
+ // version is tagged, the author of the dependency module has given us
+ // explicit information about their intended precedence of this version
+ // relative to other versions, and we shouldn't contradict that
+ // information. (For example, v1.0.1 might be a backport of a fix already
+ // incorporated into v1.1.0, in which case v1.0.1 would be chronologically
+ // newer but v1.1.0 is still an “upgrade”; or v1.0.2 might be a revert of
+ // an unsuccessful fix in v1.0.1, in which case the v1.0.2 commit may be
+ // older than the v1.0.1 commit despite the tag itself being newer.)
currentTime, err := modfetch.PseudoVersionTime(current)
- if semver.Compare(rev.Version, current) < 0 || (err == nil && rev.Time.Before(currentTime)) {
+ if err == nil && rev.Time.Before(currentTime) {
+ if err := allowed(ctx, module.Version{Path: path, Version: current}); errors.Is(err, ErrDisallowed) {
+ return nil, err
+ }
return repo.Stat(current)
}
}
@@ -303,7 +209,7 @@ func queryProxy(ctx context.Context, proxy, path, query, current string, allowed
return rev, nil
}
- if preferOlder {
+ if qm.preferLower {
if len(releases) > 0 {
return lookup(releases[0])
}
@@ -319,20 +225,25 @@ func queryProxy(ctx context.Context, proxy, path, query, current string, allowed
}
}
- if mayUseLatest {
- // Special case for "latest": if no tags match, use latest commit in repo
- // if it is allowed.
+ if qm.mayUseLatest {
latest, err := repo.Latest()
if err == nil {
- m := module.Version{Path: path, Version: latest.Version}
- if err := allowed(ctx, m); !errors.Is(err, ErrDisallowed) {
+ if qm.allowsVersion(ctx, latest.Version) {
return lookup(latest.Version)
}
- } else if !errors.Is(err, os.ErrNotExist) {
+ } else if !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
}
+ if (query == "upgrade" || query == "patch") && current != "" {
+ // "upgrade" and "patch" may stay on the current version if allowed.
+ if err := allowed(ctx, module.Version{Path: path, Version: current}); errors.Is(err, ErrDisallowed) {
+ return nil, err
+ }
+ return lookup(current)
+ }
+
return nil, &NoMatchingVersionError{query: query, current: current}
}
@@ -370,10 +281,151 @@ func isSemverPrefix(v string) bool {
return true
}
-// matchSemverPrefix reports whether the shortened semantic version p
-// matches the full-width (non-shortened) semantic version v.
-func matchSemverPrefix(p, v string) bool {
- return len(v) > len(p) && v[len(p)] == '.' && v[:len(p)] == p && semver.Prerelease(v) == ""
+type queryMatcher struct {
+ path string
+ prefix string
+ filter func(version string) bool
+ allowed AllowedFunc
+ canStat bool // if true, the query can be resolved by repo.Stat
+ preferLower bool // if true, choose the lowest matching version
+ mayUseLatest bool
+ preferIncompatible bool
+}
+
+var errRevQuery = errors.New("query refers to a non-semver revision")
+
+// newQueryMatcher returns a new queryMatcher that matches the versions
+// specified by the given query on the module with the given path.
+//
+// If the query can only be resolved by statting a non-SemVer revision,
+// newQueryMatcher returns errRevQuery.
+func newQueryMatcher(path string, query, current string, allowed AllowedFunc) (*queryMatcher, error) {
+ badVersion := func(v string) (*queryMatcher, error) {
+ return nil, fmt.Errorf("invalid semantic version %q in range %q", v, query)
+ }
+
+ matchesMajor := func(v string) bool {
+ _, pathMajor, ok := module.SplitPathVersion(path)
+ if !ok {
+ return false
+ }
+ return module.CheckPathMajor(v, pathMajor) == nil
+ }
+
+ qm := &queryMatcher{
+ path: path,
+ allowed: allowed,
+ preferIncompatible: strings.HasSuffix(current, "+incompatible"),
+ }
+
+ switch {
+ case query == "latest":
+ qm.mayUseLatest = true
+
+ case query == "upgrade":
+ if current == "" {
+ qm.mayUseLatest = true
+ } else {
+ qm.mayUseLatest = modfetch.IsPseudoVersion(current)
+ qm.filter = func(mv string) bool { return semver.Compare(mv, current) >= 0 }
+ }
+
+ case query == "patch":
+ if current == "" {
+ qm.mayUseLatest = true
+ } else {
+ qm.mayUseLatest = modfetch.IsPseudoVersion(current)
+ qm.prefix = semver.MajorMinor(current) + "."
+ qm.filter = func(mv string) bool { return semver.Compare(mv, current) >= 0 }
+ }
+
+ case strings.HasPrefix(query, "<="):
+ v := query[len("<="):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ if isSemverPrefix(v) {
+ // Refuse to say whether <=v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
+ return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
+ }
+ qm.filter = func(mv string) bool { return semver.Compare(mv, v) <= 0 }
+ if !matchesMajor(v) {
+ qm.preferIncompatible = true
+ }
+
+ case strings.HasPrefix(query, "<"):
+ v := query[len("<"):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ qm.filter = func(mv string) bool { return semver.Compare(mv, v) < 0 }
+ if !matchesMajor(v) {
+ qm.preferIncompatible = true
+ }
+
+ case strings.HasPrefix(query, ">="):
+ v := query[len(">="):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ qm.filter = func(mv string) bool { return semver.Compare(mv, v) >= 0 }
+ qm.preferLower = true
+ if !matchesMajor(v) {
+ qm.preferIncompatible = true
+ }
+
+ case strings.HasPrefix(query, ">"):
+ v := query[len(">"):]
+ if !semver.IsValid(v) {
+ return badVersion(v)
+ }
+ if isSemverPrefix(v) {
+ // Refuse to say whether >v1.2 allows v1.2.3 (remember, @v1.2 might mean v1.2.3).
+ return nil, fmt.Errorf("ambiguous semantic version %q in range %q", v, query)
+ }
+ qm.filter = func(mv string) bool { return semver.Compare(mv, v) > 0 }
+ qm.preferLower = true
+ if !matchesMajor(v) {
+ qm.preferIncompatible = true
+ }
+
+ case semver.IsValid(query):
+ if isSemverPrefix(query) {
+ qm.prefix = query + "."
+ // Do not allow the query "v1.2" to match versions lower than "v1.2.0",
+ // such as prereleases for that version. (https://golang.org/issue/31972)
+ qm.filter = func(mv string) bool { return semver.Compare(mv, query) >= 0 }
+ } else {
+ qm.canStat = true
+ qm.filter = func(mv string) bool { return semver.Compare(mv, query) == 0 }
+ qm.prefix = semver.Canonical(query)
+ }
+ if !matchesMajor(query) {
+ qm.preferIncompatible = true
+ }
+
+ default:
+ return nil, errRevQuery
+ }
+
+ return qm, nil
+}
+
+// allowsVersion reports whether version v is allowed by the prefix, filter, and
+// AllowedFunc of qm.
+func (qm *queryMatcher) allowsVersion(ctx context.Context, v string) bool {
+ if qm.prefix != "" && !strings.HasPrefix(v, qm.prefix) {
+ return false
+ }
+ if qm.filter != nil && !qm.filter(v) {
+ return false
+ }
+ if qm.allowed != nil {
+ if err := qm.allowed(ctx, module.Version{Path: qm.path, Version: v}); errors.Is(err, ErrDisallowed) {
+ return false
+ }
+ }
+ return true
}
// filterVersions classifies versions into releases and pre-releases, filtering
@@ -384,14 +436,32 @@ func matchSemverPrefix(p, v string) bool {
//
// If the allowed predicate returns an error not equivalent to ErrDisallowed,
// filterVersions returns that error.
-func filterVersions(ctx context.Context, path string, versions []string, allowed AllowedFunc, preferIncompatible bool) (releases, prereleases []string, err error) {
+func (qm *queryMatcher) filterVersions(ctx context.Context, versions []string) (releases, prereleases []string, err error) {
+ needIncompatible := qm.preferIncompatible
+
var lastCompatible string
for _, v := range versions {
- if err := allowed(ctx, module.Version{Path: path, Version: v}); errors.Is(err, ErrDisallowed) {
+ if !qm.allowsVersion(ctx, v) {
continue
}
- if !preferIncompatible {
+ if !needIncompatible {
+ // We're not yet sure whether we need to include +incomptaible versions.
+ // Keep track of the last compatible version we've seen, and use the
+ // presence (or absence) of a go.mod file in that version to decide: a
+ // go.mod file implies that the module author is supporting modules at a
+ // compatible version (and we should ignore +incompatible versions unless
+ // requested explicitly), while a lack of go.mod file implies the
+ // potential for legacy (pre-modules) versioning without semantic import
+ // paths (and thus *with* +incompatible versions).
+ //
+ // This isn't strictly accurate if the latest compatible version has been
+ // replaced by a local file path, because we do not allow file-path
+ // replacements without a go.mod file: the user would have needed to add
+ // one. However, replacing the last compatible version while
+ // simultaneously expecting to upgrade implicitly to a +incompatible
+ // version seems like an extreme enough corner case to ignore for now.
+
if !strings.HasSuffix(v, "+incompatible") {
lastCompatible = v
} else if lastCompatible != "" {
@@ -399,19 +469,22 @@ func filterVersions(ctx context.Context, path string, versions []string, allowed
// ignore any version with a higher (+incompatible) major version. (See
// https://golang.org/issue/34165.) Note that we even prefer a
// compatible pre-release over an incompatible release.
-
- ok, err := versionHasGoMod(ctx, module.Version{Path: path, Version: lastCompatible})
+ ok, err := versionHasGoMod(ctx, module.Version{Path: qm.path, Version: lastCompatible})
if err != nil {
return nil, nil, err
}
if ok {
+ // The last compatible version has a go.mod file, so that's the
+ // highest version we're willing to consider. Don't bother even
+ // looking at higher versions, because they're all +incompatible from
+ // here onward.
break
}
// No acceptable compatible release has a go.mod file, so the versioning
// for the module might not be module-aware, and we should respect
// legacy major-version tags.
- preferIncompatible = true
+ needIncompatible = true
}
}
@@ -444,7 +517,7 @@ type QueryResult struct {
// If any matching package is in the main module, QueryPattern considers only
// the main module and only the version "latest", without checking for other
// possible modules.
-func QueryPattern(ctx context.Context, pattern, query string, allowed AllowedFunc) ([]QueryResult, error) {
+func QueryPattern(ctx context.Context, pattern, query string, current func(string) string, allowed AllowedFunc) ([]QueryResult, error) {
ctx, span := trace.StartSpan(ctx, "modload.QueryPattern "+pattern+" "+query)
defer span.Done()
@@ -519,14 +592,15 @@ func QueryPattern(ctx context.Context, pattern, query string, allowed AllowedFun
ctx, span := trace.StartSpan(ctx, "modload.QueryPattern.queryModule ["+proxy+"] "+path)
defer span.Done()
- current := findCurrentVersion(path)
+ pathCurrent := current(path)
r.Mod.Path = path
- r.Rev, err = queryProxy(ctx, proxy, path, query, current, allowed)
+ r.Rev, err = queryProxy(ctx, proxy, path, query, pathCurrent, allowed)
if err != nil {
return r, err
}
r.Mod.Version = r.Rev.Version
- root, isLocal, err := fetch(ctx, r.Mod)
+ needSum := true
+ root, isLocal, err := fetch(ctx, r.Mod, needSum)
if err != nil {
return r, err
}
@@ -577,15 +651,6 @@ func modulePrefixesExcludingTarget(path string) []string {
return prefixes
}
-func findCurrentVersion(path string) string {
- for _, m := range buildList {
- if m.Path == path {
- return m.Version
- }
- }
- return ""
-}
-
type prefixResult struct {
QueryResult
err error
@@ -638,7 +703,7 @@ func queryPrefixModules(ctx context.Context, candidateModules []string, queryMod
noVersion = rErr
}
default:
- if errors.Is(rErr, os.ErrNotExist) {
+ if errors.Is(rErr, fs.ErrNotExist) {
if notExistErr == nil {
notExistErr = rErr
}
@@ -681,7 +746,7 @@ func queryPrefixModules(ctx context.Context, candidateModules []string, queryMod
// A NoMatchingVersionError indicates that Query found a module at the requested
// path, but not at any versions satisfying the query string and allow-function.
//
-// NOTE: NoMatchingVersionError MUST NOT implement Is(os.ErrNotExist).
+// NOTE: NoMatchingVersionError MUST NOT implement Is(fs.ErrNotExist).
//
// If the module came from a proxy, that proxy had to return a successful status
// code for the versions it knows about, and thus did not have the opportunity
@@ -702,7 +767,7 @@ func (e *NoMatchingVersionError) Error() string {
// module at the requested version, but that module did not contain any packages
// matching the requested pattern.
//
-// NOTE: PackageNotInModuleError MUST NOT implement Is(os.ErrNotExist).
+// NOTE: PackageNotInModuleError MUST NOT implement Is(fs.ErrNotExist).
//
// If the module came from a proxy, that proxy had to return a successful status
// code for the versions it knows about, and thus did not have the opportunity
@@ -752,7 +817,8 @@ func (e *PackageNotInModuleError) ImportPath() string {
// ModuleHasRootPackage returns whether module m contains a package m.Path.
func ModuleHasRootPackage(ctx context.Context, m module.Version) (bool, error) {
- root, isLocal, err := fetch(ctx, m)
+ needSum := false
+ root, isLocal, err := fetch(ctx, m, needSum)
if err != nil {
return false, err
}
@@ -761,10 +827,165 @@ func ModuleHasRootPackage(ctx context.Context, m module.Version) (bool, error) {
}
func versionHasGoMod(ctx context.Context, m module.Version) (bool, error) {
- root, _, err := fetch(ctx, m)
+ needSum := false
+ root, _, err := fetch(ctx, m, needSum)
if err != nil {
return false, err
}
fi, err := os.Stat(filepath.Join(root, "go.mod"))
return err == nil && !fi.IsDir(), nil
}
+
+// A versionRepo is a subset of modfetch.Repo that can report information about
+// available versions, but cannot fetch specific source files.
+type versionRepo interface {
+ ModulePath() string
+ Versions(prefix string) ([]string, error)
+ Stat(rev string) (*modfetch.RevInfo, error)
+ Latest() (*modfetch.RevInfo, error)
+}
+
+var _ versionRepo = modfetch.Repo(nil)
+
+func lookupRepo(proxy, path string) (repo versionRepo, err error) {
+ err = module.CheckPath(path)
+ if err == nil {
+ repo = modfetch.Lookup(proxy, path)
+ } else {
+ repo = emptyRepo{path: path, err: err}
+ }
+
+ if index == nil {
+ return repo, err
+ }
+ if _, ok := index.highestReplaced[path]; !ok {
+ return repo, err
+ }
+
+ return &replacementRepo{repo: repo}, nil
+}
+
+// An emptyRepo is a versionRepo that contains no versions.
+type emptyRepo struct {
+ path string
+ err error
+}
+
+var _ versionRepo = emptyRepo{}
+
+func (er emptyRepo) ModulePath() string { return er.path }
+func (er emptyRepo) Versions(prefix string) ([]string, error) { return nil, nil }
+func (er emptyRepo) Stat(rev string) (*modfetch.RevInfo, error) { return nil, er.err }
+func (er emptyRepo) Latest() (*modfetch.RevInfo, error) { return nil, er.err }
+
+// A replacementRepo augments a versionRepo to include the replacement versions
+// (if any) found in the main module's go.mod file.
+//
+// A replacementRepo suppresses "not found" errors for otherwise-nonexistent
+// modules, so a replacementRepo should only be constructed for a module that
+// actually has one or more valid replacements.
+type replacementRepo struct {
+ repo versionRepo
+}
+
+var _ versionRepo = (*replacementRepo)(nil)
+
+func (rr *replacementRepo) ModulePath() string { return rr.repo.ModulePath() }
+
+// Versions returns the versions from rr.repo augmented with any matching
+// replacement versions.
+func (rr *replacementRepo) Versions(prefix string) ([]string, error) {
+ repoVersions, err := rr.repo.Versions(prefix)
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return nil, err
+ }
+
+ versions := repoVersions
+ if index != nil && len(index.replace) > 0 {
+ path := rr.ModulePath()
+ for m, _ := range index.replace {
+ if m.Path == path && strings.HasPrefix(m.Version, prefix) && m.Version != "" && !modfetch.IsPseudoVersion(m.Version) {
+ versions = append(versions, m.Version)
+ }
+ }
+ }
+
+ if len(versions) == len(repoVersions) { // No replacement versions added.
+ return versions, nil
+ }
+
+ sort.Slice(versions, func(i, j int) bool {
+ return semver.Compare(versions[i], versions[j]) < 0
+ })
+ uniq := versions[:1]
+ for _, v := range versions {
+ if v != uniq[len(uniq)-1] {
+ uniq = append(uniq, v)
+ }
+ }
+ return uniq, nil
+}
+
+func (rr *replacementRepo) Stat(rev string) (*modfetch.RevInfo, error) {
+ info, err := rr.repo.Stat(rev)
+ if err == nil || index == nil || len(index.replace) == 0 {
+ return info, err
+ }
+
+ v := module.CanonicalVersion(rev)
+ if v != rev {
+ // The replacements in the go.mod file list only canonical semantic versions,
+ // so a non-canonical version can't possibly have a replacement.
+ return info, err
+ }
+
+ path := rr.ModulePath()
+ _, pathMajor, ok := module.SplitPathVersion(path)
+ if ok && pathMajor == "" {
+ if err := module.CheckPathMajor(v, pathMajor); err != nil && semver.Build(v) == "" {
+ v += "+incompatible"
+ }
+ }
+
+ if r := Replacement(module.Version{Path: path, Version: v}); r.Path == "" {
+ return info, err
+ }
+ return rr.replacementStat(v)
+}
+
+func (rr *replacementRepo) Latest() (*modfetch.RevInfo, error) {
+ info, err := rr.repo.Latest()
+
+ if index != nil {
+ path := rr.ModulePath()
+ if v, ok := index.highestReplaced[path]; ok {
+ if v == "" {
+ // The only replacement is a wildcard that doesn't specify a version, so
+ // synthesize a pseudo-version with an appropriate major version and a
+ // timestamp below any real timestamp. That way, if the main module is
+ // used from within some other module, the user will be able to upgrade
+ // the requirement to any real version they choose.
+ if _, pathMajor, ok := module.SplitPathVersion(path); ok && len(pathMajor) > 0 {
+ v = modfetch.PseudoVersion(pathMajor[1:], "", time.Time{}, "000000000000")
+ } else {
+ v = modfetch.PseudoVersion("v0", "", time.Time{}, "000000000000")
+ }
+ }
+
+ if err != nil || semver.Compare(v, info.Version) > 0 {
+ return rr.replacementStat(v)
+ }
+ }
+ }
+
+ return info, err
+}
+
+func (rr *replacementRepo) replacementStat(v string) (*modfetch.RevInfo, error) {
+ rev := &modfetch.RevInfo{Version: v}
+ if modfetch.IsPseudoVersion(v) {
+ rev.Time, _ = modfetch.PseudoVersionTime(v)
+ rev.Short, _ = modfetch.PseudoVersionRev(v)
+ }
+ return rev, nil
+}
diff --git a/src/cmd/go/internal/modload/query_test.go b/src/cmd/go/internal/modload/query_test.go
index 351826f2ab..777a56b977 100644
--- a/src/cmd/go/internal/modload/query_test.go
+++ b/src/cmd/go/internal/modload/query_test.go
@@ -45,7 +45,7 @@ var (
queryRepoV3 = queryRepo + "/v3"
// Empty version list (no semver tags), not actually empty.
- emptyRepo = "vcs-test.golang.org/git/emptytest.git"
+ emptyRepoPath = "vcs-test.golang.org/git/emptytest.git"
)
var queryTests = []struct {
@@ -121,14 +121,14 @@ var queryTests = []struct {
{path: queryRepo, query: "upgrade", current: "v1.9.10-pre2+metadata", vers: "v1.9.10-pre2.0.20190513201126-42abcb6df8ee"},
{path: queryRepo, query: "upgrade", current: "v0.0.0-20190513201126-42abcb6df8ee", vers: "v0.0.0-20190513201126-42abcb6df8ee"},
{path: queryRepo, query: "upgrade", allow: "NOMATCH", err: `no matching versions for query "upgrade"`},
- {path: queryRepo, query: "upgrade", current: "v1.9.9", allow: "NOMATCH", err: `no matching versions for query "upgrade" (current version is v1.9.9)`},
+ {path: queryRepo, query: "upgrade", current: "v1.9.9", allow: "NOMATCH", err: `vcs-test.golang.org/git/querytest.git@v1.9.9: disallowed module version`},
{path: queryRepo, query: "upgrade", current: "v1.99.99", err: `vcs-test.golang.org/git/querytest.git@v1.99.99: invalid version: unknown revision v1.99.99`},
{path: queryRepo, query: "patch", current: "", vers: "v1.9.9"},
{path: queryRepo, query: "patch", current: "v0.1.0", vers: "v0.1.2"},
{path: queryRepo, query: "patch", current: "v1.9.0", vers: "v1.9.9"},
{path: queryRepo, query: "patch", current: "v1.9.10-pre1", vers: "v1.9.10-pre1"},
{path: queryRepo, query: "patch", current: "v1.9.10-pre2+metadata", vers: "v1.9.10-pre2.0.20190513201126-42abcb6df8ee"},
- {path: queryRepo, query: "patch", current: "v1.99.99", err: `no matching versions for query "patch" (current version is v1.99.99)`},
+ {path: queryRepo, query: "patch", current: "v1.99.99", err: `vcs-test.golang.org/git/querytest.git@v1.99.99: invalid version: unknown revision v1.99.99`},
{path: queryRepo, query: ">v1.9.9", vers: "v1.9.10-pre1"},
{path: queryRepo, query: ">v1.10.0", err: `no matching versions for query ">v1.10.0"`},
{path: queryRepo, query: ">=v1.10.0", err: `no matching versions for query ">=v1.10.0"`},
@@ -171,9 +171,9 @@ var queryTests = []struct {
// That should prevent us from resolving any version for the /v3 path.
{path: queryRepoV3, query: "latest", err: `no matching versions for query "latest"`},
- {path: emptyRepo, query: "latest", vers: "v0.0.0-20180704023549-7bb914627242"},
- {path: emptyRepo, query: ">v0.0.0", err: `no matching versions for query ">v0.0.0"`},
- {path: emptyRepo, query: "<v10.0.0", err: `no matching versions for query "<v10.0.0"`},
+ {path: emptyRepoPath, query: "latest", vers: "v0.0.0-20180704023549-7bb914627242"},
+ {path: emptyRepoPath, query: ">v0.0.0", err: `no matching versions for query ">v0.0.0"`},
+ {path: emptyRepoPath, query: "<v10.0.0", err: `no matching versions for query "<v10.0.0"`},
}
func TestQuery(t *testing.T) {
@@ -189,7 +189,7 @@ func TestQuery(t *testing.T) {
}
allowed := func(ctx context.Context, m module.Version) error {
if ok, _ := path.Match(allow, m.Version); !ok {
- return ErrDisallowed
+ return module.VersionError(m, ErrDisallowed)
}
return nil
}
@@ -200,17 +200,17 @@ func TestQuery(t *testing.T) {
info, err := Query(ctx, tt.path, tt.query, tt.current, allowed)
if tt.err != "" {
if err == nil {
- t.Errorf("Query(%q, %q, %v) = %v, want error %q", tt.path, tt.query, allow, info.Version, tt.err)
+ t.Errorf("Query(_, %q, %q, %q, %v) = %v, want error %q", tt.path, tt.query, tt.current, allow, info.Version, tt.err)
} else if err.Error() != tt.err {
- t.Errorf("Query(%q, %q, %v): %v, want error %q", tt.path, tt.query, allow, err, tt.err)
+ t.Errorf("Query(_, %q, %q, %q, %v): %v\nwant error %q", tt.path, tt.query, tt.current, allow, err, tt.err)
}
return
}
if err != nil {
- t.Fatalf("Query(%q, %q, %v): %v", tt.path, tt.query, allow, err)
+ t.Fatalf("Query(_, %q, %q, %q, %v): %v\nwant %v", tt.path, tt.query, tt.current, allow, err, tt.vers)
}
if info.Version != tt.vers {
- t.Errorf("Query(%q, %q, %v) = %v, want %v", tt.path, tt.query, allow, info.Version, tt.vers)
+ t.Errorf("Query(_, %q, %q, %q, %v) = %v, want %v", tt.path, tt.query, tt.current, allow, info.Version, tt.vers)
}
})
}
diff --git a/src/cmd/go/internal/modload/search.go b/src/cmd/go/internal/modload/search.go
index a9bee0af4e..f6d6f5f764 100644
--- a/src/cmd/go/internal/modload/search.go
+++ b/src/cmd/go/internal/modload/search.go
@@ -7,11 +7,13 @@ package modload
import (
"context"
"fmt"
+ "io/fs"
"os"
"path/filepath"
"strings"
"cmd/go/internal/cfg"
+ "cmd/go/internal/fsys"
"cmd/go/internal/imports"
"cmd/go/internal/search"
@@ -53,7 +55,7 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
walkPkgs := func(root, importPathRoot string, prune pruning) {
root = filepath.Clean(root)
- err := filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
+ err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error {
if err != nil {
m.AddError(err)
return nil
@@ -84,8 +86,8 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
}
if !fi.IsDir() {
- if fi.Mode()&os.ModeSymlink != 0 && want {
- if target, err := os.Stat(path); err == nil && target.IsDir() {
+ if fi.Mode()&fs.ModeSymlink != 0 && want {
+ if target, err := fsys.Stat(path); err == nil && target.IsDir() {
fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
}
}
@@ -154,7 +156,8 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
isLocal = true
} else {
var err error
- root, isLocal, err = fetch(ctx, mod)
+ needSum := true
+ root, isLocal, err = fetch(ctx, mod, needSum)
if err != nil {
m.AddError(err)
continue
diff --git a/src/cmd/go/internal/modload/stat_openfile.go b/src/cmd/go/internal/modload/stat_openfile.go
index 931aaf1577..5842b858f0 100644
--- a/src/cmd/go/internal/modload/stat_openfile.go
+++ b/src/cmd/go/internal/modload/stat_openfile.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build aix js,wasm plan9
+// +build js,wasm plan9
// On plan9, per http://9p.io/magic/man2html/2/access: “Since file permissions
// are checked by the server and group information is not known to the client,
@@ -13,12 +13,13 @@
package modload
import (
+ "io/fs"
"os"
)
// hasWritePerm reports whether the current user has permission to write to the
// file with the given info.
-func hasWritePerm(path string, _ os.FileInfo) bool {
+func hasWritePerm(path string, _ fs.FileInfo) bool {
if f, err := os.OpenFile(path, os.O_WRONLY, 0); err == nil {
f.Close()
return true
diff --git a/src/cmd/go/internal/modload/stat_unix.go b/src/cmd/go/internal/modload/stat_unix.go
index ea3b801f2c..f49278ec3a 100644
--- a/src/cmd/go/internal/modload/stat_unix.go
+++ b/src/cmd/go/internal/modload/stat_unix.go
@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package modload
import (
+ "io/fs"
"os"
"syscall"
)
@@ -17,7 +18,7 @@ import (
// Although the root user on most Unix systems can write to files even without
// permission, hasWritePerm reports false if no appropriate permission bit is
// set even if the current user is root.
-func hasWritePerm(path string, fi os.FileInfo) bool {
+func hasWritePerm(path string, fi fs.FileInfo) bool {
if os.Getuid() == 0 {
// The root user can access any file, but we still want to default to
// read-only mode if the go.mod file is marked as globally non-writable.
diff --git a/src/cmd/go/internal/modload/stat_windows.go b/src/cmd/go/internal/modload/stat_windows.go
index d7826cfc6b..0ac2391347 100644
--- a/src/cmd/go/internal/modload/stat_windows.go
+++ b/src/cmd/go/internal/modload/stat_windows.go
@@ -6,13 +6,11 @@
package modload
-import (
- "os"
-)
+import "io/fs"
// hasWritePerm reports whether the current user has permission to write to the
// file with the given info.
-func hasWritePerm(_ string, fi os.FileInfo) bool {
+func hasWritePerm(_ string, fi fs.FileInfo) bool {
// Windows has a read-only attribute independent of ACLs, so use that to
// determine whether the file is intended to be overwritten.
//
diff --git a/src/cmd/go/internal/modload/testgo.go b/src/cmd/go/internal/modload/testgo.go
deleted file mode 100644
index 6b34f5be39..0000000000
--- a/src/cmd/go/internal/modload/testgo.go
+++ /dev/null
@@ -1,11 +0,0 @@
-// 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.
-
-// +build testgo
-
-package modload
-
-func init() {
- printStackInDie = true
-}
diff --git a/src/cmd/go/internal/modload/vendor.go b/src/cmd/go/internal/modload/vendor.go
index 9f34b829fc..ab29d4d014 100644
--- a/src/cmd/go/internal/modload/vendor.go
+++ b/src/cmd/go/internal/modload/vendor.go
@@ -7,8 +7,8 @@ package modload
import (
"errors"
"fmt"
+ "io/fs"
"io/ioutil"
- "os"
"path/filepath"
"strings"
"sync"
@@ -42,7 +42,7 @@ func readVendorList() {
vendorMeta = make(map[module.Version]vendorMetadata)
data, err := ioutil.ReadFile(filepath.Join(ModRoot(), "vendor/modules.txt"))
if err != nil {
- if !errors.Is(err, os.ErrNotExist) {
+ if !errors.Is(err, fs.ErrNotExist) {
base.Fatalf("go: %s", err)
}
return