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  }