Help us shape the future of Go! Take the 2026 Go Developer Survey

Source file src/cmd/go/internal/work/shell.go

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

View as plain text