diff options
author | Ayke van Laethem <[email protected]> | 2023-05-23 15:18:34 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2023-07-07 16:55:59 +0200 |
commit | e075e0591d555d3e657858f5186627f412dd500f (patch) | |
tree | ab94fd1e066c033ab93c88120b271b326336dee6 | |
parent | 46d2696363271dc3ca11d0672b255b25d72afbc2 (diff) | |
download | tinygo-e075e0591d555d3e657858f5186627f412dd500f.tar.gz tinygo-e075e0591d555d3e657858f5186627f412dd500f.zip |
main: use `go env` instead of doing all detection manually
This replaces our own manual detection of various variables (GOROOT,
GOPATH, Go version) with a simple call to `go env`.
If the `go` command is not found:
error: could not find 'go' command: executable file not found in $PATH
If the Go version is too old:
error: requires go version 1.18 through 1.20, got go1.17
If the Go tool itself outputs an error (using GOROOT=foobar here):
go: cannot find GOROOT directory: foobar
This does break the case where `go` wasn't available in $PATH but we
would detect it anyway (via some hardcoded OS-dependent paths). I'm not
sure we want to fix that: I think it's better to tell users "make sure
`go version` prints the right value" than to do some automagic detection
of Go binary locations.
-rw-r--r-- | builder/config.go | 10 | ||||
-rw-r--r-- | compiler/compiler_test.go | 2 | ||||
-rw-r--r-- | goenv/goenv.go | 152 | ||||
-rw-r--r-- | goenv/version.go | 32 | ||||
-rw-r--r-- | main.go | 2 |
5 files changed, 64 insertions, 134 deletions
diff --git a/builder/config.go b/builder/config.go index f9fe715b7..1ca3aa641 100644 --- a/builder/config.go +++ b/builder/config.go @@ -1,7 +1,6 @@ package builder import ( - "errors" "fmt" "github.com/tinygo-org/tinygo/compileopts" @@ -24,14 +23,9 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) { spec.OpenOCDCommands = options.OpenOCDCommands } - goroot := goenv.Get("GOROOT") - if goroot == "" { - return nil, errors.New("cannot locate $GOROOT, please set it manually") - } - - major, minor, err := goenv.GetGorootVersion(goroot) + major, minor, err := goenv.GetGorootVersion() if err != nil { - return nil, fmt.Errorf("could not read version from GOROOT (%v): %v", goroot, err) + return nil, err } if major != 1 || minor < 18 || minor > 20 { // Note: when this gets updated, also update the Go compatibility matrix: diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 6b5fbe13c..9675c4028 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -29,7 +29,7 @@ func TestCompiler(t *testing.T) { t.Parallel() // Determine Go minor version (e.g. 16 in go1.16.3). - _, goMinor, err := goenv.GetGorootVersion(goenv.Get("GOROOT")) + _, goMinor, err := goenv.GetGorootVersion() if err != nil { t.Fatal("could not read Go version:", err) } diff --git a/goenv/goenv.go b/goenv/goenv.go index d87f6f2e9..f6a32502a 100644 --- a/goenv/goenv.go +++ b/goenv/goenv.go @@ -4,15 +4,16 @@ package goenv import ( "bytes" + "encoding/json" "errors" "fmt" "io/fs" "os" "os/exec" - "os/user" "path/filepath" "runtime" "strings" + "sync" ) // Keys is a slice of all available environment variable keys. @@ -37,6 +38,53 @@ func init() { // directory. var TINYGOROOT string +// Variables read from a `go env` command invocation. +var goEnvVars struct { + GOPATH string + GOROOT string + GOVERSION string +} + +var goEnvVarsOnce sync.Once +var goEnvVarsErr error // error returned from cmd.Run + +// Make sure goEnvVars is fresh. This can be called multiple times, the first +// time will update all environment variables in goEnvVars. +func readGoEnvVars() error { + goEnvVarsOnce.Do(func() { + cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION") + output, err := cmd.Output() + if err != nil { + // Check for "command not found" error. + if execErr, ok := err.(*exec.Error); ok { + goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err) + return + } + // It's perhaps a bit ugly to handle this error here, but I couldn't + // think of a better place further up in the call chain. + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 { + if len(exitErr.Stderr) != 0 { + // The 'go' command exited with an error message. Print that + // message and exit, so we behave in a similar way. + os.Stderr.Write(exitErr.Stderr) + os.Exit(exitErr.ExitCode()) + } + } + // Other errors. Not sure whether there are any, but just in case. + goEnvVarsErr = err + return + } + err = json.Unmarshal(output, &goEnvVars) + if err != nil { + // This should never happen if we have a sane Go toolchain + // installed. + goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err) + } + }) + + return goEnvVarsErr +} + // Get returns a single environment variable, possibly calculating it on-demand. // The empty string is returned for unknown environment variables. func Get(name string) string { @@ -70,15 +118,11 @@ func Get(name string) string { // especially when floating point instructions are involved. return "6" case "GOROOT": - return getGoroot() + readGoEnvVars() + return goEnvVars.GOROOT case "GOPATH": - if dir := os.Getenv("GOPATH"); dir != "" { - return dir - } - - // fallback - home := getHomeDir() - return filepath.Join(home, "go") + readGoEnvVars() + return goEnvVars.GOPATH case "GOCACHE": // Get the cache directory, usually ~/.cache/tinygo dir, err := os.UserCacheDir() @@ -240,93 +284,3 @@ func isSourceDir(root string) bool { _, err = os.Stat(filepath.Join(root, "src/device/arm/arm.go")) return err == nil } - -func getHomeDir() string { - u, err := user.Current() - if err != nil { - panic("cannot get current user: " + err.Error()) - } - if u.HomeDir == "" { - // This is very unlikely, so panic here. - // Not the nicest solution, however. - panic("could not find home directory") - } - return u.HomeDir -} - -// getGoroot returns an appropriate GOROOT from various sources. If it can't be -// found, it returns an empty string. -func getGoroot() string { - // An explicitly set GOROOT always has preference. - goroot := os.Getenv("GOROOT") - if goroot != "" { - // Convert to the standard GOROOT being referenced, if it's a TinyGo cache. - return getStandardGoroot(goroot) - } - - // Check for the location of the 'go' binary and base GOROOT on that. - binpath, err := exec.LookPath("go") - if err == nil { - binpath, err = filepath.EvalSymlinks(binpath) - if err == nil { - goroot := filepath.Dir(filepath.Dir(binpath)) - if isGoroot(goroot) { - return goroot - } - } - } - - // Check what GOROOT was at compile time. - if isGoroot(runtime.GOROOT()) { - return runtime.GOROOT() - } - - // Check for some standard locations, as a last resort. - var candidates []string - switch runtime.GOOS { - case "linux": - candidates = []string{ - "/usr/local/go", // manually installed - "/usr/lib/go", // from the distribution - "/snap/go/current/", // installed using snap - } - case "darwin": - candidates = []string{ - "/usr/local/go", // manually installed - "/usr/local/opt/go/libexec", // from Homebrew - } - } - - for _, candidate := range candidates { - if isGoroot(candidate) { - return candidate - } - } - - // Can't find GOROOT... - return "" -} - -// isGoroot checks whether the given path looks like a GOROOT. -func isGoroot(goroot string) bool { - _, err := os.Stat(filepath.Join(goroot, "src", "runtime", "internal", "sys", "zversion.go")) - return err == nil -} - -// getStandardGoroot returns the physical path to a real, standard Go GOROOT -// implied by the given path. -// If the given path appears to be a TinyGo cached GOROOT, it returns the path -// referenced by symlinks contained in the cache. Otherwise, it returns the -// given path as-is. -func getStandardGoroot(path string) string { - // Check if the "bin" subdirectory of our given GOROOT is a symlink, and then - // return the _parent_ directory of its destination. - if dest, err := os.Readlink(filepath.Join(path, "bin")); nil == err { - // Clean the destination to remove any trailing slashes, so that - // filepath.Dir will always return the parent. - // (because both "/foo" and "/foo/" are valid symlink destinations, - // but filepath.Dir would return "/" and "/foo", respectively) - return filepath.Dir(filepath.Clean(dest)) - } - return path -} diff --git a/goenv/version.go b/goenv/version.go index 4be6bcbc0..d234fb3de 100644 --- a/goenv/version.go +++ b/goenv/version.go @@ -4,9 +4,6 @@ import ( "errors" "fmt" "io" - "os" - "path/filepath" - "regexp" "strings" ) @@ -22,8 +19,8 @@ var ( // GetGorootVersion returns the major and minor version for a given GOROOT path. // If the goroot cannot be determined, (0, 0) is returned. -func GetGorootVersion(goroot string) (major, minor int, err error) { - s, err := GorootVersionString(goroot) +func GetGorootVersion() (major, minor int, err error) { + s, err := GorootVersionString() if err != nil { return 0, 0, err } @@ -51,24 +48,9 @@ func GetGorootVersion(goroot string) (major, minor int, err error) { } // GorootVersionString returns the version string as reported by the Go -// toolchain for the given GOROOT path. It is usually of the form `go1.x.y` but -// can have some variations (for beta releases, for example). -func GorootVersionString(goroot string) (string, error) { - if data, err := os.ReadFile(filepath.Join(goroot, "VERSION")); err == nil { - return string(data), nil - - } else if data, err := os.ReadFile(filepath.Join( - goroot, "src", "internal", "buildcfg", "zbootstrap.go")); err == nil { - - r := regexp.MustCompile("const version = `(.*)`") - matches := r.FindSubmatch(data) - if len(matches) != 2 { - return "", errors.New("Invalid go version output:\n" + string(data)) - } - - return string(matches[1]), nil - - } else { - return "", err - } +// toolchain. It is usually of the form `go1.x.y` but can have some variations +// (for beta releases, for example). +func GorootVersionString() (string, error) { + err := readGoEnvVars() + return goEnvVars.GOVERSION, err } @@ -1871,7 +1871,7 @@ func main() { usage(command) case "version": goversion := "<unknown>" - if s, err := goenv.GorootVersionString(goenv.Get("GOROOT")); err == nil { + if s, err := goenv.GorootVersionString(); err == nil { goversion = s } version := goenv.Version |