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