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