// Package goenv returns environment variables that are used in various parts of // the compiler. You can query it manually with the `tinygo env` subcommand. package goenv import ( "bytes" "errors" "fmt" "io/fs" "os" "os/exec" "os/user" "path/filepath" "runtime" "strings" ) // Keys is a slice of all available environment variable keys. var Keys = []string{ "GOOS", "GOARCH", "GOROOT", "GOPATH", "GOCACHE", "CGO_ENABLED", "TINYGOROOT", } func init() { if Get("GOARCH") == "arm" { Keys = append(Keys, "GOARM") } } // TINYGOROOT is the path to the final location for checking tinygo files. If // unset (by a -X ldflag), then sourceDir() will fallback to the original build // directory. var TINYGOROOT string // 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 { switch name { case "GOOS": goos := os.Getenv("GOOS") if goos == "" { goos = runtime.GOOS } if goos == "android" { goos = "linux" } return goos case "GOARCH": if dir := os.Getenv("GOARCH"); dir != "" { return dir } return runtime.GOARCH case "GOARM": if goarm := os.Getenv("GOARM"); goarm != "" { return goarm } if goos := Get("GOOS"); goos == "windows" || goos == "android" { // Assume Windows and Android are running on modern CPU cores. // This matches upstream Go. return "7" } // Default to ARMv6 on other devices. // The difference between ARMv5 and ARMv6 is big, much bigger than the // difference between ARMv6 and ARMv7. ARMv6 binaries are much smaller, // especially when floating point instructions are involved. return "6" case "GOROOT": return getGoroot() case "GOPATH": if dir := os.Getenv("GOPATH"); dir != "" { return dir } // fallback home := getHomeDir() return filepath.Join(home, "go") case "GOCACHE": // Get the cache directory, usually ~/.cache/tinygo dir, err := os.UserCacheDir() if err != nil { panic("could not find cache dir: " + err.Error()) } return filepath.Join(dir, "tinygo") case "CGO_ENABLED": val := os.Getenv("CGO_ENABLED") if val == "1" || val == "0" { return val } // Default to enabling CGo. return "1" case "TINYGOROOT": return sourceDir() case "WASMOPT": if path := os.Getenv("WASMOPT"); path != "" { err := wasmOptCheckVersion(path) if err != nil { fmt.Fprintf(os.Stderr, "cannot use %q as wasm-opt (from WASMOPT environment variable): %s", path, err.Error()) os.Exit(1) } return path } return findWasmOpt() default: return "" } } // Find wasm-opt, or exit with an error. func findWasmOpt() string { tinygoroot := sourceDir() searchPaths := []string{ tinygoroot + "/bin/wasm-opt", tinygoroot + "/build/wasm-opt", } var paths []string for _, path := range searchPaths { if runtime.GOOS == "windows" { path += ".exe" } _, err := os.Stat(path) if err != nil && errors.Is(err, fs.ErrNotExist) { continue } paths = append(paths, path) } if path, err := exec.LookPath("wasm-opt"); err == nil { paths = append(paths, path) } if len(paths) == 0 { fmt.Fprintln(os.Stderr, "error: could not find wasm-opt, set the WASMOPT environment variable to override") os.Exit(1) } errs := make([]error, len(paths)) for i, path := range paths { err := wasmOptCheckVersion(path) if err == nil { return path } errs[i] = err } fmt.Fprintln(os.Stderr, "no usable wasm-opt found, update or run \"make binaryen\"") for i, path := range paths { fmt.Fprintf(os.Stderr, "\t%s: %s\n", path, errs[i].Error()) } os.Exit(1) panic("unreachable") } // wasmOptCheckVersion checks if a copy of wasm-opt is usable. func wasmOptCheckVersion(path string) error { cmd := exec.Command(path, "--version") var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { return err } str := buf.String() if strings.Contains(str, "(") { // The git tag may be placed in parentheses after the main version string. str = strings.Split(str, "(")[0] } str = strings.TrimSpace(str) var ver uint _, err = fmt.Sscanf(str, "wasm-opt version %d", &ver) if err != nil || ver < 102 { return errors.New("incompatible wasm-opt (need 102 or newer)") } return nil } // Return the TINYGOROOT, or exit with an error. func sourceDir() string { // Use $TINYGOROOT as root, if available. root := os.Getenv("TINYGOROOT") if root != "" { if !isSourceDir(root) { fmt.Fprintln(os.Stderr, "error: $TINYGOROOT was not set to the correct root") os.Exit(1) } return root } if TINYGOROOT != "" { if !isSourceDir(TINYGOROOT) { fmt.Fprintln(os.Stderr, "error: TINYGOROOT was not set to the correct root") os.Exit(1) } return TINYGOROOT } // Find root from executable path. path, err := os.Executable() if err != nil { // Very unlikely. Bail out if it happens. panic("could not get executable path: " + err.Error()) } root = filepath.Dir(filepath.Dir(path)) if isSourceDir(root) { return root } // Fallback: use the original directory from where it was built // https://stackoverflow.com/a/32163888/559350 _, path, _, _ = runtime.Caller(0) root = filepath.Dir(filepath.Dir(path)) if isSourceDir(root) { return root } fmt.Fprintln(os.Stderr, "error: could not autodetect root directory, set the TINYGOROOT environment variable to override") os.Exit(1) panic("unreachable") } // isSourceDir returns true if the directory looks like a TinyGo source directory. func isSourceDir(root string) bool { _, err := os.Stat(filepath.Join(root, "src/runtime/internal/sys/zversion.go")) if err != nil { return false } _, 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 }