Skip to content

Commit 7cd10c1

Browse files
author
Jay Conrod
committed
cmd/go: use .mod instead of .zip to determine if version has go.mod file
When checking for updates, the go command checks whether the highest compatible version has a go.mod file in order to determine whether +incompatible versions may be considered "latest". Previously, to perform this check, the go command would download the content of the module (the .zip file) to see whether a go.mod file was present at the root. This is slower than necessary, and it caused 'go list -m -u' to try to save the sum for the .zip file in go.sum in some cases. With this change, the go command only downloads the .mod file and checks whether it appears to be a fake file generated for a version that didn't have a go.mod file. This is faster and requires less verification. Fake files only have a "module" directive. It's possible to commit a file that passes this test, but it would be difficult to do accidentally: Go 1.12 and later at least add a "go" directive. A false positive here would cause version queries to have slightly different results but would not affect builds. Fixes #47377 Change-Id: Ie5ffd0b45e39bd0921328a60af99a9f6e5ab6346 Reviewed-on: https://go-review.googlesource.com/c/go/+/337850 Trust: Jay Conrod <[email protected]> Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent c8cf0f7 commit 7cd10c1

File tree

4 files changed

+114
-56
lines changed

4 files changed

+114
-56
lines changed

src/cmd/go/internal/modfetch/coderepo.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -864,22 +864,25 @@ func (r *codeRepo) GoMod(version string) (data []byte, err error) {
864864
data, err = r.code.ReadFile(rev, path.Join(dir, "go.mod"), codehost.MaxGoMod)
865865
if err != nil {
866866
if os.IsNotExist(err) {
867-
return r.legacyGoMod(rev, dir), nil
867+
return LegacyGoMod(r.modPath), nil
868868
}
869869
return nil, err
870870
}
871871
return data, nil
872872
}
873873

874-
func (r *codeRepo) legacyGoMod(rev, dir string) []byte {
875-
// We used to try to build a go.mod reflecting pre-existing
876-
// package management metadata files, but the conversion
877-
// was inherently imperfect (because those files don't have
878-
// exactly the same semantics as go.mod) and, when done
879-
// for dependencies in the middle of a build, impossible to
880-
// correct. So we stopped.
881-
// Return a fake go.mod that simply declares the module path.
882-
return []byte(fmt.Sprintf("module %s\n", modfile.AutoQuote(r.modPath)))
874+
// LegacyGoMod generates a fake go.mod file for a module that doesn't have one.
875+
// The go.mod file contains a module directive and nothing else: no go version,
876+
// no requirements.
877+
//
878+
// We used to try to build a go.mod reflecting pre-existing
879+
// package management metadata files, but the conversion
880+
// was inherently imperfect (because those files don't have
881+
// exactly the same semantics as go.mod) and, when done
882+
// for dependencies in the middle of a build, impossible to
883+
// correct. So we stopped.
884+
func LegacyGoMod(modPath string) []byte {
885+
return []byte(fmt.Sprintf("module %s\n", modfile.AutoQuote(modPath)))
883886
}
884887

885888
func (r *codeRepo) modPrefix(rev string) string {

src/cmd/go/internal/modload/modfile.go

+43-40
Original file line numberDiff line numberDiff line change
@@ -595,47 +595,14 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
595595
}
596596
c := rawGoModSummaryCache.Do(m, func() interface{} {
597597
summary := new(modFileSummary)
598-
var f *modfile.File
599-
if m.Version == "" {
600-
// m is a replacement module with only a file path.
601-
dir := m.Path
602-
if !filepath.IsAbs(dir) {
603-
dir = filepath.Join(ModRoot(), dir)
604-
}
605-
gomod := filepath.Join(dir, "go.mod")
606-
var data []byte
607-
var err error
608-
if gomodActual, ok := fsys.OverlayPath(gomod); ok {
609-
// Don't lock go.mod if it's part of the overlay.
610-
// On Plan 9, locking requires chmod, and we don't want to modify any file
611-
// in the overlay. See #44700.
612-
data, err = os.ReadFile(gomodActual)
613-
} else {
614-
data, err = lockedfile.Read(gomodActual)
615-
}
616-
if err != nil {
617-
return cached{nil, module.VersionError(m, fmt.Errorf("reading %s: %v", base.ShortPath(gomod), err))}
618-
}
619-
f, err = modfile.ParseLax(gomod, data, nil)
620-
if err != nil {
621-
return cached{nil, module.VersionError(m, fmt.Errorf("parsing %s: %v", base.ShortPath(gomod), err))}
622-
}
623-
} else {
624-
if !semver.IsValid(m.Version) {
625-
// Disallow the broader queries supported by fetch.Lookup.
626-
base.Fatalf("go: internal error: %s@%s: unexpected invalid semantic version", m.Path, m.Version)
627-
}
628-
629-
data, err := modfetch.GoMod(m.Path, m.Version)
630-
if err != nil {
631-
return cached{nil, err}
632-
}
633-
f, err = modfile.ParseLax("go.mod", data, nil)
634-
if err != nil {
635-
return cached{nil, module.VersionError(m, fmt.Errorf("parsing go.mod: %v", err))}
636-
}
598+
name, data, err := rawGoModData(m)
599+
if err != nil {
600+
return cached{nil, err}
601+
}
602+
f, err := modfile.ParseLax(name, data, nil)
603+
if err != nil {
604+
return cached{nil, module.VersionError(m, fmt.Errorf("parsing %s: %v", base.ShortPath(name), err))}
637605
}
638-
639606
if f.Module != nil {
640607
summary.module = f.Module.Mod
641608
summary.deprecated = f.Module.Deprecated
@@ -671,6 +638,42 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
671638

672639
var rawGoModSummaryCache par.Cache // module.Version → rawGoModSummary result
673640

641+
// rawGoModData returns the content of the go.mod file for module m, ignoring
642+
// all replacements that may apply to m.
643+
//
644+
// rawGoModData cannot be used on the Target module.
645+
//
646+
// Unlike rawGoModSummary, rawGoModData does not cache its results in memory.
647+
// Use rawGoModSummary instead unless you specifically need these bytes.
648+
func rawGoModData(m module.Version) (name string, data []byte, err error) {
649+
if m.Version == "" {
650+
// m is a replacement module with only a file path.
651+
dir := m.Path
652+
if !filepath.IsAbs(dir) {
653+
dir = filepath.Join(ModRoot(), dir)
654+
}
655+
gomod := filepath.Join(dir, "go.mod")
656+
if gomodActual, ok := fsys.OverlayPath(gomod); ok {
657+
// Don't lock go.mod if it's part of the overlay.
658+
// On Plan 9, locking requires chmod, and we don't want to modify any file
659+
// in the overlay. See #44700.
660+
data, err = os.ReadFile(gomodActual)
661+
} else {
662+
data, err = lockedfile.Read(gomodActual)
663+
}
664+
if err != nil {
665+
return gomod, nil, module.VersionError(m, fmt.Errorf("reading %s: %v", base.ShortPath(gomod), err))
666+
}
667+
} else {
668+
if !semver.IsValid(m.Version) {
669+
// Disallow the broader queries supported by fetch.Lookup.
670+
base.Fatalf("go: internal error: %s@%s: unexpected invalid semantic version", m.Path, m.Version)
671+
}
672+
data, err = modfetch.GoMod(m.Path, m.Version)
673+
}
674+
return "go.mod", data, err
675+
}
676+
674677
// queryLatestVersionIgnoringRetractions looks up the latest version of the
675678
// module with the given path without considering retracted or excluded
676679
// versions.

src/cmd/go/internal/modload/query.go

+24-6
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
package modload
66

77
import (
8+
"bytes"
89
"context"
910
"errors"
1011
"fmt"
1112
"io/fs"
1213
"os"
1314
pathpkg "path"
14-
"path/filepath"
1515
"sort"
1616
"strings"
1717
"sync"
@@ -931,14 +931,32 @@ func moduleHasRootPackage(ctx context.Context, m module.Version) (bool, error) {
931931
return ok, err
932932
}
933933

934-
func versionHasGoMod(ctx context.Context, m module.Version) (bool, error) {
935-
needSum := false
936-
root, _, err := fetch(ctx, m, needSum)
934+
// versionHasGoMod returns whether a version has a go.mod file.
935+
//
936+
// versionHasGoMod fetches the go.mod file (possibly a fake) and true if it
937+
// contains anything other than a module directive with the same path. When a
938+
// module does not have a real go.mod file, the go command acts as if it had one
939+
// that only contained a module directive. Normal go.mod files created after
940+
// 1.12 at least have a go directive.
941+
//
942+
// This function is a heuristic, since it's possible to commit a file that would
943+
// pass this test. However, we only need a heurstic for determining whether
944+
// +incompatible versions may be "latest", which is what this function is used
945+
// for.
946+
//
947+
// This heuristic is useful for two reasons: first, when using a proxy,
948+
// this lets us fetch from the .mod endpoint which is much faster than the .zip
949+
// endpoint. The .mod file is used anyway, even if the .zip file contains a
950+
// go.mod with different content. Second, if we don't fetch the .zip, then
951+
// we don't need to verify it in go.sum. This makes 'go list -m -u' faster
952+
// and simpler.
953+
func versionHasGoMod(_ context.Context, m module.Version) (bool, error) {
954+
_, data, err := rawGoModData(m)
937955
if err != nil {
938956
return false, err
939957
}
940-
fi, err := os.Stat(filepath.Join(root, "go.mod"))
941-
return err == nil && !fi.IsDir(), nil
958+
isFake := bytes.Equal(data, modfetch.LegacyGoMod(m.Path))
959+
return !isFake, nil
942960
}
943961

944962
// A versionRepo is a subset of modfetch.Repo that can report information about
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# When finding the latest version of a module, we should not download version
2+
# contents. Previously, we downloaded .zip files to determine whether a real
3+
# .mod file was present in order to decide whether +incompatible versions
4+
# could be "latest".
5+
#
6+
# Verifies #47377.
7+
8+
# rsc.io/breaker has two versions, neither of which has a .mod file.
9+
go list -m -versions rsc.io/breaker
10+
stdout '^rsc.io/breaker v1.0.0 v2.0.0\+incompatible$'
11+
go mod download rsc.io/[email protected]
12+
! grep '^go' $GOPATH/pkg/mod/cache/download/rsc.io/breaker/@v/v1.0.0.mod
13+
go mod download rsc.io/[email protected]+incompatible
14+
! grep '^go' $GOPATH/pkg/mod/cache/download/rsc.io/breaker/@v/v2.0.0+incompatible.mod
15+
16+
# Delete downloaded .zip files.
17+
go clean -modcache
18+
19+
# Check for updates.
20+
go list -m -u rsc.io/breaker
21+
stdout '^rsc.io/breaker v1.0.0 \[v2.0.0\+incompatible\]$'
22+
23+
# We should not have downloaded zips.
24+
! exists $GOPATH/pkg/mod/cache/download/rsc.io/breaker/@v/v1.0.0.zip
25+
! exists $GOPATH/pkg/mod/cache/download/rsc.io/breaker/@v/v2.0.0+incompatible.zip
26+
27+
-- go.mod --
28+
module m
29+
30+
go 1.16
31+
32+
require rsc.io/breaker v1.0.0
33+
-- go.sum --
34+
rsc.io/breaker v1.0.0/go.mod h1:s5yxDXvD88U1/ESC23I2FK3Lkv4YIKaB1ij/Hbm805g=

0 commit comments

Comments
 (0)