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