You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

637 lines
16 KiB

  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. // Toolstash provides a way to save, run, and restore a known good copy of the Go toolchain
  5. // and to compare the object files generated by two toolchains.
  6. //
  7. // Usage:
  8. //
  9. // toolstash [-n] [-v] save [tool...]
  10. // toolstash [-n] [-v] restore [tool...]
  11. // toolstash [-n] [-v] [-t] go run x.go
  12. // toolstash [-n] [-v] [-t] [-cmp] compile x.go
  13. //
  14. // The toolstash command manages a ``stashed'' copy of the Go toolchain
  15. // kept in $GOROOT/pkg/toolstash. In this case, the toolchain means the
  16. // tools available with the 'go tool' command as well as the go, godoc, and gofmt
  17. // binaries.
  18. //
  19. // The command ``toolstash save'', typically run when the toolchain is known to be working,
  20. // copies the toolchain from its installed location to the toolstash directory.
  21. // Its inverse, ``toolchain restore'', typically run when the toolchain is known to be broken,
  22. // copies the toolchain from the toolstash directory back to the installed locations.
  23. // If additional arguments are given, the save or restore applies only to the named tools.
  24. // Otherwise, it applies to all tools.
  25. //
  26. // Otherwise, toolstash's arguments should be a command line beginning with the
  27. // name of a toolchain binary, which may be a short name like compile or a complete path
  28. // to an installed binary. Toolstash runs the command line using the stashed
  29. // copy of the binary instead of the installed one.
  30. //
  31. // The -n flag causes toolstash to print the commands that would be executed
  32. // but not execute them. The combination -n -cmp shows the two commands
  33. // that would be compared and then exits successfully. A real -cmp run might
  34. // run additional commands for diagnosis of an output mismatch.
  35. //
  36. // The -v flag causes toolstash to print the commands being executed.
  37. //
  38. // The -t flag causes toolstash to print the time elapsed during while the
  39. // command ran.
  40. //
  41. // Comparing
  42. //
  43. // The -cmp flag causes toolstash to run both the installed and the stashed
  44. // copy of an assembler or compiler and check that they produce identical
  45. // object files. If not, toolstash reports the mismatch and exits with a failure status.
  46. // As part of reporting the mismatch, toolstash reinvokes the command with
  47. // the -S flag and identifies the first divergence in the assembly output.
  48. // If the command is a Go compiler, toolstash also determines whether the
  49. // difference is triggered by optimization passes.
  50. // On failure, toolstash leaves additional information in files named
  51. // similarly to the default output file. If the compilation would normally
  52. // produce a file x.6, the output from the stashed tool is left in x.6.stash
  53. // and the debugging traces are left in x.6.log and x.6.stash.log.
  54. //
  55. // The -cmp flag is a no-op when the command line is not invoking an
  56. // assembler or compiler.
  57. //
  58. // For example, when working on code cleanup that should not affect
  59. // compiler output, toolstash can be used to compare the old and new
  60. // compiler output:
  61. //
  62. // toolstash save
  63. // <edit compiler sources>
  64. // go tool dist install cmd/compile # install compiler only
  65. // toolstash -cmp compile x.go
  66. //
  67. // Go Command Integration
  68. //
  69. // The go command accepts a -toolexec flag that specifies a program
  70. // to use to run the build tools.
  71. //
  72. // To build with the stashed tools:
  73. //
  74. // go build -toolexec toolstash x.go
  75. //
  76. // To build with the stashed go command and the stashed tools:
  77. //
  78. // toolstash go build -toolexec toolstash x.go
  79. //
  80. // To verify that code cleanup in the compilers does not make any
  81. // changes to the objects being generated for the entire tree:
  82. //
  83. // # Build working tree and save tools.
  84. // ./make.bash
  85. // toolstash save
  86. //
  87. // <edit compiler sources>
  88. //
  89. // # Install new tools, but do not rebuild the rest of tree,
  90. // # since the compilers might generate buggy code.
  91. // go tool dist install cmd/compile
  92. //
  93. // # Check that new tools behave identically to saved tools.
  94. // go build -toolexec 'toolstash -cmp' -a std
  95. //
  96. // # If not, restore, in order to keep working on Go code.
  97. // toolstash restore
  98. //
  99. // Version Skew
  100. //
  101. // The Go tools write the current Go version to object files, and (outside
  102. // release branches) that version includes the hash and time stamp
  103. // of the most recent Git commit. Functionally equivalent
  104. // compilers built at different Git versions may produce object files that
  105. // differ only in the recorded version. Toolstash ignores version mismatches
  106. // when comparing object files, but the standard tools will refuse to compile
  107. // or link together packages with different object versions.
  108. //
  109. // For the full build in the final example above to work, both the stashed
  110. // and the installed tools must use the same version string.
  111. // One way to ensure this is not to commit any of the changes being
  112. // tested, so that the Git HEAD hash is the same for both builds.
  113. // A more robust way to force the tools to have the same version string
  114. // is to write a $GOROOT/VERSION file, which overrides the Git-based version
  115. // computation:
  116. //
  117. // echo devel >$GOROOT/VERSION
  118. //
  119. // The version can be arbitrary text, but to pass all.bash's API check, it must
  120. // contain the substring ``devel''. The VERSION file must be created before
  121. // building either version of the toolchain.
  122. //
  123. package main // import "golang.org/x/tools/cmd/toolstash"
  124. import (
  125. "bufio"
  126. "flag"
  127. "fmt"
  128. "io"
  129. "io/ioutil"
  130. "log"
  131. "os"
  132. "os/exec"
  133. "path/filepath"
  134. "runtime"
  135. "strings"
  136. "time"
  137. )
  138. var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
  139. Examples:
  140. toolstash save
  141. toolstash restore
  142. toolstash go run x.go
  143. toolstash compile x.go
  144. toolstash -cmp compile x.go
  145. For details, godoc golang.org/x/tools/cmd/toolstash
  146. `
  147. func usage() {
  148. fmt.Fprint(os.Stderr, usageMessage)
  149. os.Exit(2)
  150. }
  151. var (
  152. goCmd = flag.String("go", "go", "path to \"go\" command")
  153. norun = flag.Bool("n", false, "print but do not run commands")
  154. verbose = flag.Bool("v", false, "print commands being run")
  155. cmp = flag.Bool("cmp", false, "compare tool object files")
  156. timing = flag.Bool("t", false, "print time commands take")
  157. )
  158. var (
  159. cmd []string
  160. tool string // name of tool: "go", "compile", etc
  161. toolStash string // path to stashed tool
  162. goroot string
  163. toolDir string
  164. stashDir string
  165. binDir string
  166. )
  167. func canCmp(name string, args []string) bool {
  168. switch name {
  169. case "asm", "compile", "link":
  170. if len(args) == 1 && (args[0] == "-V" || strings.HasPrefix(args[0], "-V=")) {
  171. // cmd/go uses "compile -V=full" to query the tool's build ID.
  172. return false
  173. }
  174. return true
  175. }
  176. return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l')
  177. }
  178. var binTools = []string{"go", "godoc", "gofmt"}
  179. func isBinTool(name string) bool {
  180. return strings.HasPrefix(name, "go")
  181. }
  182. func main() {
  183. log.SetFlags(0)
  184. log.SetPrefix("toolstash: ")
  185. flag.Usage = usage
  186. flag.Parse()
  187. cmd = flag.Args()
  188. if len(cmd) < 1 {
  189. usage()
  190. }
  191. s, err := exec.Command(*goCmd, "env", "GOROOT").CombinedOutput()
  192. if err != nil {
  193. log.Fatalf("%s env GOROOT: %v", *goCmd, err)
  194. }
  195. goroot = strings.TrimSpace(string(s))
  196. toolDir = filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
  197. stashDir = filepath.Join(goroot, "pkg/toolstash")
  198. binDir = os.Getenv("GOBIN")
  199. if binDir == "" {
  200. binDir = filepath.Join(goroot, "bin")
  201. }
  202. switch cmd[0] {
  203. case "save":
  204. save()
  205. return
  206. case "restore":
  207. restore()
  208. return
  209. }
  210. tool = cmd[0]
  211. if i := strings.LastIndexAny(tool, `/\`); i >= 0 {
  212. tool = tool[i+1:]
  213. }
  214. if !strings.HasPrefix(tool, "a.out") {
  215. toolStash = filepath.Join(stashDir, tool)
  216. if _, err := os.Stat(toolStash); err != nil {
  217. log.Print(err)
  218. os.Exit(2)
  219. }
  220. if *cmp && canCmp(tool, cmd[1:]) {
  221. compareTool()
  222. return
  223. }
  224. cmd[0] = toolStash
  225. }
  226. if *norun {
  227. fmt.Printf("%s\n", strings.Join(cmd, " "))
  228. return
  229. }
  230. if *verbose {
  231. log.Print(strings.Join(cmd, " "))
  232. }
  233. xcmd := exec.Command(cmd[0], cmd[1:]...)
  234. xcmd.Stdin = os.Stdin
  235. xcmd.Stdout = os.Stdout
  236. xcmd.Stderr = os.Stderr
  237. err = xcmd.Run()
  238. if err != nil {
  239. log.Fatal(err)
  240. }
  241. os.Exit(0)
  242. }
  243. func compareTool() {
  244. if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) {
  245. cmd[0] = filepath.Join(toolDir, tool)
  246. }
  247. outfile, ok := cmpRun(false, cmd)
  248. if ok {
  249. os.Remove(outfile + ".stash")
  250. return
  251. }
  252. extra := "-S"
  253. switch {
  254. default:
  255. log.Fatalf("unknown tool %s", tool)
  256. case tool == "compile" || strings.HasSuffix(tool, "g"): // compiler
  257. useDashN := true
  258. dashcIndex := -1
  259. for i, s := range cmd {
  260. if s == "-+" {
  261. // Compiling runtime. Don't use -N.
  262. useDashN = false
  263. }
  264. if strings.HasPrefix(s, "-c=") {
  265. dashcIndex = i
  266. }
  267. }
  268. cmdN := injectflags(cmd, nil, useDashN)
  269. _, ok := cmpRun(false, cmdN)
  270. if !ok {
  271. if useDashN {
  272. log.Printf("compiler output differs, with optimizers disabled (-N)")
  273. } else {
  274. log.Printf("compiler output differs")
  275. }
  276. if dashcIndex >= 0 {
  277. cmd[dashcIndex] = "-c=1"
  278. }
  279. cmd = injectflags(cmd, []string{"-v", "-m=2"}, useDashN)
  280. break
  281. }
  282. if dashcIndex >= 0 {
  283. cmd[dashcIndex] = "-c=1"
  284. }
  285. cmd = injectflags(cmd, []string{"-v", "-m=2"}, false)
  286. log.Printf("compiler output differs, only with optimizers enabled")
  287. case tool == "asm" || strings.HasSuffix(tool, "a"): // assembler
  288. log.Printf("assembler output differs")
  289. case tool == "link" || strings.HasSuffix(tool, "l"): // linker
  290. log.Printf("linker output differs")
  291. extra = "-v=2"
  292. }
  293. cmdS := injectflags(cmd, []string{extra}, false)
  294. outfile, _ = cmpRun(true, cmdS)
  295. fmt.Fprintf(os.Stderr, "\n%s\n", compareLogs(outfile))
  296. os.Exit(2)
  297. }
  298. func injectflags(cmd []string, extra []string, addDashN bool) []string {
  299. x := []string{cmd[0]}
  300. if addDashN {
  301. x = append(x, "-N")
  302. }
  303. x = append(x, extra...)
  304. x = append(x, cmd[1:]...)
  305. return x
  306. }
  307. func cmpRun(keepLog bool, cmd []string) (outfile string, match bool) {
  308. cmdStash := make([]string, len(cmd))
  309. copy(cmdStash, cmd)
  310. cmdStash[0] = toolStash
  311. for i, arg := range cmdStash {
  312. if arg == "-o" {
  313. outfile = cmdStash[i+1]
  314. cmdStash[i+1] += ".stash"
  315. break
  316. }
  317. if strings.HasSuffix(arg, ".s") || strings.HasSuffix(arg, ".go") && '0' <= tool[0] && tool[0] <= '9' {
  318. outfile = filepath.Base(arg[:strings.LastIndex(arg, ".")] + "." + tool[:1])
  319. cmdStash = append([]string{cmdStash[0], "-o", outfile + ".stash"}, cmdStash[1:]...)
  320. break
  321. }
  322. }
  323. if outfile == "" {
  324. log.Fatalf("cannot determine output file for command: %s", strings.Join(cmd, " "))
  325. }
  326. if *norun {
  327. fmt.Printf("%s\n", strings.Join(cmd, " "))
  328. fmt.Printf("%s\n", strings.Join(cmdStash, " "))
  329. os.Exit(0)
  330. }
  331. out, err := runCmd(cmd, keepLog, outfile+".log")
  332. if err != nil {
  333. log.Printf("running: %s", strings.Join(cmd, " "))
  334. os.Stderr.Write(out)
  335. log.Fatal(err)
  336. }
  337. outStash, err := runCmd(cmdStash, keepLog, outfile+".stash.log")
  338. if err != nil {
  339. log.Printf("running: %s", strings.Join(cmdStash, " "))
  340. log.Printf("installed tool succeeded but stashed tool failed.\n")
  341. if len(out) > 0 {
  342. log.Printf("installed tool output:")
  343. os.Stderr.Write(out)
  344. }
  345. if len(outStash) > 0 {
  346. log.Printf("stashed tool output:")
  347. os.Stderr.Write(outStash)
  348. }
  349. log.Fatal(err)
  350. }
  351. return outfile, sameObject(outfile, outfile+".stash")
  352. }
  353. func sameObject(file1, file2 string) bool {
  354. f1, err := os.Open(file1)
  355. if err != nil {
  356. log.Fatal(err)
  357. }
  358. defer f1.Close()
  359. f2, err := os.Open(file2)
  360. if err != nil {
  361. log.Fatal(err)
  362. }
  363. defer f2.Close()
  364. b1 := bufio.NewReader(f1)
  365. b2 := bufio.NewReader(f2)
  366. // Go object files and archives contain lines of the form
  367. // go object <goos> <goarch> <version>
  368. // By default, the version on development branches includes
  369. // the Git hash and time stamp for the most recent commit.
  370. // We allow the versions to differ.
  371. if !skipVersion(b1, b2, file1, file2) {
  372. return false
  373. }
  374. lastByte := byte(0)
  375. for {
  376. c1, err1 := b1.ReadByte()
  377. c2, err2 := b2.ReadByte()
  378. if err1 == io.EOF && err2 == io.EOF {
  379. return true
  380. }
  381. if err1 != nil {
  382. log.Fatalf("reading %s: %v", file1, err1)
  383. }
  384. if err2 != nil {
  385. log.Fatalf("reading %s: %v", file2, err1)
  386. }
  387. if c1 != c2 {
  388. return false
  389. }
  390. if lastByte == '`' && c1 == '\n' {
  391. if !skipVersion(b1, b2, file1, file2) {
  392. return false
  393. }
  394. }
  395. lastByte = c1
  396. }
  397. }
  398. func skipVersion(b1, b2 *bufio.Reader, file1, file2 string) bool {
  399. // Consume "go object " prefix, if there.
  400. prefix := "go object "
  401. for i := 0; i < len(prefix); i++ {
  402. c1, err1 := b1.ReadByte()
  403. c2, err2 := b2.ReadByte()
  404. if err1 == io.EOF && err2 == io.EOF {
  405. return true
  406. }
  407. if err1 != nil {
  408. log.Fatalf("reading %s: %v", file1, err1)
  409. }
  410. if err2 != nil {
  411. log.Fatalf("reading %s: %v", file2, err1)
  412. }
  413. if c1 != c2 {
  414. return false
  415. }
  416. if c1 != prefix[i] {
  417. return true // matching bytes, just not a version
  418. }
  419. }
  420. // Keep comparing until second space.
  421. // Must continue to match.
  422. // If we see a \n, it's not a version string after all.
  423. for numSpace := 0; numSpace < 2; {
  424. c1, err1 := b1.ReadByte()
  425. c2, err2 := b2.ReadByte()
  426. if err1 == io.EOF && err2 == io.EOF {
  427. return true
  428. }
  429. if err1 != nil {
  430. log.Fatalf("reading %s: %v", file1, err1)
  431. }
  432. if err2 != nil {
  433. log.Fatalf("reading %s: %v", file2, err1)
  434. }
  435. if c1 != c2 {
  436. return false
  437. }
  438. if c1 == '\n' {
  439. return true
  440. }
  441. if c1 == ' ' {
  442. numSpace++
  443. }
  444. }
  445. // Have now seen 'go object goos goarch ' in both files.
  446. // Now they're allowed to diverge, until the \n, which
  447. // must be present.
  448. for {
  449. c1, err1 := b1.ReadByte()
  450. if err1 == io.EOF {
  451. log.Fatalf("reading %s: unexpected EOF", file1)
  452. }
  453. if err1 != nil {
  454. log.Fatalf("reading %s: %v", file1, err1)
  455. }
  456. if c1 == '\n' {
  457. break
  458. }
  459. }
  460. for {
  461. c2, err2 := b2.ReadByte()
  462. if err2 == io.EOF {
  463. log.Fatalf("reading %s: unexpected EOF", file2)
  464. }
  465. if err2 != nil {
  466. log.Fatalf("reading %s: %v", file2, err2)
  467. }
  468. if c2 == '\n' {
  469. break
  470. }
  471. }
  472. // Consumed "matching" versions from both.
  473. return true
  474. }
  475. func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err error) {
  476. if *verbose {
  477. log.Print(strings.Join(cmd, " "))
  478. }
  479. if *timing {
  480. t0 := time.Now()
  481. defer func() {
  482. log.Printf("%.3fs elapsed # %s\n", time.Since(t0).Seconds(), strings.Join(cmd, " "))
  483. }()
  484. }
  485. xcmd := exec.Command(cmd[0], cmd[1:]...)
  486. if !keepLog {
  487. return xcmd.CombinedOutput()
  488. }
  489. f, err := os.Create(logName)
  490. if err != nil {
  491. log.Fatal(err)
  492. }
  493. fmt.Fprintf(f, "GOOS=%s GOARCH=%s %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd, " "))
  494. xcmd.Stdout = f
  495. xcmd.Stderr = f
  496. defer f.Close()
  497. return nil, xcmd.Run()
  498. }
  499. func save() {
  500. if err := os.MkdirAll(stashDir, 0777); err != nil {
  501. log.Fatal(err)
  502. }
  503. toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH))
  504. files, err := ioutil.ReadDir(toolDir)
  505. if err != nil {
  506. log.Fatal(err)
  507. }
  508. for _, file := range files {
  509. if shouldSave(file.Name()) && file.Mode().IsRegular() {
  510. cp(filepath.Join(toolDir, file.Name()), filepath.Join(stashDir, file.Name()))
  511. }
  512. }
  513. for _, name := range binTools {
  514. if !shouldSave(name) {
  515. continue
  516. }
  517. src := filepath.Join(binDir, name)
  518. if _, err := os.Stat(src); err == nil {
  519. cp(src, filepath.Join(stashDir, name))
  520. }
  521. }
  522. checkShouldSave()
  523. }
  524. func restore() {
  525. files, err := ioutil.ReadDir(stashDir)
  526. if err != nil {
  527. log.Fatal(err)
  528. }
  529. for _, file := range files {
  530. if shouldSave(file.Name()) && file.Mode().IsRegular() {
  531. targ := toolDir
  532. if isBinTool(file.Name()) {
  533. targ = binDir
  534. }
  535. cp(filepath.Join(stashDir, file.Name()), filepath.Join(targ, file.Name()))
  536. }
  537. }
  538. checkShouldSave()
  539. }
  540. func shouldSave(name string) bool {
  541. if len(cmd) == 1 {
  542. return true
  543. }
  544. ok := false
  545. for i, arg := range cmd {
  546. if i > 0 && name == arg {
  547. ok = true
  548. cmd[i] = "DONE"
  549. }
  550. }
  551. return ok
  552. }
  553. func checkShouldSave() {
  554. var missing []string
  555. for _, arg := range cmd[1:] {
  556. if arg != "DONE" {
  557. missing = append(missing, arg)
  558. }
  559. }
  560. if len(missing) > 0 {
  561. log.Fatalf("%s did not find tools: %s", cmd[0], strings.Join(missing, " "))
  562. }
  563. }
  564. func cp(src, dst string) {
  565. if *verbose {
  566. fmt.Printf("cp %s %s\n", src, dst)
  567. }
  568. data, err := ioutil.ReadFile(src)
  569. if err != nil {
  570. log.Fatal(err)
  571. }
  572. if err := ioutil.WriteFile(dst, data, 0777); err != nil {
  573. log.Fatal(err)
  574. }
  575. }