GoPLS Viewer

Home|gopls/cmd/toolstash/main.go
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.
123package main // import "golang.org/x/tools/cmd/toolstash"
124
125import (
126    "bufio"
127    "flag"
128    "fmt"
129    exec "golang.org/x/sys/execabs"
130    "io"
131    "io/ioutil"
132    "log"
133    "os"
134    "path/filepath"
135    "runtime"
136    "strings"
137    "time"
138)
139
140var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line
141
142Examples:
143    toolstash save
144    toolstash restore
145    toolstash go run x.go
146    toolstash compile x.go
147    toolstash -cmp compile x.go
148
149For details, godoc golang.org/x/tools/cmd/toolstash
150`
151
152func usage() {
153    fmt.Fprint(os.StderrusageMessage)
154    os.Exit(2)
155}
156
157var (
158    goCmd   = flag.String("go""go""path to \"go\" command")
159    norun   = flag.Bool("n"false"print but do not run commands")
160    verbose = flag.Bool("v"false"print commands being run")
161    cmp     = flag.Bool("cmp"false"compare tool object files")
162    timing  = flag.Bool("t"false"print time commands take")
163)
164
165var (
166    cmd       []string
167    tool      string // name of tool: "go", "compile", etc
168    toolStash string // path to stashed tool
169
170    goroot   string
171    toolDir  string
172    stashDir string
173    binDir   string
174)
175
176func canCmp(name stringargs []stringbool {
177    switch name {
178    case "asm""compile""link":
179        if len(args) == 1 && (args[0] == "-V" || strings.HasPrefix(args[0], "-V=")) {
180            // cmd/go uses "compile -V=full" to query the tool's build ID.
181            return false
182        }
183        return true
184    }
185    return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l')
186}
187
188var binTools = []string{"go""godoc""gofmt"}
189
190func isBinTool(name stringbool {
191    return strings.HasPrefix(name"go")
192}
193
194func main() {
195    log.SetFlags(0)
196    log.SetPrefix("toolstash: ")
197
198    flag.Usage = usage
199    flag.Parse()
200    cmd = flag.Args()
201
202    if len(cmd) < 1 {
203        usage()
204    }
205
206    serr := exec.Command(*goCmd"env""GOROOT").CombinedOutput()
207    if err != nil {
208        log.Fatalf("%s env GOROOT: %v", *goCmderr)
209    }
210    goroot = strings.TrimSpace(string(s))
211    toolDir = filepath.Join(gorootfmt.Sprintf("pkg/tool/%s_%s"runtime.GOOSruntime.GOARCH))
212    stashDir = filepath.Join(goroot"pkg/toolstash")
213
214    binDir = os.Getenv("GOBIN")
215    if binDir == "" {
216        binDir = filepath.Join(goroot"bin")
217    }
218
219    switch cmd[0] {
220    case "save":
221        save()
222        return
223
224    case "restore":
225        restore()
226        return
227    }
228
229    tool = cmd[0]
230    if i := strings.LastIndexAny(tool`/\`); i >= 0 {
231        tool = tool[i+1:]
232    }
233
234    if !strings.HasPrefix(tool"a.out") {
235        toolStash = filepath.Join(stashDirtool)
236        if _err := os.Stat(toolStash); err != nil {
237            log.Print(err)
238            os.Exit(2)
239        }
240
241        if *cmp && canCmp(toolcmd[1:]) {
242            compareTool()
243            return
244        }
245        cmd[0] = toolStash
246    }
247
248    if *norun {
249        fmt.Printf("%s\n"strings.Join(cmd" "))
250        return
251    }
252    if *verbose {
253        log.Print(strings.Join(cmd" "))
254    }
255    xcmd := exec.Command(cmd[0], cmd[1:]...)
256    xcmd.Stdin = os.Stdin
257    xcmd.Stdout = os.Stdout
258    xcmd.Stderr = os.Stderr
259    err = xcmd.Run()
260    if err != nil {
261        log.Fatal(err)
262    }
263    os.Exit(0)
264}
265
266func compareTool() {
267    if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) {
268        cmd[0] = filepath.Join(toolDirtool)
269    }
270
271    outfileok := cmpRun(falsecmd)
272    if ok {
273        os.Remove(outfile + ".stash")
274        return
275    }
276
277    extra := "-S=2"
278    switch {
279    default:
280        log.Fatalf("unknown tool %s"tool)
281
282    case tool == "compile" || strings.HasSuffix(tool"g"): // compiler
283        useDashN := true
284        dashcIndex := -1
285        for is := range cmd {
286            if s == "-+" {
287                // Compiling runtime. Don't use -N.
288                useDashN = false
289            }
290            if strings.HasPrefix(s"-c=") {
291                dashcIndex = i
292            }
293        }
294        cmdN := injectflags(cmdniluseDashN)
295        _ok := cmpRun(falsecmdN)
296        if !ok {
297            if useDashN {
298                log.Printf("compiler output differs, with optimizers disabled (-N)")
299            } else {
300                log.Printf("compiler output differs")
301            }
302            if dashcIndex >= 0 {
303                cmd[dashcIndex] = "-c=1"
304            }
305            cmd = injectflags(cmd, []string{"-v""-m=2"}, useDashN)
306            break
307        }
308        if dashcIndex >= 0 {
309            cmd[dashcIndex] = "-c=1"
310        }
311        cmd = injectflags(cmd, []string{"-v""-m=2"}, false)
312        log.Printf("compiler output differs, only with optimizers enabled")
313
314    case tool == "asm" || strings.HasSuffix(tool"a"): // assembler
315        log.Printf("assembler output differs")
316
317    case tool == "link" || strings.HasSuffix(tool"l"): // linker
318        log.Printf("linker output differs")
319        extra = "-v=2"
320    }
321
322    cmdS := injectflags(cmd, []string{extra}, false)
323    outfile_ = cmpRun(truecmdS)
324
325    fmt.Fprintf(os.Stderr"\n%s\n"compareLogs(outfile))
326    os.Exit(2)
327}
328
329func injectflags(cmd []stringextra []stringaddDashN bool) []string {
330    x := []string{cmd[0]}
331    if addDashN {
332        x = append(x"-N")
333    }
334    x = append(xextra...)
335    x = append(xcmd[1:]...)
336    return x
337}
338
339func cmpRun(keepLog boolcmd []string) (outfile stringmatch bool) {
340    cmdStash := make([]stringlen(cmd))
341    copy(cmdStashcmd)
342    cmdStash[0] = toolStash
343    for iarg := range cmdStash {
344        if arg == "-o" {
345            outfile = cmdStash[i+1]
346            cmdStash[i+1] += ".stash"
347            break
348        }
349        if strings.HasSuffix(arg".s") || strings.HasSuffix(arg".go") && '0' <= tool[0] && tool[0] <= '9' {
350            outfile = filepath.Base(arg[:strings.LastIndex(arg".")] + "." + tool[:1])
351            cmdStash = append([]string{cmdStash[0], "-o"outfile + ".stash"}, cmdStash[1:]...)
352            break
353        }
354    }
355
356    if outfile == "" {
357        log.Fatalf("cannot determine output file for command: %s"strings.Join(cmd" "))
358    }
359
360    if *norun {
361        fmt.Printf("%s\n"strings.Join(cmd" "))
362        fmt.Printf("%s\n"strings.Join(cmdStash" "))
363        os.Exit(0)
364    }
365
366    outerr := runCmd(cmdkeepLogoutfile+".log")
367    if err != nil {
368        log.Printf("running: %s"strings.Join(cmd" "))
369        os.Stderr.Write(out)
370        log.Fatal(err)
371    }
372
373    outStasherr := runCmd(cmdStashkeepLogoutfile+".stash.log")
374    if err != nil {
375        log.Printf("running: %s"strings.Join(cmdStash" "))
376        log.Printf("installed tool succeeded but stashed tool failed.\n")
377        if len(out) > 0 {
378            log.Printf("installed tool output:")
379            os.Stderr.Write(out)
380        }
381        if len(outStash) > 0 {
382            log.Printf("stashed tool output:")
383            os.Stderr.Write(outStash)
384        }
385        log.Fatal(err)
386    }
387
388    return outfilesameObject(outfileoutfile+".stash")
389}
390
391func sameObject(file1file2 stringbool {
392    f1err := os.Open(file1)
393    if err != nil {
394        log.Fatal(err)
395    }
396    defer f1.Close()
397
398    f2err := os.Open(file2)
399    if err != nil {
400        log.Fatal(err)
401    }
402    defer f2.Close()
403
404    b1 := bufio.NewReader(f1)
405    b2 := bufio.NewReader(f2)
406
407    // Go object files and archives contain lines of the form
408    //    go object <goos> <goarch> <version>
409    // By default, the version on development branches includes
410    // the Git hash and time stamp for the most recent commit.
411    // We allow the versions to differ.
412    if !skipVersion(b1b2file1file2) {
413        return false
414    }
415
416    lastByte := byte(0)
417    for {
418        c1err1 := b1.ReadByte()
419        c2err2 := b2.ReadByte()
420        if err1 == io.EOF && err2 == io.EOF {
421            return true
422        }
423        if err1 != nil {
424            log.Fatalf("reading %s: %v"file1err1)
425        }
426        if err2 != nil {
427            log.Fatalf("reading %s: %v"file2err1)
428        }
429        if c1 != c2 {
430            return false
431        }
432        if lastByte == '`' && c1 == '\n' {
433            if !skipVersion(b1b2file1file2) {
434                return false
435            }
436        }
437        lastByte = c1
438    }
439}
440
441func skipVersion(b1b2 *bufio.Readerfile1file2 stringbool {
442    // Consume "go object " prefix, if there.
443    prefix := "go object "
444    for i := 0i < len(prefix); i++ {
445        c1err1 := b1.ReadByte()
446        c2err2 := b2.ReadByte()
447        if err1 == io.EOF && err2 == io.EOF {
448            return true
449        }
450        if err1 != nil {
451            log.Fatalf("reading %s: %v"file1err1)
452        }
453        if err2 != nil {
454            log.Fatalf("reading %s: %v"file2err1)
455        }
456        if c1 != c2 {
457            return false
458        }
459        if c1 != prefix[i] {
460            return true // matching bytes, just not a version
461        }
462    }
463
464    // Keep comparing until second space.
465    // Must continue to match.
466    // If we see a \n, it's not a version string after all.
467    for numSpace := 0numSpace < 2; {
468        c1err1 := b1.ReadByte()
469        c2err2 := b2.ReadByte()
470        if err1 == io.EOF && err2 == io.EOF {
471            return true
472        }
473        if err1 != nil {
474            log.Fatalf("reading %s: %v"file1err1)
475        }
476        if err2 != nil {
477            log.Fatalf("reading %s: %v"file2err1)
478        }
479        if c1 != c2 {
480            return false
481        }
482        if c1 == '\n' {
483            return true
484        }
485        if c1 == ' ' {
486            numSpace++
487        }
488    }
489
490    // Have now seen 'go object goos goarch ' in both files.
491    // Now they're allowed to diverge, until the \n, which
492    // must be present.
493    for {
494        c1err1 := b1.ReadByte()
495        if err1 == io.EOF {
496            log.Fatalf("reading %s: unexpected EOF"file1)
497        }
498        if err1 != nil {
499            log.Fatalf("reading %s: %v"file1err1)
500        }
501        if c1 == '\n' {
502            break
503        }
504    }
505    for {
506        c2err2 := b2.ReadByte()
507        if err2 == io.EOF {
508            log.Fatalf("reading %s: unexpected EOF"file2)
509        }
510        if err2 != nil {
511            log.Fatalf("reading %s: %v"file2err2)
512        }
513        if c2 == '\n' {
514            break
515        }
516    }
517
518    // Consumed "matching" versions from both.
519    return true
520}
521
522func runCmd(cmd []stringkeepLog boollogName string) (output []byteerr error) {
523    if *verbose {
524        log.Print(strings.Join(cmd" "))
525    }
526
527    if *timing {
528        t0 := time.Now()
529        defer func() {
530            log.Printf("%.3fs elapsed # %s\n"time.Since(t0).Seconds(), strings.Join(cmd" "))
531        }()
532    }
533
534    xcmd := exec.Command(cmd[0], cmd[1:]...)
535    if !keepLog {
536        return xcmd.CombinedOutput()
537    }
538
539    ferr := os.Create(logName)
540    if err != nil {
541        log.Fatal(err)
542    }
543    fmt.Fprintf(f"GOOS=%s GOARCH=%s %s\n"os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd" "))
544    xcmd.Stdout = f
545    xcmd.Stderr = f
546    defer f.Close()
547    return nilxcmd.Run()
548}
549
550func save() {
551    if err := os.MkdirAll(stashDir0777); err != nil {
552        log.Fatal(err)
553    }
554
555    toolDir := filepath.Join(gorootfmt.Sprintf("pkg/tool/%s_%s"runtime.GOOSruntime.GOARCH))
556    fileserr := ioutil.ReadDir(toolDir)
557    if err != nil {
558        log.Fatal(err)
559    }
560
561    for _file := range files {
562        if shouldSave(file.Name()) && file.Mode().IsRegular() {
563            cp(filepath.Join(toolDirfile.Name()), filepath.Join(stashDirfile.Name()))
564        }
565    }
566
567    for _name := range binTools {
568        if !shouldSave(name) {
569            continue
570        }
571        src := filepath.Join(binDirname)
572        if _err := os.Stat(src); err == nil {
573            cp(srcfilepath.Join(stashDirname))
574        }
575    }
576
577    checkShouldSave()
578}
579
580func restore() {
581    fileserr := ioutil.ReadDir(stashDir)
582    if err != nil {
583        log.Fatal(err)
584    }
585
586    for _file := range files {
587        if shouldSave(file.Name()) && file.Mode().IsRegular() {
588            targ := toolDir
589            if isBinTool(file.Name()) {
590                targ = binDir
591            }
592            cp(filepath.Join(stashDirfile.Name()), filepath.Join(targfile.Name()))
593        }
594    }
595
596    checkShouldSave()
597}
598
599func shouldSave(name stringbool {
600    if len(cmd) == 1 {
601        return true
602    }
603    ok := false
604    for iarg := range cmd {
605        if i > 0 && name == arg {
606            ok = true
607            cmd[i] = "DONE"
608        }
609    }
610    return ok
611}
612
613func checkShouldSave() {
614    var missing []string
615    for _arg := range cmd[1:] {
616        if arg != "DONE" {
617            missing = append(missingarg)
618        }
619    }
620    if len(missing) > 0 {
621        log.Fatalf("%s did not find tools: %s"cmd[0], strings.Join(missing" "))
622    }
623}
624
625func cp(srcdst string) {
626    if *verbose {
627        fmt.Printf("cp %s %s\n"srcdst)
628    }
629    dataerr := ioutil.ReadFile(src)
630    if err != nil {
631        log.Fatal(err)
632    }
633    if err := ioutil.WriteFile(dstdata0777); err != nil {
634        log.Fatal(err)
635    }
636}
637
MembersX
tool
canCmp.name
isBinTool.name
injectflags.extra
sameObject.BlockStmt.c2
filepath
skipVersion
runCmd.BlockStmt.t0
restore.RangeStmt_15443.file
cmd
binDir
main.s
cmpRun.outStash
main.xcmd
sameObject.b2
runCmd.f
save.RangeStmt_15123.BlockStmt.err
checkShouldSave
compareTool.ok
skipVersion.file1
skipVersion.BlockStmt.c2
save.RangeStmt_15123.BlockStmt.src
save.RangeStmt_15123.name
compareTool.BlockStmt.RangeStmt_8605.s
injectflags
sameObject.err
exec
stashDir
skipVersion.BlockStmt.err1
save.files
shouldSave.ok
compareTool.outfile
compareTool.BlockStmt.cmdN
cmpRun.cmdStash
sameObject.BlockStmt.c1
sameObject.file2
runCmd.xcmd
restore
time
canCmp.args
runCmd.output
restore.err
cp.src
runtime
canCmp
compareTool.BlockStmt.useDashN
skipVersion.b1
skipVersion.prefix
restore.files
goroot
injectflags.x
cmpRun.RangeStmt_10066.i
sameObject.lastByte
sameObject.BlockStmt.err1
runCmd
sameObject.f2
runCmd.cmd
shouldSave.RangeStmt_15798.i
skipVersion.numSpace
runCmd.keepLog
cp
main.BlockStmt._
compareTool.BlockStmt.RangeStmt_8605.i
cmpRun
sameObject.f1
save.err
compareTool.BlockStmt._
skipVersion.b2
runCmd.err
save.RangeStmt_14946.file
shouldSave
cp.err
usage
main
main.i
main.BlockStmt.err
save.RangeStmt_15123.BlockStmt._
flag
usageMessage
cmpRun.cmd
compareTool.extra
compareTool.cmdS
skipVersion.i
restore.RangeStmt_15443.BlockStmt.BlockStmt.targ
injectflags.cmd
shouldSave.name
main.err
cmpRun.out
runCmd.logName
cp.dst
cmpRun.outfile
save
toolStash
toolDir
checkShouldSave.missing
cp.data
sameObject.BlockStmt.err2
ioutil
isBinTool
compareTool
injectflags.addDashN
sameObject
cmpRun.RangeStmt_10066.arg
skipVersion.BlockStmt.c1
shouldSave.RangeStmt_15798.arg
skipVersion.BlockStmt.err2
save.toolDir
checkShouldSave.RangeStmt_15953.arg
compareTool.BlockStmt.ok
cmpRun.match
cmpRun.err
sameObject.file1
sameObject.b1
cmpRun.keepLog
skipVersion.file2
Members
X