github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/work/shell.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package work 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "io/fs" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "runtime" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/go-asm/go/cmd/go/base" 23 "github.com/go-asm/go/cmd/go/cache" 24 "github.com/go-asm/go/cmd/go/cfg" 25 "github.com/go-asm/go/cmd/go/load" 26 "github.com/go-asm/go/cmd/go/par" 27 "github.com/go-asm/go/cmd/go/str" 28 "github.com/go-asm/go/lazyregexp" 29 ) 30 31 // A Shell runs shell commands and performs shell-like file system operations. 32 // 33 // Shell tracks context related to running commands, and form a tree much like 34 // context.Context. 35 type Shell struct { 36 action *Action // nil for the root shell 37 *shellShared // per-Builder state shared across Shells 38 } 39 40 // shellShared is Shell state shared across all Shells derived from a single 41 // root shell (generally a single Builder). 42 type shellShared struct { 43 workDir string // $WORK, immutable 44 45 printLock sync.Mutex 46 printFunc func(args ...any) (int, error) 47 scriptDir string // current directory in printed script 48 49 mkdirCache par.Cache[string, error] // a cache of created directories 50 } 51 52 // NewShell returns a new Shell. 53 // 54 // Shell will internally serialize calls to the print function. 55 // If print is nil, it defaults to printing to stderr. 56 func NewShell(workDir string, print func(a ...any) (int, error)) *Shell { 57 if print == nil { 58 print = func(a ...any) (int, error) { 59 return fmt.Fprint(os.Stderr, a...) 60 } 61 } 62 shared := &shellShared{ 63 workDir: workDir, 64 printFunc: print, 65 } 66 return &Shell{shellShared: shared} 67 } 68 69 // Print emits a to this Shell's output stream, formatting it like fmt.Print. 70 // It is safe to call concurrently. 71 func (sh *Shell) Print(a ...any) { 72 sh.printLock.Lock() 73 defer sh.printLock.Unlock() 74 sh.printFunc(a...) 75 } 76 77 func (sh *Shell) printLocked(a ...any) { 78 sh.printFunc(a...) 79 } 80 81 // WithAction returns a Shell identical to sh, but bound to Action a. 82 func (sh *Shell) WithAction(a *Action) *Shell { 83 sh2 := *sh 84 sh2.action = a 85 return &sh2 86 } 87 88 // Shell returns a shell for running commands on behalf of Action a. 89 func (b *Builder) Shell(a *Action) *Shell { 90 if a == nil { 91 // The root shell has a nil Action. The point of this method is to 92 // create a Shell bound to an Action, so disallow nil Actions here. 93 panic("nil Action") 94 } 95 if a.sh == nil { 96 a.sh = b.backgroundSh.WithAction(a) 97 } 98 return a.sh 99 } 100 101 // BackgroundShell returns a Builder-wide Shell that's not bound to any Action. 102 // Try not to use this unless there's really no sensible Action available. 103 func (b *Builder) BackgroundShell() *Shell { 104 return b.backgroundSh 105 } 106 107 // moveOrCopyFile is like 'mv src dst' or 'cp src dst'. 108 func (sh *Shell) moveOrCopyFile(dst, src string, perm fs.FileMode, force bool) error { 109 if cfg.BuildN { 110 sh.ShowCmd("", "mv %s %s", src, dst) 111 return nil 112 } 113 114 // If we can update the mode and rename to the dst, do it. 115 // Otherwise fall back to standard copy. 116 117 // If the source is in the build cache, we need to copy it. 118 if strings.HasPrefix(src, cache.DefaultDir()) { 119 return sh.CopyFile(dst, src, perm, force) 120 } 121 122 // On Windows, always copy the file, so that we respect the NTFS 123 // permissions of the parent folder. https://golang.org/issue/22343. 124 // What matters here is not cfg.Goos (the system we are building 125 // for) but runtime.GOOS (the system we are building on). 126 if runtime.GOOS == "windows" { 127 return sh.CopyFile(dst, src, perm, force) 128 } 129 130 // If the destination directory has the group sticky bit set, 131 // we have to copy the file to retain the correct permissions. 132 // https://golang.org/issue/18878 133 if fi, err := os.Stat(filepath.Dir(dst)); err == nil { 134 if fi.IsDir() && (fi.Mode()&fs.ModeSetgid) != 0 { 135 return sh.CopyFile(dst, src, perm, force) 136 } 137 } 138 139 // The perm argument is meant to be adjusted according to umask, 140 // but we don't know what the umask is. 141 // Create a dummy file to find out. 142 // This avoids build tags and works even on systems like Plan 9 143 // where the file mask computation incorporates other information. 144 mode := perm 145 f, err := os.OpenFile(filepath.Clean(dst)+"-go-tmp-umask", os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) 146 if err == nil { 147 fi, err := f.Stat() 148 if err == nil { 149 mode = fi.Mode() & 0777 150 } 151 name := f.Name() 152 f.Close() 153 os.Remove(name) 154 } 155 156 if err := os.Chmod(src, mode); err == nil { 157 if err := os.Rename(src, dst); err == nil { 158 if cfg.BuildX { 159 sh.ShowCmd("", "mv %s %s", src, dst) 160 } 161 return nil 162 } 163 } 164 165 return sh.CopyFile(dst, src, perm, force) 166 } 167 168 // copyFile is like 'cp src dst'. 169 func (sh *Shell) CopyFile(dst, src string, perm fs.FileMode, force bool) error { 170 if cfg.BuildN || cfg.BuildX { 171 sh.ShowCmd("", "cp %s %s", src, dst) 172 if cfg.BuildN { 173 return nil 174 } 175 } 176 177 sf, err := os.Open(src) 178 if err != nil { 179 return err 180 } 181 defer sf.Close() 182 183 // Be careful about removing/overwriting dst. 184 // Do not remove/overwrite if dst exists and is a directory 185 // or a non-empty non-object file. 186 if fi, err := os.Stat(dst); err == nil { 187 if fi.IsDir() { 188 return fmt.Errorf("build output %q already exists and is a directory", dst) 189 } 190 if !force && fi.Mode().IsRegular() && fi.Size() != 0 && !isObject(dst) { 191 return fmt.Errorf("build output %q already exists and is not an object file", dst) 192 } 193 } 194 195 // On Windows, remove lingering ~ file from last attempt. 196 if runtime.GOOS == "windows" { 197 if _, err := os.Stat(dst + "~"); err == nil { 198 os.Remove(dst + "~") 199 } 200 } 201 202 mayberemovefile(dst) 203 df, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 204 if err != nil && runtime.GOOS == "windows" { 205 // Windows does not allow deletion of a binary file 206 // while it is executing. Try to move it out of the way. 207 // If the move fails, which is likely, we'll try again the 208 // next time we do an install of this binary. 209 if err := os.Rename(dst, dst+"~"); err == nil { 210 os.Remove(dst + "~") 211 } 212 df, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 213 } 214 if err != nil { 215 return fmt.Errorf("copying %s: %w", src, err) // err should already refer to dst 216 } 217 218 _, err = io.Copy(df, sf) 219 df.Close() 220 if err != nil { 221 mayberemovefile(dst) 222 return fmt.Errorf("copying %s to %s: %v", src, dst, err) 223 } 224 return nil 225 } 226 227 // mayberemovefile removes a file only if it is a regular file 228 // When running as a user with sufficient privileges, we may delete 229 // even device files, for example, which is not intended. 230 func mayberemovefile(s string) { 231 if fi, err := os.Lstat(s); err == nil && !fi.Mode().IsRegular() { 232 return 233 } 234 os.Remove(s) 235 } 236 237 // writeFile writes the text to file. 238 func (sh *Shell) writeFile(file string, text []byte) error { 239 if cfg.BuildN || cfg.BuildX { 240 switch { 241 case len(text) == 0: 242 sh.ShowCmd("", "echo -n > %s # internal", file) 243 case bytes.IndexByte(text, '\n') == len(text)-1: 244 // One line. Use a simpler "echo" command. 245 sh.ShowCmd("", "echo '%s' > %s # internal", bytes.TrimSuffix(text, []byte("\n")), file) 246 default: 247 // Use the most general form. 248 sh.ShowCmd("", "cat >%s << 'EOF' # internal\n%sEOF", file, text) 249 } 250 } 251 if cfg.BuildN { 252 return nil 253 } 254 return os.WriteFile(file, text, 0666) 255 } 256 257 // Mkdir makes the named directory. 258 func (sh *Shell) Mkdir(dir string) error { 259 // Make Mkdir(a.Objdir) a no-op instead of an error when a.Objdir == "". 260 if dir == "" { 261 return nil 262 } 263 264 // We can be a little aggressive about being 265 // sure directories exist. Skip repeated calls. 266 return sh.mkdirCache.Do(dir, func() error { 267 if cfg.BuildN || cfg.BuildX { 268 sh.ShowCmd("", "mkdir -p %s", dir) 269 if cfg.BuildN { 270 return nil 271 } 272 } 273 274 return os.MkdirAll(dir, 0777) 275 }) 276 } 277 278 // RemoveAll is like 'rm -rf'. It attempts to remove all paths even if there's 279 // an error, and returns the first error. 280 func (sh *Shell) RemoveAll(paths ...string) error { 281 if cfg.BuildN || cfg.BuildX { 282 // Don't say we are removing the directory if we never created it. 283 show := func() bool { 284 for _, path := range paths { 285 if _, ok := sh.mkdirCache.Get(path); ok { 286 return true 287 } 288 if _, err := os.Stat(path); !os.IsNotExist(err) { 289 return true 290 } 291 } 292 return false 293 } 294 if show() { 295 sh.ShowCmd("", "rm -rf %s", strings.Join(paths, " ")) 296 } 297 } 298 if cfg.BuildN { 299 return nil 300 } 301 302 var err error 303 for _, path := range paths { 304 if err2 := os.RemoveAll(path); err2 != nil && err == nil { 305 err = err2 306 } 307 } 308 return err 309 } 310 311 // Symlink creates a symlink newname -> oldname. 312 func (sh *Shell) Symlink(oldname, newname string) error { 313 // It's not an error to try to recreate an existing symlink. 314 if link, err := os.Readlink(newname); err == nil && link == oldname { 315 return nil 316 } 317 318 if cfg.BuildN || cfg.BuildX { 319 sh.ShowCmd("", "ln -s %s %s", oldname, newname) 320 if cfg.BuildN { 321 return nil 322 } 323 } 324 return os.Symlink(oldname, newname) 325 } 326 327 // fmtCmd formats a command in the manner of fmt.Sprintf but also: 328 // 329 // fmtCmd replaces the value of b.WorkDir with $WORK. 330 func (sh *Shell) fmtCmd(dir string, format string, args ...any) string { 331 cmd := fmt.Sprintf(format, args...) 332 if sh.workDir != "" && !strings.HasPrefix(cmd, "cat ") { 333 cmd = strings.ReplaceAll(cmd, sh.workDir, "$WORK") 334 escaped := strconv.Quote(sh.workDir) 335 escaped = escaped[1 : len(escaped)-1] // strip quote characters 336 if escaped != sh.workDir { 337 cmd = strings.ReplaceAll(cmd, escaped, "$WORK") 338 } 339 } 340 return cmd 341 } 342 343 // ShowCmd prints the given command to standard output 344 // for the implementation of -n or -x. 345 // 346 // ShowCmd also replaces the name of the current script directory with dot (.) 347 // but only when it is at the beginning of a space-separated token. 348 // 349 // If dir is not "" or "/" and not the current script directory, ShowCmd first 350 // prints a "cd" command to switch to dir and updates the script directory. 351 func (sh *Shell) ShowCmd(dir string, format string, args ...any) { 352 // Use the output lock directly so we can manage scriptDir. 353 sh.printLock.Lock() 354 defer sh.printLock.Unlock() 355 356 cmd := sh.fmtCmd(dir, format, args...) 357 358 if dir != "" && dir != "/" { 359 if dir != sh.scriptDir { 360 // Show changing to dir and update the current directory. 361 sh.printLocked(sh.fmtCmd("", "cd %s\n", dir)) 362 sh.scriptDir = dir 363 } 364 // Replace scriptDir is our working directory. Replace it 365 // with "." in the command. 366 dot := " ." 367 if dir[len(dir)-1] == filepath.Separator { 368 dot += string(filepath.Separator) 369 } 370 cmd = strings.ReplaceAll(" "+cmd, " "+dir, dot)[1:] 371 } 372 373 sh.printLocked(cmd + "\n") 374 } 375 376 // reportCmd reports the output and exit status of a command. The cmdOut and 377 // cmdErr arguments are the output and exit error of the command, respectively. 378 // 379 // The exact reporting behavior is as follows: 380 // 381 // cmdOut cmdErr Result 382 // "" nil print nothing, return nil 383 // !="" nil print output, return nil 384 // "" !=nil print nothing, return cmdErr (later printed) 385 // !="" !=nil print nothing, ignore err, return output as error (later printed) 386 // 387 // reportCmd returns a non-nil error if and only if cmdErr != nil. It assumes 388 // that the command output, if non-empty, is more detailed than the command 389 // error (which is usually just an exit status), so prefers using the output as 390 // the ultimate error. Typically, the caller should return this error from an 391 // Action, which it will be printed by the Builder. 392 // 393 // reportCmd formats the output as "# desc" followed by the given output. The 394 // output is expected to contain references to 'dir', usually the source 395 // directory for the package that has failed to build. reportCmd rewrites 396 // mentions of dir with a relative path to dir when the relative path is 397 // shorter. This is usually more pleasant. For example, if fmt doesn't compile 398 // and we are in src/html, the output is 399 // 400 // $ go build 401 // # fmt 402 // ../fmt/print.go:1090: undefined: asdf 403 // $ 404 // 405 // instead of 406 // 407 // $ go build 408 // # fmt 409 // /usr/gopher/go/src/fmt/print.go:1090: undefined: asdf 410 // $ 411 // 412 // reportCmd also replaces references to the work directory with $WORK, replaces 413 // cgo file paths with the original file path, and replaces cgo-mangled names 414 // with "C.name". 415 // 416 // desc is optional. If "", a.Package.Desc() is used. 417 // 418 // dir is optional. If "", a.Package.Dir is used. 419 func (sh *Shell) reportCmd(desc, dir string, cmdOut []byte, cmdErr error) error { 420 if len(cmdOut) == 0 && cmdErr == nil { 421 // Common case 422 return nil 423 } 424 if len(cmdOut) == 0 && cmdErr != nil { 425 // Just return the error. 426 // 427 // TODO: This is what we've done for a long time, but it may be a 428 // mistake because it loses all of the extra context and results in 429 // ultimately less descriptive output. We should probably just take the 430 // text of cmdErr as the output in this case and do everything we 431 // otherwise would. We could chain the errors if we feel like it. 432 return cmdErr 433 } 434 435 // Fetch defaults from the package. 436 var p *load.Package 437 a := sh.action 438 if a != nil { 439 p = a.Package 440 } 441 var importPath string 442 if p != nil { 443 importPath = p.ImportPath 444 if desc == "" { 445 desc = p.Desc() 446 } 447 if dir == "" { 448 dir = p.Dir 449 } 450 } 451 452 out := string(cmdOut) 453 454 if !strings.HasSuffix(out, "\n") { 455 out = out + "\n" 456 } 457 458 // Replace workDir with $WORK 459 out = replacePrefix(out, sh.workDir, "$WORK") 460 461 // Rewrite mentions of dir with a relative path to dir 462 // when the relative path is shorter. 463 for { 464 // Note that dir starts out long, something like 465 // /foo/bar/baz/root/a 466 // The target string to be reduced is something like 467 // (blah-blah-blah) /foo/bar/baz/root/sibling/whatever.go:blah:blah 468 // /foo/bar/baz/root/a doesn't match /foo/bar/baz/root/sibling, but the prefix 469 // /foo/bar/baz/root does. And there may be other niblings sharing shorter 470 // prefixes, the only way to find them is to look. 471 // This doesn't always produce a relative path -- 472 // /foo is shorter than ../../.., for example. 473 if reldir := base.ShortPath(dir); reldir != dir { 474 out = replacePrefix(out, dir, reldir) 475 if filepath.Separator == '\\' { 476 // Don't know why, sometimes this comes out with slashes, not backslashes. 477 wdir := strings.ReplaceAll(dir, "\\", "/") 478 out = replacePrefix(out, wdir, reldir) 479 } 480 } 481 dirP := filepath.Dir(dir) 482 if dir == dirP { 483 break 484 } 485 dir = dirP 486 } 487 488 // Fix up output referring to cgo-generated code to be more readable. 489 // Replace x.go:19[/tmp/.../x.cgo1.go:18] with x.go:19. 490 // Replace *[100]_Ctype_foo with *[100]C.foo. 491 // If we're using -x, assume we're debugging and want the full dump, so disable the rewrite. 492 if !cfg.BuildX && cgoLine.MatchString(out) { 493 out = cgoLine.ReplaceAllString(out, "") 494 out = cgoTypeSigRe.ReplaceAllString(out, "C.") 495 } 496 497 // Usually desc is already p.Desc(), but if not, signal cmdError.Error to 498 // add a line explicitly metioning the import path. 499 needsPath := importPath != "" && p != nil && desc != p.Desc() 500 501 err := &cmdError{desc, out, importPath, needsPath} 502 if cmdErr != nil { 503 // The command failed. Report the output up as an error. 504 return err 505 } 506 // The command didn't fail, so just print the output as appropriate. 507 if a != nil && a.output != nil { 508 // The Action is capturing output. 509 a.output = append(a.output, err.Error()...) 510 } else { 511 // Write directly to the Builder output. 512 sh.Print(err.Error()) 513 } 514 return nil 515 } 516 517 // replacePrefix is like strings.ReplaceAll, but only replaces instances of old 518 // that are preceded by ' ', '\t', or appear at the beginning of a line. 519 func replacePrefix(s, old, new string) string { 520 n := strings.Count(s, old) 521 if n == 0 { 522 return s 523 } 524 525 s = strings.ReplaceAll(s, " "+old, " "+new) 526 s = strings.ReplaceAll(s, "\n"+old, "\n"+new) 527 s = strings.ReplaceAll(s, "\n\t"+old, "\n\t"+new) 528 if strings.HasPrefix(s, old) { 529 s = new + s[len(old):] 530 } 531 return s 532 } 533 534 type cmdError struct { 535 desc string 536 text string 537 importPath string 538 needsPath bool // Set if desc does not already include the import path 539 } 540 541 func (e *cmdError) Error() string { 542 var msg string 543 if e.needsPath { 544 // Ensure the import path is part of the message. 545 // Clearly distinguish the description from the import path. 546 msg = fmt.Sprintf("# %s\n# [%s]\n", e.importPath, e.desc) 547 } else { 548 msg = "# " + e.desc + "\n" 549 } 550 return msg + e.text 551 } 552 553 func (e *cmdError) ImportPath() string { 554 return e.importPath 555 } 556 557 var cgoLine = lazyregexp.New(`\[[^\[\]]+\.(cgo1|cover)\.go:[0-9]+(:[0-9]+)?\]`) 558 var cgoTypeSigRe = lazyregexp.New(`\b_C2?(type|func|var|macro)_\B`) 559 560 // run runs the command given by cmdline in the directory dir. 561 // If the command fails, run prints information about the failure 562 // and returns a non-nil error. 563 func (sh *Shell) run(dir string, desc string, env []string, cmdargs ...any) error { 564 out, err := sh.runOut(dir, env, cmdargs...) 565 if desc == "" { 566 desc = sh.fmtCmd(dir, "%s", strings.Join(str.StringList(cmdargs...), " ")) 567 } 568 return sh.reportCmd(desc, dir, out, err) 569 } 570 571 // runOut runs the command given by cmdline in the directory dir. 572 // It returns the command output and any errors that occurred. 573 // It accumulates execution time in a. 574 func (sh *Shell) runOut(dir string, env []string, cmdargs ...any) ([]byte, error) { 575 a := sh.action 576 577 cmdline := str.StringList(cmdargs...) 578 579 for _, arg := range cmdline { 580 // GNU binutils commands, including gcc and gccgo, interpret an argument 581 // @foo anywhere in the command line (even following --) as meaning 582 // "read and insert arguments from the file named foo." 583 // Don't say anything that might be misinterpreted that way. 584 if strings.HasPrefix(arg, "@") { 585 return nil, fmt.Errorf("invalid command-line argument %s in command: %s", arg, joinUnambiguously(cmdline)) 586 } 587 } 588 589 if cfg.BuildN || cfg.BuildX { 590 var envcmdline string 591 for _, e := range env { 592 if j := strings.IndexByte(e, '='); j != -1 { 593 if strings.ContainsRune(e[j+1:], '\'') { 594 envcmdline += fmt.Sprintf("%s=%q", e[:j], e[j+1:]) 595 } else { 596 envcmdline += fmt.Sprintf("%s='%s'", e[:j], e[j+1:]) 597 } 598 envcmdline += " " 599 } 600 } 601 envcmdline += joinUnambiguously(cmdline) 602 sh.ShowCmd(dir, "%s", envcmdline) 603 if cfg.BuildN { 604 return nil, nil 605 } 606 } 607 608 var buf bytes.Buffer 609 path, err := cfg.LookPath(cmdline[0]) 610 if err != nil { 611 return nil, err 612 } 613 cmd := exec.Command(path, cmdline[1:]...) 614 if cmd.Path != "" { 615 cmd.Args[0] = cmd.Path 616 } 617 cmd.Stdout = &buf 618 cmd.Stderr = &buf 619 cleanup := passLongArgsInResponseFiles(cmd) 620 defer cleanup() 621 if dir != "." { 622 cmd.Dir = dir 623 } 624 cmd.Env = cmd.Environ() // Pre-allocate with correct PWD. 625 626 // Add the TOOLEXEC_IMPORTPATH environment variable for -toolexec tools. 627 // It doesn't really matter if -toolexec isn't being used. 628 // Note that a.Package.Desc is not really an import path, 629 // but this is consistent with 'go list -f {{.ImportPath}}'. 630 // Plus, it is useful to uniquely identify packages in 'go list -json'. 631 if a != nil && a.Package != nil { 632 cmd.Env = append(cmd.Env, "TOOLEXEC_IMPORTPATH="+a.Package.Desc()) 633 } 634 635 cmd.Env = append(cmd.Env, env...) 636 start := time.Now() 637 err = cmd.Run() 638 if a != nil && a.json != nil { 639 aj := a.json 640 aj.Cmd = append(aj.Cmd, joinUnambiguously(cmdline)) 641 aj.CmdReal += time.Since(start) 642 if ps := cmd.ProcessState; ps != nil { 643 aj.CmdUser += ps.UserTime() 644 aj.CmdSys += ps.SystemTime() 645 } 646 } 647 648 // err can be something like 'exit status 1'. 649 // Add information about what program was running. 650 // Note that if buf.Bytes() is non-empty, the caller usually 651 // shows buf.Bytes() and does not print err at all, so the 652 // prefix here does not make most output any more verbose. 653 if err != nil { 654 err = errors.New(cmdline[0] + ": " + err.Error()) 655 } 656 return buf.Bytes(), err 657 } 658 659 // joinUnambiguously prints the slice, quoting where necessary to make the 660 // output unambiguous. 661 // TODO: See issue 5279. The printing of commands needs a complete redo. 662 func joinUnambiguously(a []string) string { 663 var buf strings.Builder 664 for i, s := range a { 665 if i > 0 { 666 buf.WriteByte(' ') 667 } 668 q := strconv.Quote(s) 669 // A gccgo command line can contain -( and -). 670 // Make sure we quote them since they are special to the shell. 671 // The trimpath argument can also contain > (part of =>) and ;. Quote those too. 672 if s == "" || strings.ContainsAny(s, " ()>;") || len(q) > len(s)+2 { 673 buf.WriteString(q) 674 } else { 675 buf.WriteString(s) 676 } 677 } 678 return buf.String() 679 }