mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
f188eaf5f9
Previously, the compiler would default to hardfloat. This is not supported by some MIPS CPUs. This took me much longer than it should have because of a quirk in the LLVM Mips backend: if the target-features string is not set (like during LTO), the Mips backend picks the first function in the module and uses that. Unfortunately, in the case of TinyGo this first function is `llvm.dbg.value`, which is an LLVM intrinsic and doesn't have the target-features string. I fixed it by adding a `-mllvm -mattr=` flag to the linker.
402 lines
11 KiB
Go
402 lines
11 KiB
Go
// 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() {
|
|
switch Get("GOARCH") {
|
|
case "arm":
|
|
Keys = append(Keys, "GOARM")
|
|
case "mips", "mipsle":
|
|
Keys = append(Keys, "GOMIPS")
|
|
}
|
|
}
|
|
|
|
// 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 "GOMIPS":
|
|
gomips := os.Getenv("GOMIPS")
|
|
if gomips == "" {
|
|
// Default to hardfloat (this matches the Go toolchain).
|
|
gomips = "hardfloat"
|
|
}
|
|
return gomips
|
|
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":
|
|
// Always enable CGo. It is required by a number of targets, including
|
|
// macOS and the rp2040.
|
|
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()
|
|
case "WASMTOOLS":
|
|
if path := os.Getenv("WASMTOOLS"); path != "" {
|
|
return path
|
|
}
|
|
return "wasm-tools"
|
|
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 ""
|
|
}
|