diff --git a/buildcache.go b/buildcache.go index 4b6c63ab..44b2340a 100644 --- a/buildcache.go +++ b/buildcache.go @@ -5,16 +5,9 @@ import ( "os" "path/filepath" "time" -) -// Get the cache directory, usually ~/.cache/tinygo -func cacheDir() string { - dir, err := os.UserCacheDir() - if err != nil { - panic("could not find cache dir: " + err.Error()) - } - return filepath.Join(dir, "tinygo") -} + "github.com/tinygo-org/tinygo/goenv" +) // Return the newest timestamp of all the file paths passed in. Used to check // for stale caches. @@ -41,8 +34,7 @@ func cacheTimestamp(paths []string) (time.Time, error) { // TODO: the configKey is currently ignored. It is supposed to be used as extra // data for the cache key, like the compiler version and arguments. func cacheLoad(name, configKey string, sourceFiles []string) (string, error) { - dir := cacheDir() - cachepath := filepath.Join(dir, name) + cachepath := filepath.Join(goenv.Get("GOCACHE"), name) cacheStat, err := os.Stat(cachepath) if os.IsNotExist(err) { return "", nil // does not exist @@ -76,7 +68,7 @@ func cacheStore(tmppath, name, configKey string, sourceFiles []string) (string, // TODO: check the config key - dir := cacheDir() + dir := goenv.Get("GOCACHE") err := os.MkdirAll(dir, 0777) if err != nil { return "", err diff --git a/builtins.go b/builtins.go index 8ac127f7..15b94b66 100644 --- a/builtins.go +++ b/builtins.go @@ -10,6 +10,7 @@ import ( "time" "github.com/blakesmith/ar" + "github.com/tinygo-org/tinygo/goenv" ) // These are the GENERIC_SOURCES according to CMakeList.txt. @@ -169,13 +170,13 @@ func builtinFiles(target string) []string { // builtinsDir returns the directory where the sources for compiler-rt are kept. func builtinsDir() string { - return filepath.Join(sourceDir(), "lib", "compiler-rt", "lib", "builtins") + return filepath.Join(goenv.Get("TINYGOROOT"), "lib", "compiler-rt", "lib", "builtins") } // Get the builtins archive, possibly generating it as needed. func loadBuiltins(target string) (path string, err error) { // Try to load a precompiled compiler-rt library. - precompiledPath := filepath.Join(sourceDir(), "pkg", target, "compiler-rt.a") + precompiledPath := filepath.Join(goenv.Get("TINYGOROOT"), "pkg", target, "compiler-rt.a") if _, err := os.Stat(precompiledPath); err == nil { // Found a precompiled compiler-rt for this OS/architecture. Return the // path directly. diff --git a/goenv/goenv.go b/goenv/goenv.go new file mode 100644 index 00000000..c02d35df --- /dev/null +++ b/goenv/goenv.go @@ -0,0 +1,189 @@ +// 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 ( + "fmt" + "os" + "os/exec" + "os/user" + "path/filepath" + "runtime" +) + +// Keys is a slice of all available environment variable keys. +var Keys = []string{ + "GOOS", + "GOARCH", + "GOROOT", + "GOPATH", + "GOCACHE", + "TINYGOROOT", +} + +// 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": + if dir := os.Getenv("GOOS"); dir != "" { + return dir + } + return runtime.GOOS + case "GOARCH": + if dir := os.Getenv("GOARCH"); dir != "" { + return dir + } + return runtime.GOARCH + 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 "TINYGOROOT": + return sourceDir() + default: + return "" + } +} + +// 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 { + goroot := os.Getenv("GOROOT") + if goroot != "" { + // An explicitly set GOROOT always has preference. + return 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 + } + 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 +} diff --git a/linker-builtin.go b/linker-builtin.go index f11d5686..a9872e4b 100644 --- a/linker-builtin.go +++ b/linker-builtin.go @@ -9,6 +9,8 @@ import ( "os" "os/exec" "unsafe" + + "github.com/tinygo-org/tinygo/goenv" ) /* @@ -63,7 +65,7 @@ func Link(linker string, flags ...string) error { cmd := exec.Command(linker, flags...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Dir = sourceDir() + cmd.Dir = goenv.Get("TINYGOROOT") return cmd.Run() } } diff --git a/linker-external.go b/linker-external.go index f114b39b..082b3e54 100644 --- a/linker-external.go +++ b/linker-external.go @@ -8,6 +8,8 @@ package main import ( "os" "os/exec" + + "github.com/tinygo-org/tinygo/goenv" ) // Link invokes a linker with the given name and arguments. @@ -20,6 +22,6 @@ func Link(linker string, flags ...string) error { cmd := exec.Command(linker, flags...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Dir = sourceDir() + cmd.Dir = goenv.Get("TINYGOROOT") return cmd.Run() } diff --git a/main.go b/main.go index 120a5a91..aa621470 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "time" "github.com/tinygo-org/tinygo/compiler" + "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/interp" "github.com/tinygo-org/tinygo/loader" @@ -70,7 +71,7 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act config.gc = spec.GC } - root := sourceDir() + root := goenv.Get("TINYGOROOT") // Merge and adjust CFlags. cflags := append([]string{}, config.cFlags...) @@ -84,7 +85,7 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act ldflags = append(ldflags, strings.Replace(flag, "{root}", root, -1)) } - goroot := getGoroot() + goroot := goenv.Get("GOROOT") if goroot == "" { return errors.New("cannot locate $GOROOT, please set it manually") } @@ -123,7 +124,7 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act VerifyIR: config.verifyIR, TINYGOROOT: root, GOROOT: goroot, - GOPATH: getGopath(), + GOPATH: goenv.Get("GOPATH"), BuildTags: tags, TestConfig: config.testConfig, } @@ -454,7 +455,7 @@ func Flash(pkgName, target, port string, config *BuildConfig) error { cmd := exec.Command("/bin/sh", "-c", flashCmd) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Dir = sourceDir() + cmd.Dir = goenv.Get("TINYGOROOT") err := cmd.Run() if err != nil { return &commandError{"failed to flash", tmppath, err} @@ -719,7 +720,7 @@ func usage() { fmt.Fprintln(os.Stderr, " test: test packages") fmt.Fprintln(os.Stderr, " flash: compile and flash to the device") fmt.Fprintln(os.Stderr, " gdb: run/flash and immediately enter GDB") - fmt.Fprintln(os.Stderr, " clean: empty cache directory ("+cacheDir()+")") + fmt.Fprintln(os.Stderr, " clean: empty cache directory ("+goenv.Get("GOCACHE")+")") fmt.Fprintln(os.Stderr, " help: print this help text") fmt.Fprintln(os.Stderr, "\nflags:") flag.PrintDefaults() @@ -890,8 +891,7 @@ func main() { handleCompilerError(err) case "clean": // remove cache directory - dir := cacheDir() - err := os.RemoveAll(dir) + err := os.RemoveAll(goenv.Get("GOCACHE")) if err != nil { fmt.Fprintln(os.Stderr, "cannot clean cache:", err) os.Exit(1) diff --git a/target.go b/target.go index 25e9ff8d..92b4b564 100644 --- a/target.go +++ b/target.go @@ -8,17 +8,13 @@ import ( "io/ioutil" "os" "os/exec" - "os/user" "path/filepath" "regexp" "runtime" "strings" -) -// 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 + "github.com/tinygo-org/tinygo/goenv" +) // Target specification for a given target. Used for bare metal targets. // @@ -147,7 +143,7 @@ func (spec *TargetSpec) loadFromGivenStr(str string) error { if strings.HasSuffix(str, ".json") { path, _ = filepath.Abs(str) } else { - path = filepath.Join(sourceDir(), "targets", strings.ToLower(str)+".json") + path = filepath.Join(goenv.Get("TINYGOROOT"), "targets", strings.ToLower(str)+".json") } fp, err := os.Open(path) if err != nil { @@ -186,14 +182,8 @@ func LoadTarget(target string) (*TargetSpec, error) { if target == "" { // Configure based on GOOS/GOARCH environment variables (falling back to // runtime.GOOS/runtime.GOARCH), and generate a LLVM target based on it. - goos := os.Getenv("GOOS") - if goos == "" { - goos = runtime.GOOS - } - goarch := os.Getenv("GOARCH") - if goarch == "" { - goarch = runtime.GOARCH - } + goos := goenv.Get("GOOS") + goarch := goenv.Get("GOARCH") llvmos := goos llvmarch := map[string]string{ "386": "i386", @@ -315,141 +305,6 @@ func (spec *TargetSpec) OpenOCDConfiguration() (args []string, err error) { return args, 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(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 getGopath() string { - gopath := os.Getenv("GOPATH") - if gopath != "" { - return gopath - } - - // fallback - home := getHomeDir() - return filepath.Join(home, "go") -} - -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 { - goroot := os.Getenv("GOROOT") - if goroot != "" { - // An explicitly set GOROOT always has preference. - return 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 - } - 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 -} - // 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) {