aboutsummaryrefslogtreecommitdiffhomepage
path: root/goenv/goenv.go
blob: 4715b8769ab4911759a7325dc3b6a277a6bae2dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// Package goenv returns environment variables that are used in various parts of
// the compiler. You can query it manually with the `tinygo env` subcommand.
package goenv

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"

	"tinygo.org/x/go-llvm"
)

// Keys is a slice of all available environment variable keys.
var Keys = []string{
	"GOOS",
	"GOARCH",
	"GOROOT",
	"GOPATH",
	"GOCACHE",
	"CGO_ENABLED",
	"TINYGOROOT",
}

func init() {
	if Get("GOARCH") == "arm" {
		Keys = append(Keys, "GOARM")
	}
}

// Set to true if we're linking statically against LLVM.
var hasBuiltinTools = false

// TINYGOROOT is the path to the final location for checking tinygo files. If
// unset (by a -X ldflag), then sourceDir() will fallback to the original build
// directory.
var TINYGOROOT string

// If a particular Clang resource dir must always be used and TinyGo can't
// figure out the directory using heuristics, this global can be set using a
// linker flag.
// This is needed for Nix.
var clangResourceDir string

// Variables read from a `go env` command invocation.
var goEnvVars struct {
	GOPATH    string
	GOROOT    string
	GOVERSION string
}

var goEnvVarsOnce sync.Once
var goEnvVarsErr error // error returned from cmd.Run

// Make sure goEnvVars is fresh. This can be called multiple times, the first
// time will update all environment variables in goEnvVars.
func readGoEnvVars() error {
	goEnvVarsOnce.Do(func() {
		cmd := exec.Command("go", "env", "-json", "GOPATH", "GOROOT", "GOVERSION")
		output, err := cmd.Output()
		if err != nil {
			// Check for "command not found" error.
			if execErr, ok := err.(*exec.Error); ok {
				goEnvVarsErr = fmt.Errorf("could not find '%s' command: %w", execErr.Name, execErr.Err)
				return
			}
			// It's perhaps a bit ugly to handle this error here, but I couldn't
			// think of a better place further up in the call chain.
			if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 {
				if len(exitErr.Stderr) != 0 {
					// The 'go' command exited with an error message. Print that
					// message and exit, so we behave in a similar way.
					os.Stderr.Write(exitErr.Stderr)
					os.Exit(exitErr.ExitCode())
				}
			}
			// Other errors. Not sure whether there are any, but just in case.
			goEnvVarsErr = err
			return
		}
		err = json.Unmarshal(output, &goEnvVars)
		if err != nil {
			// This should never happen if we have a sane Go toolchain
			// installed.
			goEnvVarsErr = fmt.Errorf("unexpected error while unmarshalling `go env` output: %w", err)
		}
	})

	return goEnvVarsErr
}

// Get returns a single environment variable, possibly calculating it on-demand.
// The empty string is returned for unknown environment variables.
func Get(name string) string {
	switch name {
	case "GOOS":
		goos := os.Getenv("GOOS")
		if goos == "" {
			goos = runtime.GOOS
		}
		if goos == "android" {
			goos = "linux"
		}
		return goos
	case "GOARCH":
		if dir := os.Getenv("GOARCH"); dir != "" {
			return dir
		}
		return runtime.GOARCH
	case "GOARM":
		if goarm := os.Getenv("GOARM"); goarm != "" {
			return goarm
		}
		if goos := Get("GOOS"); goos == "windows" || goos == "android" {
			// Assume Windows and Android are running on modern CPU cores.
			// This matches upstream Go.
			return "7"
		}
		// Default to ARMv6 on other devices.
		// The difference between ARMv5 and ARMv6 is big, much bigger than the
		// difference between ARMv6 and ARMv7. ARMv6 binaries are much smaller,
		// especially when floating point instructions are involved.
		return "6"
	case "GOROOT":
		readGoEnvVars()
		return goEnvVars.GOROOT
	case "GOPATH":
		readGoEnvVars()
		return goEnvVars.GOPATH
	case "GOCACHE":
		// Get the cache directory, usually ~/.cache/tinygo
		dir, err := os.UserCacheDir()
		if err != nil {
			panic("could not find cache dir: " + err.Error())
		}
		return filepath.Join(dir, "tinygo")
	case "CGO_ENABLED":
		val := os.Getenv("CGO_ENABLED")
		if val == "1" || val == "0" {
			return val
		}
		// Default to enabling CGo.
		return "1"
	case "TINYGOROOT":
		return sourceDir()
	case "WASMOPT":
		if path := os.Getenv("WASMOPT"); path != "" {
			err := wasmOptCheckVersion(path)
			if err != nil {
				fmt.Fprintf(os.Stderr, "cannot use %q as wasm-opt (from WASMOPT environment variable): %s", path, err.Error())
				os.Exit(1)
			}

			return path
		}

		return findWasmOpt()
	default:
		return ""
	}
}

// Find wasm-opt, or exit with an error.
func findWasmOpt() string {
	tinygoroot := sourceDir()
	searchPaths := []string{
		tinygoroot + "/bin/wasm-opt",
		tinygoroot + "/build/wasm-opt",
	}

	var paths []string
	for _, path := range searchPaths {
		if runtime.GOOS == "windows" {
			path += ".exe"
		}

		_, err := os.Stat(path)
		if err != nil && errors.Is(err, fs.ErrNotExist) {
			continue
		}

		paths = append(paths, path)
	}

	if path, err := exec.LookPath("wasm-opt"); err == nil {
		paths = append(paths, path)
	}

	if len(paths) == 0 {
		fmt.Fprintln(os.Stderr, "error: could not find wasm-opt, set the WASMOPT environment variable to override")
		os.Exit(1)
	}

	errs := make([]error, len(paths))
	for i, path := range paths {
		err := wasmOptCheckVersion(path)
		if err == nil {
			return path
		}

		errs[i] = err
	}
	fmt.Fprintln(os.Stderr, "no usable wasm-opt found, update or run \"make binaryen\"")
	for i, path := range paths {
		fmt.Fprintf(os.Stderr, "\t%s: %s\n", path, errs[i].Error())
	}
	os.Exit(1)
	panic("unreachable")
}

// wasmOptCheckVersion checks if a copy of wasm-opt is usable.
func wasmOptCheckVersion(path string) error {
	cmd := exec.Command(path, "--version")
	var buf bytes.Buffer
	cmd.Stdout = &buf
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		return err
	}

	str := buf.String()
	if strings.Contains(str, "(") {
		// The git tag may be placed in parentheses after the main version string.
		str = strings.Split(str, "(")[0]
	}

	str = strings.TrimSpace(str)
	var ver uint
	_, err = fmt.Sscanf(str, "wasm-opt version %d", &ver)
	if err != nil || ver < 102 {
		return errors.New("incompatible wasm-opt (need 102 or newer)")
	}

	return nil
}

// Return the TINYGOROOT, or exit with an error.
func sourceDir() string {
	// Use $TINYGOROOT as root, if available.
	root := os.Getenv("TINYGOROOT")
	if root != "" {
		if !isSourceDir(root) {
			fmt.Fprintln(os.Stderr, "error: $TINYGOROOT was not set to the correct root")
			os.Exit(1)
		}
		return root
	}

	if TINYGOROOT != "" {
		if !isSourceDir(TINYGOROOT) {
			fmt.Fprintln(os.Stderr, "error: TINYGOROOT was not set to the correct root")
			os.Exit(1)
		}
		return TINYGOROOT
	}

	// Find root from executable path.
	path, err := os.Executable()
	if err != nil {
		// Very unlikely. Bail out if it happens.
		panic("could not get executable path: " + err.Error())
	}
	root = filepath.Dir(filepath.Dir(path))
	if isSourceDir(root) {
		return root
	}

	// Fallback: use the original directory from where it was built
	// https://stackoverflow.com/a/32163888/559350
	_, path, _, _ = runtime.Caller(0)
	root = filepath.Dir(filepath.Dir(path))
	if isSourceDir(root) {
		return root
	}

	fmt.Fprintln(os.Stderr, "error: could not autodetect root directory, set the TINYGOROOT environment variable to override")
	os.Exit(1)
	panic("unreachable")
}

// isSourceDir returns true if the directory looks like a TinyGo source directory.
func isSourceDir(root string) bool {
	_, err := os.Stat(filepath.Join(root, "src/runtime/internal/sys/zversion.go"))
	if err != nil {
		return false
	}
	_, err = os.Stat(filepath.Join(root, "src/device/arm/arm.go"))
	return err == nil
}

// ClangResourceDir returns the clang resource dir if available. This is the
// -resource-dir flag. If it isn't available, an empty string is returned and
// -resource-dir should be left unset.
// The libclang flag must be set if the resource dir is read for use by
// libclang.
// In that case, the resource dir is always returned (even when linking
// dynamically against LLVM) because libclang always needs this directory.
func ClangResourceDir(libclang bool) string {
	if clangResourceDir != "" {
		// The resource dir is forced to a particular value at build time.
		// This is needed on Nix for example, where Clang and libclang don't
		// know their own resource dir.
		// Also see:
		// https://discourse.nixos.org/t/why-is-the-clang-resource-dir-split-in-a-separate-package/34114
		return clangResourceDir
	}

	if !hasBuiltinTools && !libclang {
		// Using external tools, so the resource dir doesn't need to be
		// specified. Clang knows where to find it.
		return ""
	}

	// Check whether we're running from a TinyGo release directory.
	// This is the case for release binaries on GitHub.
	root := Get("TINYGOROOT")
	releaseHeaderDir := filepath.Join(root, "lib", "clang")
	if _, err := os.Stat(releaseHeaderDir); !errors.Is(err, fs.ErrNotExist) {
		return releaseHeaderDir
	}

	if hasBuiltinTools {
		// We are statically linked to LLVM.
		// Check whether we're running from the source directory.
		// This typically happens when TinyGo was built using `make` as part of
		// development.
		llvmMajor := strings.Split(llvm.Version, ".")[0]
		buildResourceDir := filepath.Join(root, "llvm-build", "lib", "clang", llvmMajor)
		if _, err := os.Stat(buildResourceDir); !errors.Is(err, fs.ErrNotExist) {
			return buildResourceDir
		}
	} else {
		// We use external tools, either when installed using `go install` or
		// when packaged in a Linux distribution (Linux distros typically prefer
		// dynamic linking).
		// Try to detect the system clang resources directory.
		resourceDir := findSystemClangResources(root)
		if resourceDir != "" {
			return resourceDir
		}
	}

	// Resource directory not found.
	return ""
}

// Find the Clang resource dir on this particular system.
// Return the empty string when they aren't found.
func findSystemClangResources(TINYGOROOT string) string {
	llvmMajor := strings.Split(llvm.Version, ".")[0]

	switch runtime.GOOS {
	case "linux", "android":
		// Header files are typically stored in /usr/lib/clang/<version>/include.
		// Tested on Fedora 39, Debian 12, and Arch Linux.
		path := filepath.Join("/usr/lib/clang", llvmMajor)
		_, err := os.Stat(filepath.Join(path, "include", "stdint.h"))
		if err == nil {
			return path
		}
	}

	// Could not find it.
	return ""
}