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