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