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.
Этот коммит содержится в:
Ayke van Laethem 2023-05-23 15:18:34 +02:00 коммит произвёл Ron Evans
родитель 46d2696363
коммит e075e0591d
5 изменённых файлов: 64 добавлений и 134 удалений

Просмотреть файл

@ -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:

Просмотреть файл

@ -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)
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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