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