aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2023-05-23 15:18:34 +0200
committerRon Evans <[email protected]>2023-07-07 16:55:59 +0200
commite075e0591d555d3e657858f5186627f412dd500f (patch)
treeab94fd1e066c033ab93c88120b271b326336dee6
parent46d2696363271dc3ca11d0672b255b25d72afbc2 (diff)
downloadtinygo-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.go10
-rw-r--r--compiler/compiler_test.go2
-rw-r--r--goenv/goenv.go152
-rw-r--r--goenv/version.go32
-rw-r--r--main.go2
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
}
diff --git a/main.go b/main.go
index a6a16f92d..7af036f17 100644
--- a/main.go
+++ b/main.go
@@ -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