...

Source file src/golang.org/x/tools/cmd/toolstash/main.go

Documentation: golang.org/x/tools/cmd/toolstash

     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=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.
    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  package main // import "golang.org/x/tools/cmd/toolstash"
   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 // name of tool: "go", "compile", etc
   167  	toolStash string // path to stashed tool
   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  			// cmd/go uses "compile -V=full" to query the tool's build ID.
   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"): // compiler
   282  		useDashN := true
   283  		dashcIndex := -1
   284  		for i, s := range cmd {
   285  			if s == "-+" {
   286  				// Compiling runtime. Don't use -N.
   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"): // assembler
   314  		log.Printf("assembler output differs")
   315  
   316  	case tool == "link" || strings.HasSuffix(tool, "l"): // linker
   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  	// Go object files and archives contain lines of the form
   407  	//	go object <goos> <goarch> <version>
   408  	// By default, the version on development branches includes
   409  	// the Git hash and time stamp for the most recent commit.
   410  	// We allow the versions to differ.
   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  	// Consume "go object " prefix, if there.
   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 // matching bytes, just not a version
   460  		}
   461  	}
   462  
   463  	// Keep comparing until second space.
   464  	// Must continue to match.
   465  	// If we see a \n, it's not a version string after all.
   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  	// Have now seen 'go object goos goarch ' in both files.
   490  	// Now they're allowed to diverge, until the \n, which
   491  	// must be present.
   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  	// Consumed "matching" versions from both.
   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