github.com/april1989/origin-go-tools@v0.0.32/cmd/toolstash/main.go (about)

     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.
     4  
     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.
     7  //
     8  // Usage:
     9  //
    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
    14  //
    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
    18  // binaries.
    19  //
    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.
    26  //
    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.
    31  //
    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.
    36  //
    37  // The -v flag causes toolstash to print the commands being executed.
    38  //
    39  // The -t flag causes toolstash to print the time elapsed during while the
    40  // command ran.
    41  //
    42  // Comparing
    43  //
    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 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.
    55  //
    56  // The -cmp flag is a no-op when the command line is not invoking an
    57  // assembler or compiler.
    58  //
    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
    61  // compiler output:
    62  //
    63  //	toolstash save
    64  //	<edit compiler sources>
    65  //	go tool dist install cmd/compile # install compiler only
    66  //	toolstash -cmp compile x.go
    67  //
    68  // Go Command Integration
    69  //
    70  // The go command accepts a -toolexec flag that specifies a program
    71  // to use to run the build tools.
    72  //
    73  // To build with the stashed tools:
    74  //
    75  //	go build -toolexec toolstash x.go
    76  //
    77  // To build with the stashed go command and the stashed tools:
    78  //
    79  //	toolstash go build -toolexec toolstash x.go
    80  //
    81  // To verify that code cleanup in the compilers does not make any
    82  // changes to the objects being generated for the entire tree:
    83  //
    84  //	# Build working tree and save tools.
    85  //	./make.bash
    86  //	toolstash save
    87  //
    88  //	<edit compiler sources>
    89  //
    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
    93  //
    94  //	# Check that new tools behave identically to saved tools.
    95  //	go build -toolexec 'toolstash -cmp' -a std
    96  //
    97  //	# If not, restore, in order to keep working on Go code.
    98  //	toolstash restore
    99  //
   100  // Version Skew
   101  //
   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.
   109  //
   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
   116  // computation:
   117  //
   118  //	echo devel >$GOROOT/VERSION
   119  //
   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.
   123  //
   124  package main // import "github.com/april1989/origin-go-tools/cmd/toolstash"
   125  
   126  import (
   127  	"bufio"
   128  	"flag"
   129  	"fmt"
   130  	"io"
   131  	"io/ioutil"
   132  	"log"
   133  	"os"
   134  	"os/exec"
   135  	"path/filepath"
   136  	"runtime"
   137  	"strings"
   138  	"time"
   139  )
   140  
   141  var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
   142  
   143  Examples:
   144  	toolstash save
   145  	toolstash restore
   146  	toolstash go run x.go
   147  	toolstash compile x.go
   148  	toolstash -cmp compile x.go
   149  
   150  For details, godoc github.com/april1989/origin-go-tools/cmd/toolstash
   151  `
   152  
   153  func usage() {
   154  	fmt.Fprint(os.Stderr, usageMessage)
   155  	os.Exit(2)
   156  }
   157  
   158  var (
   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")
   164  )
   165  
   166  var (
   167  	cmd       []string
   168  	tool      string // name of tool: "go", "compile", etc
   169  	toolStash string // path to stashed tool
   170  
   171  	goroot   string
   172  	toolDir  string
   173  	stashDir string
   174  	binDir   string
   175  )
   176  
   177  func canCmp(name string, args []string) bool {
   178  	switch name {
   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.
   182  			return false
   183  		}
   184  		return true
   185  	}
   186  	return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l')
   187  }
   188  
   189  var binTools = []string{"go", "godoc", "gofmt"}
   190  
   191  func isBinTool(name string) bool {
   192  	return strings.HasPrefix(name, "go")
   193  }
   194  
   195  func main() {
   196  	log.SetFlags(0)
   197  	log.SetPrefix("toolstash: ")
   198  
   199  	flag.Usage = usage
   200  	flag.Parse()
   201  	cmd = flag.Args()
   202  
   203  	if len(cmd) < 1 {
   204  		usage()
   205  	}
   206  
   207  	s, err := exec.Command(*goCmd, "env", "GOROOT").CombinedOutput()
   208  	if err != nil {
   209  		log.Fatalf("%s env GOROOT: %v", *goCmd, err)
   210  	}
   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")
   214  
   215  	binDir = os.Getenv("GOBIN")
   216  	if binDir == "" {
   217  		binDir = filepath.Join(goroot, "bin")
   218  	}
   219  
   220  	switch cmd[0] {
   221  	case "save":
   222  		save()
   223  		return
   224  
   225  	case "restore":
   226  		restore()
   227  		return
   228  	}
   229  
   230  	tool = cmd[0]
   231  	if i := strings.LastIndexAny(tool, `/\`); i >= 0 {
   232  		tool = tool[i+1:]
   233  	}
   234  
   235  	if !strings.HasPrefix(tool, "a.out") {
   236  		toolStash = filepath.Join(stashDir, tool)
   237  		if _, err := os.Stat(toolStash); err != nil {
   238  			log.Print(err)
   239  			os.Exit(2)
   240  		}
   241  
   242  		if *cmp && canCmp(tool, cmd[1:]) {
   243  			compareTool()
   244  			return
   245  		}
   246  		cmd[0] = toolStash
   247  	}
   248  
   249  	if *norun {
   250  		fmt.Printf("%s\n", strings.Join(cmd, " "))
   251  		return
   252  	}
   253  	if *verbose {
   254  		log.Print(strings.Join(cmd, " "))
   255  	}
   256  	xcmd := exec.Command(cmd[0], cmd[1:]...)
   257  	xcmd.Stdin = os.Stdin
   258  	xcmd.Stdout = os.Stdout
   259  	xcmd.Stderr = os.Stderr
   260  	err = xcmd.Run()
   261  	if err != nil {
   262  		log.Fatal(err)
   263  	}
   264  	os.Exit(0)
   265  }
   266  
   267  func compareTool() {
   268  	if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) {
   269  		cmd[0] = filepath.Join(toolDir, tool)
   270  	}
   271  
   272  	outfile, ok := cmpRun(false, cmd)
   273  	if ok {
   274  		os.Remove(outfile + ".stash")
   275  		return
   276  	}
   277  
   278  	extra := "-S"
   279  	switch {
   280  	default:
   281  		log.Fatalf("unknown tool %s", tool)
   282  
   283  	case tool == "compile" || strings.HasSuffix(tool, "g"): // compiler
   284  		useDashN := true
   285  		dashcIndex := -1
   286  		for i, s := range cmd {
   287  			if s == "-+" {
   288  				// Compiling runtime. Don't use -N.
   289  				useDashN = false
   290  			}
   291  			if strings.HasPrefix(s, "-c=") {
   292  				dashcIndex = i
   293  			}
   294  		}
   295  		cmdN := injectflags(cmd, nil, useDashN)
   296  		_, ok := cmpRun(false, cmdN)
   297  		if !ok {
   298  			if useDashN {
   299  				log.Printf("compiler output differs, with optimizers disabled (-N)")
   300  			} else {
   301  				log.Printf("compiler output differs")
   302  			}
   303  			if dashcIndex >= 0 {
   304  				cmd[dashcIndex] = "-c=1"
   305  			}
   306  			cmd = injectflags(cmd, []string{"-v", "-m=2"}, useDashN)
   307  			break
   308  		}
   309  		if dashcIndex >= 0 {
   310  			cmd[dashcIndex] = "-c=1"
   311  		}
   312  		cmd = injectflags(cmd, []string{"-v", "-m=2"}, false)
   313  		log.Printf("compiler output differs, only with optimizers enabled")
   314  
   315  	case tool == "asm" || strings.HasSuffix(tool, "a"): // assembler
   316  		log.Printf("assembler output differs")
   317  
   318  	case tool == "link" || strings.HasSuffix(tool, "l"): // linker
   319  		log.Printf("linker output differs")
   320  		extra = "-v=2"
   321  	}
   322  
   323  	cmdS := injectflags(cmd, []string{extra}, false)
   324  	outfile, _ = cmpRun(true, cmdS)
   325  
   326  	fmt.Fprintf(os.Stderr, "\n%s\n", compareLogs(outfile))
   327  	os.Exit(2)
   328  }
   329  
   330  func injectflags(cmd []string, extra []string, addDashN bool) []string {
   331  	x := []string{cmd[0]}
   332  	if addDashN {
   333  		x = append(x, "-N")
   334  	}
   335  	x = append(x, extra...)
   336  	x = append(x, cmd[1:]...)
   337  	return x
   338  }
   339  
   340  func cmpRun(keepLog bool, cmd []string) (outfile string, match bool) {
   341  	cmdStash := make([]string, len(cmd))
   342  	copy(cmdStash, cmd)
   343  	cmdStash[0] = toolStash
   344  	for i, arg := range cmdStash {
   345  		if arg == "-o" {
   346  			outfile = cmdStash[i+1]
   347  			cmdStash[i+1] += ".stash"
   348  			break
   349  		}
   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:]...)
   353  			break
   354  		}
   355  	}
   356  
   357  	if outfile == "" {
   358  		log.Fatalf("cannot determine output file for command: %s", strings.Join(cmd, " "))
   359  	}
   360  
   361  	if *norun {
   362  		fmt.Printf("%s\n", strings.Join(cmd, " "))
   363  		fmt.Printf("%s\n", strings.Join(cmdStash, " "))
   364  		os.Exit(0)
   365  	}
   366  
   367  	out, err := runCmd(cmd, keepLog, outfile+".log")
   368  	if err != nil {
   369  		log.Printf("running: %s", strings.Join(cmd, " "))
   370  		os.Stderr.Write(out)
   371  		log.Fatal(err)
   372  	}
   373  
   374  	outStash, err := runCmd(cmdStash, keepLog, outfile+".stash.log")
   375  	if err != nil {
   376  		log.Printf("running: %s", strings.Join(cmdStash, " "))
   377  		log.Printf("installed tool succeeded but stashed tool failed.\n")
   378  		if len(out) > 0 {
   379  			log.Printf("installed tool output:")
   380  			os.Stderr.Write(out)
   381  		}
   382  		if len(outStash) > 0 {
   383  			log.Printf("stashed tool output:")
   384  			os.Stderr.Write(outStash)
   385  		}
   386  		log.Fatal(err)
   387  	}
   388  
   389  	return outfile, sameObject(outfile, outfile+".stash")
   390  }
   391  
   392  func sameObject(file1, file2 string) bool {
   393  	f1, err := os.Open(file1)
   394  	if err != nil {
   395  		log.Fatal(err)
   396  	}
   397  	defer f1.Close()
   398  
   399  	f2, err := os.Open(file2)
   400  	if err != nil {
   401  		log.Fatal(err)
   402  	}
   403  	defer f2.Close()
   404  
   405  	b1 := bufio.NewReader(f1)
   406  	b2 := bufio.NewReader(f2)
   407  
   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) {
   414  		return false
   415  	}
   416  
   417  	lastByte := byte(0)
   418  	for {
   419  		c1, err1 := b1.ReadByte()
   420  		c2, err2 := b2.ReadByte()
   421  		if err1 == io.EOF && err2 == io.EOF {
   422  			return true
   423  		}
   424  		if err1 != nil {
   425  			log.Fatalf("reading %s: %v", file1, err1)
   426  		}
   427  		if err2 != nil {
   428  			log.Fatalf("reading %s: %v", file2, err1)
   429  		}
   430  		if c1 != c2 {
   431  			return false
   432  		}
   433  		if lastByte == '`' && c1 == '\n' {
   434  			if !skipVersion(b1, b2, file1, file2) {
   435  				return false
   436  			}
   437  		}
   438  		lastByte = c1
   439  	}
   440  }
   441  
   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 {
   449  			return true
   450  		}
   451  		if err1 != nil {
   452  			log.Fatalf("reading %s: %v", file1, err1)
   453  		}
   454  		if err2 != nil {
   455  			log.Fatalf("reading %s: %v", file2, err1)
   456  		}
   457  		if c1 != c2 {
   458  			return false
   459  		}
   460  		if c1 != prefix[i] {
   461  			return true // matching bytes, just not a version
   462  		}
   463  	}
   464  
   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 {
   472  			return true
   473  		}
   474  		if err1 != nil {
   475  			log.Fatalf("reading %s: %v", file1, err1)
   476  		}
   477  		if err2 != nil {
   478  			log.Fatalf("reading %s: %v", file2, err1)
   479  		}
   480  		if c1 != c2 {
   481  			return false
   482  		}
   483  		if c1 == '\n' {
   484  			return true
   485  		}
   486  		if c1 == ' ' {
   487  			numSpace++
   488  		}
   489  	}
   490  
   491  	// Have now seen 'go object goos goarch ' in both files.
   492  	// Now they're allowed to diverge, until the \n, which
   493  	// must be present.
   494  	for {
   495  		c1, err1 := b1.ReadByte()
   496  		if err1 == io.EOF {
   497  			log.Fatalf("reading %s: unexpected EOF", file1)
   498  		}
   499  		if err1 != nil {
   500  			log.Fatalf("reading %s: %v", file1, err1)
   501  		}
   502  		if c1 == '\n' {
   503  			break
   504  		}
   505  	}
   506  	for {
   507  		c2, err2 := b2.ReadByte()
   508  		if err2 == io.EOF {
   509  			log.Fatalf("reading %s: unexpected EOF", file2)
   510  		}
   511  		if err2 != nil {
   512  			log.Fatalf("reading %s: %v", file2, err2)
   513  		}
   514  		if c2 == '\n' {
   515  			break
   516  		}
   517  	}
   518  
   519  	// Consumed "matching" versions from both.
   520  	return true
   521  }
   522  
   523  func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err error) {
   524  	if *verbose {
   525  		log.Print(strings.Join(cmd, " "))
   526  	}
   527  
   528  	if *timing {
   529  		t0 := time.Now()
   530  		defer func() {
   531  			log.Printf("%.3fs elapsed # %s\n", time.Since(t0).Seconds(), strings.Join(cmd, " "))
   532  		}()
   533  	}
   534  
   535  	xcmd := exec.Command(cmd[0], cmd[1:]...)
   536  	if !keepLog {
   537  		return xcmd.CombinedOutput()
   538  	}
   539  
   540  	f, err := os.Create(logName)
   541  	if err != nil {
   542  		log.Fatal(err)
   543  	}
   544  	fmt.Fprintf(f, "GOOS=%s GOARCH=%s %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd, " "))
   545  	xcmd.Stdout = f
   546  	xcmd.Stderr = f
   547  	defer f.Close()
   548  	return nil, xcmd.Run()
   549  }
   550  
   551  func save() {
   552  	if err := os.MkdirAll(stashDir, 0777); err != nil {
   553  		log.Fatal(err)
   554  	}
   555  
   556  	toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
   557  	files, err := ioutil.ReadDir(toolDir)
   558  	if err != nil {
   559  		log.Fatal(err)
   560  	}
   561  
   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()))
   565  		}
   566  	}
   567  
   568  	for _, name := range binTools {
   569  		if !shouldSave(name) {
   570  			continue
   571  		}
   572  		src := filepath.Join(binDir, name)
   573  		if _, err := os.Stat(src); err == nil {
   574  			cp(src, filepath.Join(stashDir, name))
   575  		}
   576  	}
   577  
   578  	checkShouldSave()
   579  }
   580  
   581  func restore() {
   582  	files, err := ioutil.ReadDir(stashDir)
   583  	if err != nil {
   584  		log.Fatal(err)
   585  	}
   586  
   587  	for _, file := range files {
   588  		if shouldSave(file.Name()) && file.Mode().IsRegular() {
   589  			targ := toolDir
   590  			if isBinTool(file.Name()) {
   591  				targ = binDir
   592  			}
   593  			cp(filepath.Join(stashDir, file.Name()), filepath.Join(targ, file.Name()))
   594  		}
   595  	}
   596  
   597  	checkShouldSave()
   598  }
   599  
   600  func shouldSave(name string) bool {
   601  	if len(cmd) == 1 {
   602  		return true
   603  	}
   604  	ok := false
   605  	for i, arg := range cmd {
   606  		if i > 0 && name == arg {
   607  			ok = true
   608  			cmd[i] = "DONE"
   609  		}
   610  	}
   611  	return ok
   612  }
   613  
   614  func checkShouldSave() {
   615  	var missing []string
   616  	for _, arg := range cmd[1:] {
   617  		if arg != "DONE" {
   618  			missing = append(missing, arg)
   619  		}
   620  	}
   621  	if len(missing) > 0 {
   622  		log.Fatalf("%s did not find tools: %s", cmd[0], strings.Join(missing, " "))
   623  	}
   624  }
   625  
   626  func cp(src, dst string) {
   627  	if *verbose {
   628  		fmt.Printf("cp %s %s\n", src, dst)
   629  	}
   630  	data, err := ioutil.ReadFile(src)
   631  	if err != nil {
   632  		log.Fatal(err)
   633  	}
   634  	if err := ioutil.WriteFile(dst, data, 0777); err != nil {
   635  		log.Fatal(err)
   636  	}
   637  }