]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: forbid resolving import to modules when outside of a module
authorJay Conrod <jayconrod@google.com>
Wed, 2 Oct 2019 21:51:54 +0000 (17:51 -0400)
committerJay Conrod <jayconrod@google.com>
Wed, 9 Oct 2019 23:03:55 +0000 (23:03 +0000)
When in module mode outside of any module, 'go build' and most other
commands will now report an error instead of resolving a package path
to a module.

Previously, most commands would attempt to find the latest version of
a module providing the package. This could be very slow if many
packages needed to be resolved this way. Since there is no go.mod file
where module requirements can be saved, it's a repeatedly slow and
confusing experience.

After this change, 'go build' and other commands may still be used
outside of a module on packages in std and source files (.go
arguments) that only import packages in std. Listing any other package
on the command line or importing a package outside std will cause an
error.

'go get' is exempted from the new behavior, since it's expected that
'go get' resolves paths to modules at new versions.

Updates #32027

Change-Id: Ia9d3a3b4ad738ca5423472e17818d62b96a2c959
Reviewed-on: https://go-review.googlesource.com/c/go/+/198778
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
src/cmd/go/internal/modget/get.go
src/cmd/go/internal/modload/import.go
src/cmd/go/internal/modload/import_test.go
src/cmd/go/internal/modload/init.go
src/cmd/go/testdata/script/build_trimpath.txt
src/cmd/go/testdata/script/mod_missingpkg_prerelease.txt
src/cmd/go/testdata/script/mod_outside.txt

index ced5abcc71b729635c0173a9cef5d6166b7fe5c3..6e67eac9832ab531cb4e802390093884c85e7f14 100644 (file)
@@ -284,6 +284,10 @@ func runGet(cmd *base.Command, args []string) {
        // what was requested.
        modload.DisallowWriteGoMod()
 
+       // Allow looking up modules for import paths outside of a module.
+       // 'go get' is expected to do this, unlike other commands.
+       modload.AllowMissingModuleImports()
+
        // Parse command-line arguments and report errors. The command-line
        // arguments are of the form path@version or simply path, with implicit
        // @upgrade. path@none is "downgrade away".
index 5b1f0ce02709e5c6563e497154c13695c7abe016..cda56fa7c813cb8319bd9b407b2229bf475e58f1 100644 (file)
@@ -185,6 +185,12 @@ func Import(path string) (m module.Version, dir string, err error) {
        if cfg.BuildMod == "readonly" {
                return module.Version{}, "", fmt.Errorf("import lookup disabled by -mod=%s", cfg.BuildMod)
        }
+       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.
index c58892e2ab2a93c6e44f64ffaf8db4a376ec1ffa..accc60eecdf37e57bf6225461a200f09927220dc 100644 (file)
@@ -44,6 +44,10 @@ var importTests = []struct {
 func TestImport(t *testing.T) {
        testenv.MustHaveExternalNetwork(t)
        testenv.MustHaveExecPath(t, "git")
+       defer func(old bool) {
+               allowMissingModuleImports = old
+       }(allowMissingModuleImports)
+       AllowMissingModuleImports()
 
        for _, tt := range importTests {
                t.Run(strings.ReplaceAll(tt.path, "/", "_"), func(t *testing.T) {
index 8bc41d258dc8eff436f65ec516c0897ac3654be3..4872bc33906d68294682036b51d72c84fc83bc6d 100644 (file)
@@ -57,6 +57,8 @@ var (
 
        CmdModInit   bool   // running 'go mod init'
        CmdModModule string // module argument for 'go mod init'
+
+       allowMissingModuleImports bool
 )
 
 // ModFile returns the parsed go.mod file.
@@ -199,28 +201,21 @@ func Init() {
        if modRoot == "" {
                // We're in module mode, but not inside a module.
                //
-               // If the command is 'go get' or 'go list' and all of the args are in the
-               // same existing module, we could use that module's download directory in
-               // the module cache as the module root, applying any replacements and/or
-               // exclusions specified by that module. However, that would leave us in a
-               // strange state: we want 'go get' to be consistent with 'go list', and 'go
-               // list' should be able to operate on multiple modules. Moreover, the 'get'
-               // target might specify relative file paths (e.g. in the same repository) as
-               // replacements, and we would not be able to apply those anyway: we would
-               // need to either error out or ignore just those replacements, when a build
-               // from an empty module could proceed without error.
+               // Commands like 'go build', 'go run', 'go list' have no go.mod file to
+               // read or write. They would need to find and download the latest versions
+               // of a potentially large number of modules with no way to save version
+               // information. We can succeed slowly (but not reproducibly), but that's
+               // not usually a good experience.
                //
-               // Instead, we'll operate as though we're in some ephemeral external module,
-               // ignoring all replacements and exclusions uniformly.
-
-               // Normally we check sums using the go.sum file from the main module, but
-               // without a main module we do not have an authoritative go.sum file.
+               // Instead, we forbid resolving import paths to modules other than std and
+               // cmd. Users may still build packages specified with .go files on the
+               // command line, but they'll see an error if those files import anything
+               // outside std.
                //
-               // TODO(bcmills): In Go 1.13, check sums when outside the main module.
+               // This can be overridden by calling AllowMissingModuleImports.
+               // For example, 'go get' does this, since it is expected to resolve paths.
                //
-               // One possible approach is to merge the go.sum files from all of the
-               // modules we download: that doesn't protect us against bad top-level
-               // modules, but it at least ensures consistency for transitive dependencies.
+               // See golang.org/issue/32027.
        } else {
                modfetch.GoSumFile = filepath.Join(modRoot, "go.sum")
                search.SetModRoot(modRoot)
@@ -360,6 +355,14 @@ func InitMod() {
        }
 }
 
+// AllowMissingModuleImports allows import paths to be resolved to modules
+// when there is no module root. Normally, this is forbidden because it's slow
+// and there's no way to make the result reproducible, but some commands
+// like 'go get' are expected to do this.
+func AllowMissingModuleImports() {
+       allowMissingModuleImports = true
+}
+
 // modFileToBuildList initializes buildList from the modFile.
 func modFileToBuildList() {
        Target = modFile.Module.Mod
index ec817a5ecd8718af1378d3150365a5bf4726bd14..2c39e4cec48c7e2f341525ed42a9b92d40321e59 100644 (file)
@@ -21,9 +21,9 @@ go build -trimpath -o hello.exe hello.go
 # the current workspace or GOROOT.
 cd $WORK
 env GO111MODULE=on
-go build -trimpath -o fortune.exe rsc.io/fortune
-! grep -q $GOROOT_REGEXP fortune.exe
-! grep -q $WORK_REGEXP fortune.exe
+go get -trimpath rsc.io/fortune
+! grep -q $GOROOT_REGEXP $GOPATH/bin/fortune$GOEXE
+! grep -q $WORK_REGEXP $GOPATH/bin/fortune$GOEXE
 
 # Two binaries built from identical packages in different directories
 # should be identical.
index 6203606c22d8c4c6501f95b919ab16bd52f8bd5b..319ff8558720811269a920bf49840a818b8236f3 100644 (file)
@@ -3,6 +3,11 @@ env GO111MODULE=on
 ! go list use.go
 stderr 'example.com/missingpkg/deprecated: package provided by example.com/missingpkg at latest version v1.0.0 but not at required version v1.0.1-beta'
 
+-- go.mod --
+module m
+
+go 1.14
+
 -- use.go --
 package use
 
index 4182e71dde1aa9ed2abb94b7b60067fd25e3dfce..dd4e2d580067433be5ae9303614d5118a720c7b9 100644 (file)
@@ -17,7 +17,7 @@ go list -m
 stdout '^command-line-arguments$'
 # 'go list' in the working directory should fail even if there is a a 'package
 # main' present: without a main module, we do not know its package path.
-! go list ./foo
+! go list ./needmod
 stderr 'cannot find main module'
 
 # 'go list all' lists the transitive import graph of the main module,
@@ -38,7 +38,7 @@ go list $GOROOT/src/fmt
 stdout '^fmt$'
 
 # 'go list' should work with file arguments.
-go list ./foo/foo.go
+go list ./needmod/needmod.go
 stdout 'command-line-arguments'
 
 # 'go list -m' with an explicit version should resolve that version.
@@ -104,7 +104,7 @@ stdout 'all modules verified'
 stderr 'cannot find main module'
 ! go get -u
 stderr 'cannot find main module'
-! go get -u ./foo
+! go get -u ./needmod
 stderr 'cannot find main module'
 
 # 'go get -u all' upgrades the transitive import graph of the main module,
@@ -126,35 +126,75 @@ exists $GOPATH/pkg/mod/example.com/version@v1.0.0
 
 
 # 'go build' without arguments implicitly operates on the current directory, and should fail.
-cd foo
+cd needmod
 ! go build
 stderr 'cannot find main module'
 cd ..
 
 # 'go build' of a non-module directory should fail too.
-! go build ./foo
+! go build ./needmod
 stderr 'cannot find main module'
 
-# However, 'go build' should succeed for standard-library packages.
+# 'go build' of source files should fail if they import anything outside std.
+! go build -n ./needmod/needmod.go
+stderr 'needmod[/\\]needmod.go:10:2: cannot find module providing package example.com/version: working directory is not part of a module'
+
+# 'go build' of source files should succeed if they do not import anything outside std.
+go build -n -o ignore ./stdonly/stdonly.go
+
+# 'go build' should succeed for standard-library packages.
 go build -n fmt
 
 
-# TODO(golang.org/issue/28992): 'go doc' should document the latest version.
-# For now it does not.
+# 'go doc' without arguments implicitly operates on the current directory, and should fail.
+# TODO(golang.org/issue/32027): currently, it succeeds.
+cd needmod
+go doc
+cd ..
+
+# 'go doc' of a non-module directory should also succeed.
+go doc ./needmod
+
+# 'go doc' should succeed for standard-library packages.
+go doc fmt
+
+# 'go doc' should fail for a package path outside a module.
 ! go doc example.com/version
-stderr 'no such package'
+stderr 'doc: cannot find module providing package example.com/version: working directory is not part of a module'
 
 # 'go install' with a version should fail due to syntax.
 ! go install example.com/printversion@v1.0.0
 stderr 'can only use path@version syntax with'
 
+# 'go install' should fail if a package argument must be resolved to a module.
+! go install example.com/printversion
+stderr 'cannot find module providing package example.com/printversion: working directory is not part of a module'
+
+# 'go install' should fail if a source file imports a package that must be
+# resolved to a module.
+! go install ./needmod/needmod.go
+stderr 'needmod[/\\]needmod.go:10:2: cannot find module providing package example.com/version: working directory is not part of a module'
+
+
+# 'go run' with a verison should fail due to syntax.
+! go run example.com/printversion@v1.0.0
+stderr 'can only use path@version syntax with'
+
+# 'go run' should fail if a package argument must be resolved to a module.
+! go run example.com/printversion
+stderr 'cannot find module providing package example.com/printversion: working directory is not part of a module'
+
+# 'go run' should fail if a source file imports a package that must be
+# resolved to a module.
+! go run ./needmod/needmod.go
+stderr 'needmod[/\\]needmod.go:10:2: cannot find module providing package example.com/version: working directory is not part of a module'
+
 
 # 'go fmt' should be able to format files outside of a module.
-go fmt foo/foo.go
+go fmt needmod/needmod.go
 
 
 # The remainder of the test checks dependencies by linking and running binaries.
-[short] stop
 
 # 'go get' of a binary without a go.mod should install the requested version,
 # resolving outside dependencies to the latest available versions.
@@ -180,39 +220,31 @@ stdout 'path is example.com/printversion'
 stdout 'main is example.com/printversion v1.0.0'
 stdout 'using example.com/version v1.0.1'
 
-# 'go install' without a version should install the latest version
-# using its minimal module requirements.
-go install example.com/printversion
-exec ../bin/printversion
-stdout 'path is example.com/printversion'
-stdout 'main is example.com/printversion v1.0.0'
-stdout 'using example.com/version v1.0.0'
-
-# 'go run' should use 'main' as the effective module and import path.
-go run ./foo/foo.go
+# 'go run' should work with file arguments if they don't import anything
+# outside std.
+go run ./stdonly/stdonly.go
 stdout 'path is command-line-arguments$'
 stdout 'main is command-line-arguments \(devel\)'
-stdout 'using example.com/version v1.1.0'
 
 # 'go generate' should work with file arguments.
-[exec:touch] go generate ./foo/foo.go
-[exec:touch] exists ./foo/gen.txt
+[exec:touch] go generate ./needmod/needmod.go
+[exec:touch] exists ./needmod/gen.txt
 
 # 'go install' should work with file arguments.
-go install ./foo/foo.go
+go install ./stdonly/stdonly.go
 
 # 'go test' should work with file arguments.
-go test -v ./foo/foo_test.go
-stdout 'foo was tested'
+go test -v ./stdonly/stdonly_test.go
+stdout 'stdonly was tested'
 
 # 'go vet' should work with file arguments.
-go vet ./foo/foo.go
+go vet ./stdonly/stdonly.go
 
 
 -- README.txt --
 There is no go.mod file in the working directory.
 
--- foo/foo.go --
+-- needmod/needmod.go --
 //go:generate touch gen.txt
 
 package main
@@ -237,7 +269,28 @@ func main() {
        }
 }
 
--- foo/foo_test.go --
+-- stdonly/stdonly.go --
+package main
+
+import (
+       "fmt"
+       "os"
+       "runtime/debug"
+)
+
+func main() {
+       info, ok := debug.ReadBuildInfo()
+       if !ok {
+               panic("missing build info")
+       }
+       fmt.Fprintf(os.Stdout, "path is %s\n", info.Path)
+       fmt.Fprintf(os.Stdout, "main is %s %s\n", info.Main.Path, info.Main.Version)
+       for _, m := range info.Deps {
+               fmt.Fprintf(os.Stdout, "using %s %s\n", m.Path, m.Version)
+       }
+}
+
+-- stdonly/stdonly_test.go --
 package main
 
 import (
@@ -245,6 +298,7 @@ import (
        "testing"
 )
 
-func TestFoo(t *testing.T) {
-       fmt.Println("foo was tested")
+func Test(t *testing.T) {
+       fmt.Println("stdonly was tested")
 }
+