goenv.go raw

   1  // Package goenv returns environment variables that are used in various parts of
   2  // the compiler. You can query it manually with the `moxie env` subcommand.
   3  package goenv
   4  
   5  import (
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io/fs"
  10  	"os"
  11  	"os/exec"
  12  	"path/filepath"
  13  	"runtime"
  14  	"strings"
  15  )
  16  
  17  // Keys is a slice of all available environment variable keys.
  18  var Keys = []string{
  19  	"GOOS",
  20  	"GOARCH",
  21  	"GOROOT",
  22  	"GOPATH",
  23  	"GOCACHE",
  24  	"CGO_ENABLED",
  25  	"MOXIEROOT",
  26  }
  27  
  28  // Set to true if we're linking statically against LLVM.
  29  var hasBuiltinTools = false
  30  
  31  // MOXIEROOT is the path to the final location for checking moxie files. If
  32  // unset (by a -X ldflag), then sourceDir() will fallback to the original build
  33  // directory.
  34  var MOXIEROOT string
  35  
  36  // If a particular Clang resource dir must always be used and Moxie can't
  37  // figure out the directory using heuristics, this global can be set using a
  38  // linker flag.
  39  // This is needed for Nix.
  40  var clangResourceDir string
  41  
  42  // Variables read from a `moxie env` command invocation.
  43  var goEnvVars struct {
  44  	GOPATH    string
  45  	GOROOT    string
  46  	GOVERSION string
  47  }
  48  
  49  var goEnvVarsDone bool
  50  var goEnvVarsErr error // error returned from cmd.Run
  51  
  52  // Make sure goEnvVars is fresh. This can be called multiple times, the first
  53  // time will update all environment variables in goEnvVars.
  54  func readGoEnvVars() error {
  55  	if goEnvVarsDone {
  56  		return goEnvVarsErr
  57  	}
  58  	goEnvVarsDone = true
  59  	func() {
  60  		cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION")
  61  		output, err := cmd.Output()
  62  		if err != nil {
  63  			// Check for "command not found" error.
  64  			if execErr, ok := err.(*exec.Error); ok {
  65  				goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err)
  66  				return
  67  			}
  68  			// It's perhaps a bit ugly to handle this error here, but I couldn't
  69  			// think of a better place further up in the call chain.
  70  			if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 {
  71  				if len(exitErr.Stderr) != 0 {
  72  					// The 'go' command exited with an error message. Print that
  73  					// message and exit, so we behave in a similar way.
  74  					os.Stderr.Write(exitErr.Stderr)
  75  					os.Exit(exitErr.ExitCode())
  76  				}
  77  			}
  78  			// Other errors. Not sure whether there are any, but just in case.
  79  			goEnvVarsErr = err
  80  			return
  81  		}
  82  		err = json.Unmarshal(output, &goEnvVars)
  83  		if err != nil {
  84  			// This should never happen if we have a sane Go toolchain
  85  			// installed.
  86  			goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err)
  87  		}
  88  	}()
  89  
  90  	return goEnvVarsErr
  91  }
  92  
  93  // Get returns a single environment variable, possibly calculating it on-demand.
  94  // The empty string is returned for unknown environment variables.
  95  func Get(name string) string {
  96  	switch name {
  97  	case "GOOS":
  98  		goos := os.Getenv("GOOS")
  99  		if goos == "" {
 100  			goos = runtime.GOOS
 101  		}
 102  		return goos
 103  	case "GOARCH":
 104  		if dir := os.Getenv("GOARCH"); dir != "" {
 105  			return dir
 106  		}
 107  		return runtime.GOARCH
 108  	case "GOROOT":
 109  		readGoEnvVars()
 110  		return goEnvVars.GOROOT
 111  	case "GOPATH":
 112  		readGoEnvVars()
 113  		return goEnvVars.GOPATH
 114  	case "GOCACHE":
 115  		dir, err := os.UserCacheDir()
 116  		if err != nil {
 117  			panic("could not find cache dir: " + err.Error())
 118  		}
 119  		return filepath.Join(dir, "moxie")
 120  	case "CGO_ENABLED":
 121  		return "1"
 122  	case "MOXIEROOT":
 123  		return sourceDir()
 124  	case "WASMOPT":
 125  		if s := os.Getenv("WASMOPT"); s != "" {
 126  			return s
 127  		}
 128  		return "wasm-opt"
 129  	default:
 130  		return ""
 131  	}
 132  }
 133  
 134  // Return the MOXIEROOT, or exit with an error.
 135  func sourceDir() string {
 136  	root := os.Getenv("MOXIEROOT")
 137  	if root != "" {
 138  		if !isSourceDir(root) {
 139  			fmt.Fprintln(os.Stderr, "error: $MOXIEROOT was not set to the correct root")
 140  			os.Exit(1)
 141  		}
 142  		return root
 143  	}
 144  
 145  	if MOXIEROOT != "" {
 146  		if !isSourceDir(MOXIEROOT) {
 147  			fmt.Fprintln(os.Stderr, "error: MOXIEROOT was not set to the correct root")
 148  			os.Exit(1)
 149  		}
 150  		return MOXIEROOT
 151  	}
 152  
 153  	// Find root from executable path.
 154  	path, err := os.Executable()
 155  	if err != nil {
 156  		panic("could not get executable path: " + err.Error())
 157  	}
 158  	root = filepath.Dir(filepath.Dir(path))
 159  	if isSourceDir(root) {
 160  		return root
 161  	}
 162  
 163  	// Fallback: use the original directory from where it was built.
 164  	_, path, _, _ = runtime.Caller(0)
 165  	root = filepath.Dir(filepath.Dir(path))
 166  	if isSourceDir(root) {
 167  		return root
 168  	}
 169  
 170  	fmt.Fprintln(os.Stderr, "error: could not autodetect root directory, set the MOXIEROOT environment variable to override")
 171  	os.Exit(1)
 172  	panic("unreachable")
 173  }
 174  
 175  // isSourceDir returns true if the directory looks like a moxie source directory.
 176  func isSourceDir(root string) bool {
 177  	_, err := os.Stat(filepath.Join(root, "src/runtime/internal/sys/zversion.mx"))
 178  	if err != nil {
 179  		_, err = os.Stat(filepath.Join(root, "src/runtime/internal/sys/zversion.go"))
 180  	}
 181  	return err == nil
 182  }
 183  
 184  // ClangResourceDir returns the clang resource dir. Uses llvm-config to detect.
 185  func ClangResourceDir() string {
 186  	if clangResourceDir != "" {
 187  		return clangResourceDir
 188  	}
 189  
 190  	root := Get("MOXIEROOT")
 191  	releaseHeaderDir := filepath.Join(root, "lib", "clang")
 192  	if _, err := os.Stat(releaseHeaderDir); !errors.Is(err, fs.ErrNotExist) {
 193  		return releaseHeaderDir
 194  	}
 195  
 196  	// Try versioned llvm-config first (e.g. llvm-config-19), then fall back
 197  	// to unversioned. The unversioned llvm-config may be a different LLVM
 198  	// version than the one linked into moxie.
 199  	var llvmMajor string
 200  	for _, cfgName := range []string{"llvm-config-19", "llvm-config"} {
 201  		cmd := exec.Command(cfgName, "--version")
 202  		out, err := cmd.Output()
 203  		if err == nil {
 204  			llvmMajor = strings.Split(strings.TrimSpace(string(out)), ".")[0]
 205  			break
 206  		}
 207  	}
 208  	if llvmMajor == "" {
 209  		return ""
 210  	}
 211  
 212  	switch runtime.GOOS {
 213  	case "linux":
 214  		// Check versioned install paths (e.g. /usr/lib/llvm19/lib/clang/19).
 215  		for _, base := range []string{
 216  			filepath.Join("/usr/lib/llvm"+llvmMajor, "lib", "clang", llvmMajor),
 217  			filepath.Join("/usr/lib/llvm-"+llvmMajor, "lib", "clang", llvmMajor),
 218  			filepath.Join("/usr/lib/clang", llvmMajor),
 219  		} {
 220  			if _, err := os.Stat(filepath.Join(base, "include", "stdint.h")); err == nil {
 221  				return base
 222  			}
 223  		}
 224  	case "darwin":
 225  		var prefix string
 226  		switch runtime.GOARCH {
 227  		case "amd64":
 228  			prefix = "/usr/local/opt/llvm@" + llvmMajor
 229  		case "arm64":
 230  			prefix = "/opt/homebrew/opt/llvm@" + llvmMajor
 231  		}
 232  		if prefix != "" {
 233  			path := fmt.Sprintf("%s/lib/clang/%s", prefix, llvmMajor)
 234  			if _, err := os.Stat(path + "/include/stdint.h"); err == nil {
 235  				return path
 236  			}
 237  		}
 238  	}
 239  
 240  	return ""
 241  }
 242