From 133d2e05a6f22bbda4616960360deba6b97ef209 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Thu, 3 Nov 2016 17:09:50 -0700 Subject: [PATCH] cmd/toolstash: import from rsc.io This CL copies rsc.io/toolstash verbatim at commit 7508e1dd47d11b2fc45f544415e014e4f25d3f95 to golang.org/x/tools/cmd/toolstash. There are no code changes to adapt it to its new home; those will happen in a follow-up CL. rsc.io/toolstash will be updated to contain only a README and a doc.go redirecting readers. Change-Id: Icbef4d72215a8b124f857587905b45902d6cdece Reviewed-on: https://go-review.googlesource.com/32681 Reviewed-by: Matthew Dempsky --- cmd/toolstash/LICENSE | 27 ++ cmd/toolstash/README.md | 3 + cmd/toolstash/buildall | 61 ++++ cmd/toolstash/cmp.go | 157 +++++++++++ cmd/toolstash/main.go | 600 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 848 insertions(+) create mode 100644 cmd/toolstash/LICENSE create mode 100644 cmd/toolstash/README.md create mode 100755 cmd/toolstash/buildall create mode 100644 cmd/toolstash/cmp.go create mode 100644 cmd/toolstash/main.go diff --git a/cmd/toolstash/LICENSE b/cmd/toolstash/LICENSE new file mode 100644 index 0000000000..6a66aea5ea --- /dev/null +++ b/cmd/toolstash/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/toolstash/README.md b/cmd/toolstash/README.md new file mode 100644 index 0000000000..7d71c8b16f --- /dev/null +++ b/cmd/toolstash/README.md @@ -0,0 +1,3 @@ +go get rsc.io/toolstash + +http://godoc.org/rsc.io/toolstash diff --git a/cmd/toolstash/buildall b/cmd/toolstash/buildall new file mode 100755 index 0000000000..65c6846435 --- /dev/null +++ b/cmd/toolstash/buildall @@ -0,0 +1,61 @@ +#!/bin/bash + +# Usage: buildall [-e] [-nocmp] [-work] +# +# Builds everything (std) for every GOOS/GOARCH combination but installs nothing. +# +# By default, runs the builds with -toolexec 'toolstash -cmp', to test that the +# toolchain is producing bit identical output to a previous known good toolchain. +# +# Options: +# -e: stop at first failure +# -nocmp: turn off toolstash -cmp; just check that ordinary builds succeed +# -work: pass -work to go command + +sete=false +if [ "$1" = "-e" ]; then + sete=true + shift +fi + +cmp=true +if [ "$1" = "-nocmp" ]; then + cmp=false + shift +fi + +work="" +if [ "$1" = "-work" ]; then + work="-work" + shift +fi + +cd $(go env GOROOT)/src +go install cmd/compile cmd/link cmd/asm || exit 1 +pattern="$1" +if [ "$pattern" = "" ]; then + pattern=. +fi + +# put linux, nacl first in the target list to get all the architectures up front. +targets="$((ls runtime | 9 sed -n 's/^rt0_(.*)_(.*)\.s/\1-\2/p'; echo linux-386-387) | sort | egrep -v android-arm | egrep "$pattern" | egrep 'linux|nacl') + $(ls runtime | 9 sed -n 's/^rt0_(.*)_(.*)\.s/\1-\2/p' | egrep -v 'android-arm|darwin-arm' | egrep "$pattern" | egrep -v 'linux|nacl')" +if [ "$sete" = true ]; then + set -e +fi +for target in $targets +do + echo $target + export GOOS=$(echo $target | sed 's/-.*//') + export GOARCH=$(echo $target | sed 's/.*-//') + unset GO386 + if [ "$GOARCH" = "387" ]; then + export GOARCH=386 + export GO386=387 + fi + if $cmp; then + go build $work -a -toolexec 'toolstash -cmp' std cmd + else + go build $work -a std + fi +done diff --git a/cmd/toolstash/cmp.go b/cmd/toolstash/cmp.go new file mode 100644 index 0000000000..f5974e12ba --- /dev/null +++ b/cmd/toolstash/cmp.go @@ -0,0 +1,157 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "os" + "regexp" + "strconv" + "strings" +) + +var ( + hexDumpRE = regexp.MustCompile(`^\t(0x[0-9a-f]{4,})(( ([0-9a-f]{2}| )){16}) [ -\x7F]{1,16}\n`) + listingRE = regexp.MustCompile(`^\t(0x[0-9a-f]{4,}) ([0-9]{4,}) \(.*:[0-9]+\)\t`) +) + +// okdiffs lists regular expressions for lines to consider minor mismatches. +// If one of these regexps matches both of a pair of unequal lines, the mismatch +// is reported but not treated as the one worth looking for. +// For example, differences in the TEXT line are typically frame size +// changes due to optimization decisions made in the body of the function. +// Better to keep looking for the actual difference. +// Similarly, forward jumps might have different offsets due to a +// change in instruction encoding later on. +// Better to find that change. +var okdiffs = []*regexp.Regexp{ + regexp.MustCompile(`\) TEXT[ ].*,\$`), + regexp.MustCompile(`\) WORD[ ].*,\$`), + regexp.MustCompile(`\) (B|BR|JMP) `), + regexp.MustCompile(`\) FUNCDATA `), + regexp.MustCompile(`\\(z|x00)`), + regexp.MustCompile(`\$\([0-9]\.[0-9]+e[+\-][0-9]+\)`), + regexp.MustCompile(`size=.*value=.*args=.*locals=`), +} + +func compareLogs(outfile string) string { + f1, err := os.Open(outfile + ".log") + if err != nil { + log.Fatal(err) + } + defer f1.Close() + + f2, err := os.Open(outfile + ".stash.log") + if err != nil { + log.Fatal(err) + } + defer f2.Close() + + b1 := bufio.NewReader(f1) + b2 := bufio.NewReader(f2) + + offset := int64(0) + textOffset := offset + textLineno := 0 + lineno := 0 + var line1, line2 string + var prefix bytes.Buffer +Reading: + for { + var err1, err2 error + line1, err1 = b1.ReadString('\n') + line2, err2 = b2.ReadString('\n') + if strings.Contains(line1, ")\tTEXT\t") { + textOffset = offset + textLineno = lineno + } + offset += int64(len(line1)) + lineno++ + if err1 == io.EOF && err2 == io.EOF { + return "no differences in debugging output" + } + + if lineno == 1 || line1 == line2 && err1 == nil && err2 == nil { + continue + } + // Lines are inconsistent. Worth stopping? + for _, re := range okdiffs { + if re.MatchString(line1) && re.MatchString(line2) { + fmt.Fprintf(&prefix, "inconsistent log line:\n%s:%d:\n\t%s\n%s:%d:\n\t%s\n\n", + f1.Name(), lineno, strings.TrimSuffix(line1, "\n"), + f2.Name(), lineno, strings.TrimSuffix(line2, "\n")) + continue Reading + } + } + + if err1 != nil { + line1 = err1.Error() + } + if err2 != nil { + line2 = err2.Error() + } + break + } + + msg := fmt.Sprintf("inconsistent log line:\n%s:%d:\n\t%s\n%s:%d:\n\t%s", + f1.Name(), lineno, strings.TrimSuffix(line1, "\n"), + f2.Name(), lineno, strings.TrimSuffix(line2, "\n")) + + if m := hexDumpRE.FindStringSubmatch(line1); m != nil { + target, err := strconv.ParseUint(m[1], 0, 64) + if err != nil { + goto Skip + } + + m2 := hexDumpRE.FindStringSubmatch(line2) + if m2 == nil { + goto Skip + } + + fields1 := strings.Fields(m[2]) + fields2 := strings.Fields(m2[2]) + i := 0 + for i < len(fields1) && i < len(fields2) && fields1[i] == fields2[i] { + i++ + } + target += uint64(i) + + f1.Seek(textOffset, 0) + b1 = bufio.NewReader(f1) + last := "" + lineno := textLineno + limitAddr := uint64(0) + lastAddr := uint64(0) + for { + line1, err1 := b1.ReadString('\n') + if err1 != nil { + break + } + lineno++ + if m := listingRE.FindStringSubmatch(line1); m != nil { + addr, _ := strconv.ParseUint(m[1], 0, 64) + if addr > target { + limitAddr = addr + break + } + last = line1 + lastAddr = addr + } else if hexDumpRE.FindStringSubmatch(line1) != nil { + break + } + } + if last != "" { + msg = fmt.Sprintf("assembly instruction at %#04x-%#04x:\n%s:%d\n\t%s\n\n%s", + lastAddr, limitAddr, f1.Name(), lineno-1, strings.TrimSuffix(last, "\n"), msg) + } + } +Skip: + + return prefix.String() + msg +} diff --git a/cmd/toolstash/main.go b/cmd/toolstash/main.go new file mode 100644 index 0000000000..2004cd7d6f --- /dev/null +++ b/cmd/toolstash/main.go @@ -0,0 +1,600 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Toolstash provides a way to save, run, and restore a known good copy of the Go toolchain +// and to compare the object files generated by two toolchains. +// +// Usage: +// +// toolstash [-n] [-v] save [tool...] +// toolstash [-n] [-v] restore [tool...] +// toolstash [-n] [-v] [-t] go run x.go +// toolstash [-n] [-v] [-t] [-cmp] compile x.go +// +// The toolstash command manages a ``stashed'' copy of the Go toolchain +// kept in $GOROOT/pkg/toolstash. In this case, the toolchain means the +// tools available with the 'go tool' command as well as the go, godoc, and gofmt +// binaries. +// +// The command ``toolstash save'', typically run when the toolchain is known to be working, +// copies the toolchain from its installed location to the toolstash directory. +// Its inverse, ``toolchain restore'', typically run when the toolchain is known to be broken, +// copies the toolchain from the toolstash directory back to the installed locations. +// If additional arguments are given, the save or restore applies only to the named tools. +// Otherwise, it applies to all tools. +// +// Otherwise, toolstash's arguments should be a command line beginning with the +// name of a toolchain binary, which may be a short name like compile or a complete path +// to an installed binary. Toolstash runs the command line using the stashed +// copy of the binary instead of the installed one. +// +// The -n flag causes toolstash to print the commands that would be executed +// but not execute them. The combination -n -cmp shows the two commands +// that would be compared and then exits successfully. A real -cmp run might +// run additional commands for diagnosis of an output mismatch. +// +// The -v flag causes toolstash to print the commands being executed. +// +// The -t flag causes toolstash to print the time elapsed during while the +// command ran. +// +// Comparing +// +// The -cmp flag causes toolstash to run both the installed and the stashed +// copy of an assembler or compiler and check that they produce identical +// object files. If not, toolstash reports the mismatch and exits with a failure status. +// As part of reporting the mismatch, toolstash reinvokes the command with +// the -S flag and identifies the first divergence in the assembly output. +// If the command is a Go compiler, toolstash also determines whether the +// difference is triggered by optimization passes. +// On failure, toolstash leaves additional information in files named +// similarly to the default output file. If the compilation would normally +// produce a file x.6, the output from the stashed tool is left in x.6.stash +// and the debugging traces are left in x.6.log and x.6.stash.log. +// +// The -cmp flag is a no-op when the command line is not invoking an +// assembler or compiler. +// +// For example, when working on code cleanup that should not affect +// compiler output, toolstash can be used to compare the old and new +// compiler output: +// +// toolstash save +// +// go tool dist install cmd/compile # install compiler only +// toolstash -cmp compile x.go +// +// Go Command Integration +// +// The go command accepts a -toolexec flag that specifies a program +// to use to run the build tools. +// +// To build with the stashed tools: +// +// go build -toolexec toolstash x.go +// +// To build with the stashed go command and the stashed tools: +// +// toolstash go build -toolexec toolstash x.go +// +// To verify that code cleanup in the compilers does not make any +// changes to the objects being generated for the entire tree: +// +// # Build working tree and save tools. +// ./make.bash +// toolstash save +// +// +// +// # Install new tools, but do not rebuild the rest of tree, +// # since the compilers might generate buggy code. +// go tool dist install cmd/compile +// +// # Check that new tools behave identically to saved tools. +// go build -toolexec 'toolstash -cmp' -a std +// +// # If not, restore, in order to keep working on Go code. +// toolstash restore +// +// Version Skew +// +// The Go tools write the current Go version to object files, and (outside +// release branches) that version includes the hash and time stamp +// of the most recent Git commit. Functionally equivalent +// compilers built at different Git versions may produce object files that +// differ only in the recorded version. Toolstash ignores version mismatches +// when comparing object files, but the standard tools will refuse to compile +// or link together packages with different object versions. +// +// For the full build in the final example above to work, both the stashed +// and the installed tools must use the same version string. +// One way to ensure this is not to commit any of the changes being +// tested, so that the Git HEAD hash is the same for both builds. +// A more robust way to force the tools to have the same version string +// is to write a $GOROOT/VERSION file, which overrides the Git-based version +// computation: +// +// echo devel >$GOROOT/VERSION +// +// The version can be arbitrary text, but to pass all.bash's API check, it must +// contain the substring ``devel''. The VERSION file must be created before +// building either version of the toolchain. +// +package main // import "rsc.io/toolstash" + +import ( + "bufio" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +var usageMessage = `usage: toolstash [-n] [-v] [-cmp] command line + +Examples: + toolstash save + toolstash restore + toolstash go run x.go + toolstash compile x.go + toolstash -cmp compile x.go + +For details, godoc rsc.io/toolstash +` + +func usage() { + fmt.Fprint(os.Stderr, usageMessage) + os.Exit(2) +} + +var ( + norun = flag.Bool("n", false, "print but do not run commands") + verbose = flag.Bool("v", false, "print commands being run") + cmp = flag.Bool("cmp", false, "compare tool object files") + timing = flag.Bool("t", false, "print time commands take") +) + +var ( + cmd []string + tool string // name of tool: "go", "compile", etc + toolStash string // path to stashed tool + + goroot string + toolDir string + stashDir string + binDir string +) + +func canCmp(name string) bool { + switch name { + case "compile", "link", "asm": + return true + } + return len(name) == 2 && '0' <= name[0] && name[0] <= '9' && (name[1] == 'a' || name[1] == 'g' || name[1] == 'l') +} + +var binTools = []string{"go", "godoc", "gofmt"} + +func isBinTool(name string) bool { + return strings.HasPrefix(name, "go") +} + +func main() { + log.SetFlags(0) + log.SetPrefix("toolstash: ") + + flag.Usage = usage + flag.Parse() + cmd = flag.Args() + + if len(cmd) < 1 { + usage() + } + + goroot = runtime.GOROOT() + toolDir = filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH)) + stashDir = filepath.Join(goroot, "pkg/toolstash") + + binDir = os.Getenv("GOBIN") + if binDir == "" { + binDir = filepath.Join(goroot, "bin") + } + + switch cmd[0] { + case "save": + save() + return + + case "restore": + restore() + return + } + + tool = cmd[0] + if i := strings.LastIndex(tool, "/"); i >= 0 { + tool = tool[i+1:] + } + if i := strings.LastIndex(tool, `\`); i >= 0 { + tool = tool[i+1:] + } + + if !strings.HasPrefix(tool, "a.out") { + toolStash = filepath.Join(stashDir, tool) + if _, err := os.Stat(toolStash); err != nil { + log.Print(err) + os.Exit(2) + } + + if *cmp && canCmp(tool) { + compareTool() + return + } + cmd[0] = toolStash + } + + if *norun { + fmt.Printf("%s\n", strings.Join(cmd, " ")) + return + } + if *verbose { + log.Print(strings.Join(cmd, " ")) + } + xcmd := exec.Command(cmd[0], cmd[1:]...) + xcmd.Stdin = os.Stdin + xcmd.Stdout = os.Stdout + xcmd.Stderr = os.Stderr + err := xcmd.Run() + if err != nil { + log.Fatal(err) + } + os.Exit(0) +} + +func compareTool() { + if !strings.Contains(cmd[0], "/") && !strings.Contains(cmd[0], `\`) { + cmd[0] = filepath.Join(toolDir, tool) + } + + outfile, ok := cmpRun(false, cmd) + if ok { + os.Remove(outfile + ".stash") + return + } + + extra := "-S" + switch { + default: + log.Fatalf("unknown tool %s", tool) + + case tool == "compile" || strings.HasSuffix(tool, "g"): // compiler + cmdN := append([]string{cmd[0], "-N"}, cmd[1:]...) + _, ok := cmpRun(false, cmdN) + if !ok { + log.Printf("compiler output differs, even with optimizers disabled (-N)") + cmd = append([]string{cmd[0], "-v", "-N", "-m=2"}, cmd[1:]...) + break + } + cmd = append([]string{cmd[0], "-v", "-m=2"}, cmd[1:]...) + log.Printf("compiler output differs, only with optimizers enabled") + + case tool == "asm" || strings.HasSuffix(tool, "a"): // assembler + log.Printf("assembler output differs") + + case tool == "link" || strings.HasSuffix(tool, "l"): // linker + log.Printf("linker output differs") + extra = "-v=2" + } + + cmdS := append([]string{cmd[0], extra}, cmd[1:]...) + outfile, ok = cmpRun(true, cmdS) + + fmt.Fprintf(os.Stderr, "\n%s\n", compareLogs(outfile)) + os.Exit(2) +} + +func cmpRun(keepLog bool, cmd []string) (outfile string, match bool) { + cmdStash := make([]string, len(cmd)) + copy(cmdStash, cmd) + cmdStash[0] = toolStash + for i, arg := range cmdStash { + if arg == "-o" { + outfile = cmdStash[i+1] + cmdStash[i+1] += ".stash" + break + } + if strings.HasSuffix(arg, ".s") || strings.HasSuffix(arg, ".go") && '0' <= tool[0] && tool[0] <= '9' { + outfile = filepath.Base(arg[:strings.LastIndex(arg, ".")] + "." + tool[:1]) + cmdStash = append([]string{cmdStash[0], "-o", outfile + ".stash"}, cmdStash[1:]...) + break + } + } + + if outfile == "" { + log.Fatalf("cannot determine output file for command: %s", strings.Join(cmd, " ")) + } + + if *norun { + fmt.Printf("%s\n", strings.Join(cmd, " ")) + fmt.Printf("%s\n", strings.Join(cmdStash, " ")) + os.Exit(0) + } + + out, err := runCmd(cmd, keepLog, outfile+".log") + if err != nil { + log.Printf("running: %s", strings.Join(cmd, " ")) + os.Stderr.Write(out) + log.Fatal(err) + } + + outStash, err := runCmd(cmdStash, keepLog, outfile+".stash.log") + if err != nil { + log.Printf("running: %s", strings.Join(cmdStash, " ")) + log.Printf("installed tool succeeded but stashed tool failed.\n") + if len(out) > 0 { + log.Printf("installed tool output:") + os.Stderr.Write(out) + } + if len(outStash) > 0 { + log.Printf("stashed tool output:") + os.Stderr.Write(outStash) + } + log.Fatal(err) + } + + return outfile, sameObject(outfile, outfile+".stash") +} + +func sameObject(file1, file2 string) bool { + f1, err := os.Open(file1) + if err != nil { + log.Fatal(err) + } + defer f1.Close() + + f2, err := os.Open(file2) + if err != nil { + log.Fatal(err) + } + defer f2.Close() + + b1 := bufio.NewReader(f1) + b2 := bufio.NewReader(f2) + + // Go object files and archives contain lines of the form + // go object + // By default, the version on development branches includes + // the Git hash and time stamp for the most recent commit. + // We allow the versions to differ. + if !skipVersion(b1, b2, file1, file2) { + return false + } + + lastByte := byte(0) + for { + c1, err1 := b1.ReadByte() + c2, err2 := b2.ReadByte() + if err1 == io.EOF && err2 == io.EOF { + return true + } + if err1 != nil { + log.Fatalf("reading %s: %v", file1, err1) + } + if err2 != nil { + log.Fatalf("reading %s: %v", file2, err1) + } + if c1 != c2 { + return false + } + if lastByte == '`' && c1 == '\n' { + if !skipVersion(b1, b2, file1, file2) { + return false + } + } + lastByte = c1 + } +} + +func skipVersion(b1, b2 *bufio.Reader, file1, file2 string) bool { + // Consume "go object " prefix, if there. + prefix := "go object " + for i := 0; i < len(prefix); i++ { + c1, err1 := b1.ReadByte() + c2, err2 := b2.ReadByte() + if err1 == io.EOF && err2 == io.EOF { + return true + } + if err1 != nil { + log.Fatalf("reading %s: %v", file1, err1) + } + if err2 != nil { + log.Fatalf("reading %s: %v", file2, err1) + } + if c1 != c2 { + return false + } + if c1 != prefix[i] { + return true // matching bytes, just not a version + } + } + + // Keep comparing until second space. + // Must continue to match. + // If we see a \n, it's not a version string after all. + for numSpace := 0; numSpace < 2; { + c1, err1 := b1.ReadByte() + c2, err2 := b2.ReadByte() + if err1 == io.EOF && err2 == io.EOF { + return true + } + if err1 != nil { + log.Fatalf("reading %s: %v", file1, err1) + } + if err2 != nil { + log.Fatalf("reading %s: %v", file2, err1) + } + if c1 != c2 { + return false + } + if c1 == '\n' { + return true + } + if c1 == ' ' { + numSpace++ + } + } + + // Have now seen 'go object goos goarch ' in both files. + // Now they're allowed to diverge, until the \n, which + // must be present. + for { + c1, err1 := b1.ReadByte() + if err1 == io.EOF { + log.Fatalf("reading %s: unexpected EOF", file1, err1) + } + if err1 != nil { + log.Fatalf("reading %s: %v", file1, err1) + } + if c1 == '\n' { + break + } + } + for { + c2, err2 := b2.ReadByte() + if err2 == io.EOF { + log.Fatalf("reading %s: unexpected EOF", file2, err2) + } + if err2 != nil { + log.Fatalf("reading %s: %v", file2, err2) + } + if c2 == '\n' { + break + } + } + + // Consumed "matching" versions from both. + return true +} + +func runCmd(cmd []string, keepLog bool, logName string) (output []byte, err error) { + if *verbose { + log.Print(strings.Join(cmd, " ")) + } + + if *timing { + t0 := time.Now() + defer func() { + log.Printf("%.3fs elapsed # %s\n", time.Since(t0).Seconds(), strings.Join(cmd, " ")) + }() + } + + xcmd := exec.Command(cmd[0], cmd[1:]...) + if !keepLog { + return xcmd.CombinedOutput() + } + + f, err := os.Create(logName) + if err != nil { + log.Fatal(err) + } + fmt.Fprintf(f, "GOOS=%s GOARCH=%s %s\n", os.Getenv("GOOS"), os.Getenv("GOARCH"), strings.Join(cmd, " ")) + xcmd.Stdout = f + xcmd.Stderr = f + defer f.Close() + return nil, xcmd.Run() +} + +func save() { + if err := os.MkdirAll(stashDir, 0777); err != nil { + log.Fatal(err) + } + + toolDir := filepath.Join(goroot, fmt.Sprintf("pkg/tool/%s_%s", runtime.GOOS, runtime.GOARCH)) + files, err := ioutil.ReadDir(toolDir) + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + if shouldSave(file.Name()) && file.Mode().IsRegular() { + cp(filepath.Join(toolDir, file.Name()), filepath.Join(stashDir, file.Name())) + } + } + + for _, name := range binTools { + if !shouldSave(name) { + continue + } + src := filepath.Join(binDir, name) + if _, err := os.Stat(src); err == nil { + cp(src, filepath.Join(stashDir, name)) + } + } + + checkShouldSave() +} + +func restore() { + files, err := ioutil.ReadDir(stashDir) + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + if shouldSave(file.Name()) && file.Mode().IsRegular() { + targ := toolDir + if isBinTool(file.Name()) { + targ = binDir + } + cp(filepath.Join(stashDir, file.Name()), filepath.Join(targ, file.Name())) + } + } + + checkShouldSave() +} + +func shouldSave(name string) bool { + if len(cmd) == 1 { + return true + } + ok := false + for i, arg := range cmd { + if i > 0 && name == arg { + ok = true + cmd[i] = "DONE" + } + } + return ok +} + +func checkShouldSave() { + var missing []string + for _, arg := range cmd[1:] { + if arg != "DONE" { + missing = append(missing, arg) + } + } + if len(missing) > 0 { + log.Fatalf("%s did not find tools: %s", cmd[0], strings.Join(missing, " ")) + } +} + +func cp(src, dst string) { + if *verbose { + fmt.Printf("cp %s %s\n", src, dst) + } + data, err := ioutil.ReadFile(src) + if err != nil { + log.Fatal(err) + } + if err := ioutil.WriteFile(dst, data, 0777); err != nil { + log.Fatal(err) + } +}