github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/goenv/goenv.go (about) 1 // Package goenv returns environment variables that are used in various parts of 2 // the compiler. You can query it manually with the `tinygo env` subcommand. 3 package goenv 4 5 import ( 6 "bytes" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io/fs" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "runtime" 15 "strings" 16 "sync" 17 18 "tinygo.org/x/go-llvm" 19 ) 20 21 // Keys is a slice of all available environment variable keys. 22 var Keys = []string{ 23 "GOOS", 24 "GOARCH", 25 "GOROOT", 26 "GOPATH", 27 "GOCACHE", 28 "CGO_ENABLED", 29 "TINYGOROOT", 30 } 31 32 func init() { 33 if Get("GOARCH") == "arm" { 34 Keys = append(Keys, "GOARM") 35 } 36 } 37 38 // Set to true if we're linking statically against LLVM. 39 var hasBuiltinTools = false 40 41 // TINYGOROOT is the path to the final location for checking tinygo files. If 42 // unset (by a -X ldflag), then sourceDir() will fallback to the original build 43 // directory. 44 var TINYGOROOT string 45 46 // If a particular Clang resource dir must always be used and TinyGo can't 47 // figure out the directory using heuristics, this global can be set using a 48 // linker flag. 49 // This is needed for Nix. 50 var clangResourceDir string 51 52 // Variables read from a `go env` command invocation. 53 var goEnvVars struct { 54 GOPATH string 55 GOROOT string 56 GOVERSION string 57 } 58 59 var goEnvVarsOnce sync.Once 60 var goEnvVarsErr error // error returned from cmd.Run 61 62 // Make sure goEnvVars is fresh. This can be called multiple times, the first 63 // time will update all environment variables in goEnvVars. 64 func readGoEnvVars() error { 65 goEnvVarsOnce.Do(func() { 66 cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION") 67 output, err := cmd.Output() 68 if err != nil { 69 // Check for "command not found" error. 70 if execErr, ok := err.(*exec.Error); ok { 71 goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err) 72 return 73 } 74 // It's perhaps a bit ugly to handle this error here, but I couldn't 75 // think of a better place further up in the call chain. 76 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 { 77 if len(exitErr.Stderr) != 0 { 78 // The 'go' command exited with an error message. Print that 79 // message and exit, so we behave in a similar way. 80 os.Stderr.Write(exitErr.Stderr) 81 os.Exit(exitErr.ExitCode()) 82 } 83 } 84 // Other errors. Not sure whether there are any, but just in case. 85 goEnvVarsErr = err 86 return 87 } 88 err = json.Unmarshal(output, &goEnvVars) 89 if err != nil { 90 // This should never happen if we have a sane Go toolchain 91 // installed. 92 goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err) 93 } 94 }) 95 96 return goEnvVarsErr 97 } 98 99 // Get returns a single environment variable, possibly calculating it on-demand. 100 // The empty string is returned for unknown environment variables. 101 func Get(name string) string { 102 switch name { 103 case "GOOS": 104 goos := os.Getenv("GOOS") 105 if goos == "" { 106 goos = runtime.GOOS 107 } 108 if goos == "android" { 109 goos = "linux" 110 } 111 return goos 112 case "GOARCH": 113 if dir := os.Getenv("GOARCH"); dir != "" { 114 return dir 115 } 116 return runtime.GOARCH 117 case "GOARM": 118 if goarm := os.Getenv("GOARM"); goarm != "" { 119 return goarm 120 } 121 if goos := Get("GOOS"); goos == "windows" || goos == "android" { 122 // Assume Windows and Android are running on modern CPU cores. 123 // This matches upstream Go. 124 return "7" 125 } 126 // Default to ARMv6 on other devices. 127 // The difference between ARMv5 and ARMv6 is big, much bigger than the 128 // difference between ARMv6 and ARMv7. ARMv6 binaries are much smaller, 129 // especially when floating point instructions are involved. 130 return "6" 131 case "GOROOT": 132 readGoEnvVars() 133 return goEnvVars.GOROOT 134 case "GOPATH": 135 readGoEnvVars() 136 return goEnvVars.GOPATH 137 case "GOCACHE": 138 // Get the cache directory, usually ~/.cache/tinygo 139 dir, err := os.UserCacheDir() 140 if err != nil { 141 panic("could not find cache dir: " + err.Error()) 142 } 143 return filepath.Join(dir, "tinygo") 144 case "CGO_ENABLED": 145 // Always enable CGo. It is required by a number of targets, including 146 // macOS and the rp2040. 147 return "1" 148 case "TINYGOROOT": 149 return sourceDir() 150 case "WASMOPT": 151 if path := os.Getenv("WASMOPT"); path != "" { 152 err := wasmOptCheckVersion(path) 153 if err != nil { 154 fmt.Fprintf(os.Stderr, "cannot use %q as wasm-opt (from WASMOPT environment variable): %s", path, err.Error()) 155 os.Exit(1) 156 } 157 158 return path 159 } 160 161 return findWasmOpt() 162 default: 163 return "" 164 } 165 } 166 167 // Find wasm-opt, or exit with an error. 168 func findWasmOpt() string { 169 tinygoroot := sourceDir() 170 searchPaths := []string{ 171 tinygoroot + "/bin/wasm-opt", 172 tinygoroot + "/build/wasm-opt", 173 } 174 175 var paths []string 176 for _, path := range searchPaths { 177 if runtime.GOOS == "windows" { 178 path += ".exe" 179 } 180 181 _, err := os.Stat(path) 182 if err != nil && errors.Is(err, fs.ErrNotExist) { 183 continue 184 } 185 186 paths = append(paths, path) 187 } 188 189 if path, err := exec.LookPath("wasm-opt"); err == nil { 190 paths = append(paths, path) 191 } 192 193 if len(paths) == 0 { 194 fmt.Fprintln(os.Stderr, "error: could not find wasm-opt, set the WASMOPT environment variable to override") 195 os.Exit(1) 196 } 197 198 errs := make([]error, len(paths)) 199 for i, path := range paths { 200 err := wasmOptCheckVersion(path) 201 if err == nil { 202 return path 203 } 204 205 errs[i] = err 206 } 207 fmt.Fprintln(os.Stderr, "no usable wasm-opt found, update or run \"make binaryen\"") 208 for i, path := range paths { 209 fmt.Fprintf(os.Stderr, "\t%s: %s\n", path, errs[i].Error()) 210 } 211 os.Exit(1) 212 panic("unreachable") 213 } 214 215 // wasmOptCheckVersion checks if a copy of wasm-opt is usable. 216 func wasmOptCheckVersion(path string) error { 217 cmd := exec.Command(path, "--version") 218 var buf bytes.Buffer 219 cmd.Stdout = &buf 220 cmd.Stderr = os.Stderr 221 err := cmd.Run() 222 if err != nil { 223 return err 224 } 225 226 str := buf.String() 227 if strings.Contains(str, "(") { 228 // The git tag may be placed in parentheses after the main version string. 229 str = strings.Split(str, "(")[0] 230 } 231 232 str = strings.TrimSpace(str) 233 var ver uint 234 _, err = fmt.Sscanf(str, "wasm-opt version %d", &ver) 235 if err != nil || ver < 102 { 236 return errors.New("incompatible wasm-opt (need 102 or newer)") 237 } 238 239 return nil 240 } 241 242 // Return the TINYGOROOT, or exit with an error. 243 func sourceDir() string { 244 // Use $TINYGOROOT as root, if available. 245 root := os.Getenv("TINYGOROOT") 246 if root != "" { 247 if !isSourceDir(root) { 248 fmt.Fprintln(os.Stderr, "error: $TINYGOROOT was not set to the correct root") 249 os.Exit(1) 250 } 251 return root 252 } 253 254 if TINYGOROOT != "" { 255 if !isSourceDir(TINYGOROOT) { 256 fmt.Fprintln(os.Stderr, "error: TINYGOROOT was not set to the correct root") 257 os.Exit(1) 258 } 259 return TINYGOROOT 260 } 261 262 // Find root from executable path. 263 path, err := os.Executable() 264 if err != nil { 265 // Very unlikely. Bail out if it happens. 266 panic("could not get executable path: " + err.Error()) 267 } 268 root = filepath.Dir(filepath.Dir(path)) 269 if isSourceDir(root) { 270 return root 271 } 272 273 // Fallback: use the original directory from where it was built 274 // https://stackoverflow.com/a/32163888/559350 275 _, path, _, _ = runtime.Caller(0) 276 root = filepath.Dir(filepath.Dir(path)) 277 if isSourceDir(root) { 278 return root 279 } 280 281 fmt.Fprintln(os.Stderr, "error: could not autodetect root directory, set the TINYGOROOT environment variable to override") 282 os.Exit(1) 283 panic("unreachable") 284 } 285 286 // isSourceDir returns true if the directory looks like a TinyGo source directory. 287 func isSourceDir(root string) bool { 288 _, err := os.Stat(filepath.Join(root, "src/runtime/internal/sys/zversion.go")) 289 if err != nil { 290 return false 291 } 292 _, err = os.Stat(filepath.Join(root, "src/device/arm/arm.go")) 293 return err == nil 294 } 295 296 // ClangResourceDir returns the clang resource dir if available. This is the 297 // -resource-dir flag. If it isn't available, an empty string is returned and 298 // -resource-dir should be left unset. 299 // The libclang flag must be set if the resource dir is read for use by 300 // libclang. 301 // In that case, the resource dir is always returned (even when linking 302 // dynamically against LLVM) because libclang always needs this directory. 303 func ClangResourceDir(libclang bool) string { 304 if clangResourceDir != "" { 305 // The resource dir is forced to a particular value at build time. 306 // This is needed on Nix for example, where Clang and libclang don't 307 // know their own resource dir. 308 // Also see: 309 // https://discourse.nixos.org/t/why-is-the-clang-resource-dir-split-in-a-separate-package/34114 310 return clangResourceDir 311 } 312 313 if !hasBuiltinTools && !libclang { 314 // Using external tools, so the resource dir doesn't need to be 315 // specified. Clang knows where to find it. 316 return "" 317 } 318 319 // Check whether we're running from a TinyGo release directory. 320 // This is the case for release binaries on GitHub. 321 root := Get("TINYGOROOT") 322 releaseHeaderDir := filepath.Join(root, "lib", "clang") 323 if _, err := os.Stat(releaseHeaderDir); !errors.Is(err, fs.ErrNotExist) { 324 return releaseHeaderDir 325 } 326 327 if hasBuiltinTools { 328 // We are statically linked to LLVM. 329 // Check whether we're running from the source directory. 330 // This typically happens when TinyGo was built using `make` as part of 331 // development. 332 llvmMajor := strings.Split(llvm.Version, ".")[0] 333 buildResourceDir := filepath.Join(root, "llvm-build", "lib", "clang", llvmMajor) 334 if _, err := os.Stat(buildResourceDir); !errors.Is(err, fs.ErrNotExist) { 335 return buildResourceDir 336 } 337 } else { 338 // We use external tools, either when installed using `go install` or 339 // when packaged in a Linux distribution (Linux distros typically prefer 340 // dynamic linking). 341 // Try to detect the system clang resources directory. 342 resourceDir := findSystemClangResources(root) 343 if resourceDir != "" { 344 return resourceDir 345 } 346 } 347 348 // Resource directory not found. 349 return "" 350 } 351 352 // Find the Clang resource dir on this particular system. 353 // Return the empty string when they aren't found. 354 func findSystemClangResources(TINYGOROOT string) string { 355 llvmMajor := strings.Split(llvm.Version, ".")[0] 356 357 switch runtime.GOOS { 358 case "linux", "android": 359 // Header files are typically stored in /usr/lib/clang/<version>/include. 360 // Tested on Fedora 39, Debian 12, and Arch Linux. 361 path := filepath.Join("/usr/lib/clang", llvmMajor) 362 _, err := os.Stat(filepath.Join(path, "include", "stdint.h")) 363 if err == nil { 364 return path 365 } 366 case "darwin": 367 // This assumes a Homebrew installation, like in builder/commands.go. 368 var prefix string 369 switch runtime.GOARCH { 370 case "amd64": 371 prefix = "/usr/local/opt/llvm@" + llvmMajor 372 case "arm64": 373 prefix = "/opt/homebrew/opt/llvm@" + llvmMajor 374 default: 375 return "" // very unlikely for now 376 } 377 path := fmt.Sprintf("%s/lib/clang/%s", prefix, llvmMajor) 378 _, err := os.Stat(path + "/include/stdint.h") 379 if err == nil { 380 return path 381 } 382 } 383 384 // Could not find it. 385 return "" 386 }