golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/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=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 }