1
2
3
4
5 package work
6
7 import (
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/str"
14 "cmd/internal/par"
15 "cmd/internal/pathcache"
16 "fmt"
17 "io"
18 "io/fs"
19 "os"
20 "os/exec"
21 "path/filepath"
22 "runtime"
23 "strconv"
24 "strings"
25 "sync"
26 "time"
27 )
28
29
30
31
32
33 type Shell struct {
34 action *Action
35 *shellShared
36 }
37
38
39
40 type shellShared struct {
41 workDir string
42
43 printLock sync.Mutex
44 printer load.Printer
45 scriptDir string
46
47 mkdirCache par.Cache[string, error]
48 }
49
50
51
52
53
54 func NewShell(workDir string, printer load.Printer) *Shell {
55 if printer == nil {
56 printer = load.DefaultPrinter()
57 }
58 shared := &shellShared{
59 workDir: workDir,
60 printer: printer,
61 }
62 return &Shell{shellShared: shared}
63 }
64
65 func (sh *Shell) pkg() *load.Package {
66 if sh.action == nil {
67 return nil
68 }
69 return sh.action.Package
70 }
71
72
73
74 func (sh *Shell) Printf(format string, a ...any) {
75 sh.printLock.Lock()
76 defer sh.printLock.Unlock()
77 sh.printer.Printf(sh.pkg(), format, a...)
78 }
79
80 func (sh *Shell) printfLocked(format string, a ...any) {
81 sh.printer.Printf(sh.pkg(), format, a...)
82 }
83
84
85 func (sh *Shell) Errorf(format string, a ...any) {
86 sh.printLock.Lock()
87 defer sh.printLock.Unlock()
88 sh.printer.Errorf(sh.pkg(), format, a...)
89 }
90
91
92 func (sh *Shell) WithAction(a *Action) *Shell {
93 sh2 := *sh
94 sh2.action = a
95 return &sh2
96 }
97
98
99 func (b *Builder) Shell(a *Action) *Shell {
100 if a == nil {
101
102
103 panic("nil Action")
104 }
105 if a.sh == nil {
106 a.sh = b.backgroundSh.WithAction(a)
107 }
108 return a.sh
109 }
110
111
112
113 func (b *Builder) BackgroundShell() *Shell {
114 return b.backgroundSh
115 }
116
117
118 func (sh *Shell) moveOrCopyFile(dst, src string, perm fs.FileMode, force bool) error {
119 if cfg.BuildN {
120 sh.ShowCmd("", "mv %s %s", src, dst)
121 return nil
122 }
123
124 err := checkDstOverwrite(dst, force)
125 if err != nil {
126 return err
127 }
128
129
130
131
132
133 dir, _, _ := cache.DefaultDir()
134 if strings.HasPrefix(src, dir) {
135 return sh.CopyFile(dst, src, perm, force)
136 }
137
138
139
140
141
142 if runtime.GOOS == "windows" {
143 return sh.CopyFile(dst, src, perm, force)
144 }
145
146
147
148
149 if fi, err := os.Stat(filepath.Dir(dst)); err == nil {
150 if fi.IsDir() && (fi.Mode()&fs.ModeSetgid) != 0 {
151 return sh.CopyFile(dst, src, perm, force)
152 }
153 }
154
155
156
157
158
159
160 mode := perm
161 f, err := os.OpenFile(filepath.Clean(dst)+"-go-tmp-umask", os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
162 if err == nil {
163 fi, err := f.Stat()
164 if err == nil {
165 mode = fi.Mode() & 0777
166 }
167 name := f.Name()
168 f.Close()
169 os.Remove(name)
170 }
171
172 if err := os.Chmod(src, mode); err == nil {
173 if err := os.Rename(src, dst); err == nil {
174 if cfg.BuildX {
175 sh.ShowCmd("", "mv %s %s", src, dst)
176 }
177 return nil
178 }
179 }
180
181 return sh.CopyFile(dst, src, perm, force)
182 }
183
184
185 func (sh *Shell) CopyFile(dst, src string, perm fs.FileMode, force bool) error {
186 if cfg.BuildN || cfg.BuildX {
187 sh.ShowCmd("", "cp %s %s", src, dst)
188 if cfg.BuildN {
189 return nil
190 }
191 }
192
193 sf, err := os.Open(src)
194 if err != nil {
195 return err
196 }
197 defer sf.Close()
198
199 err = checkDstOverwrite(dst, force)
200 if err != nil {
201 return err
202 }
203
204
205 if runtime.GOOS == "windows" {
206 if _, err := os.Stat(dst + "~"); err == nil {
207 os.Remove(dst + "~")
208 }
209 }
210
211 mayberemovefile(dst)
212 df, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
213 if err != nil && runtime.GOOS == "windows" {
214
215
216
217
218 if err := os.Rename(dst, dst+"~"); err == nil {
219 os.Remove(dst + "~")
220 }
221 df, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
222 }
223 if err != nil {
224 return fmt.Errorf("copying %s: %w", src, err)
225 }
226
227 _, err = io.Copy(df, sf)
228 df.Close()
229 if err != nil {
230 mayberemovefile(dst)
231 return fmt.Errorf("copying %s to %s: %v", src, dst, err)
232 }
233 return nil
234 }
235
236
237
238
239 func mayberemovefile(s string) {
240 if fi, err := os.Lstat(s); err == nil && !fi.Mode().IsRegular() {
241 return
242 }
243 os.Remove(s)
244 }
245
246
247
248
249 func checkDstOverwrite(dst string, force bool) error {
250 if fi, err := os.Stat(dst); err == nil {
251 if fi.IsDir() {
252 return fmt.Errorf("build output %q already exists and is a directory", dst)
253 }
254 if !force && fi.Mode().IsRegular() && fi.Size() != 0 && !isObject(dst) {
255 return fmt.Errorf("build output %q already exists and is not an object file", dst)
256 }
257 }
258 return nil
259 }
260
261
262 func (sh *Shell) writeFile(file string, text []byte) error {
263 if cfg.BuildN || cfg.BuildX {
264 switch {
265 case len(text) == 0:
266 sh.ShowCmd("", "echo -n > %s # internal", file)
267 case bytes.IndexByte(text, '\n') == len(text)-1:
268
269 sh.ShowCmd("", "echo '%s' > %s # internal", bytes.TrimSuffix(text, []byte("\n")), file)
270 default:
271
272 sh.ShowCmd("", "cat >%s << 'EOF' # internal\n%sEOF", file, text)
273 }
274 }
275 if cfg.BuildN {
276 return nil
277 }
278 return os.WriteFile(file, text, 0666)
279 }
280
281
282 func (sh *Shell) Mkdir(dir string) error {
283
284 if dir == "" {
285 return nil
286 }
287
288
289
290 return sh.mkdirCache.Do(dir, func() error {
291 if cfg.BuildN || cfg.BuildX {
292 sh.ShowCmd("", "mkdir -p %s", dir)
293 if cfg.BuildN {
294 return nil
295 }
296 }
297
298 return os.MkdirAll(dir, 0777)
299 })
300 }
301
302
303
304 func (sh *Shell) RemoveAll(paths ...string) error {
305 if cfg.BuildN || cfg.BuildX {
306
307 show := func() bool {
308 for _, path := range paths {
309 if _, ok := sh.mkdirCache.Get(path); ok {
310 return true
311 }
312 if _, err := os.Stat(path); !os.IsNotExist(err) {
313 return true
314 }
315 }
316 return false
317 }
318 if show() {
319 sh.ShowCmd("", "rm -rf %s", strings.Join(paths, " "))
320 }
321 }
322 if cfg.BuildN {
323 return nil
324 }
325
326 var err error
327 for _, path := range paths {
328 if err2 := os.RemoveAll(path); err2 != nil && err == nil {
329 err = err2
330 }
331 }
332 return err
333 }
334
335
336 func (sh *Shell) Symlink(oldname, newname string) error {
337
338 if link, err := os.Readlink(newname); err == nil && link == oldname {
339 return nil
340 }
341
342 if cfg.BuildN || cfg.BuildX {
343 sh.ShowCmd("", "ln -s %s %s", oldname, newname)
344 if cfg.BuildN {
345 return nil
346 }
347 }
348 return os.Symlink(oldname, newname)
349 }
350
351
352
353
354 func (sh *Shell) fmtCmd(dir string, format string, args ...any) string {
355 cmd := fmt.Sprintf(format, args...)
356 if sh.workDir != "" && !strings.HasPrefix(cmd, "cat ") {
357 cmd = strings.ReplaceAll(cmd, sh.workDir, "$WORK")
358 escaped := strconv.Quote(sh.workDir)
359 escaped = escaped[1 : len(escaped)-1]
360 if escaped != sh.workDir {
361 cmd = strings.ReplaceAll(cmd, escaped, "$WORK")
362 }
363 }
364 return cmd
365 }
366
367
368
369
370
371
372
373
374
375 func (sh *Shell) ShowCmd(dir string, format string, args ...any) {
376
377 sh.printLock.Lock()
378 defer sh.printLock.Unlock()
379
380 cmd := sh.fmtCmd(dir, format, args...)
381
382 if dir != "" && dir != "/" {
383 if dir != sh.scriptDir {
384
385 sh.printfLocked("%s", sh.fmtCmd("", "cd %s\n", dir))
386 sh.scriptDir = dir
387 }
388
389
390 dot := " ."
391 if dir[len(dir)-1] == filepath.Separator {
392 dot += string(filepath.Separator)
393 }
394 cmd = strings.ReplaceAll(" "+cmd, " "+dir, dot)[1:]
395 }
396
397 sh.printfLocked("%s\n", cmd)
398 }
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443 func (sh *Shell) reportCmd(desc, dir string, cmdOut []byte, cmdErr error) error {
444 if len(cmdOut) == 0 && cmdErr == nil {
445
446 return nil
447 }
448 if len(cmdOut) == 0 && cmdErr != nil {
449
450
451
452
453
454
455
456 return cmdErr
457 }
458
459
460 var p *load.Package
461 a := sh.action
462 if a != nil {
463 p = a.Package
464 }
465 var importPath string
466 if p != nil {
467 importPath = p.ImportPath
468 if desc == "" {
469 desc = p.Desc()
470 }
471 if dir == "" {
472 dir = p.Dir
473 }
474 }
475
476 out := string(cmdOut)
477
478 if !strings.HasSuffix(out, "\n") {
479 out = out + "\n"
480 }
481
482
483 out = replacePrefix(out, sh.workDir, "$WORK")
484
485
486
487 for {
488
489
490
491
492
493
494
495
496
497 if reldir := base.ShortPath(dir); reldir != dir {
498 out = replacePrefix(out, dir, reldir)
499 if filepath.Separator == '\\' {
500
501 wdir := strings.ReplaceAll(dir, "\\", "/")
502 out = replacePrefix(out, wdir, reldir)
503 }
504 }
505 dirP := filepath.Dir(dir)
506 if dir == dirP {
507 break
508 }
509 dir = dirP
510 }
511
512
513
514 needsPath := importPath != "" && p != nil && desc != p.Desc()
515
516 err := &cmdError{desc, out, importPath, needsPath}
517 if cmdErr != nil {
518
519 return err
520 }
521
522 if a != nil && a.output != nil {
523
524 a.output = append(a.output, err.Error()...)
525 } else {
526
527 sh.Printf("%s", err)
528 }
529 return nil
530 }
531
532
533
534 func replacePrefix(s, old, new string) string {
535 n := strings.Count(s, old)
536 if n == 0 {
537 return s
538 }
539
540 s = strings.ReplaceAll(s, " "+old, " "+new)
541 s = strings.ReplaceAll(s, "\n"+old, "\n"+new)
542 s = strings.ReplaceAll(s, "\n\t"+old, "\n\t"+new)
543 if strings.HasPrefix(s, old) {
544 s = new + s[len(old):]
545 }
546 return s
547 }
548
549 type cmdError struct {
550 desc string
551 text string
552 importPath string
553 needsPath bool
554 }
555
556 func (e *cmdError) Error() string {
557 var msg string
558 if e.needsPath {
559
560
561 msg = fmt.Sprintf("# %s\n# [%s]\n", e.importPath, e.desc)
562 } else {
563 msg = "# " + e.desc + "\n"
564 }
565 return msg + e.text
566 }
567
568 func (e *cmdError) ImportPath() string {
569 return e.importPath
570 }
571
572
573
574
575 func (sh *Shell) run(dir string, desc string, env []string, cmdargs ...any) error {
576 out, err := sh.runOut(dir, env, cmdargs...)
577 if desc == "" {
578 desc = sh.fmtCmd(dir, "%s", strings.Join(str.StringList(cmdargs...), " "))
579 }
580 return sh.reportCmd(desc, dir, out, err)
581 }
582
583
584
585
586 func (sh *Shell) runOut(dir string, env []string, cmdargs ...any) ([]byte, error) {
587 sc, err := sh.startOut(dir, env, nil, nil, cmdargs...)
588 if err != nil || sc == nil {
589 return nil, err
590 }
591 return sc.wait()
592 }
593
594 func (sh *Shell) startOut(dir string, env []string, extraFiles []*os.File, done func(), cmdargs ...any) (*shellCmd, error) {
595 for _, f := range extraFiles {
596 defer f.Close()
597 }
598 a := sh.action
599
600 cmdline := str.StringList(cmdargs...)
601
602 for _, arg := range cmdline {
603
604
605
606
607 if strings.HasPrefix(arg, "@") {
608 return nil, fmt.Errorf("invalid command-line argument %s in command: %s", arg, joinUnambiguously(cmdline))
609 }
610 }
611
612 if cfg.BuildN || cfg.BuildX {
613 var envcmdline string
614 for _, e := range env {
615 if j := strings.IndexByte(e, '='); j != -1 {
616 if strings.ContainsRune(e[j+1:], '\'') {
617 envcmdline += fmt.Sprintf("%s=%q", e[:j], e[j+1:])
618 } else {
619 envcmdline += fmt.Sprintf("%s='%s'", e[:j], e[j+1:])
620 }
621 envcmdline += " "
622 }
623 }
624 envcmdline += joinUnambiguously(cmdline)
625 sh.ShowCmd(dir, "%s", envcmdline)
626 if cfg.BuildN {
627 return nil, nil
628 }
629 }
630
631 path, err := pathcache.LookPath(cmdline[0])
632 if err != nil {
633 return nil, err
634 }
635 cmd := exec.Command(path, cmdline[1:]...)
636 if cmd.Path != "" {
637 cmd.Args[0] = cmd.Path
638 }
639 sc := &shellCmd{sh: sh, cmd: cmd, cmdline: cmdline, done: done}
640 cmd.Stdout = &sc.buf
641 cmd.Stderr = &sc.buf
642 cmd.ExtraFiles = extraFiles
643 sc.cleanup = passLongArgsInResponseFiles(cmd)
644 if dir != "." {
645 cmd.Dir = dir
646 }
647 cmd.Env = cmd.Environ()
648
649
650
651
652
653
654 if a != nil && a.Package != nil {
655 cmd.Env = append(cmd.Env, "TOOLEXEC_IMPORTPATH="+a.Package.Desc())
656 }
657
658 cmd.Env = append(cmd.Env, env...)
659 sc.start = time.Now()
660 if err := cmd.Start(); err != nil {
661 sc.cleanup()
662 return nil, fmt.Errorf("%s: %w", cmdline[0], err)
663 }
664 return sc, nil
665 }
666
667 type shellCmd struct {
668 sh *Shell
669 cmd *exec.Cmd
670 cmdline []string
671 buf bytes.Buffer
672 start time.Time
673 cleanup func()
674 done func()
675 }
676
677 func (sc *shellCmd) wait() ([]byte, error) {
678 err := sc.cmd.Wait()
679 sc.cleanup()
680 if sc.done != nil {
681 sc.done()
682 }
683 if a := sc.sh.action; a != nil && a.json != nil {
684 aj := a.json
685 aj.Cmd = append(aj.Cmd, joinUnambiguously(sc.cmdline))
686 aj.CmdReal += time.Since(sc.start)
687 if ps := sc.cmd.ProcessState; ps != nil {
688 aj.CmdUser += ps.UserTime()
689 aj.CmdSys += ps.SystemTime()
690 }
691 }
692
693
694
695
696
697
698 if err != nil {
699 err = fmt.Errorf("%s: %w", sc.cmdline[0], err)
700 }
701 return sc.buf.Bytes(), err
702 }
703
704
705
706
707 func joinUnambiguously(a []string) string {
708 var buf strings.Builder
709 for i, s := range a {
710 if i > 0 {
711 buf.WriteByte(' ')
712 }
713 q := strconv.Quote(s)
714
715
716
717 if s == "" || strings.ContainsAny(s, " ()>;") || len(q) > len(s)+2 {
718 buf.WriteString(q)
719 } else {
720 buf.WriteString(s)
721 }
722 }
723 return buf.String()
724 }
725
View as plain text