+++ /dev/null
-// Copyright 2015 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Toolstash provides a way to save, run, and restore a known good copy of the Go toolchain
-// and to compare the object files generated by two toolchains.
-//
-// Usage:
-//
-// toolstash [-n] [-v] save [tool...]
-// toolstash [-n] [-v] restore [tool...]
-// toolstash [-n] [-v] [-t] go run x.go
-// toolstash [-n] [-v] [-t] [-cmp] compile x.go
-//
-// The toolstash command manages a ``stashed'' copy of the Go toolchain
-// kept in $GOROOT/pkg/toolstash. In this case, the toolchain means the
-// tools available with the 'go tool' command as well as the go, godoc, and gofmt
-// binaries.
-//
-// The command ``toolstash save'', typically run when the toolchain is known to be working,
-// copies the toolchain from its installed location to the toolstash directory.
-// Its inverse, ``toolchain restore'', typically run when the toolchain is known to be broken,
-// copies the toolchain from the toolstash directory back to the installed locations.
-// If additional arguments are given, the save or restore applies only to the named tools.
-// Otherwise, it applies to all tools.
-//
-// Otherwise, toolstash's arguments should be a command line beginning with the
-// name of a toolchain binary, which may be a short name like compile or a complete path
-// to an installed binary. Toolstash runs the command line using the stashed
-// copy of the binary instead of the installed one.
-//
-// The -n flag causes toolstash to print the commands that would be executed
-// but not execute them. The combination -n -cmp shows the two commands
-// that would be compared and then exits successfully. A real -cmp run might
-// run additional commands for diagnosis of an output mismatch.
-//
-// The -v flag causes toolstash to print the commands being executed.
-//
-// The -t flag causes toolstash to print the time elapsed during while the
-// command ran.
-//
-// Comparing
-//
-// The -cmp flag causes toolstash to run both the installed and the stashed
-// copy of an assembler or compiler and check that they produce identical
-// object files. If not, toolstash reports the mismatch and exits with a failure status.
-// As part of reporting the mismatch, toolstash reinvokes the command with
-// the -S=2 flag and identifies the first divergence in the assembly output.
-// If the command is a Go compiler, toolstash also determines whether the
-// difference is triggered by optimization passes.
-// On failure, toolstash leaves additional information in files named
-// similarly to the default output file. If the compilation would normally
-// produce a file x.6, the output from the stashed tool is left in x.6.stash
-// and the debugging traces are left in x.6.log and x.6.stash.log.
-//
-// The -cmp flag is a no-op when the command line is not invoking an
-// assembler or compiler.
-//
-// For example, when working on code cleanup that should not affect
-// compiler output, toolstash can be used to compare the old and new
-// compiler output:
-//
-// toolstash save
-// <edit compiler sources>
-// go tool dist install cmd/compile # install compiler only
-// toolstash -cmp compile x.go
-//
-// Go Command Integration
-//
-// The go command accepts a -toolexec flag that specifies a program
-// to use to run the build tools.
-//
-// To build with the stashed tools:
-//
-// go build -toolexec toolstash x.go
-//
-// To build with the stashed go command and the stashed tools:
-//
-// toolstash go build -toolexec toolstash x.go
-//
-// To verify that code cleanup in the compilers does not make any
-// changes to the objects being generated for the entire tree:
-//
-// # Build working tree and save tools.
-// ./make.bash
-// toolstash save
-//
-// <edit compiler sources>
-//
-// # Install new tools, but do not rebuild the rest of tree,
-// # since the compilers might generate buggy code.
-// go tool dist install cmd/compile
-//
-// # Check that new tools behave identically to saved tools.
-// go build -toolexec 'toolstash -cmp' -a std
-//
-// # If not, restore, in order to keep working on Go code.
-// toolstash restore
-//
-// Version Skew
-//
-// The Go tools write the current Go version to object files, and (outside
-// release branches) that version includes the hash and time stamp
-// of the most recent Git commit. Functionally equivalent
-// compilers built at different Git versions may produce object files that
-// differ only in the recorded version. Toolstash ignores version mismatches
-// when comparing object files, but the standard tools will refuse to compile
-// or link together packages with different object versions.
-//
-// For the full build in the final example above to work, both the stashed
-// and the installed tools must use the same version string.
-// One way to ensure this is not to commit any of the changes being
-// tested, so that the Git HEAD hash is the same for both builds.
-// A more robust way to force the tools to have the same version string
-// is to write a $GOROOT/VERSION file, which overrides the Git-based version
-// computation:
-//
-// echo devel >$GOROOT/VERSION
-//
-// The version can be arbitrary text, but to pass all.bash's API check, it must
-// contain the substring ``devel''. The VERSION file must be created before
-// building either version of the toolchain.
-//
-package main // import "golang.org/x/tools/cmd/toolstash"
-
-import (
- "bufio"
- "flag"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "time"
-)
-
-var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
-
-Examples:
- toolstash save
- toolstash restore
- toolstash go run x.go
- toolstash compile x.go
- toolstash -cmp compile x.go
-
-For details, godoc golang.org/x/tools/cmd/toolstash
-`
-
-func usage() {
- fmt.Fprint(os.Stderr, usageMessage)
- os.Exit(2)
-}
-
-var (
- goCmd = flag.String("go", "go", "path to \"go\" command")
- norun = flag.Bool("n", false, "print but do not run commands")
- verbose = flag.Bool("v", false, "print commands being run")
- cmp = flag.Bool("cmp", false, "compare tool object files")
- timing = flag.Bool("t", false, "print time commands take")
-)
-
-var (
- cmd []string
- tool string // name of tool: "go", "compile", etc
- toolStash string // path to stashed tool
-
- goroot string
- toolDir string
- stashDir string
- binDir string
-)
-
-func canCmp(name string, args []string) bool {
- switch name {
- case "asm", "compile", "link":
- if len(args) == 1 && (args[0] == "-V" || strings.HasPrefix(args[0], "-V=")) {
- // cmd/go uses "compile -V=full" to query the tool's build ID.
- return false
- }
- return true
- }
- return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l')
-}
-
-var binTools = []string{"go", "godoc", "gofmt"}
-
-func isBinTool(name string) bool {
- return strings.HasPrefix(name, "go")
-}
-
-func main() {
- log.SetFlags(0)
- log.SetPrefix("toolstash: ")
-
- flag.Usage = usage
- flag.Parse()
- cmd = flag.Args()
-
- if len(cmd) < 1 {
- usage()
- }
-
- s, err := exec.Command(*goCmd, "env", "GOROOT").CombinedOutput()
- if err != nil {
- log.Fatalf("%s env GOROOT: %v", *goCmd, err)
- }
- goroot = strings.TrimSpace(string(s))
- toolDir = filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
- stashDir = filepath.Join(goroot, "pkg/toolstash")
-
- binDir = os.Getenv("GOBIN")
- if binDir == "" {
- binDir = filepath.Join(goroot, "bin")
- }
-
- switch cmd[0] {
- case "save":
- save()
- return
-
- case "restore":
- restore()
- return
- }
-
- tool = cmd[0]
- if i := strings.LastIndexAny(tool, `/\`); i >= 0 {
- tool = tool[i+1:]
- }
-
- if !strings.HasPrefix(tool, "a.out") {
- toolStash = filepath.Join(stashDir, tool)
- if _, err := os.Stat(toolStash); err != nil {
- log.Print(err)
- os.Exit(2)
- }
-
- if *cmp && canCmp(tool, cmd[1:]) {
- compareTool()
- return
- }
- cmd[0] = toolStash
- }
-
- if *norun {
- fmt.Printf("%s\n", strings.Join(cmd, " "))
- return
- }
- if *verbose {
- log.Print(strings.Join(cmd, " "))
- }
- xcmd := exec.Command(cmd[0], cmd[1:]...)
- xcmd.Stdin = os.Stdin
- xcmd.Stdout = os.Stdout
- xcmd.Stderr = os.Stderr
- err = xcmd.Run()
- if err != nil {
- log.Fatal(err)
- }
- os.Exit(0)
-}
-
-func compareTool() {
- if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) {
- cmd[0] = filepath.Join(toolDir, tool)
- }
-
- outfile, ok := cmpRun(false, cmd)
- if ok {
- os.Remove(outfile + ".stash")
- return
- }
-
- extra := "-S=2"
- switch {
- default:
- log.Fatalf("unknown tool %s", tool)
-
- case tool == "compile" || strings.HasSuffix(tool, "g"): // compiler
- useDashN := true
- dashcIndex := -1
- for i, s := range cmd {
- if s == "-+" {
- // Compiling runtime. Don't use -N.
- useDashN = false
- }
- if strings.HasPrefix(s, "-c=") {
- dashcIndex = i
- }
- }
- cmdN := injectflags(cmd, nil, useDashN)
- _, ok := cmpRun(false, cmdN)
- if !ok {
- if useDashN {
- log.Printf("compiler output differs, with optimizers disabled (-N)")
- } else {
- log.Printf("compiler output differs")
- }
- if dashcIndex >= 0 {
- cmd[dashcIndex] = "-c=1"
- }
- cmd = injectflags(cmd, []string{"-v", "-m=2"}, useDashN)
- break
- }
- if dashcIndex >= 0 {
- cmd[dashcIndex] = "-c=1"
- }
- cmd = injectflags(cmd, []string{"-v", "-m=2"}, false)
- log.Printf("compiler output differs, only with optimizers enabled")
-
- case tool == "asm" || strings.HasSuffix(tool, "a"): // assembler
- log.Printf("assembler output differs")
-
- case tool == "link" || strings.HasSuffix(tool, "l"): // linker
- log.Printf("linker output differs")
- extra = "-v=2"
- }
-
- cmdS := injectflags(cmd, []string{extra}, false)
- outfile, _ = cmpRun(true, cmdS)
-
- fmt.Fprintf(os.Stderr, "\n%s\n", compareLogs(outfile))
- os.Exit(2)
-}
-
-func injectflags(cmd []string, extra []string, addDashN bool) []string {
- x := []string{cmd[0]}
- if addDashN {
- x = append(x, "-N")
- }
- x = append(x, extra...)
- x = append(x, cmd[1:]...)
- return x
-}
-
-func cmpRun(keepLog bool, cmd []string) (outfile string, match bool) {
- cmdStash := make([]string, len(cmd))
- copy(cmdStash, cmd)
- cmdStash[0] = toolStash
- for i, arg := range cmdStash {
- if arg == "-o" {
- outfile = cmdStash[i+1]
- cmdStash[i+1] += ".stash"
- break
- }
- if strings.HasSuffix(arg, ".s") || strings.HasSuffix(arg, ".go") && '0' <= tool[0] && tool[0] <= '9' {
- outfile = filepath.Base(arg[:strings.LastIndex(arg, ".")] + "." + tool[:1])
- cmdStash = append([]string{cmdStash[0], "-o", outfile + ".stash"}, cmdStash[1:]...)
- break
- }
- }
-
- if outfile == "" {
- log.Fatalf("cannot determine output file for command: %s", strings.Join(cmd, " "))
- }
-
- if *norun {
- fmt.Printf("%s\n", strings.Join(cmd, " "))
- fmt.Printf("%s\n", strings.Join(cmdStash, " "))
- os.Exit(0)
- }
-
- out, err := runCmd(cmd, keepLog, outfile+".log")
- if err != nil {
- log.Printf("running: %s", strings.Join(cmd, " "))
- os.Stderr.Write(out)
- log.Fatal(err)
- }
-
- outStash, err := runCmd(cmdStash, keepLog, outfile+".stash.log")
- if err != nil {
- log.Printf("running: %s", strings.Join(cmdStash, " "))
- log.Printf("installed tool succeeded but stashed tool failed.\n")
- if len(out) > 0 {
- log.Printf("installed tool output:")
- os.Stderr.Write(out)
- }
- if len(outStash) > 0 {
- log.Printf("stashed tool output:")
- os.Stderr.Write(outStash)
- }
- log.Fatal(err)
- }
-
- return outfile, sameObject(outfile, outfile+".stash")
-}
-
-func sameObject(file1, file2 string) bool {
- f1, err := os.Open(file1)
- if err != nil {
- log.Fatal(err)
- }
- defer f1.Close()
-
- f2, err := os.Open(file2)
- if err != nil {
- log.Fatal(err)
- }
- defer f2.Close()
-
- b1 := bufio.NewReader(f1)
- b2 := bufio.NewReader(f2)
-
- // Go object files and archives contain lines of the form
- // go object <goos> <goarch> <version>
- // By default, the version on development branches includes
- // the Git hash and time stamp for the most recent commit.
- // We allow the versions to differ.
- if !skipVersion(b1, b2, file1, file2) {
- return false
- }
-
- lastByte := byte(0)
- for {
- c1, err1 := b1.ReadByte()
- c2, err2 := b2.ReadByte()
- if err1 == io.EOF && err2 == io.EOF {
- return true
- }
- if err1 != nil {
- log.Fatalf("reading %s: %v", file1, err1)
- }
- if err2 != nil {
- log.Fatalf("reading %s: %v", file2, err1)
- }
- if c1 != c2 {
- return false
- }
- if lastByte == '`' && c1 == '\n' {
- if !skipVersion(b1, b2, file1, file2) {
- return false
- }
- }
- lastByte = c1
- }
-}
-
-func skipVersion(b1, b2 *bufio.Reader, file1, file2 string) bool {
- // Consume "go object " prefix, if there.
- prefix := "go object "
- for i := 0; i < len(prefix); i++ {
- c1, err1 := b1.ReadByte()
- c2, err2 := b2.ReadByte()
- if err1 == io.EOF && err2 == io.EOF {
- return true
- }
- if err1 != nil {
- log.Fatalf("reading %s: %v", file1, err1)
- }
- if err2 != nil {
- log.Fatalf("reading %s: %v", file2, err1)
- }
- if c1 != c2 {
- return false
- }
- if c1 != prefix[i] {
- return true // matching bytes, just not a version
- }
- }
-
- // Keep comparing until second space.
- // Must continue to match.
- // If we see a \n, it's not a version string after all.
- for numSpace := 0; numSpace < 2; {
- c1, err1 := b1.ReadByte()
- c2, err2 := b2.ReadByte()
- if err1 == io.EOF && err2 == io.EOF {
- return true
- }
- if err1 != nil {
- log.Fatalf("reading %s: %v", file1, err1)
- }
- if err2 != nil {
- log.Fatalf("reading %s: %v", file2, err1)
- }
- if c1 != c2 {
- return false
- }
- if c1 == '\n' {
- return true
- }
- if c1 == ' ' {
- numSpace++
- }
- }
-
- // Have now seen 'go object goos goarch ' in both files.
- // Now they're allowed to diverge, until the \n, which
- // must be present.
- for {
- c1, err1 := b1.ReadByte()
- if err1 == io.EOF {
- log.Fatalf("reading %s: unexpected EOF", file1)
- }
- if err1 != nil {
- log.Fatalf("reading %s: %v", file1, err1)
- }
- if c1 == '\n' {
- break
- }
- }
- for {
- c2, err2 := b2.ReadByte()
- if err2 == io.EOF {
- log.Fatalf("reading %s: unexpected EOF", file2)
- }
- if err2 != nil {
- log.Fatalf("reading %s: %v", file2, err2)
- }
- if c2 == '\n' {
- break
- }
- }
-
- // Consumed "matching" versions from both.
- return true
-}
-
-func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err error) {
- if *verbose {
- log.Print(strings.Join(cmd, " "))
- }
-
- if *timing {
- t0 := time.Now()
- defer func() {
- log.Printf("%.3fs elapsed # %s\n", time.Since(t0).Seconds(), strings.Join(cmd, " "))
- }()
- }
-
- xcmd := exec.Command(cmd[0], cmd[1:]...)
- if !keepLog {
- return xcmd.CombinedOutput()
- }
-
- f, err := os.Create(logName)
- if err != nil {
- log.Fatal(err)
- }
- fmt.Fprintf(f, "GOOS=%s GOARCH=%s %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd, " "))
- xcmd.Stdout = f
- xcmd.Stderr = f
- defer f.Close()
- return nil, xcmd.Run()
-}
-
-func save() {
- if err := os.MkdirAll(stashDir, 0777); err != nil {
- log.Fatal(err)
- }
-
- toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
- files, err := ioutil.ReadDir(toolDir)
- if err != nil {
- log.Fatal(err)
- }
-
- for _, file := range files {
- if shouldSave(file.Name()) && file.Mode().IsRegular() {
- cp(filepath.Join(toolDir, file.Name()), filepath.Join(stashDir, file.Name()))
- }
- }
-
- for _, name := range binTools {
- if !shouldSave(name) {
- continue
- }
- src := filepath.Join(binDir, name)
- if _, err := os.Stat(src); err == nil {
- cp(src, filepath.Join(stashDir, name))
- }
- }
-
- checkShouldSave()
-}
-
-func restore() {
- files, err := ioutil.ReadDir(stashDir)
- if err != nil {
- log.Fatal(err)
- }
-
- for _, file := range files {
- if shouldSave(file.Name()) && file.Mode().IsRegular() {
- targ := toolDir
- if isBinTool(file.Name()) {
- targ = binDir
- }
- cp(filepath.Join(stashDir, file.Name()), filepath.Join(targ, file.Name()))
- }
- }
-
- checkShouldSave()
-}
-
-func shouldSave(name string) bool {
- if len(cmd) == 1 {
- return true
- }
- ok := false
- for i, arg := range cmd {
- if i > 0 && name == arg {
- ok = true
- cmd[i] = "DONE"
- }
- }
- return ok
-}
-
-func checkShouldSave() {
- var missing []string
- for _, arg := range cmd[1:] {
- if arg != "DONE" {
- missing = append(missing, arg)
- }
- }
- if len(missing) > 0 {
- log.Fatalf("%s did not find tools: %s", cmd[0], strings.Join(missing, " "))
- }
-}
-
-func cp(src, dst string) {
- if *verbose {
- fmt.Printf("cp %s %s\n", src, dst)
- }
- data, err := ioutil.ReadFile(src)
- if err != nil {
- log.Fatal(err)
- }
- if err := ioutil.WriteFile(dst, data, 0777); err != nil {
- log.Fatal(err)
- }
-}