xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/builders/env.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1// Copyright 2017 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"bytes"
19	"errors"
20	"flag"
21	"fmt"
22	"io"
23	"io/ioutil"
24	"log"
25	"os"
26	"os/exec"
27	"path/filepath"
28	"runtime"
29	"strconv"
30	"strings"
31)
32
33var (
34	// cgoEnvVars is the list of all cgo environment variable
35	cgoEnvVars = []string{"CGO_CFLAGS", "CGO_CXXFLAGS", "CGO_CPPFLAGS", "CGO_LDFLAGS"}
36	// cgoAbsEnvFlags are all the flags that need absolute path in cgoEnvVars
37	cgoAbsEnvFlags = []string{"-I", "-L", "-isysroot", "-isystem", "-iquote", "-include", "-gcc-toolchain", "--sysroot", "-resource-dir", "-fsanitize-blacklist", "-fsanitize-ignorelist"}
38)
39
40// env holds a small amount of Go environment and toolchain information
41// which is common to multiple builders. Most Bazel-agnostic build information
42// is collected in go/build.Default though.
43//
44// See ./README.rst for more information about handling arguments and
45// environment variables.
46type env struct {
47	// sdk is the path to the Go SDK, which contains tools for the host
48	// platform. This may be different than GOROOT.
49	sdk string
50
51	// installSuffix is the name of the directory below GOROOT/pkg that contains
52	// the .a files for the standard library we should build against.
53	// For example, linux_amd64_race.
54	installSuffix string
55
56	// verbose indicates whether subprocess command lines should be printed.
57	verbose bool
58
59	// workDirPath is a temporary work directory. It is created lazily.
60	workDirPath string
61
62	shouldPreserveWorkDir bool
63}
64
65// envFlags registers flags common to multiple builders and returns an env
66// configured with those flags.
67func envFlags(flags *flag.FlagSet) *env {
68	env := &env{}
69	flags.StringVar(&env.sdk, "sdk", "", "Path to the Go SDK.")
70	flags.Var(&tagFlag{}, "tags", "List of build tags considered true.")
71	flags.StringVar(&env.installSuffix, "installsuffix", "", "Standard library under GOROOT/pkg")
72	flags.BoolVar(&env.verbose, "v", false, "Whether subprocess command lines should be printed")
73	flags.BoolVar(&env.shouldPreserveWorkDir, "work", false, "if true, the temporary work directory will be preserved")
74	return env
75}
76
77// checkFlags checks whether env flags were set to valid values. checkFlags
78// should be called after parsing flags.
79func (e *env) checkFlags() error {
80	if e.sdk == "" {
81		return errors.New("-sdk was not set")
82	}
83	return nil
84}
85
86// workDir returns a path to a temporary work directory. The same directory
87// is returned on multiple calls. The caller is responsible for cleaning
88// up the work directory by calling cleanup.
89func (e *env) workDir() (path string, cleanup func(), err error) {
90	if e.workDirPath != "" {
91		return e.workDirPath, func() {}, nil
92	}
93	// Keep the stem "rules_go_work" in sync with reproducible_binary_test.go.
94	e.workDirPath, err = ioutil.TempDir("", "rules_go_work-")
95	if err != nil {
96		return "", func() {}, err
97	}
98	if e.verbose {
99		log.Printf("WORK=%s\n", e.workDirPath)
100	}
101	if e.shouldPreserveWorkDir {
102		cleanup = func() {}
103	} else {
104		cleanup = func() { os.RemoveAll(e.workDirPath) }
105	}
106	return e.workDirPath, cleanup, nil
107}
108
109// goTool returns a slice containing the path to an executable at
110// $GOROOT/pkg/$GOOS_$GOARCH/$tool and additional arguments.
111func (e *env) goTool(tool string, args ...string) []string {
112	platform := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
113	toolPath := filepath.Join(e.sdk, "pkg", "tool", platform, tool)
114	if runtime.GOOS == "windows" {
115		toolPath += ".exe"
116	}
117	return append([]string{toolPath}, args...)
118}
119
120// goCmd returns a slice containing the path to the go executable
121// and additional arguments.
122func (e *env) goCmd(cmd string, args ...string) []string {
123	exe := filepath.Join(e.sdk, "bin", "go")
124	if runtime.GOOS == "windows" {
125		exe += ".exe"
126	}
127	return append([]string{exe, cmd}, args...)
128}
129
130// runCommand executes a subprocess that inherits stdout, stderr, and the
131// environment from this process.
132func (e *env) runCommand(args []string) error {
133	cmd := exec.Command(args[0], args[1:]...)
134	// Redirecting stdout to stderr. This mirrors behavior in the go command:
135	// https://go.googlesource.com/go/+/refs/tags/go1.15.2/src/cmd/go/internal/work/exec.go#1958
136	buf := &bytes.Buffer{}
137	cmd.Stdout = buf
138	cmd.Stderr = buf
139	err := runAndLogCommand(cmd, e.verbose)
140	os.Stderr.Write(relativizePaths(buf.Bytes()))
141	return err
142}
143
144// runCommandToFile executes a subprocess and writes stdout/stderr to the given
145// writers.
146func (e *env) runCommandToFile(out, err io.Writer, args []string) error {
147	cmd := exec.Command(args[0], args[1:]...)
148	cmd.Stdout = out
149	cmd.Stderr = err
150	return runAndLogCommand(cmd, e.verbose)
151}
152
153func absEnv(envNameList []string, argList []string) error {
154	for _, envName := range envNameList {
155		splitedEnv := strings.Fields(os.Getenv(envName))
156		absArgs(splitedEnv, argList)
157		if err := os.Setenv(envName, strings.Join(splitedEnv, " ")); err != nil {
158			return err
159		}
160	}
161	return nil
162}
163
164func runAndLogCommand(cmd *exec.Cmd, verbose bool) error {
165	if verbose {
166		fmt.Fprintln(os.Stderr, formatCommand(cmd))
167	}
168	cleanup := passLongArgsInResponseFiles(cmd)
169	defer cleanup()
170	if err := cmd.Run(); err != nil {
171		return fmt.Errorf("error running subcommand %s: %v", cmd.Path, err)
172	}
173	return nil
174}
175
176// expandParamsFiles looks for arguments in args of the form
177// "-param=filename". When it finds these arguments it reads the file "filename"
178// and replaces the argument with its content.
179// It returns the expanded arguments as well as a bool that is true if any param
180// files have been passed.
181func expandParamsFiles(args []string) ([]string, bool, error) {
182	var paramsIndices []int
183	for i, arg := range args {
184		if strings.HasPrefix(arg, "-param=") {
185			paramsIndices = append(paramsIndices, i)
186		}
187	}
188	if len(paramsIndices) == 0 {
189		return args, false, nil
190	}
191	var expandedArgs []string
192	last := 0
193	for _, pi := range paramsIndices {
194		expandedArgs = append(expandedArgs, args[last:pi]...)
195		last = pi + 1
196
197		fileName := args[pi][len("-param="):]
198		fileArgs, err := readParamsFile(fileName)
199		if err != nil {
200			return nil, true, err
201		}
202		expandedArgs = append(expandedArgs, fileArgs...)
203	}
204	expandedArgs = append(expandedArgs, args[last:]...)
205	return expandedArgs, true, nil
206}
207
208// readParamsFiles parses a Bazel params file in "shell" format. The file
209// should contain one argument per line. Arguments may be quoted with single
210// quotes. All characters within quoted strings are interpreted literally
211// including newlines and excepting single quotes. Characters outside quoted
212// strings may be escaped with a backslash.
213func readParamsFile(name string) ([]string, error) {
214	data, err := ioutil.ReadFile(name)
215	if err != nil {
216		return nil, err
217	}
218
219	var args []string
220	var arg []byte
221	quote := false
222	escape := false
223	for p := 0; p < len(data); p++ {
224		b := data[p]
225		switch {
226		case escape:
227			arg = append(arg, b)
228			escape = false
229
230		case b == '\'':
231			quote = !quote
232
233		case !quote && b == '\\':
234			escape = true
235
236		case !quote && b == '\n':
237			args = append(args, string(arg))
238			arg = arg[:0]
239
240		default:
241			arg = append(arg, b)
242		}
243	}
244	if quote {
245		return nil, fmt.Errorf("unterminated quote")
246	}
247	if escape {
248		return nil, fmt.Errorf("unterminated escape")
249	}
250	if len(arg) > 0 {
251		args = append(args, string(arg))
252	}
253	return args, nil
254}
255
256// writeParamsFile formats a list of arguments in Bazel's "shell" format and writes
257// it to a file.
258func writeParamsFile(path string, args []string) error {
259	buf := new(bytes.Buffer)
260	for _, arg := range args {
261		if !strings.ContainsAny(arg, "'\n\\") {
262			fmt.Fprintln(buf, arg)
263			continue
264		}
265		buf.WriteByte('\'')
266		for _, r := range arg {
267			if r == '\'' {
268				buf.WriteString(`'\''`)
269			} else {
270				buf.WriteRune(r)
271			}
272		}
273		buf.WriteString("'\n")
274	}
275	return ioutil.WriteFile(path, buf.Bytes(), 0666)
276}
277
278// splitArgs splits a list of command line arguments into two parts: arguments
279// that should be interpreted by the builder (before "--"), and arguments
280// that should be passed through to the underlying tool (after "--").
281func splitArgs(args []string) (builderArgs []string, toolArgs []string) {
282	for i, arg := range args {
283		if arg == "--" {
284			return args[:i], args[i+1:]
285		}
286	}
287	return args, nil
288}
289
290// abs returns the absolute representation of path. Some tools/APIs require
291// absolute paths to work correctly. Most notably, golang on Windows cannot
292// handle relative paths to files whose absolute path is > ~250 chars, while
293// it can handle absolute paths. See http://goo.gl/eqeWjm.
294//
295// Note that strings that begin with "__BAZEL_" are not absolutized. These are
296// used on macOS for paths that the compiler wrapper (wrapped_clang) is
297// supposed to know about.
298func abs(path string) string {
299	if strings.HasPrefix(path, "__BAZEL_") {
300		return path
301	}
302
303	if abs, err := filepath.Abs(path); err != nil {
304		return path
305	} else {
306		return abs
307	}
308}
309
310// absArgs applies abs to strings that appear in args. Only paths that are
311// part of options named by flags are modified.
312func absArgs(args []string, flags []string) {
313	absNext := false
314	for i := range args {
315		if absNext {
316			args[i] = abs(args[i])
317			absNext = false
318			continue
319		}
320		for _, f := range flags {
321			if !strings.HasPrefix(args[i], f) {
322				continue
323			}
324			possibleValue := args[i][len(f):]
325			if len(possibleValue) == 0 {
326				absNext = true
327				break
328			}
329			separator := ""
330			if possibleValue[0] == '=' {
331				possibleValue = possibleValue[1:]
332				separator = "="
333			}
334			args[i] = fmt.Sprintf("%s%s%s", f, separator, abs(possibleValue))
335			break
336		}
337	}
338}
339
340// relativizePaths converts absolute paths found in the given output string to
341// relative, if they are within the working directory.
342func relativizePaths(output []byte) []byte {
343	dir, err := os.Getwd()
344	if dir == "" || err != nil {
345		return output
346	}
347	dirBytes := make([]byte, len(dir), len(dir)+1)
348	copy(dirBytes, dir)
349	if bytes.HasSuffix(dirBytes, []byte{filepath.Separator}) {
350		return bytes.ReplaceAll(output, dirBytes, nil)
351	}
352
353	// This is the common case.
354	// Replace "$CWD/" with "" and "$CWD" with "."
355	dirBytes = append(dirBytes, filepath.Separator)
356	output = bytes.ReplaceAll(output, dirBytes, nil)
357	dirBytes = dirBytes[:len(dirBytes)-1]
358	return bytes.ReplaceAll(output, dirBytes, []byte{'.'})
359}
360
361// formatCommand formats cmd as a string that can be pasted into a shell.
362// Spaces in environment variables and arguments are escaped as needed.
363func formatCommand(cmd *exec.Cmd) string {
364	quoteIfNeeded := func(s string) string {
365		if strings.IndexByte(s, ' ') < 0 {
366			return s
367		}
368		return strconv.Quote(s)
369	}
370	quoteEnvIfNeeded := func(s string) string {
371		eq := strings.IndexByte(s, '=')
372		if eq < 0 {
373			return s
374		}
375		key, value := s[:eq], s[eq+1:]
376		if strings.IndexByte(value, ' ') < 0 {
377			return s
378		}
379		return fmt.Sprintf("%s=%s", key, strconv.Quote(value))
380	}
381	var w bytes.Buffer
382	environ := cmd.Env
383	if environ == nil {
384		environ = os.Environ()
385	}
386	for _, e := range environ {
387		fmt.Fprintf(&w, "%s \\\n", quoteEnvIfNeeded(e))
388	}
389
390	sep := ""
391	for _, arg := range cmd.Args {
392		fmt.Fprintf(&w, "%s%s", sep, quoteIfNeeded(arg))
393		sep = " "
394	}
395	return w.String()
396}
397
398// passLongArgsInResponseFiles modifies cmd such that, for
399// certain programs, long arguments are passed in "response files", a
400// file on disk with the arguments, with one arg per line. An actual
401// argument starting with '@' means that the rest of the argument is
402// a filename of arguments to expand.
403//
404// See https://github.com/golang/go/issues/18468 (Windows) and
405// https://github.com/golang/go/issues/37768 (Darwin).
406func passLongArgsInResponseFiles(cmd *exec.Cmd) (cleanup func()) {
407	cleanup = func() {} // no cleanup by default
408	var argLen int
409	for _, arg := range cmd.Args {
410		argLen += len(arg)
411	}
412	// If we're not approaching 32KB of args, just pass args normally.
413	// (use 30KB instead to be conservative; not sure how accounting is done)
414	if !useResponseFile(cmd.Path, argLen) {
415		return
416	}
417	tf, err := ioutil.TempFile("", "args")
418	if err != nil {
419		log.Fatalf("error writing long arguments to response file: %v", err)
420	}
421	cleanup = func() { os.Remove(tf.Name()) }
422	var buf bytes.Buffer
423	for _, arg := range cmd.Args[1:] {
424		fmt.Fprintf(&buf, "%s\n", arg)
425	}
426	if _, err := tf.Write(buf.Bytes()); err != nil {
427		tf.Close()
428		cleanup()
429		log.Fatalf("error writing long arguments to response file: %v", err)
430	}
431	if err := tf.Close(); err != nil {
432		cleanup()
433		log.Fatalf("error writing long arguments to response file: %v", err)
434	}
435	cmd.Args = []string{cmd.Args[0], "@" + tf.Name()}
436	return cleanup
437}
438
439// quotePathIfNeeded quotes path if it contains whitespace and isn't already quoted.
440// Use this for paths that will be passed through
441// https://github.com/golang/go/blob/06264b740e3bfe619f5e90359d8f0d521bd47806/src/cmd/internal/quoted/quoted.go#L25
442func quotePathIfNeeded(path string) string {
443	if strings.HasPrefix(path, "\"") || strings.HasPrefix(path, "'") {
444		// Assume already quoted
445		return path
446	}
447	// https://github.com/golang/go/blob/06264b740e3bfe619f5e90359d8f0d521bd47806/src/cmd/internal/quoted/quoted.go#L16
448	if strings.IndexAny(path, " \t\n\r") < 0 {
449		// Does not require quoting
450		return path
451	}
452	// Escaping quotes is not supported, so we can assume path doesn't contain any quotes.
453	return "'" + path + "'"
454}
455
456func useResponseFile(path string, argLen int) bool {
457	// Unless the program uses objabi.Flagparse, which understands
458	// response files, don't use response files.
459	// TODO: do we need more commands? asm? cgo? For now, no.
460	prog := strings.TrimSuffix(filepath.Base(path), ".exe")
461	switch prog {
462	case "compile", "link":
463	default:
464		return false
465	}
466	// Windows has a limit of 32 KB arguments. To be conservative and not
467	// worry about whether that includes spaces or not, just use 30 KB.
468	// Darwin's limit is less clear. The OS claims 256KB, but we've seen
469	// failures with arglen as small as 50KB.
470	if argLen > (30 << 10) {
471		return true
472	}
473	return false
474}
475