1
2
3
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
29
30
31
32 type Shell struct {
33 action *Action
34 *shellShared
35 }
36
37
38
39 type shellShared struct {
40 workDir string
41
42 printLock sync.Mutex
43 printer load.Printer
44 scriptDir string
45
46 mkdirCache par.Cache[string, error]
47 }
48
49
50
51
52
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
72
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
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
91 func (sh *Shell) WithAction(a *Action) *Shell {
92 sh2 := *sh
93 sh2.action = a
94 return &sh2
95 }
96
97
98 func (b *Builder) Shell(a *Action) *Shell {
99 if a == nil {
100
101
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
111
112 func (b *Builder) BackgroundShell() *Shell {
113 return b.backgroundSh
114 }
115
116
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
129
130
131
132
133 if sh.workDir == "" || !str.HasFilePathPrefix(src, sh.workDir) {
134 return sh.CopyFile(dst, src, perm, force)
135 }
136
137
138
139
140
141 if runtime.GOOS == "windows" {
142 return sh.CopyFile(dst, src, perm, force)
143 }
144
145
146
147
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
155
156
157
158
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
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
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
214
215
216
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)
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
236
237
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
246
247
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
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
268 sh.ShowCmd("", "echo '%s' > %s # internal", bytes.TrimSuffix(text, []byte("\n")), file)
269 default:
270
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
281 func (sh *Shell) Mkdir(dir string) error {
282
283 if dir == "" {
284 return nil
285 }
286
287
288
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
302
303 func (sh *Shell) RemoveAll(paths ...string) error {
304 if cfg.BuildN || cfg.BuildX {
305
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
335 func (sh *Shell) Symlink(oldname, newname string) error {
336
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
351
352
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]
359 if escaped != sh.workDir {
360 cmd = strings.ReplaceAll(cmd, escaped, "$WORK")
361 }
362 }
363 return cmd
364 }
365
366
367
368
369
370
371
372
373
374 func (sh *Shell) ShowCmd(dir string, format string, args ...any) {
375
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
384 sh.printfLocked("%s", sh.fmtCmd("", "cd %s\n", dir))
385 sh.scriptDir = dir
386 }
387
388
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
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 func (sh *Shell) reportCmd(desc, dir string, cmdOut []byte, cmdErr error) error {
443 if len(cmdOut) == 0 && cmdErr == nil {
444
445 return nil
446 }
447 if len(cmdOut) == 0 && cmdErr != nil {
448
449
450
451
452
453
454
455 return cmdErr
456 }
457
458
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
482 out = replacePrefix(out, sh.workDir, "$WORK")
483
484
485
486 for {
487
488
489
490
491
492
493
494
495
496 if reldir := base.ShortPath(dir); reldir != dir {
497 out = replacePrefix(out, dir, reldir)
498 if filepath.Separator == '\\' {
499
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
512
513 needsPath := importPath != "" && p != nil && desc != p.Desc()
514
515 err := &cmdError{desc, out, importPath, needsPath}
516 if cmdErr != nil {
517
518 return err
519 }
520
521 if a != nil && a.output != nil {
522
523 a.output = append(a.output, err.Error()...)
524 } else {
525
526 sh.Printf("%s", err)
527 }
528 return nil
529 }
530
531
532
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
553 }
554
555 func (e *cmdError) Error() string {
556 var msg string
557 if e.needsPath {
558
559
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
572
573
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
583
584
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
603
604
605
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()
647
648
649
650
651
652
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
693
694
695
696
697 if err != nil {
698 err = fmt.Errorf("%s: %w", sc.cmdline[0], err)
699 }
700 return sc.buf.Bytes(), err
701 }
702
703
704
705
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
714
715
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