Skip to content

Commit e075e05

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent 46d2696 commit e075e05

File tree

5 files changed

+64
-134
lines changed

5 files changed

+64
-134
lines changed

builder/config.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package builder
22

33
import (
4-
"errors"
54
"fmt"
65

76
"github.com/tinygo-org/tinygo/compileopts"
@@ -24,14 +23,9 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) {
2423
spec.OpenOCDCommands = options.OpenOCDCommands
2524
}
2625

27-
goroot := goenv.Get("GOROOT")
28-
if goroot == "" {
29-
return nil, errors.New("cannot locate $GOROOT, please set it manually")
30-
}
31-
32-
major, minor, err := goenv.GetGorootVersion(goroot)
26+
major, minor, err := goenv.GetGorootVersion()
3327
if err != nil {
34-
return nil, fmt.Errorf("could not read version from GOROOT (%v): %v", goroot, err)
28+
return nil, err
3529
}
3630
if major != 1 || minor < 18 || minor > 20 {
3731
// Note: when this gets updated, also update the Go compatibility matrix:

compiler/compiler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestCompiler(t *testing.T) {
2929
t.Parallel()
3030

3131
// Determine Go minor version (e.g. 16 in go1.16.3).
32-
_, goMinor, err := goenv.GetGorootVersion(goenv.Get("GOROOT"))
32+
_, goMinor, err := goenv.GetGorootVersion()
3333
if err != nil {
3434
t.Fatal("could not read Go version:", err)
3535
}

goenv/goenv.go

Lines changed: 53 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ package goenv
44

55
import (
66
"bytes"
7+
"encoding/json"
78
"errors"
89
"fmt"
910
"io/fs"
1011
"os"
1112
"os/exec"
12-
"os/user"
1313
"path/filepath"
1414
"runtime"
1515
"strings"
16+
"sync"
1617
)
1718

1819
// Keys is a slice of all available environment variable keys.
@@ -37,6 +38,53 @@ func init() {
3738
// directory.
3839
var TINYGOROOT string
3940

41+
// Variables read from a `go env` command invocation.
42+
var goEnvVars struct {
43+
GOPATH string
44+
GOROOT string
45+
GOVERSION string
46+
}
47+
48+
var goEnvVarsOnce sync.Once
49+
var goEnvVarsErr error // error returned from cmd.Run
50+
51+
// Make sure goEnvVars is fresh. This can be called multiple times, the first
52+
// time will update all environment variables in goEnvVars.
53+
func readGoEnvVars() error {
54+
goEnvVarsOnce.Do(func() {
55+
cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION")
56+
output, err := cmd.Output()
57+
if err != nil {
58+
// Check for "command not found" error.
59+
if execErr, ok := err.(*exec.Error); ok {
60+
goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err)
61+
return
62+
}
63+
// It's perhaps a bit ugly to handle this error here, but I couldn't
64+
// think of a better place further up in the call chain.
65+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 {
66+
if len(exitErr.Stderr) != 0 {
67+
// The 'go' command exited with an error message. Print that
68+
// message and exit, so we behave in a similar way.
69+
os.Stderr.Write(exitErr.Stderr)
70+
os.Exit(exitErr.ExitCode())
71+
}
72+
}
73+
// Other errors. Not sure whether there are any, but just in case.
74+
goEnvVarsErr = err
75+
return
76+
}
77+
err = json.Unmarshal(output, &goEnvVars)
78+
if err != nil {
79+
// This should never happen if we have a sane Go toolchain
80+
// installed.
81+
goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err)
82+
}
83+
})
84+
85+
return goEnvVarsErr
86+
}
87+
4088
// Get returns a single environment variable, possibly calculating it on-demand.
4189
// The empty string is returned for unknown environment variables.
4290
func Get(name string) string {
@@ -70,15 +118,11 @@ func Get(name string) string {
70118
// especially when floating point instructions are involved.
71119
return "6"
72120
case "GOROOT":
73-
return getGoroot()
121+
readGoEnvVars()
122+
return goEnvVars.GOROOT
74123
case "GOPATH":
75-
if dir := os.Getenv("GOPATH"); dir != "" {
76-
return dir
77-
}
78-
79-
// fallback
80-
home := getHomeDir()
81-
return filepath.Join(home, "go")
124+
readGoEnvVars()
125+
return goEnvVars.GOPATH
82126
case "GOCACHE":
83127
// Get the cache directory, usually ~/.cache/tinygo
84128
dir, err := os.UserCacheDir()
@@ -240,93 +284,3 @@ func isSourceDir(root string) bool {
240284
_, err = os.Stat(filepath.Join(root, "src/device/arm/arm.go"))
241285
return err == nil
242286
}
243-
244-
func getHomeDir() string {
245-
u, err := user.Current()
246-
if err != nil {
247-
panic("cannot get current user: " + err.Error())
248-
}
249-
if u.HomeDir == "" {
250-
// This is very unlikely, so panic here.
251-
// Not the nicest solution, however.
252-
panic("could not find home directory")
253-
}
254-
return u.HomeDir
255-
}
256-
257-
// getGoroot returns an appropriate GOROOT from various sources. If it can't be
258-
// found, it returns an empty string.
259-
func getGoroot() string {
260-
// An explicitly set GOROOT always has preference.
261-
goroot := os.Getenv("GOROOT")
262-
if goroot != "" {
263-
// Convert to the standard GOROOT being referenced, if it's a TinyGo cache.
264-
return getStandardGoroot(goroot)
265-
}
266-
267-
// Check for the location of the 'go' binary and base GOROOT on that.
268-
binpath, err := exec.LookPath("go")
269-
if err == nil {
270-
binpath, err = filepath.EvalSymlinks(binpath)
271-
if err == nil {
272-
goroot := filepath.Dir(filepath.Dir(binpath))
273-
if isGoroot(goroot) {
274-
return goroot
275-
}
276-
}
277-
}
278-
279-
// Check what GOROOT was at compile time.
280-
if isGoroot(runtime.GOROOT()) {
281-
return runtime.GOROOT()
282-
}
283-
284-
// Check for some standard locations, as a last resort.
285-
var candidates []string
286-
switch runtime.GOOS {
287-
case "linux":
288-
candidates = []string{
289-
"/usr/local/go", // manually installed
290-
"/usr/lib/go", // from the distribution
291-
"/snap/go/current/", // installed using snap
292-
}
293-
case "darwin":
294-
candidates = []string{
295-
"/usr/local/go", // manually installed
296-
"/usr/local/opt/go/libexec", // from Homebrew
297-
}
298-
}
299-
300-
for _, candidate := range candidates {
301-
if isGoroot(candidate) {
302-
return candidate
303-
}
304-
}
305-
306-
// Can't find GOROOT...
307-
return ""
308-
}
309-
310-
// isGoroot checks whether the given path looks like a GOROOT.
311-
func isGoroot(goroot string) bool {
312-
_, err := os.Stat(filepath.Join(goroot, "src", "runtime", "internal", "sys", "zversion.go"))
313-
return err == nil
314-
}
315-
316-
// getStandardGoroot returns the physical path to a real, standard Go GOROOT
317-
// implied by the given path.
318-
// If the given path appears to be a TinyGo cached GOROOT, it returns the path
319-
// referenced by symlinks contained in the cache. Otherwise, it returns the
320-
// given path as-is.
321-
func getStandardGoroot(path string) string {
322-
// Check if the "bin" subdirectory of our given GOROOT is a symlink, and then
323-
// return the _parent_ directory of its destination.
324-
if dest, err := os.Readlink(filepath.Join(path, "bin")); nil == err {
325-
// Clean the destination to remove any trailing slashes, so that
326-
// filepath.Dir will always return the parent.
327-
// (because both "/foo" and "/foo/" are valid symlink destinations,
328-
// but filepath.Dir would return "/" and "/foo", respectively)
329-
return filepath.Dir(filepath.Clean(dest))
330-
}
331-
return path
332-
}

goenv/version.go

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ import (
44
"errors"
55
"fmt"
66
"io"
7-
"os"
8-
"path/filepath"
9-
"regexp"
107
"strings"
118
)
129

@@ -22,8 +19,8 @@ var (
2219

2320
// GetGorootVersion returns the major and minor version for a given GOROOT path.
2421
// If the goroot cannot be determined, (0, 0) is returned.
25-
func GetGorootVersion(goroot string) (major, minor int, err error) {
26-
s, err := GorootVersionString(goroot)
22+
func GetGorootVersion() (major, minor int, err error) {
23+
s, err := GorootVersionString()
2724
if err != nil {
2825
return 0, 0, err
2926
}
@@ -51,24 +48,9 @@ func GetGorootVersion(goroot string) (major, minor int, err error) {
5148
}
5249

5350
// GorootVersionString returns the version string as reported by the Go
54-
// toolchain for the given GOROOT path. It is usually of the form `go1.x.y` but
55-
// can have some variations (for beta releases, for example).
56-
func GorootVersionString(goroot string) (string, error) {
57-
if data, err := os.ReadFile(filepath.Join(goroot, "VERSION")); err == nil {
58-
return string(data), nil
59-
60-
} else if data, err := os.ReadFile(filepath.Join(
61-
goroot, "src", "internal", "buildcfg", "zbootstrap.go")); err == nil {
62-
63-
r := regexp.MustCompile("const version = `(.*)`")
64-
matches := r.FindSubmatch(data)
65-
if len(matches) != 2 {
66-
return "", errors.New("Invalid go version output:\n" + string(data))
67-
}
68-
69-
return string(matches[1]), nil
70-
71-
} else {
72-
return "", err
73-
}
51+
// toolchain. It is usually of the form `go1.x.y` but can have some variations
52+
// (for beta releases, for example).
53+
func GorootVersionString() (string, error) {
54+
err := readGoEnvVars()
55+
return goEnvVars.GOVERSION, err
7456
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1871,7 +1871,7 @@ func main() {
18711871
usage(command)
18721872
case "version":
18731873
goversion := "<unknown>"
1874-
if s, err := goenv.GorootVersionString(goenv.Get("GOROOT")); err == nil {
1874+
if s, err := goenv.GorootVersionString(); err == nil {
18751875
goversion = s
18761876
}
18771877
version := goenv.Version

0 commit comments

Comments
 (0)