Commit c7385e27 authored by Bryan C. Mills's avatar Bryan C. Mills Committed by Russ Cox

cmd/go: when resolving packages, try all module paths before falling back to the next proxy

Since we're mucking with error-propagation in modload.Query* anyway,
simplify the classification logic. Ensure that “module not found”
errors are reported as such in various places, since non-“not found”
errors terminate the module search.

Fixes #31785

Change-Id: Ie3ca5f4eec10a5f2a6037ec7e1c2cf47bd37a232
Reviewed-on: https://go-review.googlesource.com/c/go/+/177958
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarRuss Cox <rsc@golang.org>
parent 496b8dbb
...@@ -28,7 +28,7 @@ func TestMain(m *testing.M) { ...@@ -28,7 +28,7 @@ func TestMain(m *testing.M) {
} }
func testMain(m *testing.M) int { func testMain(m *testing.M) int {
modfetch.SetProxy("direct") cfg.GOPROXY = "direct"
if _, err := exec.LookPath("git"); err != nil { if _, err := exec.LookPath("git"); err != nil {
fmt.Fprintln(os.Stderr, "skipping because git binary not found") fmt.Fprintln(os.Stderr, "skipping because git binary not found")
......
...@@ -258,12 +258,12 @@ func (r *cachingRepo) Zip(dst io.Writer, version string) error { ...@@ -258,12 +258,12 @@ func (r *cachingRepo) Zip(dst io.Writer, version string) error {
// Stat is like Lookup(path).Stat(rev) but avoids the // Stat is like Lookup(path).Stat(rev) but avoids the
// repository path resolution in Lookup if the result is // repository path resolution in Lookup if the result is
// already cached on local disk. // already cached on local disk.
func Stat(path, rev string) (*RevInfo, error) { func Stat(proxy, path, rev string) (*RevInfo, error) {
_, info, err := readDiskStat(path, rev) _, info, err := readDiskStat(path, rev)
if err == nil { if err == nil {
return info, nil return info, nil
} }
repo, err := Lookup(path) repo, err := Lookup(proxy, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -276,9 +276,22 @@ func InfoFile(path, version string) (string, error) { ...@@ -276,9 +276,22 @@ func InfoFile(path, version string) (string, error) {
if !semver.IsValid(version) { if !semver.IsValid(version) {
return "", fmt.Errorf("invalid version %q", version) return "", fmt.Errorf("invalid version %q", version)
} }
if _, err := Stat(path, version); err != nil {
if file, _, err := readDiskStat(path, version); err == nil {
return file, nil
}
err := TryProxies(func(proxy string) error {
repo, err := Lookup(proxy, path)
if err == nil {
_, err = repo.Stat(version)
}
return err
})
if err != nil {
return "", err return "", err
} }
// Stat should have populated the disk cache for us. // Stat should have populated the disk cache for us.
file, _, err := readDiskStat(path, version) file, _, err := readDiskStat(path, version)
if err != nil { if err != nil {
...@@ -294,21 +307,39 @@ func GoMod(path, rev string) ([]byte, error) { ...@@ -294,21 +307,39 @@ func GoMod(path, rev string) ([]byte, error) {
// Convert commit hash to pseudo-version // Convert commit hash to pseudo-version
// to increase cache hit rate. // to increase cache hit rate.
if !semver.IsValid(rev) { if !semver.IsValid(rev) {
info, err := Stat(path, rev) if _, info, err := readDiskStat(path, rev); err == nil {
if err != nil { rev = info.Version
return nil, err } else {
err := TryProxies(func(proxy string) error {
repo, err := Lookup(proxy, path)
if err != nil {
return err
}
info, err := repo.Stat(rev)
if err == nil {
rev = info.Version
}
return err
})
if err != nil {
return nil, err
}
} }
rev = info.Version
} }
_, data, err := readDiskGoMod(path, rev) _, data, err := readDiskGoMod(path, rev)
if err == nil { if err == nil {
return data, nil return data, nil
} }
repo, err := Lookup(path)
if err != nil { err = TryProxies(func(proxy string) error {
return nil, err repo, err := Lookup(proxy, path)
} if err == nil {
return repo.GoMod(rev) data, err = repo.GoMod(rev)
}
return err
})
return data, err
} }
// GoModFile is like GoMod but returns the name of the file containing // GoModFile is like GoMod but returns the name of the file containing
......
...@@ -25,7 +25,7 @@ func TestMain(m *testing.M) { ...@@ -25,7 +25,7 @@ func TestMain(m *testing.M) {
} }
func testMain(m *testing.M) int { func testMain(m *testing.M) int {
SetProxy("direct") cfg.GOPROXY = "direct"
// The sum database is populated using a released version of the go command, // The sum database is populated using a released version of the go command,
// but this test may include fixes for additional modules that previously // but this test may include fixes for additional modules that previously
...@@ -360,7 +360,7 @@ func TestCodeRepo(t *testing.T) { ...@@ -360,7 +360,7 @@ func TestCodeRepo(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
t.Parallel() t.Parallel()
repo, err := Lookup(tt.path) repo, err := Lookup("direct", tt.path)
if tt.lookerr != "" { if tt.lookerr != "" {
if err != nil && err.Error() == tt.lookerr { if err != nil && err.Error() == tt.lookerr {
return return
...@@ -561,7 +561,7 @@ func TestCodeRepoVersions(t *testing.T) { ...@@ -561,7 +561,7 @@ func TestCodeRepoVersions(t *testing.T) {
tt := tt tt := tt
t.Parallel() t.Parallel()
repo, err := Lookup(tt.path) repo, err := Lookup("direct", tt.path)
if err != nil { if err != nil {
t.Fatalf("Lookup(%q): %v", tt.path, err) t.Fatalf("Lookup(%q): %v", tt.path, err)
} }
...@@ -616,7 +616,7 @@ func TestLatest(t *testing.T) { ...@@ -616,7 +616,7 @@ func TestLatest(t *testing.T) {
tt := tt tt := tt
t.Parallel() t.Parallel()
repo, err := Lookup(tt.path) repo, err := Lookup("direct", tt.path)
if err != nil { if err != nil {
t.Fatalf("Lookup(%q): %v", tt.path, err) t.Fatalf("Lookup(%q): %v", tt.path, err)
} }
......
...@@ -205,13 +205,16 @@ func downloadZip(mod module.Version, zipfile string) (err error) { ...@@ -205,13 +205,16 @@ func downloadZip(mod module.Version, zipfile string) (err error) {
} }
}() }()
repo, err := Lookup(mod.Path) err = TryProxies(func(proxy string) error {
repo, err := Lookup(proxy, mod.Path)
if err != nil {
return err
}
return repo.Zip(f, mod.Version)
})
if err != nil { if err != nil {
return err return err
} }
if err := repo.Zip(f, mod.Version); err != nil {
return err
}
// Double-check that the paths within the zip file are well-formed. // Double-check that the paths within the zip file are well-formed.
// //
......
...@@ -84,16 +84,6 @@ cached module versions with GOPROXY=https://example.com/proxy. ...@@ -84,16 +84,6 @@ cached module versions with GOPROXY=https://example.com/proxy.
`, `,
} }
var proxyURL = cfg.Getenv("GOPROXY")
// SetProxy sets the proxy to use when fetching modules.
// It accepts the same syntax as the GOPROXY environment variable,
// which also provides its default configuration.
// SetProxy must not be called after the first module fetch has begun.
func SetProxy(url string) {
proxyURL = url
}
var proxyOnce struct { var proxyOnce struct {
sync.Once sync.Once
list []string list []string
...@@ -102,13 +92,25 @@ var proxyOnce struct { ...@@ -102,13 +92,25 @@ var proxyOnce struct {
func proxyURLs() ([]string, error) { func proxyURLs() ([]string, error) {
proxyOnce.Do(func() { proxyOnce.Do(func() {
for _, proxyURL := range strings.Split(proxyURL, ",") { if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" {
proxyOnce.list = append(proxyOnce.list, "noproxy")
}
for _, proxyURL := range strings.Split(cfg.GOPROXY, ",") {
proxyURL = strings.TrimSpace(proxyURL)
if proxyURL == "" { if proxyURL == "" {
continue continue
} }
if proxyURL == "off" {
// "off" always fails hard, so can stop walking list.
proxyOnce.list = append(proxyOnce.list, "off")
break
}
if proxyURL == "direct" { if proxyURL == "direct" {
proxyOnce.list = append(proxyOnce.list, "direct") proxyOnce.list = append(proxyOnce.list, "direct")
continue // For now, "direct" is the end of the line. We may decide to add some
// sort of fallback behavior for them in the future, so ignore
// subsequent entries for forward-compatibility.
break
} }
// Check that newProxyRepo accepts the URL. // Check that newProxyRepo accepts the URL.
...@@ -125,32 +127,30 @@ func proxyURLs() ([]string, error) { ...@@ -125,32 +127,30 @@ func proxyURLs() ([]string, error) {
return proxyOnce.list, proxyOnce.err return proxyOnce.list, proxyOnce.err
} }
func lookupProxy(path string) (Repo, error) { // TryProxies iterates f over each configured proxy (including "noproxy" and
list, err := proxyURLs() // "direct" if applicable) until f returns an error that is not
// equivalent to os.ErrNotExist.
//
// TryProxies then returns that final error.
//
// If GOPROXY is set to "off", TryProxies invokes f once with the argument
// "off".
func TryProxies(f func(proxy string) error) error {
proxies, err := proxyURLs()
if err != nil { if err != nil {
return nil, err return err
}
if len(proxies) == 0 {
return f("off")
} }
var repos listRepo for _, proxy := range proxies {
for _, u := range list { err = f(proxy)
var r Repo if !errors.Is(err, os.ErrNotExist) {
if u == "direct" { break
// lookupDirect does actual network traffic.
// Especially if GOPROXY="http://mainproxy,direct",
// avoid the network until we need it by using a lazyRepo wrapper.
r = &lazyRepo{setup: lookupDirect, path: path}
} else {
// The URL itself was checked in proxyURLs.
// The only possible error here is a bad path,
// so we can return it unconditionally.
r, err = newProxyRepo(u, path)
if err != nil {
return nil, err
}
} }
repos = append(repos, r)
} }
return repos, nil return err
} }
type proxyRepo struct { type proxyRepo struct {
...@@ -342,117 +342,3 @@ func (p *proxyRepo) Zip(dst io.Writer, version string) error { ...@@ -342,117 +342,3 @@ func (p *proxyRepo) Zip(dst io.Writer, version string) error {
func pathEscape(s string) string { func pathEscape(s string) string {
return strings.ReplaceAll(url.PathEscape(s), "%2F", "/") return strings.ReplaceAll(url.PathEscape(s), "%2F", "/")
} }
// A lazyRepo is a lazily-initialized Repo,
// constructed on demand by calling setup.
type lazyRepo struct {
path string
setup func(string) (Repo, error)
once sync.Once
repo Repo
err error
}
func (r *lazyRepo) init() {
r.repo, r.err = r.setup(r.path)
}
func (r *lazyRepo) ModulePath() string {
return r.path
}
func (r *lazyRepo) Versions(prefix string) ([]string, error) {
if r.once.Do(r.init); r.err != nil {
return nil, r.err
}
return r.repo.Versions(prefix)
}
func (r *lazyRepo) Stat(rev string) (*RevInfo, error) {
if r.once.Do(r.init); r.err != nil {
return nil, r.err
}
return r.repo.Stat(rev)
}
func (r *lazyRepo) Latest() (*RevInfo, error) {
if r.once.Do(r.init); r.err != nil {
return nil, r.err
}
return r.repo.Latest()
}
func (r *lazyRepo) GoMod(version string) ([]byte, error) {
if r.once.Do(r.init); r.err != nil {
return nil, r.err
}
return r.repo.GoMod(version)
}
func (r *lazyRepo) Zip(dst io.Writer, version string) error {
if r.once.Do(r.init); r.err != nil {
return r.err
}
return r.repo.Zip(dst, version)
}
// A listRepo is a preference list of Repos.
// The list must be non-empty and all Repos
// must return the same result from ModulePath.
// For each method, the repos are tried in order
// until one succeeds or returns a non-ErrNotExist (non-404) error.
type listRepo []Repo
func (l listRepo) ModulePath() string {
return l[0].ModulePath()
}
func (l listRepo) Versions(prefix string) ([]string, error) {
for i, r := range l {
v, err := r.Versions(prefix)
if i == len(l)-1 || !errors.Is(err, os.ErrNotExist) {
return v, err
}
}
panic("no repos")
}
func (l listRepo) Stat(rev string) (*RevInfo, error) {
for i, r := range l {
info, err := r.Stat(rev)
if i == len(l)-1 || !errors.Is(err, os.ErrNotExist) {
return info, err
}
}
panic("no repos")
}
func (l listRepo) Latest() (*RevInfo, error) {
for i, r := range l {
info, err := r.Latest()
if i == len(l)-1 || !errors.Is(err, os.ErrNotExist) {
return info, err
}
}
panic("no repos")
}
func (l listRepo) GoMod(version string) ([]byte, error) {
for i, r := range l {
data, err := r.GoMod(version)
if i == len(l)-1 || !errors.Is(err, os.ErrNotExist) {
return data, err
}
}
panic("no repos")
}
func (l listRepo) Zip(dst io.Writer, version string) error {
for i, r := range l {
err := r.Zip(dst, version)
if i == len(l)-1 || !errors.Is(err, os.ErrNotExist) {
return err
}
}
panic("no repos")
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package modfetch package modfetch
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
...@@ -172,20 +173,32 @@ type RevInfo struct { ...@@ -172,20 +173,32 @@ type RevInfo struct {
var lookupCache par.Cache var lookupCache par.Cache
// Lookup returns the module with the given module path. type lookupCacheKey struct {
proxy, path string
}
// Lookup returns the module with the given module path,
// fetched through the given proxy.
//
// The distinguished proxy "direct" indicates that the path should be fetched
// from its origin, and "noproxy" indicates that the patch should be fetched
// directly only if GONOPROXY matches the given path.
//
// For the distinguished proxy "off", Lookup always returns a non-nil error.
//
// A successful return does not guarantee that the module // A successful return does not guarantee that the module
// has any defined versions. // has any defined versions.
func Lookup(path string) (Repo, error) { func Lookup(proxy, path string) (Repo, error) {
if traceRepo { if traceRepo {
defer logCall("Lookup(%q)", path)() defer logCall("Lookup(%q, %q)", proxy, path)()
} }
type cached struct { type cached struct {
r Repo r Repo
err error err error
} }
c := lookupCache.Do(path, func() interface{} { c := lookupCache.Do(lookupCacheKey{proxy, path}, func() interface{} {
r, err := lookup(path) r, err := lookup(proxy, path)
if err == nil { if err == nil {
if traceRepo { if traceRepo {
r = newLoggingRepo(r) r = newLoggingRepo(r)
...@@ -199,19 +212,39 @@ func Lookup(path string) (Repo, error) { ...@@ -199,19 +212,39 @@ func Lookup(path string) (Repo, error) {
} }
// lookup returns the module with the given module path. // lookup returns the module with the given module path.
func lookup(path string) (r Repo, err error) { func lookup(proxy, path string) (r Repo, err error) {
if cfg.BuildMod == "vendor" { if cfg.BuildMod == "vendor" {
return nil, fmt.Errorf("module lookup disabled by -mod=%s", cfg.BuildMod) return nil, errModVendor
} }
if proxyURL == "off" {
return nil, fmt.Errorf("module lookup disabled by GOPROXY=%s", proxyURL) if str.GlobsMatchPath(cfg.GONOPROXY, path) {
switch proxy {
case "noproxy", "direct":
return lookupDirect(path)
default:
return nil, errNoproxy
}
} }
if proxyURL != "" && proxyURL != "direct" && !str.GlobsMatchPath(cfg.GONOPROXY, path) {
return lookupProxy(path) switch proxy {
case "off":
return nil, errProxyOff
case "direct":
return lookupDirect(path)
case "noproxy":
return nil, errUseProxy
default:
return newProxyRepo(proxy, path)
} }
return lookupDirect(path)
} }
var (
errModVendor = errors.New("module lookup disabled by -mod=vendor")
errProxyOff = errors.New("module lookup disabled by GOPROXY=off")
errNoproxy error = notExistError("disabled by GONOPROXY")
errUseProxy error = notExistError("path does not match GONOPROXY")
)
func lookupDirect(path string) (Repo, error) { func lookupDirect(path string) (Repo, error) {
security := web.SecureOnly security := web.SecureOnly
if get.Insecure { if get.Insecure {
...@@ -220,7 +253,7 @@ func lookupDirect(path string) (Repo, error) { ...@@ -220,7 +253,7 @@ func lookupDirect(path string) (Repo, error) {
rr, err := get.RepoRootForImportPath(path, get.PreferMod, security) rr, err := get.RepoRootForImportPath(path, get.PreferMod, security)
if err != nil { if err != nil {
// We don't know where to find code for a module with this path. // We don't know where to find code for a module with this path.
return nil, err return nil, notExistError(err.Error())
} }
if rr.VCS == "mod" { if rr.VCS == "mod" {
...@@ -362,3 +395,13 @@ func (l *loggingRepo) Zip(dst io.Writer, version string) error { ...@@ -362,3 +395,13 @@ func (l *loggingRepo) Zip(dst io.Writer, version string) error {
defer logCall("Repo[%s]: Zip(%s, %q)", l.r.ModulePath(), dstName, version)() defer logCall("Repo[%s]: Zip(%s, %q)", l.r.ModulePath(), dstName, version)()
return l.r.Zip(dst, version) return l.r.Zip(dst, version)
} }
// A notExistError is like os.ErrNotExist, but with a custom message
type notExistError string
func (e notExistError) Error() string {
return string(e)
}
func (notExistError) Is(target error) bool {
return target == os.ErrNotExist
}
...@@ -142,6 +142,9 @@ func (c *dbClient) initBase() { ...@@ -142,6 +142,9 @@ func (c *dbClient) initBase() {
return return
} }
for _, proxyURL := range urls { for _, proxyURL := range urls {
if proxyURL == "noproxy" {
continue
}
if proxyURL == "direct" { if proxyURL == "direct" {
break break
} }
......
...@@ -18,7 +18,6 @@ import ( ...@@ -18,7 +18,6 @@ import (
"cmd/go/internal/cfg" "cmd/go/internal/cfg"
"cmd/go/internal/modfetch" "cmd/go/internal/modfetch"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/module" "cmd/go/internal/module"
"cmd/go/internal/par" "cmd/go/internal/par"
"cmd/go/internal/search" "cmd/go/internal/search"
...@@ -188,10 +187,13 @@ func Import(path string) (m module.Version, dir string, err error) { ...@@ -188,10 +187,13 @@ func Import(path string) (m module.Version, dir string, err error) {
candidates, err := QueryPackage(path, "latest", Allowed) candidates, err := QueryPackage(path, "latest", Allowed)
if err != nil { if err != nil {
if _, ok := err.(*codehost.VCSError); ok { if errors.Is(err, os.ErrNotExist) {
// Return "cannot find module providing package […]" instead of whatever
// low-level error QueryPackage produced.
return module.Version{}, "", &ImportMissingError{ImportPath: path}
} else {
return module.Version{}, "", err return module.Version{}, "", err
} }
return module.Version{}, "", &ImportMissingError{ImportPath: path}
} }
m = candidates[0].Mod m = candidates[0].Mod
newMissingVersion := "" newMissingVersion := ""
......
...@@ -21,7 +21,7 @@ var importTests = []struct { ...@@ -21,7 +21,7 @@ var importTests = []struct {
}, },
{ {
path: "golang.org/x/net", path: "golang.org/x/net",
err: "cannot find module providing package golang.org/x/net", err: "module golang.org/x/net@.* found, but does not contain package golang.org/x/net",
}, },
{ {
path: "golang.org/x/text", path: "golang.org/x/text",
......
...@@ -1208,11 +1208,15 @@ func (*mvsReqs) Upgrade(m module.Version) (module.Version, error) { ...@@ -1208,11 +1208,15 @@ func (*mvsReqs) Upgrade(m module.Version) (module.Version, error) {
func versions(path string) ([]string, error) { func versions(path string) ([]string, error) {
// Note: modfetch.Lookup and repo.Versions are cached, // Note: modfetch.Lookup and repo.Versions are cached,
// so there's no need for us to add extra caching here. // so there's no need for us to add extra caching here.
repo, err := modfetch.Lookup(path) var versions []string
if err != nil { err := modfetch.TryProxies(func(proxy string) error {
return nil, err repo, err := modfetch.Lookup(proxy, path)
} if err == nil {
return repo.Versions("") versions, err = repo.Versions("")
}
return err
})
return versions, err
} }
// Previous returns the tagged version of m.Path immediately prior to // Previous returns the tagged version of m.Path immediately prior to
......
...@@ -5,13 +5,14 @@ ...@@ -5,13 +5,14 @@
package modload package modload
import ( import (
"errors"
"fmt" "fmt"
"os"
pathpkg "path" pathpkg "path"
"strings" "strings"
"sync" "sync"
"cmd/go/internal/modfetch" "cmd/go/internal/modfetch"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/module" "cmd/go/internal/module"
"cmd/go/internal/search" "cmd/go/internal/search"
"cmd/go/internal/semver" "cmd/go/internal/semver"
...@@ -38,6 +39,15 @@ import ( ...@@ -38,6 +39,15 @@ import (
// If path is the path of the main module and the query is "latest", // If path is the path of the main module and the query is "latest",
// Query returns Target.Version as the version. // Query returns Target.Version as the version.
func Query(path, query string, allowed func(module.Version) bool) (*modfetch.RevInfo, error) { func Query(path, query string, allowed func(module.Version) bool) (*modfetch.RevInfo, error) {
var info *modfetch.RevInfo
err := modfetch.TryProxies(func(proxy string) (err error) {
info, err = queryProxy(proxy, path, query, allowed)
return err
})
return info, err
}
func queryProxy(proxy, path, query string, allowed func(module.Version) bool) (*modfetch.RevInfo, error) {
if allowed == nil { if allowed == nil {
allowed = func(module.Version) bool { return true } allowed = func(module.Version) bool { return true }
} }
...@@ -112,14 +122,14 @@ func Query(path, query string, allowed func(module.Version) bool) (*modfetch.Rev ...@@ -112,14 +122,14 @@ func Query(path, query string, allowed func(module.Version) bool) (*modfetch.Rev
// If the identifier is not a canonical semver tag — including if it's a // 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 // semver tag with a +metadata suffix — then modfetch.Stat will populate
// info.Version with a suitable pseudo-version. // info.Version with a suitable pseudo-version.
info, err := modfetch.Stat(path, query) info, err := modfetch.Stat(proxy, path, query)
if err != nil { if err != nil {
queryErr := err queryErr := err
// The full query doesn't correspond to a tag. If it is a semantic version // 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: // with a +metadata suffix, see if there is a tag without that suffix:
// semantic versioning defines them to be equivalent. // semantic versioning defines them to be equivalent.
if vers := module.CanonicalVersion(query); vers != "" && vers != query { if vers := module.CanonicalVersion(query); vers != "" && vers != query {
info, err = modfetch.Stat(path, vers) info, err = modfetch.Stat(proxy, path, vers)
} }
if err != nil { if err != nil {
return nil, queryErr return nil, queryErr
...@@ -146,7 +156,7 @@ func Query(path, query string, allowed func(module.Version) bool) (*modfetch.Rev ...@@ -146,7 +156,7 @@ func Query(path, query string, allowed func(module.Version) bool) (*modfetch.Rev
} }
// Load versions and execute query. // Load versions and execute query.
repo, err := modfetch.Lookup(path) repo, err := modfetch.Lookup(proxy, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -248,7 +258,7 @@ func QueryPackage(path, query string, allowed func(module.Version) bool) ([]Quer ...@@ -248,7 +258,7 @@ func QueryPackage(path, query string, allowed func(module.Version) bool) ([]Quer
// If any matching package is in the main module, QueryPattern considers only // If any matching package is in the main module, QueryPattern considers only
// the main module and only the version "latest", without checking for other // the main module and only the version "latest", without checking for other
// possible modules. // possible modules.
func QueryPattern(pattern string, query string, allowed func(module.Version) bool) ([]QueryResult, error) { func QueryPattern(pattern, query string, allowed func(module.Version) bool) ([]QueryResult, error) {
base := pattern base := pattern
var match func(m module.Version, root string, isLocal bool) (pkgs []string) var match func(m module.Version, root string, isLocal bool) (pkgs []string)
...@@ -288,138 +298,151 @@ func QueryPattern(pattern string, query string, allowed func(module.Version) boo ...@@ -288,138 +298,151 @@ func QueryPattern(pattern string, query string, allowed func(module.Version) boo
} }
} }
// If the path we're attempting is not in the module cache and we don't have a var (
// fetch result cached either, we'll end up making a (potentially slow) results []QueryResult
// request to the proxy or (often even slower) the origin server. candidateModules = modulePrefixesExcludingTarget(base)
// To minimize latency, execute all of those requests in parallel. )
type result struct { if len(candidateModules) == 0 {
QueryResult return nil, fmt.Errorf("package %s is not in the main module (%s)", pattern, Target.Path)
err error
} }
results := make([]result, strings.Count(base, "/")+1) // by descending path length
i, p := 0, base err := modfetch.TryProxies(func(proxy string) error {
var wg sync.WaitGroup queryModule := func(path string) (r QueryResult, err error) {
wg.Add(len(results)) r.Mod.Path = path
for { r.Rev, err = queryProxy(proxy, path, query, allowed)
go func(p string, r *result) (err error) { if err != nil {
defer func() { return r, err
r.err = err }
wg.Done() r.Mod.Version = r.Rev.Version
}() root, isLocal, err := fetch(r.Mod)
if err != nil {
r.Mod.Path = p return r, err
if HasModRoot() && p == Target.Path {
r.Mod.Version = Target.Version
r.Rev = &modfetch.RevInfo{Version: Target.Version}
// We already know (from above) that Target does not contain any
// packages matching pattern, so leave r.Packages empty.
} else {
r.Rev, err = Query(p, query, allowed)
if err != nil {
return err
}
r.Mod.Version = r.Rev.Version
root, isLocal, err := fetch(r.Mod)
if err != nil {
return err
}
r.Packages = match(r.Mod, root, isLocal)
} }
r.Packages = match(r.Mod, root, isLocal)
if len(r.Packages) == 0 { if len(r.Packages) == 0 {
return &packageNotInModuleError{ return r, &packageNotInModuleError{
mod: r.Mod, mod: r.Mod,
query: query, query: query,
pattern: pattern, pattern: pattern,
} }
} }
return nil return r, nil
}(p, &results[i]) }
j := strings.LastIndexByte(p, '/') var err error
if i++; i == len(results) { results, err = queryPrefixModules(candidateModules, queryModule)
if j >= 0 { return err
panic("undercounted slashes") })
return results, err
}
// modulePrefixesExcludingTarget returns all prefixes of path that may plausibly
// exist as a module, excluding targetPrefix but otherwise including path
// itself, sorted by descending length.
func modulePrefixesExcludingTarget(path string) []string {
prefixes := make([]string, 0, strings.Count(path, "/")+1)
for {
if path != targetPrefix {
if _, _, ok := module.SplitPathVersion(path); ok {
prefixes = append(prefixes, path)
} }
break
} }
j := strings.LastIndexByte(path, '/')
if j < 0 { if j < 0 {
panic("overcounted slashes") break
} }
p = p[:j] path = path[:j]
}
return prefixes
}
type prefixResult struct {
QueryResult
err error
}
func queryPrefixModules(candidateModules []string, queryModule func(path string) (QueryResult, error)) (found []QueryResult, err error) {
// If the path we're attempting is not in the module cache and we don't have a
// fetch result cached either, we'll end up making a (potentially slow)
// request to the proxy or (often even slower) the origin server.
// To minimize latency, execute all of those requests in parallel.
type result struct {
QueryResult
err error
}
results := make([]result, len(candidateModules))
var wg sync.WaitGroup
wg.Add(len(candidateModules))
for i, p := range candidateModules {
go func(p string, r *result) {
r.QueryResult, r.err = queryModule(p)
wg.Done()
}(p, &results[i])
} }
wg.Wait() wg.Wait()
// Classify the results. In case of failure, identify the error that the user // Classify the results. In case of failure, identify the error that the user
// is most likely to find helpful. // is most likely to find helpful.
var ( var (
successes []QueryResult noVersion *NoMatchingVersionError
mostUseful result noPackage *packageNotInModuleError
notExistErr error
) )
for _, r := range results { for _, r := range results {
if r.err == nil { switch rErr := r.err.(type) {
successes = append(successes, r.QueryResult)
continue
}
switch mostUseful.err.(type) {
case nil: case nil:
mostUseful = r found = append(found, r.QueryResult)
continue case *NoMatchingVersionError:
case *packageNotInModuleError: if noVersion == nil {
// Any other error is more useful than one that reports that the main noVersion = rErr
// module does not contain the requested packages.
if mostUseful.Mod.Path == Target.Path {
mostUseful = r
continue
} }
}
switch r.err.(type) {
case *codehost.VCSError:
// A VCSError means that we've located a repository, but couldn't look
// inside it for packages. That's a very strong signal, and should
// override any others.
return nil, r.err
case *packageNotInModuleError: case *packageNotInModuleError:
if r.Mod.Path == Target.Path { if noPackage == nil {
// Don't override a potentially-useful error for some other module with noPackage = rErr
// a trivial error for the main module.
continue
} }
// A module with an appropriate prefix exists at the requested version, default:
// but it does not contain the requested package(s). if errors.Is(rErr, os.ErrNotExist) {
if _, worsePath := mostUseful.err.(*packageNotInModuleError); !worsePath { if notExistErr == nil {
mostUseful = r notExistErr = rErr
} }
case *NoMatchingVersionError: } else {
// A module with an appropriate prefix exists, but not at the requested err = r.err
// version.
_, worseError := mostUseful.err.(*packageNotInModuleError)
_, worsePath := mostUseful.err.(*NoMatchingVersionError)
if !(worseError || worsePath) {
mostUseful = r
} }
} }
} }
// TODO(#26232): If len(successes) == 0 and some of the errors are 4xx HTTP // TODO(#26232): If len(found) == 0 and some of the errors are 4xx HTTP
// codes, have the auth package recheck the failed paths. // codes, have the auth package recheck the failed paths.
// If we obtain new credentials for any of them, re-run the above loop. // If we obtain new credentials for any of them, re-run the above loop.
if len(successes) == 0 { if len(found) == 0 && err == nil {
// All of the possible module paths either did not exist at the requested switch {
// version, or did not contain the requested package(s). case noPackage != nil:
return nil, mostUseful.err err = noPackage
case noVersion != nil:
err = noVersion
case notExistErr != nil:
err = notExistErr
default:
panic("queryPrefixModules: no modules found, but no error detected")
}
} }
// At least one module at the requested version contained the requested return found, err
// package(s). Any remaining errors only describe the non-existence of
// alternatives, so ignore them.
return successes, nil
} }
// A NoMatchingVersionError indicates that Query found a module at the requested // A NoMatchingVersionError indicates that Query found a module at the requested
// path, but not at any versions satisfying the query string and allow-function. // path, but not at any versions satisfying the query string and allow-function.
//
// NOTE: NoMatchingVersionError MUST NOT implement Is(os.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
// to return a non-400 status code to suppress fallback.
type NoMatchingVersionError struct { type NoMatchingVersionError struct {
query string query string
} }
...@@ -431,6 +454,12 @@ func (e *NoMatchingVersionError) Error() string { ...@@ -431,6 +454,12 @@ func (e *NoMatchingVersionError) Error() string {
// A packageNotInModuleError indicates that QueryPattern found a candidate // A packageNotInModuleError indicates that QueryPattern found a candidate
// module at the requested version, but that module did not contain any packages // module at the requested version, but that module did not contain any packages
// matching the requested pattern. // matching the requested pattern.
//
// NOTE: packageNotInModuleError MUST NOT implement Is(os.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
// to return a non-400 status code to suppress fallback.
type packageNotInModuleError struct { type packageNotInModuleError struct {
mod module.Version mod module.Version
query string query string
......
...@@ -25,7 +25,7 @@ func TestMain(m *testing.M) { ...@@ -25,7 +25,7 @@ func TestMain(m *testing.M) {
} }
func testMain(m *testing.M) int { func testMain(m *testing.M) int {
modfetch.SetProxy("direct") cfg.GOPROXY = "direct"
dir, err := ioutil.TempDir("", "modload-test-") dir, err := ioutil.TempDir("", "modload-test-")
if err != nil { if err != nil {
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"flag" "flag"
"fmt" "fmt"
"io" "io"
...@@ -253,7 +254,11 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { ...@@ -253,7 +254,11 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
if !quiet { if !quiet {
fmt.Fprintf(os.Stderr, "go proxy: no archive %s %s: %v\n", path, vers, err) fmt.Fprintf(os.Stderr, "go proxy: no archive %s %s: %v\n", path, vers, err)
} }
http.Error(w, "cannot load archive", 500) if errors.Is(err, os.ErrNotExist) {
http.NotFound(w, r)
} else {
http.Error(w, "cannot load archive", 500)
}
return return
} }
......
...@@ -6,11 +6,11 @@ go build -o $WORK/testimport.exe ./testimport ...@@ -6,11 +6,11 @@ go build -o $WORK/testimport.exe ./testimport
# GO111MODULE=off # GO111MODULE=off
env GO111MODULE=off env GO111MODULE=off
! exec $WORK/testimport.exe x/y/z/w . ! exec $WORK/testimport.exe gobuild.example.com/x/y/z/w .
# GO111MODULE=auto in GOPATH/src # GO111MODULE=auto in GOPATH/src
env GO111MODULE=auto env GO111MODULE=auto
exec $WORK/testimport.exe x/y/z/w . exec $WORK/testimport.exe gobuild.example.com/x/y/z/w .
# GO111MODULE=auto outside GOPATH/src # GO111MODULE=auto outside GOPATH/src
cd $GOPATH/other cd $GOPATH/other
...@@ -18,8 +18,8 @@ env GO111MODULE=auto ...@@ -18,8 +18,8 @@ env GO111MODULE=auto
exec $WORK/testimport.exe other/x/y/z/w . exec $WORK/testimport.exe other/x/y/z/w .
stdout w2.go stdout w2.go
! exec $WORK/testimport.exe x/y/z/w . ! exec $WORK/testimport.exe gobuild.example.com/x/y/z/w .
stderr 'cannot find module providing package x/y/z/w' stderr 'cannot find module providing package gobuild.example.com/x/y/z/w'
cd z cd z
exec $WORK/testimport.exe other/x/y/z/w . exec $WORK/testimport.exe other/x/y/z/w .
...@@ -36,17 +36,17 @@ stdout w2.go ...@@ -36,17 +36,17 @@ stdout w2.go
# GO111MODULE=on in GOPATH/src # GO111MODULE=on in GOPATH/src
cd $GOPATH/src cd $GOPATH/src
env GO111MODULE= env GO111MODULE=
exec $WORK/testimport.exe x/y/z/w . exec $WORK/testimport.exe gobuild.example.com/x/y/z/w .
stdout w1.go stdout w1.go
env GO111MODULE=on env GO111MODULE=on
exec $WORK/testimport.exe x/y/z/w . exec $WORK/testimport.exe gobuild.example.com/x/y/z/w .
stdout w1.go stdout w1.go
cd w cd w
exec $WORK/testimport.exe x/y/z/w .. exec $WORK/testimport.exe gobuild.example.com/x/y/z/w ..
stdout w1.go stdout w1.go
-- go.mod -- -- go.mod --
module x/y/z module gobuild.example.com/x/y/z
-- z.go -- -- z.go --
package z package z
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment