github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/goenv/goenv.go (about)

     1  // Package goenv returns environment variables that are used in various parts of
     2  // the compiler. You can query it manually with the `tinygo env` subcommand.
     3  package goenv
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io/fs"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"sync"
    17  
    18  	"tinygo.org/x/go-llvm"
    19  )
    20  
    21  // Keys is a slice of all available environment variable keys.
    22  var Keys = []string{
    23  	"GOOS",
    24  	"GOARCH",
    25  	"GOROOT",
    26  	"GOPATH",
    27  	"GOCACHE",
    28  	"CGO_ENABLED",
    29  	"TINYGOROOT",
    30  }
    31  
    32  func init() {
    33  	if Get("GOARCH") == "arm" {
    34  		Keys = append(Keys, "GOARM")
    35  	}
    36  }
    37  
    38  // Set to true if we're linking statically against LLVM.
    39  var hasBuiltinTools = false
    40  
    41  // TINYGOROOT is the path to the final location for checking tinygo files. If
    42  // unset (by a -X ldflag), then sourceDir() will fallback to the original build
    43  // directory.
    44  var TINYGOROOT string
    45  
    46  // If a particular Clang resource dir must always be used and TinyGo can't
    47  // figure out the directory using heuristics, this global can be set using a
    48  // linker flag.
    49  // This is needed for Nix.
    50  var clangResourceDir string
    51  
    52  // Variables read from a `go env` command invocation.
    53  var goEnvVars struct {
    54  	GOPATH    string
    55  	GOROOT    string
    56  	GOVERSION string
    57  }
    58  
    59  var goEnvVarsOnce sync.Once
    60  var goEnvVarsErr error // error returned from cmd.Run
    61  
    62  // Make sure goEnvVars is fresh. This can be called multiple times, the first
    63  // time will update all environment variables in goEnvVars.
    64  func readGoEnvVars() error {
    65  	goEnvVarsOnce.Do(func() {
    66  		cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION")
    67  		output, err := cmd.Output()
    68  		if err != nil {
    69  			// Check for "command not found" error.
    70  			if execErr, ok := err.(*exec.Error); ok {
    71  				goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err)
    72  				return
    73  			}
    74  			// It's perhaps a bit ugly to handle this error here, but I couldn't
    75  			// think of a better place further up in the call chain.
    76  			if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 {
    77  				if len(exitErr.Stderr) != 0 {
    78  					// The 'go' command exited with an error message. Print that
    79  					// message and exit, so we behave in a similar way.
    80  					os.Stderr.Write(exitErr.Stderr)
    81  					os.Exit(exitErr.ExitCode())
    82  				}
    83  			}
    84  			// Other errors. Not sure whether there are any, but just in case.
    85  			goEnvVarsErr = err
    86  			return
    87  		}
    88  		err = json.Unmarshal(output, &goEnvVars)
    89  		if err != nil {
    90  			// This should never happen if we have a sane Go toolchain
    91  			// installed.
    92  			goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err)
    93  		}
    94  	})
    95  
    96  	return goEnvVarsErr
    97  }
    98  
    99  // Get returns a single environment variable, possibly calculating it on-demand.
   100  // The empty string is returned for unknown environment variables.
   101  func Get(name string) string {
   102  	switch name {
   103  	case "GOOS":
   104  		goos := os.Getenv("GOOS")
   105  		if goos == "" {
   106  			goos = runtime.GOOS
   107  		}
   108  		if goos == "android" {
   109  			goos = "linux"
   110  		}
   111  		return goos
   112  	case "GOARCH":
   113  		if dir := os.Getenv("GOARCH"); dir != "" {
   114  			return dir
   115  		}
   116  		return runtime.GOARCH
   117  	case "GOARM":
   118  		if goarm := os.Getenv("GOARM"); goarm != "" {
   119  			return goarm
   120  		}
   121  		if goos := Get("GOOS"); goos == "windows" || goos == "android" {
   122  			// Assume Windows and Android are running on modern CPU cores.
   123  			// This matches upstream Go.
   124  			return "7"
   125  		}
   126  		// Default to ARMv6 on other devices.
   127  		// The difference between ARMv5 and ARMv6 is big, much bigger than the
   128  		// difference between ARMv6 and ARMv7. ARMv6 binaries are much smaller,
   129  		// especially when floating point instructions are involved.
   130  		return "6"
   131  	case "GOROOT":
   132  		readGoEnvVars()
   133  		return goEnvVars.GOROOT
   134  	case "GOPATH":
   135  		readGoEnvVars()
   136  		return goEnvVars.GOPATH
   137  	case "GOCACHE":
   138  		// Get the cache directory, usually ~/.cache/tinygo
   139  		dir, err := os.UserCacheDir()
   140  		if err != nil {
   141  			panic("could not find cache dir: " + err.Error())
   142  		}
   143  		return filepath.Join(dir, "tinygo")
   144  	case "CGO_ENABLED":
   145  		// Always enable CGo. It is required by a number of targets, including
   146  		// macOS and the rp2040.
   147  		return "1"
   148  	case "TINYGOROOT":
   149  		return sourceDir()
   150  	case "WASMOPT":
   151  		if path := os.Getenv("WASMOPT"); path != "" {
   152  			err := wasmOptCheckVersion(path)
   153  			if err != nil {
   154  				fmt.Fprintf(os.Stderr, "cannot use %q as wasm-opt (from WASMOPT environment variable): %s", path, err.Error())
   155  				os.Exit(1)
   156  			}
   157  
   158  			return path
   159  		}
   160  
   161  		return findWasmOpt()
   162  	default:
   163  		return ""
   164  	}
   165  }
   166  
   167  // Find wasm-opt, or exit with an error.
   168  func findWasmOpt() string {
   169  	tinygoroot := sourceDir()
   170  	searchPaths := []string{
   171  		tinygoroot + "/bin/wasm-opt",
   172  		tinygoroot + "/build/wasm-opt",
   173  	}
   174  
   175  	var paths []string
   176  	for _, path := range searchPaths {
   177  		if runtime.GOOS == "windows" {
   178  			path += ".exe"
   179  		}
   180  
   181  		_, err := os.Stat(path)
   182  		if err != nil && errors.Is(err, fs.ErrNotExist) {
   183  			continue
   184  		}
   185  
   186  		paths = append(paths, path)
   187  	}
   188  
   189  	if path, err := exec.LookPath("wasm-opt"); err == nil {
   190  		paths = append(paths, path)
   191  	}
   192  
   193  	if len(paths) == 0 {
   194  		fmt.Fprintln(os.Stderr, "error: could not find wasm-opt, set the WASMOPT environment variable to override")
   195  		os.Exit(1)
   196  	}
   197  
   198  	errs := make([]error, len(paths))
   199  	for i, path := range paths {
   200  		err := wasmOptCheckVersion(path)
   201  		if err == nil {
   202  			return path
   203  		}
   204  
   205  		errs[i] = err
   206  	}
   207  	fmt.Fprintln(os.Stderr, "no usable wasm-opt found, update or run \"make binaryen\"")
   208  	for i, path := range paths {
   209  		fmt.Fprintf(os.Stderr, "\t%s: %s\n", path, errs[i].Error())
   210  	}
   211  	os.Exit(1)
   212  	panic("unreachable")
   213  }
   214  
   215  // wasmOptCheckVersion checks if a copy of wasm-opt is usable.
   216  func wasmOptCheckVersion(path string) error {
   217  	cmd := exec.Command(path, "--version")
   218  	var buf bytes.Buffer
   219  	cmd.Stdout = &buf
   220  	cmd.Stderr = os.Stderr
   221  	err := cmd.Run()
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	str := buf.String()
   227  	if strings.Contains(str, "(") {
   228  		// The git tag may be placed in parentheses after the main version string.
   229  		str = strings.Split(str, "(")[0]
   230  	}
   231  
   232  	str = strings.TrimSpace(str)
   233  	var ver uint
   234  	_, err = fmt.Sscanf(str, "wasm-opt version %d", &ver)
   235  	if err != nil || ver < 102 {
   236  		return errors.New("incompatible wasm-opt (need 102 or newer)")
   237  	}
   238  
   239  	return nil
   240  }
   241  
   242  // Return the TINYGOROOT, or exit with an error.
   243  func sourceDir() string {
   244  	// Use $TINYGOROOT as root, if available.
   245  	root := os.Getenv("TINYGOROOT")
   246  	if root != "" {
   247  		if !isSourceDir(root) {
   248  			fmt.Fprintln(os.Stderr, "error: $TINYGOROOT was not set to the correct root")
   249  			os.Exit(1)
   250  		}
   251  		return root
   252  	}
   253  
   254  	if TINYGOROOT != "" {
   255  		if !isSourceDir(TINYGOROOT) {
   256  			fmt.Fprintln(os.Stderr, "error: TINYGOROOT was not set to the correct root")
   257  			os.Exit(1)
   258  		}
   259  		return TINYGOROOT
   260  	}
   261  
   262  	// Find root from executable path.
   263  	path, err := os.Executable()
   264  	if err != nil {
   265  		// Very unlikely. Bail out if it happens.
   266  		panic("could not get executable path: " + err.Error())
   267  	}
   268  	root = filepath.Dir(filepath.Dir(path))
   269  	if isSourceDir(root) {
   270  		return root
   271  	}
   272  
   273  	// Fallback: use the original directory from where it was built
   274  	// https://stackoverflow.com/a/32163888/559350
   275  	_, path, _, _ = runtime.Caller(0)
   276  	root = filepath.Dir(filepath.Dir(path))
   277  	if isSourceDir(root) {
   278  		return root
   279  	}
   280  
   281  	fmt.Fprintln(os.Stderr, "error: could not autodetect root directory, set the TINYGOROOT environment variable to override")
   282  	os.Exit(1)
   283  	panic("unreachable")
   284  }
   285  
   286  // isSourceDir returns true if the directory looks like a TinyGo source directory.
   287  func isSourceDir(root string) bool {
   288  	_, err := os.Stat(filepath.Join(root, "src/runtime/internal/sys/zversion.go"))
   289  	if err != nil {
   290  		return false
   291  	}
   292  	_, err = os.Stat(filepath.Join(root, "src/device/arm/arm.go"))
   293  	return err == nil
   294  }
   295  
   296  // ClangResourceDir returns the clang resource dir if available. This is the
   297  // -resource-dir flag. If it isn't available, an empty string is returned and
   298  // -resource-dir should be left unset.
   299  // The libclang flag must be set if the resource dir is read for use by
   300  // libclang.
   301  // In that case, the resource dir is always returned (even when linking
   302  // dynamically against LLVM) because libclang always needs this directory.
   303  func ClangResourceDir(libclang bool) string {
   304  	if clangResourceDir != "" {
   305  		// The resource dir is forced to a particular value at build time.
   306  		// This is needed on Nix for example, where Clang and libclang don't
   307  		// know their own resource dir.
   308  		// Also see:
   309  		// https://discourse.nixos.org/t/why-is-the-clang-resource-dir-split-in-a-separate-package/34114
   310  		return clangResourceDir
   311  	}
   312  
   313  	if !hasBuiltinTools && !libclang {
   314  		// Using external tools, so the resource dir doesn't need to be
   315  		// specified. Clang knows where to find it.
   316  		return ""
   317  	}
   318  
   319  	// Check whether we're running from a TinyGo release directory.
   320  	// This is the case for release binaries on GitHub.
   321  	root := Get("TINYGOROOT")
   322  	releaseHeaderDir := filepath.Join(root, "lib", "clang")
   323  	if _, err := os.Stat(releaseHeaderDir); !errors.Is(err, fs.ErrNotExist) {
   324  		return releaseHeaderDir
   325  	}
   326  
   327  	if hasBuiltinTools {
   328  		// We are statically linked to LLVM.
   329  		// Check whether we're running from the source directory.
   330  		// This typically happens when TinyGo was built using `make` as part of
   331  		// development.
   332  		llvmMajor := strings.Split(llvm.Version, ".")[0]
   333  		buildResourceDir := filepath.Join(root, "llvm-build", "lib", "clang", llvmMajor)
   334  		if _, err := os.Stat(buildResourceDir); !errors.Is(err, fs.ErrNotExist) {
   335  			return buildResourceDir
   336  		}
   337  	} else {
   338  		// We use external tools, either when installed using `go install` or
   339  		// when packaged in a Linux distribution (Linux distros typically prefer
   340  		// dynamic linking).
   341  		// Try to detect the system clang resources directory.
   342  		resourceDir := findSystemClangResources(root)
   343  		if resourceDir != "" {
   344  			return resourceDir
   345  		}
   346  	}
   347  
   348  	// Resource directory not found.
   349  	return ""
   350  }
   351  
   352  // Find the Clang resource dir on this particular system.
   353  // Return the empty string when they aren't found.
   354  func findSystemClangResources(TINYGOROOT string) string {
   355  	llvmMajor := strings.Split(llvm.Version, ".")[0]
   356  
   357  	switch runtime.GOOS {
   358  	case "linux", "android":
   359  		// Header files are typically stored in /usr/lib/clang/<version>/include.
   360  		// Tested on Fedora 39, Debian 12, and Arch Linux.
   361  		path := filepath.Join("/usr/lib/clang", llvmMajor)
   362  		_, err := os.Stat(filepath.Join(path, "include", "stdint.h"))
   363  		if err == nil {
   364  			return path
   365  		}
   366  	case "darwin":
   367  		// This assumes a Homebrew installation, like in builder/commands.go.
   368  		var prefix string
   369  		switch runtime.GOARCH {
   370  		case "amd64":
   371  			prefix = "/usr/local/opt/llvm@" + llvmMajor
   372  		case "arm64":
   373  			prefix = "/opt/homebrew/opt/llvm@" + llvmMajor
   374  		default:
   375  			return "" // very unlikely for now
   376  		}
   377  		path := fmt.Sprintf("%s/lib/clang/%s", prefix, llvmMajor)
   378  		_, err := os.Stat(path + "/include/stdint.h")
   379  		if err == nil {
   380  			return path
   381  		}
   382  	}
   383  
   384  	// Could not find it.
   385  	return ""
   386  }