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