1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123 package main
124
125 import (
126 "bufio"
127 "flag"
128 "fmt"
129 "io"
130 "log"
131 "os"
132 "os/exec"
133 "path/filepath"
134 "runtime"
135 "strings"
136 "time"
137 )
138
139 var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
140
141 Examples:
142 toolstash save
143 toolstash restore
144 toolstash go run x.go
145 toolstash compile x.go
146 toolstash -cmp compile x.go
147
148 For details, godoc golang.org/x/tools/cmd/toolstash
149 `
150
151 func usage() {
152 fmt.Fprint(os.Stderr, usageMessage)
153 os.Exit(2)
154 }
155
156 var (
157 goCmd = flag.String("go", "go", "path to \"go\" command")
158 norun = flag.Bool("n", false, "print but do not run commands")
159 verbose = flag.Bool("v", false, "print commands being run")
160 cmp = flag.Bool("cmp", false, "compare tool object files")
161 timing = flag.Bool("t", false, "print time commands take")
162 )
163
164 var (
165 cmd []string
166 tool string
167 toolStash string
168
169 goroot string
170 toolDir string
171 stashDir string
172 binDir string
173 )
174
175 func canCmp(name string, args []string) bool {
176 switch name {
177 case "asm", "compile", "link":
178 if len(args) == 1 && (args[0] == "-V" || strings.HasPrefix(args[0], "-V=")) {
179
180 return false
181 }
182 return true
183 }
184 return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l')
185 }
186
187 var binTools = []string{"go", "godoc", "gofmt"}
188
189 func isBinTool(name string) bool {
190 return strings.HasPrefix(name, "go")
191 }
192
193 func main() {
194 log.SetFlags(0)
195 log.SetPrefix("toolstash: ")
196
197 flag.Usage = usage
198 flag.Parse()
199 cmd = flag.Args()
200
201 if len(cmd) < 1 {
202 usage()
203 }
204
205 s, err := exec.Command(*goCmd, "env", "GOROOT").CombinedOutput()
206 if err != nil {
207 log.Fatalf("%s env GOROOT: %v", *goCmd, err)
208 }
209 goroot = strings.TrimSpace(string(s))
210 toolDir = filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
211 stashDir = filepath.Join(goroot, "pkg/toolstash")
212
213 binDir = os.Getenv("GOBIN")
214 if binDir == "" {
215 binDir = filepath.Join(goroot, "bin")
216 }
217
218 switch cmd[0] {
219 case "save":
220 save()
221 return
222
223 case "restore":
224 restore()
225 return
226 }
227
228 tool = cmd[0]
229 if i := strings.LastIndexAny(tool, `/\`); i >= 0 {
230 tool = tool[i+1:]
231 }
232
233 if !strings.HasPrefix(tool, "a.out") {
234 toolStash = filepath.Join(stashDir, tool)
235 if _, err := os.Stat(toolStash); err != nil {
236 log.Print(err)
237 os.Exit(2)
238 }
239
240 if *cmp && canCmp(tool, cmd[1:]) {
241 compareTool()
242 return
243 }
244 cmd[0] = toolStash
245 }
246
247 if *norun {
248 fmt.Printf("%s\n", strings.Join(cmd, " "))
249 return
250 }
251 if *verbose {
252 log.Print(strings.Join(cmd, " "))
253 }
254 xcmd := exec.Command(cmd[0], cmd[1:]...)
255 xcmd.Stdin = os.Stdin
256 xcmd.Stdout = os.Stdout
257 xcmd.Stderr = os.Stderr
258 err = xcmd.Run()
259 if err != nil {
260 log.Fatal(err)
261 }
262 os.Exit(0)
263 }
264
265 func compareTool() {
266 if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) {
267 cmd[0] = filepath.Join(toolDir, tool)
268 }
269
270 outfile, ok := cmpRun(false, cmd)
271 if ok {
272 os.Remove(outfile + ".stash")
273 return
274 }
275
276 extra := "-S=2"
277 switch {
278 default:
279 log.Fatalf("unknown tool %s", tool)
280
281 case tool == "compile" || strings.HasSuffix(tool, "g"):
282 useDashN := true
283 dashcIndex := -1
284 for i, s := range cmd {
285 if s == "-+" {
286
287 useDashN = false
288 }
289 if strings.HasPrefix(s, "-c=") {
290 dashcIndex = i
291 }
292 }
293 cmdN := injectflags(cmd, nil, useDashN)
294 _, ok := cmpRun(false, cmdN)
295 if !ok {
296 if useDashN {
297 log.Printf("compiler output differs, with optimizers disabled (-N)")
298 } else {
299 log.Printf("compiler output differs")
300 }
301 if dashcIndex >= 0 {
302 cmd[dashcIndex] = "-c=1"
303 }
304 cmd = injectflags(cmd, []string{"-v", "-m=2"}, useDashN)
305 break
306 }
307 if dashcIndex >= 0 {
308 cmd[dashcIndex] = "-c=1"
309 }
310 cmd = injectflags(cmd, []string{"-v", "-m=2"}, false)
311 log.Printf("compiler output differs, only with optimizers enabled")
312
313 case tool == "asm" || strings.HasSuffix(tool, "a"):
314 log.Printf("assembler output differs")
315
316 case tool == "link" || strings.HasSuffix(tool, "l"):
317 log.Printf("linker output differs")
318 extra = "-v=2"
319 }
320
321 cmdS := injectflags(cmd, []string{extra}, false)
322 outfile, _ = cmpRun(true, cmdS)
323
324 fmt.Fprintf(os.Stderr, "\n%s\n", compareLogs(outfile))
325 os.Exit(2)
326 }
327
328 func injectflags(cmd []string, extra []string, addDashN bool) []string {
329 x := []string{cmd[0]}
330 if addDashN {
331 x = append(x, "-N")
332 }
333 x = append(x, extra...)
334 x = append(x, cmd[1:]...)
335 return x
336 }
337
338 func cmpRun(keepLog bool, cmd []string) (outfile string, match bool) {
339 cmdStash := make([]string, len(cmd))
340 copy(cmdStash, cmd)
341 cmdStash[0] = toolStash
342 for i, arg := range cmdStash {
343 if arg == "-o" {
344 outfile = cmdStash[i+1]
345 cmdStash[i+1] += ".stash"
346 break
347 }
348 if strings.HasSuffix(arg, ".s") || strings.HasSuffix(arg, ".go") && '0' <= tool[0] && tool[0] <= '9' {
349 outfile = filepath.Base(arg[:strings.LastIndex(arg, ".")] + "." + tool[:1])
350 cmdStash = append([]string{cmdStash[0], "-o", outfile + ".stash"}, cmdStash[1:]...)
351 break
352 }
353 }
354
355 if outfile == "" {
356 log.Fatalf("cannot determine output file for command: %s", strings.Join(cmd, " "))
357 }
358
359 if *norun {
360 fmt.Printf("%s\n", strings.Join(cmd, " "))
361 fmt.Printf("%s\n", strings.Join(cmdStash, " "))
362 os.Exit(0)
363 }
364
365 out, err := runCmd(cmd, keepLog, outfile+".log")
366 if err != nil {
367 log.Printf("running: %s", strings.Join(cmd, " "))
368 os.Stderr.Write(out)
369 log.Fatal(err)
370 }
371
372 outStash, err := runCmd(cmdStash, keepLog, outfile+".stash.log")
373 if err != nil {
374 log.Printf("running: %s", strings.Join(cmdStash, " "))
375 log.Printf("installed tool succeeded but stashed tool failed.\n")
376 if len(out) > 0 {
377 log.Printf("installed tool output:")
378 os.Stderr.Write(out)
379 }
380 if len(outStash) > 0 {
381 log.Printf("stashed tool output:")
382 os.Stderr.Write(outStash)
383 }
384 log.Fatal(err)
385 }
386
387 return outfile, sameObject(outfile, outfile+".stash")
388 }
389
390 func sameObject(file1, file2 string) bool {
391 f1, err := os.Open(file1)
392 if err != nil {
393 log.Fatal(err)
394 }
395 defer f1.Close()
396
397 f2, err := os.Open(file2)
398 if err != nil {
399 log.Fatal(err)
400 }
401 defer f2.Close()
402
403 b1 := bufio.NewReader(f1)
404 b2 := bufio.NewReader(f2)
405
406
407
408
409
410
411 if !skipVersion(b1, b2, file1, file2) {
412 return false
413 }
414
415 lastByte := byte(0)
416 for {
417 c1, err1 := b1.ReadByte()
418 c2, err2 := b2.ReadByte()
419 if err1 == io.EOF && err2 == io.EOF {
420 return true
421 }
422 if err1 != nil {
423 log.Fatalf("reading %s: %v", file1, err1)
424 }
425 if err2 != nil {
426 log.Fatalf("reading %s: %v", file2, err2)
427 }
428 if c1 != c2 {
429 return false
430 }
431 if lastByte == '`' && c1 == '\n' {
432 if !skipVersion(b1, b2, file1, file2) {
433 return false
434 }
435 }
436 lastByte = c1
437 }
438 }
439
440 func skipVersion(b1, b2 *bufio.Reader, file1, file2 string) bool {
441
442 prefix := "go object "
443 for i := 0; i < len(prefix); i++ {
444 c1, err1 := b1.ReadByte()
445 c2, err2 := b2.ReadByte()
446 if err1 == io.EOF && err2 == io.EOF {
447 return true
448 }
449 if err1 != nil {
450 log.Fatalf("reading %s: %v", file1, err1)
451 }
452 if err2 != nil {
453 log.Fatalf("reading %s: %v", file2, err2)
454 }
455 if c1 != c2 {
456 return false
457 }
458 if c1 != prefix[i] {
459 return true
460 }
461 }
462
463
464
465
466 for numSpace := 0; numSpace < 2; {
467 c1, err1 := b1.ReadByte()
468 c2, err2 := b2.ReadByte()
469 if err1 == io.EOF && err2 == io.EOF {
470 return true
471 }
472 if err1 != nil {
473 log.Fatalf("reading %s: %v", file1, err1)
474 }
475 if err2 != nil {
476 log.Fatalf("reading %s: %v", file2, err2)
477 }
478 if c1 != c2 {
479 return false
480 }
481 if c1 == '\n' {
482 return true
483 }
484 if c1 == ' ' {
485 numSpace++
486 }
487 }
488
489
490
491
492 for {
493 c1, err1 := b1.ReadByte()
494 if err1 == io.EOF {
495 log.Fatalf("reading %s: unexpected EOF", file1)
496 }
497 if err1 != nil {
498 log.Fatalf("reading %s: %v", file1, err1)
499 }
500 if c1 == '\n' {
501 break
502 }
503 }
504 for {
505 c2, err2 := b2.ReadByte()
506 if err2 == io.EOF {
507 log.Fatalf("reading %s: unexpected EOF", file2)
508 }
509 if err2 != nil {
510 log.Fatalf("reading %s: %v", file2, err2)
511 }
512 if c2 == '\n' {
513 break
514 }
515 }
516
517
518 return true
519 }
520
521 func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err error) {
522 if *verbose {
523 log.Print(strings.Join(cmd, " "))
524 }
525
526 if *timing {
527 t0 := time.Now()
528 defer func() {
529 log.Printf("%.3fs elapsed # %s\n", time.Since(t0).Seconds(), strings.Join(cmd, " "))
530 }()
531 }
532
533 xcmd := exec.Command(cmd[0], cmd[1:]...)
534 if !keepLog {
535 return xcmd.CombinedOutput()
536 }
537
538 f, err := os.Create(logName)
539 if err != nil {
540 log.Fatal(err)
541 }
542 fmt.Fprintf(f, "GOOS=%s GOARCH=%s %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd, " "))
543 xcmd.Stdout = f
544 xcmd.Stderr = f
545 defer f.Close()
546 return nil, xcmd.Run()
547 }
548
549 func save() {
550 if err := os.MkdirAll(stashDir, 0777); err != nil {
551 log.Fatal(err)
552 }
553
554 toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
555 files, err := os.ReadDir(toolDir)
556 if err != nil {
557 log.Fatal(err)
558 }
559
560 for _, file := range files {
561 info, err := file.Info()
562 if err != nil {
563 log.Fatal(err)
564 }
565 if shouldSave(file.Name()) && info.Mode().IsRegular() {
566 cp(filepath.Join(toolDir, file.Name()), filepath.Join(stashDir, file.Name()))
567 }
568 }
569
570 for _, name := range binTools {
571 if !shouldSave(name) {
572 continue
573 }
574 src := filepath.Join(binDir, name)
575 if _, err := os.Stat(src); err == nil {
576 cp(src, filepath.Join(stashDir, name))
577 }
578 }
579
580 checkShouldSave()
581 }
582
583 func restore() {
584 files, err := os.ReadDir(stashDir)
585 if err != nil {
586 log.Fatal(err)
587 }
588
589 for _, file := range files {
590 info, err := file.Info()
591 if err != nil {
592 log.Fatal(err)
593 }
594 if shouldSave(file.Name()) && info.Mode().IsRegular() {
595 targ := toolDir
596 if isBinTool(file.Name()) {
597 targ = binDir
598 }
599 cp(filepath.Join(stashDir, file.Name()), filepath.Join(targ, file.Name()))
600 }
601 }
602
603 checkShouldSave()
604 }
605
606 func shouldSave(name string) bool {
607 if len(cmd) == 1 {
608 return true
609 }
610 ok := false
611 for i, arg := range cmd {
612 if i > 0 && name == arg {
613 ok = true
614 cmd[i] = "DONE"
615 }
616 }
617 return ok
618 }
619
620 func checkShouldSave() {
621 var missing []string
622 for _, arg := range cmd[1:] {
623 if arg != "DONE" {
624 missing = append(missing, arg)
625 }
626 }
627 if len(missing) > 0 {
628 log.Fatalf("%s did not find tools: %s", cmd[0], strings.Join(missing, " "))
629 }
630 }
631
632 func cp(src, dst string) {
633 if *verbose {
634 fmt.Printf("cp %s %s\n", src, dst)
635 }
636 data, err := os.ReadFile(src)
637 if err != nil {
638 log.Fatal(err)
639 }
640 if err := os.WriteFile(dst, data, 0777); err != nil {
641 log.Fatal(err)
642 }
643 }
644
View as plain text