mirror of https://github.com/tinygo-org/tinygo.git
wasmstm32webassemblymicrocontrollerarmavrspiwasiadafruitarduinocircuitplayground-expressgpioi2cllvmmicrobitnrf51nrf52nrf52840samd21tinygo
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
11 KiB
389 lines
11 KiB
// 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"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
// 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")
|
|
}
|
|
}
|
|
|
|
// Set to true if we're linking statically against LLVM.
|
|
var hasBuiltinTools = false
|
|
|
|
// 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
|
|
|
|
// If a particular Clang resource dir must always be used and TinyGo can't
|
|
// figure out the directory using heuristics, this global can be set using a
|
|
// linker flag.
|
|
// This is needed for Nix.
|
|
var clangResourceDir string
|
|
|
|
// Variables read from a `go env` command invocation.
|
|
var goEnvVars struct {
|
|
GOPATH string
|
|
GOROOT string
|
|
GOVERSION string
|
|
}
|
|
|
|
var goEnvVarsOnce sync.Once
|
|
var goEnvVarsErr error // error returned from cmd.Run
|
|
|
|
// Make sure goEnvVars is fresh. This can be called multiple times, the first
|
|
// time will update all environment variables in goEnvVars.
|
|
func readGoEnvVars() error {
|
|
goEnvVarsOnce.Do(func() {
|
|
cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
// Check for "command not found" error.
|
|
if execErr, ok := err.(*exec.Error); ok {
|
|
goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err)
|
|
return
|
|
}
|
|
// It's perhaps a bit ugly to handle this error here, but I couldn't
|
|
// think of a better place further up in the call chain.
|
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 {
|
|
if len(exitErr.Stderr) != 0 {
|
|
// The 'go' command exited with an error message. Print that
|
|
// message and exit, so we behave in a similar way.
|
|
os.Stderr.Write(exitErr.Stderr)
|
|
os.Exit(exitErr.ExitCode())
|
|
}
|
|
}
|
|
// Other errors. Not sure whether there are any, but just in case.
|
|
goEnvVarsErr = err
|
|
return
|
|
}
|
|
err = json.Unmarshal(output, &goEnvVars)
|
|
if err != nil {
|
|
// This should never happen if we have a sane Go toolchain
|
|
// installed.
|
|
goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err)
|
|
}
|
|
})
|
|
|
|
return goEnvVarsErr
|
|
}
|
|
|
|
// 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":
|
|
readGoEnvVars()
|
|
return goEnvVars.GOROOT
|
|
case "GOPATH":
|
|
readGoEnvVars()
|
|
return goEnvVars.GOPATH
|
|
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
|
|
}
|
|
|
|
// ClangResourceDir returns the clang resource dir if available. This is the
|
|
// -resource-dir flag. If it isn't available, an empty string is returned and
|
|
// -resource-dir should be left unset.
|
|
// The libclang flag must be set if the resource dir is read for use by
|
|
// libclang.
|
|
// In that case, the resource dir is always returned (even when linking
|
|
// dynamically against LLVM) because libclang always needs this directory.
|
|
func ClangResourceDir(libclang bool) string {
|
|
if clangResourceDir != "" {
|
|
// The resource dir is forced to a particular value at build time.
|
|
// This is needed on Nix for example, where Clang and libclang don't
|
|
// know their own resource dir.
|
|
// Also see:
|
|
// https://discourse.nixos.org/t/why-is-the-clang-resource-dir-split-in-a-separate-package/34114
|
|
return clangResourceDir
|
|
}
|
|
|
|
if !hasBuiltinTools && !libclang {
|
|
// Using external tools, so the resource dir doesn't need to be
|
|
// specified. Clang knows where to find it.
|
|
return ""
|
|
}
|
|
|
|
// Check whether we're running from a TinyGo release directory.
|
|
// This is the case for release binaries on GitHub.
|
|
root := Get("TINYGOROOT")
|
|
releaseHeaderDir := filepath.Join(root, "lib", "clang")
|
|
if _, err := os.Stat(releaseHeaderDir); !errors.Is(err, fs.ErrNotExist) {
|
|
return releaseHeaderDir
|
|
}
|
|
|
|
if hasBuiltinTools {
|
|
// We are statically linked to LLVM.
|
|
// Check whether we're running from the source directory.
|
|
// This typically happens when TinyGo was built using `make` as part of
|
|
// development.
|
|
llvmMajor := strings.Split(llvm.Version, ".")[0]
|
|
buildResourceDir := filepath.Join(root, "llvm-build", "lib", "clang", llvmMajor)
|
|
if _, err := os.Stat(buildResourceDir); !errors.Is(err, fs.ErrNotExist) {
|
|
return buildResourceDir
|
|
}
|
|
} else {
|
|
// We use external tools, either when installed using `go install` or
|
|
// when packaged in a Linux distribution (Linux distros typically prefer
|
|
// dynamic linking).
|
|
// Try to detect the system clang resources directory.
|
|
resourceDir := findSystemClangResources(root)
|
|
if resourceDir != "" {
|
|
return resourceDir
|
|
}
|
|
}
|
|
|
|
// Resource directory not found.
|
|
return ""
|
|
}
|
|
|
|
// Find the Clang resource dir on this particular system.
|
|
// Return the empty string when they aren't found.
|
|
func findSystemClangResources(TINYGOROOT string) string {
|
|
llvmMajor := strings.Split(llvm.Version, ".")[0]
|
|
|
|
switch runtime.GOOS {
|
|
case "linux", "android":
|
|
// Header files are typically stored in /usr/lib/clang/<version>/include.
|
|
// Tested on Fedora 39, Debian 12, and Arch Linux.
|
|
path := filepath.Join("/usr/lib/clang", llvmMajor)
|
|
_, err := os.Stat(filepath.Join(path, "include", "stdint.h"))
|
|
if err == nil {
|
|
return path
|
|
}
|
|
case "darwin":
|
|
// This assumes a Homebrew installation, like in builder/commands.go.
|
|
var prefix string
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
prefix = "/usr/local/opt/llvm@" + llvmMajor
|
|
case "arm64":
|
|
prefix = "/opt/homebrew/opt/llvm@" + llvmMajor
|
|
default:
|
|
return "" // very unlikely for now
|
|
}
|
|
path := fmt.Sprintf("%s/lib/clang/%s", prefix, llvmMajor)
|
|
_, err := os.Stat(path + "/include/stdint.h")
|
|
if err == nil {
|
|
return path
|
|
}
|
|
}
|
|
|
|
// Could not find it.
|
|
return ""
|
|
}
|
|
|