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
5package work
6
7import (
8	"bytes"
9	"cmd/go/internal/base"
10	"cmd/go/internal/cache"
11	"cmd/go/internal/cfg"
12	"cmd/go/internal/load"
13	"cmd/go/internal/par"
14	"cmd/go/internal/str"
15	"errors"
16	"fmt"
17	"internal/lazyregexp"
18	"io"
19	"io/fs"
20	"os"
21	"os/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.
34type 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).
41type 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.
55func 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.
70func (sh *Shell) Print(a ...any) {
71	sh.printLock.Lock()
72	defer sh.printLock.Unlock()
73	sh.printFunc(a...)
74}
75
76func (sh *Shell) printLocked(a ...any) {
77	sh.printFunc(a...)
78}
79
80// WithAction returns a Shell identical to sh, but bound to Action a.
81func (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.
88func (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.
102func (b *Builder) BackgroundShell() *Shell {
103	return b.backgroundSh
104}
105
106// moveOrCopyFile is like 'mv src dst' or 'cp src dst'.
107func (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	dir, _ := cache.DefaultDir()
118	if strings.HasPrefix(src, dir) {
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'.
169func (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.
230func 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.
238func (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.
258func (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.
280func (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.
312func (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.
330func (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.
351func (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.
419func (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 mentioning 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.
519func 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
534type 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
541func (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
553func (e *cmdError) ImportPath() string {
554	return e.importPath
555}
556
557var cgoLine = lazyregexp.New(`\[[^\[\]]+\.(cgo1|cover)\.go:[0-9]+(:[0-9]+)?\]`)
558var 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.
563func (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.
574func (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.
662func 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}
680