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