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