Repository: goplus/gop
Pull request #959 (2021-11-28 15:05:37)

Find mod file in gop root
Find order:

0. Parent directories of the directory
1. Check $GOPROOT, panic if it's invalid
2. The parent directory of the executable
3. Check build.GopRoot
4. Check $HOME/gop/, $HOME/goplus/


Patch:
diff --git a/build/build.go b/build/build.go
index 54976067..311399c2 100644
--- a/build/build.go
+++ b/build/build.go
@@ -28,6 +28,8 @@ var (
 	Branch string
 	// Commit git commit id
 	Commit string
+	// Default GOPROOT
+	GopRoot string
 )
 
 // Build information
diff --git a/cmd/internal/run/path.go b/cmd/internal/run/path.go
new file mode 100644
index 00000000..bd3e1497
--- /dev/null
+++ b/cmd/internal/run/path.go
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package run
+
+import (
+	"os"
+	"path/filepath"
+	"syscall"
+
+	"github.com/goplus/gop/build"
+	"github.com/goplus/gop/cl"
+	"github.com/qiniu/x/log"
+)
+
+const ErrNotFound = syscall.ENOENT
+
+const (
+	ENV_GOPROOT = "GOPROOT"
+	ENV_HOME    = "HOME"
+)
+
+func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
+	modfile, err = cl.FindGoModFile(dir)
+
+	if err != nil {
+		gopRoot, err := findGopRoot()
+		if err == nil {
+			modfile = filepath.Join(gopRoot, "go.mod")
+			return modfile, true, nil
+		}
+	}
+	return
+}
+
+func findGopRoot() (string, error) {
+	envGopRoot := os.Getenv(ENV_GOPROOT)
+	if envGopRoot != "" {
+		// GOPROOT must valid
+		if isValidGopRoot(envGopRoot) {
+			return envGopRoot, nil
+		}
+		log.Panicf("\n%s (%s) is not valid\n", ENV_GOPROOT, envGopRoot)
+	}
+
+	// if parent directory is a valid gop root, use it
+	exePath, err := executableRealPath()
+	if err == nil {
+		dir := filepath.Dir(exePath)
+		parentDir := filepath.Dir(dir)
+		if parentDir != dir && isValidGopRoot(parentDir) {
+			return parentDir, nil
+		}
+	}
+
+	// check build.GopRoot, if it is valid, use it
+	if build.GopRoot != "" && isValidGopRoot(build.GopRoot) {
+		return build.GopRoot, nil
+	}
+
+	// Compatible with old GOPROOT
+	if home := os.Getenv(ENV_HOME); home != "" {
+		gopRoot := filepath.Join(home, "gop")
+		if isValidGopRoot(gopRoot) {
+			return gopRoot, nil
+		}
+		goplusRoot := filepath.Join(home, "goplus")
+		if isValidGopRoot(goplusRoot) {
+			return goplusRoot, nil
+		}
+	}
+
+	return "", ErrNotFound
+}
+
+// Mockable for testing.
+var executable = func() (string, error) {
+	return os.Executable()
+}
+
+func executableRealPath() (path string, err error) {
+	path, err = executable()
+	if err != nil {
+		return
+	}
+
+	path, err = filepath.EvalSymlinks(path)
+	if err != nil {
+		return
+	}
+
+	path, err = filepath.Abs(path)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+func isFileExists(path string) bool {
+	st, err := os.Stat(path)
+	return err == nil && !st.IsDir()
+}
+
+func isDirExists(path string) bool {
+	st, err := os.Stat(path)
+	return err == nil && st.IsDir()
+}
+
+// A valid gop root has go.mod, go.sum, builtin/
+func isValidGopRoot(path string) bool {
+	if !isFileExists(filepath.Join(path, "go.mod")) ||
+		!isFileExists(filepath.Join(path, "go.sum")) ||
+		!isDirExists(filepath.Join(path, "builtin")) {
+		return false
+	}
+
+	return true
+}
diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go
index c2d535f2..dc0a101f 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -74,22 +74,6 @@ func saveGoFile(gofile string, pkg *gox.Package) error {
 	return gox.WriteFile(gofile, pkg, false)
 }
 
-func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
-	modfile, err = cl.FindGoModFile(dir)
-	if err != nil {
-		home := os.Getenv("HOME")
-		modfile = home + "/gop/go.mod"
-		if fi, e := os.Lstat(modfile); e == nil && !fi.IsDir() {
-			return modfile, true, nil
-		}
-		modfile = home + "/goplus/go.mod"
-		if fi, e := os.Lstat(modfile); e == nil && !fi.IsDir() {
-			return modfile, true, nil
-		}
-	}
-	return
-}
-
 func findGoModDir(dir string) (string, bool) {
 	modfile, nocachefile, err := findGoModFile(dir)
 	if err != nil {


Test patch:
diff --git a/cmd/internal/run/path_test.go b/cmd/internal/run/path_test.go
new file mode 100644
index 00000000..1e165bc2
--- /dev/null
+++ b/cmd/internal/run/path_test.go
@@ -0,0 +1,360 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package run
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/goplus/gop/build"
+)
+
+// Common testing directory structure:
+// testing_root/
+//   src/
+//	   subdir/
+//   valid_goproot/
+//	   go.mod
+//	   go.sum
+//     builtin/
+func makeTestDir(t *testing.T) (root string, src string, gopRoot string) {
+	root, _ = filepath.EvalSymlinks(t.TempDir())
+	src = filepath.Join(root, "src")
+	gopRoot = filepath.Join(root, "valid_goproot")
+	makeValidGopRoot(gopRoot)
+	os.Mkdir(src, 0755)
+	return
+}
+
+func makeValidGopRoot(root string) {
+	os.Mkdir(root, 0755)
+	os.Mkdir(filepath.Join(root, "builtin"), 0755)
+	ioutil.WriteFile(filepath.Join(root, "go.mod"), []byte(""), 0644)
+	ioutil.WriteFile(filepath.Join(root, "go.sum"), []byte(""), 0644)
+}
+
+func writeDummyFile(path string) {
+	ioutil.WriteFile(path, []byte(""), 0644)
+}
+
+func cleanup() {
+	os.Setenv("GOPROOT", "")
+	os.Setenv("HOME", "")
+	build.GopRoot = ""
+}
+
+func TestFindGoModFileInGoModDir(t *testing.T) {
+	cleanup()
+
+	t.Run("the src/ is a valid mod dir", func(tt *testing.T) {
+		tt.Cleanup(cleanup)
+		_, src, _ := makeTestDir(tt)
+		subdir := filepath.Join(src, "subdir")
+		writeDummyFile(filepath.Join(src, "go.mod"))
+		os.Mkdir(subdir, 0755)
+
+		{
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || modfile != filepath.Join(src, "go.mod") || noCacheFile {
+				tt.Fatal("got:", modfile, noCacheFile, err)
+			}
+		}
+
+		{
+			// Should found go.mod in parent dir
+			modfile, noCacheFile, err := findGoModFile(subdir)
+
+			if err != nil || modfile != filepath.Join(src, "go.mod") || noCacheFile {
+				tt.Fatal("got:", modfile, noCacheFile, err)
+			}
+		}
+	})
+
+	t.Run("the src/ is not a valid mod dir", func(tt *testing.T) {
+		tt.Cleanup(cleanup)
+		_, src, _ := makeTestDir(tt)
+
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err == nil {
+			tt.Fatal("should not found the mod file, but got:", modfile, noCacheFile)
+		}
+	})
+}
+
+func TestFindGoModFileInGopRoot(t *testing.T) {
+	origiExecutable := executable
+	t.Cleanup(func() {
+		executable = origiExecutable
+	})
+
+	home := t.TempDir()
+	bin := filepath.Join(home, "bin")
+
+	// Don't find go.mod in gop source dir when testing
+	os.Chdir(home)
+
+	cleanupAll := func() {
+		cleanup()
+		executable = func() (string, error) {
+			return filepath.Join(bin, "run"), nil
+		}
+	}
+	cleanupAll()
+
+	t.Run("without gop root", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, _, _ := makeTestDir(tt)
+
+		modfile, noCacheFile, err := findGoModFile(root)
+
+		if err == nil || noCacheFile || modfile != "" {
+			tt.Fatal("should not found go.mod without gop root, got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("set GOPROOT to a valid goproot path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		_, src, gopRoot := makeTestDir(tt)
+
+		os.Setenv("GOPROOT", gopRoot)
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err != nil || modfile != filepath.Join(gopRoot, "go.mod") || !noCacheFile {
+			tt.Fatal("should found mod file in GOPROOT, got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("set GOPROOT to an invalid goproot path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+		invalidGopRoot := filepath.Join(root, "invalid_goproot")
+
+		defer func() {
+			r := recover()
+			if r == nil {
+				tt.Fatal("should panic, but not")
+			}
+		}()
+
+		os.Setenv("GOPROOT", invalidGopRoot)
+		findGoModFile(src)
+	})
+
+	t.Run("set build.GopRoot to a valid goproot path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		_, src, gopRoot := makeTestDir(tt)
+
+		build.GopRoot = gopRoot
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err != nil || modfile != filepath.Join(gopRoot, "go.mod") || !noCacheFile {
+			tt.Fatal("should found go.mod in the dir of build.GopRoot, got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("set build.GopRoot to an invalid path", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+		invalidGopRoot := filepath.Join(root, "invalid_goproot")
+
+		build.GopRoot = invalidGopRoot
+
+		{
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod when build.GopRoot isn't exists, got:", modfile, noCacheFile, err)
+			}
+		}
+
+		{
+			os.Mkdir(invalidGopRoot, 0755)
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod when build.GopRoot isn't an valid gop root dir, got:", modfile, noCacheFile, err)
+			}
+		}
+	})
+
+	t.Run("use $HOME/gop or $HOME/goplus", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+		home := filepath.Join(root, "home")
+		os.Mkdir(home, 0755)
+		os.Setenv("HOME", home)
+
+		{
+			gopRoot := filepath.Join(home, "goplus")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/goplus, but got:", modfile, noCacheFile, err)
+			}
+		}
+
+		{
+			gopRoot := filepath.Join(home, "gop")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/gop, but got:", modfile, noCacheFile, err)
+			}
+		}
+	})
+
+	t.Run("check if parent dir of the executable is valid gop root", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		_, src, gopRoot := makeTestDir(tt)
+		bin := filepath.Join(gopRoot, "bin")
+		exePath := filepath.Join(bin, "run")
+		os.Mkdir(bin, 0755)
+		writeDummyFile(exePath)
+
+		// Mock executable location
+		executable = func() (string, error) {
+			return exePath, nil
+		}
+
+		modfile, noCacheFile, err := findGoModFile(src)
+
+		if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+			tt.Fatal("should found go.mod in gopRoot, but got:", modfile, noCacheFile, err)
+		}
+	})
+
+	t.Run("test gop root priority", func(tt *testing.T) {
+		tt.Cleanup(cleanupAll)
+		root, src, _ := makeTestDir(tt)
+
+		tt.Run("without gop root", func(tt *testing.T) {
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod without gop root, got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set HOME but hasn't $HOME/gop/ and $HOME/goplus/", func(tt *testing.T) {
+			os.Setenv("HOME", root)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err == nil || noCacheFile || modfile != "" {
+				tt.Fatal("should not found go.mod without gop root, got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set HOME, and has valid $HOME/goplus/", func(tt *testing.T) {
+			gopRoot := filepath.Join(root, "goplus")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/goplus, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set HOME, and has valid $HOME/gop/", func(tt *testing.T) {
+			gopRoot := filepath.Join(root, "gop")
+			makeValidGopRoot(gopRoot)
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/gop, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set build.GopRoot to an invalid gop root dir", func(tt *testing.T) {
+			gopRoot := filepath.Join(root, "gop")
+
+			build.GopRoot = filepath.Join(root, "invalid_goproot")
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(gopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in $HOME/gop, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set build.GopRoot to a valid gop root dir", func(tt *testing.T) {
+			newGopRoot := filepath.Join(root, "new_gop_root")
+			makeValidGopRoot(newGopRoot)
+
+			build.GopRoot = newGopRoot
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(newGopRoot, "go.mod") {
+				tt.Fatal("should found go.mod in new_gop_root/, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("the executable's parent dir is a valid gop root dir", func(tt *testing.T) {
+			newGopRoot2 := filepath.Join(root, "new_gop_root2")
+			makeValidGopRoot(newGopRoot2)
+			bin := filepath.Join(newGopRoot2, "bin")
+			exePath := filepath.Join(bin, "run")
+			os.Mkdir(bin, 0755)
+			writeDummyFile(exePath)
+			// Mock executable location
+			executable = func() (string, error) {
+				return exePath, nil
+			}
+
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(newGopRoot2, "go.mod") {
+				tt.Fatal("should found go.mod in new_gop_root2/, but got:", modfile, noCacheFile, err)
+			}
+		})
+
+		tt.Run("set GOPROOT to an invalid gop root dir", func(tt *testing.T) {
+			newGopRoot3 := filepath.Join(root, "new_gop_root3")
+
+			defer func() {
+				r := recover()
+				if r == nil {
+					tt.Fatal("should panic, but not")
+				}
+			}()
+
+			os.Setenv("GOPROOT", newGopRoot3)
+			findGoModFile(src)
+		})
+
+		tt.Run("set GOPROOT to a valid gop root dir", func(tt *testing.T) {
+			newGopRoot4 := filepath.Join(root, "new_gop_root4")
+			makeValidGopRoot(newGopRoot4)
+
+			os.Setenv("GOPROOT", newGopRoot4)
+			modfile, noCacheFile, err := findGoModFile(src)
+
+			if err != nil || !noCacheFile || modfile != filepath.Join(newGopRoot4, "go.mod") {
+				tt.Fatal("should found go.mod in new_gop_root3/, but got:", modfile, noCacheFile, err)
+			}
+		})
+	})
+}