Repository: goplus/gop
Pull request #979 (2021-11-29 15:07:31)

gop.mod


Patch:
diff --git a/.gitignore b/.gitignore
index d543e4a1..020f65ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,6 @@ _testmain.go
 coverage.txt
 
 .gop/
-gop_autogen.go
 gop_autogen*.go
 go.json
 *.cache
diff --git a/cl/compile.go b/cl/compile.go
index 65071302..5c150ebc 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -95,12 +95,8 @@ type Config struct {
 	// If WorkingDir or TargetDir is empty, it is same as Dir.
 	WorkingDir, TargetDir string
 
-	// ModPath specifies module path.
-	ModPath string
-
 	// ModRootDir specifies root dir of this module.
-	// If ModRootDir is empty, will lookup go.mod in all ancestor directories of Dir.
-	// If you specify ModPath, you should specify ModRootDir at the same time.
+	// If ModRootDir is empty, will lookup gop.mod in all ancestor directories of Dir.
 	ModRootDir string
 
 	// CacheFile specifies where cache data to write.
diff --git a/cl/internal/.gitignore b/cl/internal/.gitignore
new file mode 100644
index 00000000..7e2c1230
--- /dev/null
+++ b/cl/internal/.gitignore
@@ -0,0 +1,3 @@
+.gop/
+go.mod
+gop_autogen*.go
diff --git a/cl/internal/gop.mod b/cl/internal/gop.mod
new file mode 100644
index 00000000..bd102f50
--- /dev/null
+++ b/cl/internal/gop.mod
@@ -0,0 +1,4 @@
+module github.com/goplus/gop/cl/internal
+
+go 1.16
+gop 1.0
diff --git a/cl/packages.go b/cl/packages.go
index 14410f1a..b4f1b25e 100644
--- a/cl/packages.go
+++ b/cl/packages.go
@@ -19,37 +19,20 @@ package cl
 import (
 	"fmt"
 	"io/ioutil"
+	"log"
 	"os"
 	"path/filepath"
 	"strings"
 	"syscall"
 
+	"github.com/goplus/gop/env"
+	"github.com/goplus/gop/x/mod/modfile"
 	"github.com/goplus/gox"
-	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/go/packages"
 )
 
 // -----------------------------------------------------------------------------
 
-func FindGoModFile(dir string) (file string, err error) {
-	if dir == "" {
-		dir = "."
-	}
-	if dir, err = filepath.Abs(dir); err != nil {
-		return
-	}
-	for dir != "" {
-		file = filepath.Join(dir, "go.mod")
-		if fi, e := os.Lstat(file); e == nil && !fi.IsDir() {
-			return
-		}
-		if dir, file = filepath.Split(strings.TrimRight(dir, "/\\")); file == "" {
-			break
-		}
-	}
-	return "", syscall.ENOENT
-}
-
 func GetModulePath(file string) (pkgPath string, err error) {
 	src, err := ioutil.ReadFile(file)
 	if err != nil {
@@ -62,25 +45,6 @@ func GetModulePath(file string) (pkgPath string, err error) {
 	return f.Module.Mod.Path, nil
 }
 
-func findModPaths(dir string) (root string, modPath string, err error) {
-	file, err := FindGoModFile(dir)
-	if err == nil {
-		root, _ = filepath.Split(file)
-		modPath, err = GetModulePath(file)
-	}
-	return
-}
-
-func modPaths(conf *Config) (root string, modPath string) {
-	modPath = conf.ModPath
-	if modPath == "" {
-		root, modPath, _ = findModPaths(conf.Dir)
-	} else {
-		root = conf.ModRootDir
-	}
-	return
-}
-
 // -----------------------------------------------------------------------------
 
 type PkgsLoader struct {
@@ -88,12 +52,17 @@ type PkgsLoader struct {
 	genGoPkg   func(pkgDir string, base *Config) error
 	LoadPkgs   gox.LoadPkgsFunc
 	BaseConfig *Config
-	modPath    string
 }
 
 func initPkgsLoader(base *Config) {
-	base.ModRootDir, base.ModPath = modPaths(base)
-	p := &PkgsLoader{genGoPkg: base.GenGoPkg, BaseConfig: base, modPath: base.ModPath}
+	if base.ModRootDir == "" {
+		modfile, err := env.GOPMOD(base.Dir)
+		if err != nil {
+			log.Panicln("gop.mod not found:", err)
+		}
+		base.ModRootDir, _ = filepath.Split(modfile)
+	}
+	p := &PkgsLoader{genGoPkg: base.GenGoPkg, BaseConfig: base}
 	if base.PersistLoadPkgs {
 		if base.CacheFile == "" && base.ModRootDir != "" {
 			dir := base.ModRootDir + "/.gop"
@@ -123,10 +92,15 @@ func (p *PkgsLoader) GenGoPkgs(cfg *packages.Config, notFounds []string) (err er
 	if p.genGoPkg == nil {
 		return syscall.ENOENT
 	}
-	root, pkgPath, err := findModPaths(cfg.Dir)
+	modfile, err := env.GOPMOD(cfg.Dir)
+	if err != nil {
+		return
+	}
+	pkgPath, err := GetModulePath(modfile)
 	if err != nil {
 		return
 	}
+	root, _ := filepath.Split(modfile)
 	pkgPathSlash := pkgPath + "/"
 	for _, notFound := range notFounds {
 		if strings.HasPrefix(notFound, pkgPathSlash) || notFound == pkgPath {
diff --git a/cmd/internal/env/env.go b/cmd/internal/env/env.go
index 4fd7b993..a69bc51b 100644
--- a/cmd/internal/env/env.go
+++ b/cmd/internal/env/env.go
@@ -66,7 +66,9 @@ func runCmd(_ *base.Command, args []string) {
 
 	gopEnv["GOPVERSION"] = env.Version()
 	gopEnv["GOPROOT"] = env.GOPROOT()
-	// TODO: gopEnv["GOPMOD"]
+	gopEnv["GOPMOD"], _ = env.GOPMOD("")
+	gopEnv["GOPATH()"] = env.GOPATH() // TODO: remove (only for debug)
+	gopEnv["GOMODCACHE()"] = env.GOMODCACHE()
 
 	onlyValues := false
 	vars := flag.Args()
diff --git a/cmd/internal/modfetch/cache.go b/cmd/internal/modfetch/cache.go
index 912aa1e4..2959b699 100644
--- a/cmd/internal/modfetch/cache.go
+++ b/cmd/internal/modfetch/cache.go
@@ -22,7 +22,7 @@ import (
 	"golang.org/x/mod/module"
 	"golang.org/x/mod/semver"
 
-	"github.com/goplus/gop/cmd/internal/env"
+	"github.com/goplus/gop/env"
 )
 
 var (
diff --git a/cmd/internal/run/_run.go b/cmd/internal/run/_run.go
new file mode 100644
index 00000000..966dc11a
--- /dev/null
+++ b/cmd/internal/run/_run.go
@@ -0,0 +1,309 @@
+/*
+ * 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 implements the ``gop run'' command.
+package run
+
+import (
+	"crypto/sha1"
+	"encoding/base64"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+
+	"github.com/qiniu/x/log"
+	"golang.org/x/tools/go/packages"
+
+	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/cl"
+	"github.com/goplus/gop/cmd/gengo"
+	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/parser"
+	"github.com/goplus/gop/scanner"
+	"github.com/goplus/gop/token"
+	"github.com/goplus/gox"
+)
+
+// -----------------------------------------------------------------------------
+
+// Cmd - gop run
+var Cmd = &base.Command{
+	UsageLine: "gop run [-asm -quiet -debug -nr -gop -prof] <gopSrcDir|gopSrcFile>",
+	Short:     "Run a Go+ program",
+}
+
+var (
+	flag        = &Cmd.Flag
+	flagAsm     = flag.Bool("asm", false, "generates `asm` code of Go+ bytecode backend")
+	flagVerbose = flag.Bool("v", false, "print verbose information")
+	flagQuiet   = flag.Bool("quiet", false, "don't generate any compiling stage log")
+	flagDebug   = flag.Bool("debug", false, "set log level to debug")
+	flagNorun   = flag.Bool("nr", false, "don't run if no change")
+	flagRTOE    = flag.Bool("rtoe", false, "remove tempfile on error")
+	flagGop     = flag.Bool("gop", false, "parse a .go file as a .gop file")
+	flagProf    = flag.Bool("prof", false, "do profile and generate profile report")
+)
+
+const (
+	parserMode = parser.ParseComments
+)
+
+func init() {
+	Cmd.Run = runCmd
+}
+
+func saveGoFile(gofile string, pkg *gox.Package) error {
+	dir := filepath.Dir(gofile)
+	err := os.MkdirAll(dir, 0755)
+	if err != nil {
+		return err
+	}
+	return gox.WriteFile(gofile, pkg, false)
+}
+
+/*
+func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
+	modfile, err = cl.FindGoModFile(dir)
+	if err != nil {
+		modfile = filepath.Join(env.GOPROOT(), "go.mod")
+		return modfile, true, nil
+	}
+	return
+}
+
+func findModDir(dir string, fname string) (string, bool) {
+	modfile, nocachefile, err := findModFile(dir, fname)
+	if err != nil {
+		log.Fatalln("findModFile:", err)
+	}
+	return filepath.Dir(modfile), nocachefile
+}
+*/
+
+func runCmd(cmd *base.Command, args []string) {
+	err := flag.Parse(args)
+	if err != nil {
+		log.Fatalln("parse input arguments failed:", err)
+	}
+	if flag.NArg() < 1 {
+		cmd.Usage(os.Stderr)
+	}
+	args = flag.Args()[1:]
+
+	if *flagQuiet {
+		log.SetOutputLevel(0x7000)
+	} else if *flagDebug {
+		log.SetOutputLevel(log.Ldebug)
+		gox.SetDebug(gox.DbgFlagAll)
+		cl.SetDebug(cl.DbgFlagAll)
+	}
+	if *flagVerbose {
+		gox.SetDebug(gox.DbgFlagAll &^ gox.DbgFlagComments)
+		cl.SetDebug(cl.DbgFlagAll)
+		cl.SetDisableRecover(true)
+	} else if *flagAsm {
+		gox.SetDebug(gox.DbgFlagInstruction)
+	}
+	if *flagProf {
+		panic("TODO: profile not impl")
+	}
+
+	fset := token.NewFileSet()
+	src, _ := filepath.Abs(flag.Arg(0))
+	fi, err := os.Stat(src)
+	if err != nil {
+		log.Fatalln("input arg check failed:", err)
+	}
+	isDir := fi.IsDir()
+
+	var isDirty bool
+	var srcDir, file, gofile string
+	var pkgs map[string]*ast.Package
+	onExit = nil
+	if isDir {
+		srcDir = src
+		gofile = src + "/gop_autogen.go"
+		modload.Load()
+		isDirty = true // TODO: check if code changed
+		if isDirty {
+			pkgs, err = parser.ParseDir(fset, src, nil, parserMode)
+		} else if *flagNorun {
+			return
+		}
+	} else {
+		srcDir, file = filepath.Split(src)
+		modload.Load()
+		isGo := filepath.Ext(file) == ".go"
+		if isGo {
+			hash := sha1.Sum([]byte(src))
+			dir := os.Getenv("HOME") + "/.gop/run"
+			os.MkdirAll(dir, 0755)
+			gofile = dir + "/g" + base64.RawURLEncoding.EncodeToString(hash[:]) + file
+			if *flagRTOE { // remove tempfile on error
+				onExit = func() {
+					os.Remove(gofile)
+				}
+			}
+		} else if hasMultiFiles(srcDir, ".gop") {
+			gofile = filepath.Join(srcDir, "gop_autogen_"+file+".go")
+		} else {
+			gofile = srcDir + "/gop_autogen.go"
+		}
+		isDirty = fileIsDirty(fi, gofile)
+		if isDirty {
+			if isGo {
+				fmt.Println("==> GenGo to", gofile)
+			}
+			if *flagGop {
+				pkgs, err = parser.Parse(fset, src, nil, parserMode)
+			} else {
+				pkgs, err = parser.Parse(fset, src, nil, 0) // TODO: only to check dependencies
+			}
+		} else if *flagNorun {
+			return
+		}
+	}
+	if err != nil {
+		scanner.PrintError(os.Stderr, err)
+		os.Exit(10)
+	}
+
+	if isDirty {
+		mainPkg, ok := pkgs["main"]
+		if !ok {
+			if len(pkgs) == 0 && isDir { // not a Go+ package, try runGoPkg
+				runGoPkg(src, args, true)
+				return
+			}
+			fmt.Fprintln(os.Stderr, "TODO: not a main package")
+			os.Exit(12)
+		}
+
+		modDir, noCacheFile := findModDir(srcDir, "gop.mod")
+		conf := &cl.Config{
+			Dir: modDir, TargetDir: srcDir, Fset: fset, CacheLoadPkgs: true, PersistLoadPkgs: !noCacheFile}
+		out, err := cl.NewPackage("", mainPkg, conf)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			os.Exit(11)
+		}
+		err = saveGoFile(gofile, out)
+		if err != nil {
+			log.Fatalln("saveGoFile failed:", err)
+		}
+		conf.PkgsLoader.Save()
+	}
+
+	goRun(gofile, args)
+	if *flagProf {
+		panic("TODO: profile not impl")
+	}
+}
+
+func fileIsDirty(fi os.FileInfo, gofile string) bool {
+	fiDest, err := os.Stat(gofile)
+	if err != nil {
+		return true
+	}
+	return fi.ModTime().After(fiDest.ModTime())
+}
+
+var (
+	onExit func()
+)
+
+func goRun(file string, args []string) {
+	goArgs := make([]string, len(args)+2)
+	goArgs[0] = "run"
+	goArgs[1] = file
+	copy(goArgs[2:], args)
+	cmd := exec.Command("go", goArgs...)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Env = os.Environ()
+	err := cmd.Run()
+	if err != nil {
+		if onExit != nil {
+			onExit()
+		}
+		switch e := err.(type) {
+		case *exec.ExitError:
+			os.Exit(e.ExitCode())
+		default:
+			log.Fatalln("go run failed:", err)
+		}
+	}
+}
+
+func runGoPkg(src string, args []string, doRun bool) {
+	modfile, noCacheFile, err := findModFile(src, "gop.mod")
+	if err != nil {
+		log.Fatalln("findModFile:", err)
+	}
+	base := filepath.Dir(modfile)
+	rel, _ := filepath.Rel(base, src)
+	modPath, err := cl.GetModulePath(modfile)
+	if err != nil {
+		log.Fatalln("GetModulePath:", err)
+	}
+	pkgPath := filepath.Join(modPath, rel)
+	const (
+		loadTypes = packages.NeedImports | packages.NeedDeps | packages.NeedTypes
+		loadModes = loadTypes | packages.NeedName | packages.NeedModule
+	)
+	baseConf := &cl.Config{
+		Fset:            token.NewFileSet(),
+		GenGoPkg:        new(gengo.Runner).GenGoPkg,
+		CacheLoadPkgs:   true,
+		PersistLoadPkgs: !noCacheFile,
+		NoFileLine:      true,
+	}
+	loadConf := &packages.Config{Mode: loadModes, Fset: baseConf.Fset}
+	pkgs, err := baseConf.Ensure().PkgsLoader.Load(loadConf, pkgPath)
+	if err != nil || len(pkgs) == 0 {
+		log.Fatalln("PkgsLoader.Load failed:", err)
+	}
+	if pkgs[0].Name != "main" {
+		fmt.Fprintln(os.Stderr, "TODO: not a main package")
+		os.Exit(12)
+	}
+	baseConf.PkgsLoader.Save()
+	if doRun {
+		goRun(src+"/.", args)
+	}
+}
+
+func hasMultiFiles(srcDir string, ext string) bool {
+	var has bool
+	if f, err := os.Open(srcDir); err == nil {
+		defer f.Close()
+		fis, _ := f.ReadDir(-1)
+		for _, fi := range fis {
+			if !fi.IsDir() && filepath.Ext(fi.Name()) == ext {
+				if has {
+					return true
+				}
+				has = true
+			}
+		}
+	}
+	return false
+}
+
+// -----------------------------------------------------------------------------
diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go
index 3edc5072..af712853 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -78,8 +78,7 @@ func saveGoFile(gofile string, pkg *gox.Package) error {
 }
 
 func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
-	modfile, err = cl.FindGoModFile(dir)
-
+	modfile, err = env.GOPMOD(dir)
 	if err != nil {
 		modfile = filepath.Join(env.GOPROOT(), "go.mod")
 		return modfile, true, nil
diff --git a/env/goenv.go b/env/goenv.go
new file mode 100644
index 00000000..dc8d1b12
--- /dev/null
+++ b/env/goenv.go
@@ -0,0 +1,65 @@
+/*
+ * 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 env
+
+import (
+	"os"
+	"path/filepath"
+	"runtime"
+)
+
+// -----------------------------------------------------------------------------
+
+func GOPATH() string {
+	s := os.Getenv("GOPATH")
+	if s == "" {
+		return defaultGOPATH()
+	}
+	return s
+}
+
+func defaultGOPATH() string {
+	if home := os.Getenv(envHOME); home != "" {
+		def := filepath.Join(home, "go")
+		if filepath.Clean(def) == filepath.Clean(runtime.GOROOT()) {
+			// Don't set the default GOPATH to GOROOT,
+			// as that will trigger warnings from the go tool.
+			return ""
+		}
+		return def
+	}
+	return ""
+}
+
+// -----------------------------------------------------------------------------
+
+func GOMODCACHE() string {
+	val := os.Getenv("GOMODCACHE")
+	if val == "" {
+		return gopathJoin("pkg/mod")
+	}
+	return val
+}
+
+func gopathJoin(rel string) string {
+	list := filepath.SplitList(GOPATH())
+	if len(list) == 0 || list[0] == "" {
+		return ""
+	}
+	return filepath.Join(list[0], rel)
+}
+
+// -----------------------------------------------------------------------------
diff --git a/env/mod.go b/env/mod.go
new file mode 100644
index 00000000..a916ed7f
--- /dev/null
+++ b/env/mod.go
@@ -0,0 +1,51 @@
+/*
+ * 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 env
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"syscall"
+)
+
+// -----------------------------------------------------------------------------
+
+func GOPMOD(dir string) (file string, err error) {
+	if dir == "" {
+		dir = "."
+	}
+	if dir, err = filepath.Abs(dir); err != nil {
+		return
+	}
+	for dir != "" {
+		file = filepath.Join(dir, "gop.mod")
+		if fi, e := os.Lstat(file); e == nil && !fi.IsDir() {
+			return
+		}
+		file = filepath.Join(dir, "go.mod")
+		if fi, e := os.Lstat(file); e == nil && !fi.IsDir() {
+			return
+		}
+		if dir, file = filepath.Split(strings.TrimRight(dir, "/\\")); file == "" {
+			break
+		}
+	}
+	return "", syscall.ENOENT
+}
+
+// -----------------------------------------------------------------------------
diff --git a/env/path.go b/env/path.go
index 7d72e7c8..8aab424e 100644
--- a/env/path.go
+++ b/env/path.go
@@ -36,13 +36,8 @@ func GOPROOT() string {
 	return gopRoot
 }
 
-const (
-	errNotFound = syscall.ENOENT
-)
-
 const (
 	envGOPROOT = "GOPROOT"
-	envHOME    = "HOME"
 )
 
 func findGopRoot() (string, error) {
@@ -81,7 +76,7 @@ func findGopRoot() (string, error) {
 			return goplusRoot, nil
 		}
 	}
-	return "", errNotFound
+	return "", syscall.ENOENT
 }
 
 // Mockable for testing.
diff --git a/cmd/internal/env/goenv.go b/env/sys_others.go
similarity index 64%
rename from cmd/internal/env/goenv.go
rename to env/sys_others.go
index f691966c..305816f3 100644
--- a/cmd/internal/env/goenv.go
+++ b/env/sys_others.go
@@ -1,3 +1,6 @@
+//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || netbsd || openbsd || solaris
+// +build aix darwin dragonfly freebsd js,wasm linux netbsd openbsd solaris
+
 /*
  * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
  *
@@ -13,30 +16,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package env
 
-import (
-	"go/build"
-	"os"
-	"path/filepath"
-)
+package env
 
-var (
-	GOPATH = build.Default.GOPATH
+const (
+	envHOME = "HOME"
 )
-
-func GOMODCACHE() string {
-	val := os.Getenv("GOMODCACHE")
-	if val == "" {
-		val = gopathJoin("pkg/mod")
-	}
-	return val
-}
-
-func gopathJoin(rel string) string {
-	list := filepath.SplitList(GOPATH)
-	if len(list) == 0 || list[0] == "" {
-		return ""
-	}
-	return filepath.Join(list[0], rel)
-}
diff --git a/env/sys_plan9.go b/env/sys_plan9.go
new file mode 100644
index 00000000..11a567f5
--- /dev/null
+++ b/env/sys_plan9.go
@@ -0,0 +1,21 @@
+/*
+ * 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 env
+
+const (
+	envHOME = "home"
+)
diff --git a/env/sys_windows.go b/env/sys_windows.go
new file mode 100644
index 00000000..40149490
--- /dev/null
+++ b/env/sys_windows.go
@@ -0,0 +1,21 @@
+/*
+ * 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 env
+
+const (
+	envHOME = "USERPROFILE"
+)


Test patch:
diff --git a/cl/compile_test.go b/cl/compile_test.go
index a1d9c99c..0c1d1322 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -43,8 +43,7 @@ func init() {
 	gblFset = token.NewFileSet()
 	baseConf = &cl.Config{
 		Fset:          gblFset,
-		ModPath:       "github.com/goplus/gop",
-		ModRootDir:    ".",
+		ModRootDir:    "./internal",
 		GenGoPkg:      new(gengo.Runner).GenGoPkg,
 		CacheLoadPkgs: true,
 		NoFileLine:    true,
diff --git a/env/mod_test.go b/env/mod_test.go
new file mode 100644
index 00000000..a990bf06
--- /dev/null
+++ b/env/mod_test.go
@@ -0,0 +1,99 @@
+/*
+ * 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 env
+
+import (
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+const (
+	sep = string(os.PathSeparator)
+)
+
+func TestGopMod(t *testing.T) {
+	file, err := GOPMOD("")
+	if err != nil {
+		t.Fatal("GOPMOD failed:", err)
+	}
+	if !strings.HasSuffix(file, "gop"+sep+"go.mod") {
+		t.Fatal("GOPMOD failed:", file)
+	}
+	file, err = GOPMOD("../cl/internal/spx")
+	if err != nil {
+		t.Fatal("GOPMOD spx failed:", err)
+	}
+	if !strings.HasSuffix(file, "cl"+sep+"internal"+sep+"gop.mod") {
+		t.Fatal("GOPMOD spx failed:", file)
+	}
+}
+
+func TestGOPATH(t *testing.T) {
+	os.Setenv("GOPATH", "")
+	gopath := GOPATH()
+	home := os.Getenv(envHOME)
+	expect := filepath.Join(home, "go")
+	if expect == filepath.Clean(runtime.GOROOT()) {
+		if gopath != "" {
+			t.Fatal("TestGOPATH failed:", gopath)
+		}
+	} else if gopath != expect {
+		t.Fatal("TestGOPATH failed:", gopath)
+	}
+	const ugopath = "/abc/work"
+	os.Setenv("GOPATH", ugopath)
+	gopath = GOPATH()
+	if gopath != ugopath {
+		t.Fatal("TestGOPATH failed:", gopath)
+	}
+
+	os.Setenv("GOPATH", "")
+	os.Setenv(envHOME, "")
+	defer os.Setenv(envHOME, home)
+	if gopath := GOPATH(); gopath != "" {
+		t.Fatal("TestGOPATH failed:", gopath)
+	}
+}
+
+func TestGOMODCACHE(t *testing.T) {
+	gopath := GOPATH()
+	os.Setenv("GOMODCACHE", "")
+	modcache := GOMODCACHE()
+	if filepath.Join(gopath, "pkg/mod") != modcache {
+		t.Fatal("TestGOMODCACHE failed:", modcache)
+	}
+	const umodcache = "/abc/mod"
+	os.Setenv("GOMODCACHE", umodcache)
+	modcache = GOMODCACHE()
+	if modcache != umodcache {
+		t.Fatal("TestGOMODCACHE (umodcache) failed:", modcache)
+	}
+	/* TODO:
+	os.Setenv("GOMODCACHE", "")
+	os.Setenv("GOPATH", "")
+	home := os.Getenv(envHOME)
+	defer os.Setenv(envHOME, home)
+	os.Setenv(envHOME, strings.TrimRight(runtime.GOROOT(), "go"))
+	modcache = GOMODCACHE()
+	if modcache != "" {
+		t.Fatal("TestGOMODCACHE (GOROOT) failed:", modcache)
+	}
+	*/
+}
diff --git a/env/path_test.go b/env/path_test.go
index 164de546..f20a30e1 100644
--- a/env/path_test.go
+++ b/env/path_test.go
@@ -21,13 +21,10 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
-
-	"github.com/goplus/gop/cl"
 )
 
 func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
-	modfile, err = cl.FindGoModFile(dir)
-
+	modfile, err = GOPMOD(dir)
 	if err != nil {
 		gopRoot, err := findGopRoot()
 		if err == nil {
@@ -68,7 +65,7 @@ func writeDummyFile(path string) {
 
 func cleanup() {
 	os.Setenv("GOPROOT", "")
-	os.Setenv("HOME", "")
+	os.Setenv(envHOME, "")
 	defaultGopRoot = ""
 }
 
@@ -224,7 +221,7 @@ func TestFindGoModFileInGopRoot(t *testing.T) {
 		root, src, _ := makeTestDir(tt)
 		home := filepath.Join(root, "home")
 		os.Mkdir(home, 0755)
-		os.Setenv("HOME", home)
+		os.Setenv(envHOME, home)
 
 		{
 			gopRoot := filepath.Join(home, "goplus")
@@ -282,7 +279,7 @@ func TestFindGoModFileInGopRoot(t *testing.T) {
 		})
 
 		tt.Run("set HOME but hasn't $HOME/gop/ and $HOME/goplus/", func(tt *testing.T) {
-			os.Setenv("HOME", root)
+			os.Setenv(envHOME, root)
 
 			modfile, noCacheFile, err := findGoModFile(src)
 
diff --git a/env/version_test.go b/env/version_test.go
index e4f77726..0a4fd1e6 100644
--- a/env/version_test.go
+++ b/env/version_test.go
@@ -1,3 +1,19 @@
+/*
+ * 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 env
 
 import "testing"