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 }