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