1 // Copyright 2015 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // Toolstash provides a way to save, run, and restore a known good copy of the Go toolchain
6 // and to compare the object files generated by two toolchains.
10 // toolstash [-n] [-v] save [tool...]
11 // toolstash [-n] [-v] restore [tool...]
12 // toolstash [-n] [-v] [-t] go run x.go
13 // toolstash [-n] [-v] [-t] [-cmp] compile x.go
15 // The toolstash command manages a ``stashed'' copy of the Go toolchain
16 // kept in $GOROOT/pkg/toolstash. In this case, the toolchain means the
17 // tools available with the 'go tool' command as well as the go, godoc, and gofmt
20 // The command ``toolstash save'', typically run when the toolchain is known to be working,
21 // copies the toolchain from its installed location to the toolstash directory.
22 // Its inverse, ``toolchain restore'', typically run when the toolchain is known to be broken,
23 // copies the toolchain from the toolstash directory back to the installed locations.
24 // If additional arguments are given, the save or restore applies only to the named tools.
25 // Otherwise, it applies to all tools.
27 // Otherwise, toolstash's arguments should be a command line beginning with the
28 // name of a toolchain binary, which may be a short name like compile or a complete path
29 // to an installed binary. Toolstash runs the command line using the stashed
30 // copy of the binary instead of the installed one.
32 // The -n flag causes toolstash to print the commands that would be executed
33 // but not execute them. The combination -n -cmp shows the two commands
34 // that would be compared and then exits successfully. A real -cmp run might
35 // run additional commands for diagnosis of an output mismatch.
37 // The -v flag causes toolstash to print the commands being executed.
39 // The -t flag causes toolstash to print the time elapsed during while the
44 // The -cmp flag causes toolstash to run both the installed and the stashed
45 // copy of an assembler or compiler and check that they produce identical
46 // object files. If not, toolstash reports the mismatch and exits with a failure status.
47 // As part of reporting the mismatch, toolstash reinvokes the command with
48 // the -S=2 flag and identifies the first divergence in the assembly output.
49 // If the command is a Go compiler, toolstash also determines whether the
50 // difference is triggered by optimization passes.
51 // On failure, toolstash leaves additional information in files named
52 // similarly to the default output file. If the compilation would normally
53 // produce a file x.6, the output from the stashed tool is left in x.6.stash
54 // and the debugging traces are left in x.6.log and x.6.stash.log.
56 // The -cmp flag is a no-op when the command line is not invoking an
57 // assembler or compiler.
59 // For example, when working on code cleanup that should not affect
60 // compiler output, toolstash can be used to compare the old and new
64 // <edit compiler sources>
65 // go tool dist install cmd/compile # install compiler only
66 // toolstash -cmp compile x.go
68 // Go Command Integration
70 // The go command accepts a -toolexec flag that specifies a program
71 // to use to run the build tools.
73 // To build with the stashed tools:
75 // go build -toolexec toolstash x.go
77 // To build with the stashed go command and the stashed tools:
79 // toolstash go build -toolexec toolstash x.go
81 // To verify that code cleanup in the compilers does not make any
82 // changes to the objects being generated for the entire tree:
84 // # Build working tree and save tools.
88 // <edit compiler sources>
90 // # Install new tools, but do not rebuild the rest of tree,
91 // # since the compilers might generate buggy code.
92 // go tool dist install cmd/compile
94 // # Check that new tools behave identically to saved tools.
95 // go build -toolexec 'toolstash -cmp' -a std
97 // # If not, restore, in order to keep working on Go code.
102 // The Go tools write the current Go version to object files, and (outside
103 // release branches) that version includes the hash and time stamp
104 // of the most recent Git commit. Functionally equivalent
105 // compilers built at different Git versions may produce object files that
106 // differ only in the recorded version. Toolstash ignores version mismatches
107 // when comparing object files, but the standard tools will refuse to compile
108 // or link together packages with different object versions.
110 // For the full build in the final example above to work, both the stashed
111 // and the installed tools must use the same version string.
112 // One way to ensure this is not to commit any of the changes being
113 // tested, so that the Git HEAD hash is the same for both builds.
114 // A more robust way to force the tools to have the same version string
115 // is to write a $GOROOT/VERSION file, which overrides the Git-based version
118 // echo devel >$GOROOT/VERSION
120 // The version can be arbitrary text, but to pass all.bash's API check, it must
121 // contain the substring ``devel''. The VERSION file must be created before
122 // building either version of the toolchain.
124 package main // import "golang.org/x/tools/cmd/toolstash"
141 var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
146 toolstash go run x.go
147 toolstash compile x.go
148 toolstash -cmp compile x.go
150 For details, godoc golang.org/x/tools/cmd/toolstash
154 fmt.Fprint(os.Stderr, usageMessage)
159 goCmd = flag.String("go", "go", "path to \"go\" command")
160 norun = flag.Bool("n", false, "print but do not run commands")
161 verbose = flag.Bool("v", false, "print commands being run")
162 cmp = flag.Bool("cmp", false, "compare tool object files")
163 timing = flag.Bool("t", false, "print time commands take")
168 tool string // name of tool: "go", "compile", etc
169 toolStash string // path to stashed tool
177 func canCmp(name string, args []string) bool {
179 case "asm", "compile", "link":
180 if len(args) == 1 && (args[0] == "-V" || strings.HasPrefix(args[0], "-V=")) {
181 // cmd/go uses "compile -V=full" to query the tool's build ID.
186 return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l')
189 var binTools = []string{"go", "godoc", "gofmt"}
191 func isBinTool(name string) bool {
192 return strings.HasPrefix(name, "go")
197 log.SetPrefix("toolstash: ")
207 s, err := exec.Command(*goCmd, "env", "GOROOT").CombinedOutput()
209 log.Fatalf("%s env GOROOT: %v", *goCmd, err)
211 goroot = strings.TrimSpace(string(s))
212 toolDir = filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
213 stashDir = filepath.Join(goroot, "pkg/toolstash")
215 binDir = os.Getenv("GOBIN")
217 binDir = filepath.Join(goroot, "bin")
231 if i := strings.LastIndexAny(tool, `/\`); i >= 0 {
235 if !strings.HasPrefix(tool, "a.out") {
236 toolStash = filepath.Join(stashDir, tool)
237 if _, err := os.Stat(toolStash); err != nil {
242 if *cmp && canCmp(tool, cmd[1:]) {
250 fmt.Printf("%s\n", strings.Join(cmd, " "))
254 log.Print(strings.Join(cmd, " "))
256 xcmd := exec.Command(cmd[0], cmd[1:]...)
257 xcmd.Stdin = os.Stdin
258 xcmd.Stdout = os.Stdout
259 xcmd.Stderr = os.Stderr
268 if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) {
269 cmd[0] = filepath.Join(toolDir, tool)
272 outfile, ok := cmpRun(false, cmd)
274 os.Remove(outfile + ".stash")
281 log.Fatalf("unknown tool %s", tool)
283 case tool == "compile" || strings.HasSuffix(tool, "g"): // compiler
286 for i, s := range cmd {
288 // Compiling runtime. Don't use -N.
291 if strings.HasPrefix(s, "-c=") {
295 cmdN := injectflags(cmd, nil, useDashN)
296 _, ok := cmpRun(false, cmdN)
299 log.Printf("compiler output differs, with optimizers disabled (-N)")
301 log.Printf("compiler output differs")
304 cmd[dashcIndex] = "-c=1"
306 cmd = injectflags(cmd, []string{"-v", "-m=2"}, useDashN)
310 cmd[dashcIndex] = "-c=1"
312 cmd = injectflags(cmd, []string{"-v", "-m=2"}, false)
313 log.Printf("compiler output differs, only with optimizers enabled")
315 case tool == "asm" || strings.HasSuffix(tool, "a"): // assembler
316 log.Printf("assembler output differs")
318 case tool == "link" || strings.HasSuffix(tool, "l"): // linker
319 log.Printf("linker output differs")
323 cmdS := injectflags(cmd, []string{extra}, false)
324 outfile, _ = cmpRun(true, cmdS)
326 fmt.Fprintf(os.Stderr, "\n%s\n", compareLogs(outfile))
330 func injectflags(cmd []string, extra []string, addDashN bool) []string {
331 x := []string{cmd[0]}
335 x = append(x, extra...)
336 x = append(x, cmd[1:]...)
340 func cmpRun(keepLog bool, cmd []string) (outfile string, match bool) {
341 cmdStash := make([]string, len(cmd))
343 cmdStash[0] = toolStash
344 for i, arg := range cmdStash {
346 outfile = cmdStash[i+1]
347 cmdStash[i+1] += ".stash"
350 if strings.HasSuffix(arg, ".s") || strings.HasSuffix(arg, ".go") && '0' <= tool[0] && tool[0] <= '9' {
351 outfile = filepath.Base(arg[:strings.LastIndex(arg, ".")] + "." + tool[:1])
352 cmdStash = append([]string{cmdStash[0], "-o", outfile + ".stash"}, cmdStash[1:]...)
358 log.Fatalf("cannot determine output file for command: %s", strings.Join(cmd, " "))
362 fmt.Printf("%s\n", strings.Join(cmd, " "))
363 fmt.Printf("%s\n", strings.Join(cmdStash, " "))
367 out, err := runCmd(cmd, keepLog, outfile+".log")
369 log.Printf("running: %s", strings.Join(cmd, " "))
374 outStash, err := runCmd(cmdStash, keepLog, outfile+".stash.log")
376 log.Printf("running: %s", strings.Join(cmdStash, " "))
377 log.Printf("installed tool succeeded but stashed tool failed.\n")
379 log.Printf("installed tool output:")
382 if len(outStash) > 0 {
383 log.Printf("stashed tool output:")
384 os.Stderr.Write(outStash)
389 return outfile, sameObject(outfile, outfile+".stash")
392 func sameObject(file1, file2 string) bool {
393 f1, err := os.Open(file1)
399 f2, err := os.Open(file2)
405 b1 := bufio.NewReader(f1)
406 b2 := bufio.NewReader(f2)
408 // Go object files and archives contain lines of the form
409 // go object <goos> <goarch> <version>
410 // By default, the version on development branches includes
411 // the Git hash and time stamp for the most recent commit.
412 // We allow the versions to differ.
413 if !skipVersion(b1, b2, file1, file2) {
419 c1, err1 := b1.ReadByte()
420 c2, err2 := b2.ReadByte()
421 if err1 == io.EOF && err2 == io.EOF {
425 log.Fatalf("reading %s: %v", file1, err1)
428 log.Fatalf("reading %s: %v", file2, err1)
433 if lastByte == '`' && c1 == '\n' {
434 if !skipVersion(b1, b2, file1, file2) {
442 func skipVersion(b1, b2 *bufio.Reader, file1, file2 string) bool {
443 // Consume "go object " prefix, if there.
444 prefix := "go object "
445 for i := 0; i < len(prefix); i++ {
446 c1, err1 := b1.ReadByte()
447 c2, err2 := b2.ReadByte()
448 if err1 == io.EOF && err2 == io.EOF {
452 log.Fatalf("reading %s: %v", file1, err1)
455 log.Fatalf("reading %s: %v", file2, err1)
461 return true // matching bytes, just not a version
465 // Keep comparing until second space.
466 // Must continue to match.
467 // If we see a \n, it's not a version string after all.
468 for numSpace := 0; numSpace < 2; {
469 c1, err1 := b1.ReadByte()
470 c2, err2 := b2.ReadByte()
471 if err1 == io.EOF && err2 == io.EOF {
475 log.Fatalf("reading %s: %v", file1, err1)
478 log.Fatalf("reading %s: %v", file2, err1)
491 // Have now seen 'go object goos goarch ' in both files.
492 // Now they're allowed to diverge, until the \n, which
495 c1, err1 := b1.ReadByte()
497 log.Fatalf("reading %s: unexpected EOF", file1)
500 log.Fatalf("reading %s: %v", file1, err1)
507 c2, err2 := b2.ReadByte()
509 log.Fatalf("reading %s: unexpected EOF", file2)
512 log.Fatalf("reading %s: %v", file2, err2)
519 // Consumed "matching" versions from both.
523 func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err error) {
525 log.Print(strings.Join(cmd, " "))
531 log.Printf("%.3fs elapsed # %s\n", time.Since(t0).Seconds(), strings.Join(cmd, " "))
535 xcmd := exec.Command(cmd[0], cmd[1:]...)
537 return xcmd.CombinedOutput()
540 f, err := os.Create(logName)
544 fmt.Fprintf(f, "GOOS=%s GOARCH=%s %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd, " "))
548 return nil, xcmd.Run()
552 if err := os.MkdirAll(stashDir, 0777); err != nil {
556 toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
557 files, err := ioutil.ReadDir(toolDir)
562 for _, file := range files {
563 if shouldSave(file.Name()) && file.Mode().IsRegular() {
564 cp(filepath.Join(toolDir, file.Name()), filepath.Join(stashDir, file.Name()))
568 for _, name := range binTools {
569 if !shouldSave(name) {
572 src := filepath.Join(binDir, name)
573 if _, err := os.Stat(src); err == nil {
574 cp(src, filepath.Join(stashDir, name))
582 files, err := ioutil.ReadDir(stashDir)
587 for _, file := range files {
588 if shouldSave(file.Name()) && file.Mode().IsRegular() {
590 if isBinTool(file.Name()) {
593 cp(filepath.Join(stashDir, file.Name()), filepath.Join(targ, file.Name()))
600 func shouldSave(name string) bool {
605 for i, arg := range cmd {
606 if i > 0 && name == arg {
614 func checkShouldSave() {
616 for _, arg := range cmd[1:] {
618 missing = append(missing, arg)
621 if len(missing) > 0 {
622 log.Fatalf("%s did not find tools: %s", cmd[0], strings.Join(missing, " "))
626 func cp(src, dst string) {
628 fmt.Printf("cp %s %s\n", src, dst)
630 data, err := ioutil.ReadFile(src)
634 if err := ioutil.WriteFile(dst, data, 0777); err != nil {