@@ -4,15 +4,16 @@ package goenv
44
55import (
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.
3839var 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.
4290func 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- }
0 commit comments