|  | // 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 ( | 
|  | "archive/tar" | 
|  | "bufio" | 
|  | "bytes" | 
|  | "compress/gzip" | 
|  | "context" | 
|  | "crypto/sha1" | 
|  | "errors" | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "log" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "sort" | 
|  | "strings" | 
|  |  | 
|  | "golang.org/x/build/buildlet" | 
|  | "golang.org/x/build/internal/gomote/protos" | 
|  | "golang.org/x/sync/errgroup" | 
|  | ) | 
|  |  | 
|  | func push(args []string) error { | 
|  | fs := flag.NewFlagSet("push", flag.ContinueOnError) | 
|  | var dryRun bool | 
|  | fs.BoolVar(&dryRun, "dry-run", false, "print what would be done only") | 
|  | fs.Usage = func() { | 
|  | fmt.Fprintln(os.Stderr, "push usage: gomote push <instance>") | 
|  | fs.PrintDefaults() | 
|  | os.Exit(1) | 
|  | } | 
|  | fs.Parse(args) | 
|  |  | 
|  | goroot, err := getGOROOT() | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | var pushSet []string | 
|  | if fs.NArg() == 1 { | 
|  | pushSet = append(pushSet, fs.Arg(0)) | 
|  | } else if activeGroup != nil { | 
|  | for _, inst := range activeGroup.Instances { | 
|  | pushSet = append(pushSet, inst) | 
|  | } | 
|  | } else { | 
|  | fs.Usage() | 
|  | } | 
|  |  | 
|  | detailedProgress := len(pushSet) == 1 | 
|  | eg, ctx := errgroup.WithContext(context.Background()) | 
|  | for _, inst := range pushSet { | 
|  | inst := inst | 
|  | eg.Go(func() error { | 
|  | fmt.Fprintf(os.Stderr, "# Pushing GOROOT %q to %q...\n", goroot, inst) | 
|  | return doPush(ctx, inst, goroot, dryRun, detailedProgress) | 
|  | }) | 
|  | } | 
|  | return eg.Wait() | 
|  | } | 
|  |  | 
|  | func doPush(ctx context.Context, name, goroot string, dryRun, detailedProgress bool) error { | 
|  | logf := func(s string, a ...interface{}) { | 
|  | if detailedProgress { | 
|  | log.Printf(s, a...) | 
|  | } | 
|  | } | 
|  | haveGo14 := false | 
|  | remote := map[string]buildlet.DirEntry{} // keys like "src/make.bash" | 
|  |  | 
|  | client := gomoteServerClient(ctx) | 
|  | resp, err := client.ListDirectory(ctx, &protos.ListDirectoryRequest{ | 
|  | GomoteId:  name, | 
|  | Directory: ".", | 
|  | Recursive: true, | 
|  | SkipFiles: []string{ | 
|  | // Ignore binary output directories: | 
|  | "go/pkg", "go/bin", | 
|  | // We don't care about the digest of | 
|  | // particular source files for Go 1.4.  And | 
|  | // exclude /pkg. This leaves go1.4/bin, which | 
|  | // is enough to know whether we have Go 1.4 or | 
|  | // not. | 
|  | "go1.4/src", "go1.4/pkg", | 
|  | // Ignore the cache and tmp directories, these slowly grow, and will | 
|  | // eventually cause the listing to exceed the maximum gRPC message | 
|  | // size. | 
|  | "gocache", "goplscache", "tmp", | 
|  | }, | 
|  | Digest: true, | 
|  | }) | 
|  | if err != nil { | 
|  | return fmt.Errorf("error listing buildlet's existing files: %w", err) | 
|  | } | 
|  | for _, entry := range resp.GetEntries() { | 
|  | de := buildlet.DirEntry{Line: entry} | 
|  | en := de.Name() | 
|  | if strings.HasPrefix(en, "go1.4/") { | 
|  | haveGo14 = true | 
|  | continue | 
|  | } | 
|  | if strings.HasPrefix(en, "go/") && en != "go/" { | 
|  | remote[en[len("go/"):]] = de | 
|  | } | 
|  | } | 
|  | if !haveGo14 { | 
|  | logf("installing go1.4") | 
|  | if dryRun { | 
|  | logf("(Dry-run) Would have pushed go1.4") | 
|  | } else { | 
|  | _, err := client.AddBootstrap(ctx, &protos.AddBootstrapRequest{ | 
|  | GomoteId: name, | 
|  | }) | 
|  | if err != nil { | 
|  | return fmt.Errorf("unable to add bootstrap version of Go to instance: %w", err) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Invoke 'git check-ignore' and use it to query whether paths have been gitignored. | 
|  | // If anything goes wrong at any point, fall back to assuming that nothing is gitignored. | 
|  | var isGitIgnored func(string) bool | 
|  | gci := exec.Command("git", "-C", goroot, "check-ignore", "--stdin", "-n", "-v", "-z") | 
|  | gci.Env = append(os.Environ(), "GIT_FLUSH=1") | 
|  | gciIn, errIn := gci.StdinPipe() | 
|  | defer gciIn.Close() // allow git process to exit | 
|  | gciOut, errOut := gci.StdoutPipe() | 
|  | gciErr := &bytes.Buffer{} | 
|  | gci.Stderr = gciErr | 
|  | errStart := gci.Start() | 
|  | if errIn != nil || errOut != nil || errStart != nil { | 
|  | isGitIgnored = func(string) bool { return false } | 
|  | } else { | 
|  | var failed bool | 
|  | br := bufio.NewReader(gciOut) | 
|  | isGitIgnored = func(path string) bool { | 
|  | if failed { | 
|  | return false | 
|  | } | 
|  | fmt.Fprintf(gciIn, "%s\x00", path) | 
|  | // Response is of form "<source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname> <NULL>" | 
|  | // Read all four and check that the path is correct. | 
|  | // If so, the path is ignored iff the source (reason why ignored) is non-empty. | 
|  | var resp [4][]byte | 
|  | for i := range resp { | 
|  | b, err := br.ReadBytes(0) | 
|  | if err != nil { | 
|  | gci.Wait() | 
|  | logf("git check-ignore %q exited unexpectedly:\n%v", path, gciErr.String()) | 
|  | failed = true | 
|  | return false | 
|  | } | 
|  | resp[i] = b[:len(b)-1] // drop trailing NULL | 
|  | } | 
|  | // Sanity check | 
|  | if string(resp[3]) != path { | 
|  | panic("git check-ignore path did not roundtrip, got " + string(resp[3]) + " sent " + path) | 
|  | } | 
|  | return len(resp[0]) > 0 | 
|  | } | 
|  | } | 
|  |  | 
|  | type fileInfo struct { | 
|  | fi   os.FileInfo | 
|  | sha1 string // if regular file | 
|  | } | 
|  | local := map[string]fileInfo{} // keys like "src/make.bash" | 
|  |  | 
|  | // Ensure that the goroot passed to filepath.Walk ends in a trailing slash, | 
|  | // so that if GOROOT is a symlink we walk the underlying directory. | 
|  | walkRoot := goroot | 
|  | if walkRoot != "" && !os.IsPathSeparator(walkRoot[len(walkRoot)-1]) { | 
|  | walkRoot += string(filepath.Separator) | 
|  | } | 
|  | if err := filepath.Walk(walkRoot, func(path string, fi os.FileInfo, err error) error { | 
|  | if isEditorBackup(path) { | 
|  | return nil | 
|  | } | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | rel, err := filepath.Rel(goroot, path) | 
|  | if err != nil { | 
|  | return fmt.Errorf("error calculating relative path from %q to %q", goroot, path) | 
|  | } | 
|  | rel = filepath.ToSlash(rel) | 
|  | if rel == "." { | 
|  | return nil | 
|  | } | 
|  | if rel == ".git" { | 
|  | if fi.IsDir() { | 
|  | return filepath.SkipDir | 
|  | } | 
|  | return nil // .git is a file in `git worktree` checkouts. | 
|  | } | 
|  | if fi.IsDir() { | 
|  | switch rel { | 
|  | case "pkg", "bin": | 
|  | return filepath.SkipDir | 
|  | } | 
|  | } | 
|  | inf := fileInfo{fi: fi} | 
|  | if isGitIgnored(path) { | 
|  | if fi.IsDir() { | 
|  | return filepath.SkipDir | 
|  | } | 
|  | return nil | 
|  | } | 
|  | if fi.Mode().IsRegular() { | 
|  | inf.sha1, err = fileSHA1(path) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | local[rel] = inf | 
|  | return nil | 
|  | }); err != nil { | 
|  | return fmt.Errorf("error enumerating local GOROOT files: %w", err) | 
|  | } | 
|  |  | 
|  | var toDel []string | 
|  | for rel := range remote { | 
|  | if rel == "VERSION" { | 
|  | // Don't delete this. It's harmless, and | 
|  | // necessary. Clients can overwrite it if they | 
|  | // want. But if there's no VERSION file there, | 
|  | // make.bash/bat assumes there's a git repo in | 
|  | // place, but there's not only not a git repo | 
|  | // there with gomote, but there's no git tool | 
|  | // available either. | 
|  | continue | 
|  | } | 
|  | // Also don't delete the auto-generated files from cmd/dist. | 
|  | // Otherwise gomote users can't gomote push + gomote run make.bash | 
|  | // and then iteratively: | 
|  | // -- hack locally | 
|  | // -- gomote push | 
|  | // -- gomote run go test -v ... | 
|  | // Because the go test would fail remotely without | 
|  | // these files if they were deleted by gomote push. | 
|  | if isGoToolDistGenerated(rel) { | 
|  | continue | 
|  | } | 
|  | if isGitIgnored(rel) { | 
|  | // Don't delete remote gitignored files; this breaks built toolchains. | 
|  | continue | 
|  | } | 
|  | rel = strings.TrimRight(rel, "/") | 
|  | if rel == "" { | 
|  | continue | 
|  | } | 
|  | if _, ok := local[rel]; !ok { | 
|  | toDel = append(toDel, rel) | 
|  | } | 
|  | } | 
|  | if len(toDel) > 0 { | 
|  | withGo := make([]string, len(toDel)) // with the "go/" prefix | 
|  | for i, v := range toDel { | 
|  | withGo[i] = "go/" + v | 
|  | } | 
|  | sort.Strings(withGo) | 
|  | if dryRun { | 
|  | logf("(Dry-run) Would have deleted remote files: %q", withGo) | 
|  | } else { | 
|  | logf("Deleting remote files: %q", withGo) | 
|  | if _, err := client.RemoveFiles(ctx, &protos.RemoveFilesRequest{ | 
|  | GomoteId: name, | 
|  | Paths:    withGo, | 
|  | }); err != nil { | 
|  | return fmt.Errorf("failed to delete remote unwanted files: %w", err) | 
|  | } | 
|  | } | 
|  | } | 
|  | var toSend []string | 
|  | notHave := 0 | 
|  | const maxNotHavePrint = 5 | 
|  | for rel, inf := range local { | 
|  | if isGoToolDistGenerated(rel) || rel == "VERSION.cache" { | 
|  | continue | 
|  | } | 
|  | if !inf.fi.Mode().IsRegular() { | 
|  | if !inf.fi.IsDir() { | 
|  | logf("Ignoring local non-regular, non-directory file %s: %v", rel, inf.fi.Mode()) | 
|  | } | 
|  | continue | 
|  | } | 
|  | rem, ok := remote[rel] | 
|  | if !ok { | 
|  | if notHave++; notHave <= maxNotHavePrint { | 
|  | logf("Remote doesn't have %q", rel) | 
|  | } | 
|  | toSend = append(toSend, rel) | 
|  | continue | 
|  | } | 
|  | if rem.Digest() != inf.sha1 { | 
|  | logf("Remote's %s digest is %q; want %q", rel, rem.Digest(), inf.sha1) | 
|  | toSend = append(toSend, rel) | 
|  | } | 
|  | } | 
|  | if notHave > maxNotHavePrint { | 
|  | logf("Remote doesn't have %d files (only showed %d).", notHave, maxNotHavePrint) | 
|  | } | 
|  | _, localHasVersion := local["VERSION"] | 
|  | if _, remoteHasVersion := remote["VERSION"]; !remoteHasVersion && !localHasVersion { | 
|  | logf("Remote lacks a VERSION file; sending a fake one") | 
|  | toSend = append(toSend, "VERSION") | 
|  | } | 
|  | if len(toSend) > 0 { | 
|  | sort.Strings(toSend) | 
|  | tgz, err := generateDeltaTgz(goroot, toSend) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | logf("Uploading %d new/changed files; %d byte .tar.gz", len(toSend), tgz.Len()) | 
|  | if dryRun { | 
|  | logf("(Dry-run mode; not doing anything.") | 
|  | return nil | 
|  | } | 
|  | resp, err := client.UploadFile(ctx, &protos.UploadFileRequest{}) | 
|  | if err != nil { | 
|  | return fmt.Errorf("unable to request credentials for a file upload: %w", err) | 
|  | } | 
|  | if err := uploadToGCS(ctx, resp.GetFields(), tgz, resp.GetObjectName(), resp.GetUrl()); err != nil { | 
|  | return fmt.Errorf("unable to upload file to GCS: %w", err) | 
|  | } | 
|  | if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{ | 
|  | GomoteId:  name, | 
|  | Url:       fmt.Sprintf("%s%s", resp.GetUrl(), resp.GetObjectName()), | 
|  | Directory: "go", | 
|  | }); err != nil { | 
|  | return fmt.Errorf("failed writing tarball to buildlet: %w", err) | 
|  | } | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func isGoToolDistGenerated(path string) bool { | 
|  | switch path { | 
|  | case "src/cmd/cgo/zdefaultcc.go", | 
|  | "src/cmd/go/internal/cfg/zdefaultcc.go", | 
|  | "src/cmd/go/internal/cfg/zosarch.go", | 
|  | "src/cmd/internal/objabi/zbootstrap.go", | 
|  | "src/go/build/zcgo.go", | 
|  | "src/internal/buildcfg/zbootstrap.go", | 
|  | "src/runtime/internal/sys/zversion.go", | 
|  | "src/time/tzdata/zzipdata.go": | 
|  | return true | 
|  | } | 
|  | return false | 
|  | } | 
|  |  | 
|  | func isEditorBackup(path string) bool { | 
|  | base := filepath.Base(path) | 
|  | if strings.HasPrefix(base, ".") && strings.HasSuffix(base, ".swp") { | 
|  | // vi | 
|  | return true | 
|  | } | 
|  | if strings.HasSuffix(path, "~") || strings.HasSuffix(path, "#") || | 
|  | strings.HasPrefix(base, "#") || strings.HasPrefix(base, ".#") { | 
|  | // emacs | 
|  | return true | 
|  | } | 
|  | return false | 
|  | } | 
|  |  | 
|  | // file is forward-slash separated | 
|  | func generateDeltaTgz(goroot string, files []string) (*bytes.Buffer, error) { | 
|  | var buf bytes.Buffer | 
|  | zw := gzip.NewWriter(&buf) | 
|  | tw := tar.NewWriter(zw) | 
|  | for _, file := range files { | 
|  | // Special. | 
|  | if file == "VERSION" && !localFileExists(filepath.Join(goroot, file)) { | 
|  | // TODO(bradfitz): a dummy VERSION file's contents to make things | 
|  | // happy. Notably it starts with "devel ". Do we care about it | 
|  | // being accurate beyond that? | 
|  | version := "devel gomote.XXXXX" | 
|  | if err := tw.WriteHeader(&tar.Header{ | 
|  | Name: "VERSION", | 
|  | Mode: 0644, | 
|  | Size: int64(len(version)), | 
|  | }); err != nil { | 
|  | return nil, err | 
|  | } | 
|  | if _, err := io.WriteString(tw, version); err != nil { | 
|  | return nil, err | 
|  | } | 
|  | continue | 
|  | } | 
|  | f, err := os.Open(filepath.Join(goroot, file)) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | fi, err := f.Stat() | 
|  | if err != nil { | 
|  | f.Close() | 
|  | return nil, err | 
|  | } | 
|  | header, err := tar.FileInfoHeader(fi, "") | 
|  | if err != nil { | 
|  | f.Close() | 
|  | return nil, err | 
|  | } | 
|  | header.Name = file // forward slash | 
|  | if err := tw.WriteHeader(header); err != nil { | 
|  | f.Close() | 
|  | return nil, err | 
|  | } | 
|  | if _, err := io.CopyN(tw, f, header.Size); err != nil { | 
|  | f.Close() | 
|  | return nil, fmt.Errorf("error copying contents of %s: %w", file, err) | 
|  | } | 
|  | f.Close() | 
|  | } | 
|  | if err := tw.Close(); err != nil { | 
|  | return nil, err | 
|  | } | 
|  | if err := zw.Close(); err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | return &buf, nil | 
|  | } | 
|  |  | 
|  | func fileSHA1(path string) (string, error) { | 
|  | f, err := os.Open(path) | 
|  | if err != nil { | 
|  | return "", err | 
|  | } | 
|  | defer f.Close() | 
|  | s1 := sha1.New() | 
|  | if _, err := io.Copy(s1, f); err != nil { | 
|  | return "", err | 
|  | } | 
|  | return fmt.Sprintf("%x", s1.Sum(nil)), nil | 
|  | } | 
|  |  | 
|  | func getGOROOT() (string, error) { | 
|  | goroot := os.Getenv("GOROOT") | 
|  | if goroot == "" { | 
|  | slurp, err := exec.Command("go", "env", "GOROOT").Output() | 
|  | if err != nil { | 
|  | return "", fmt.Errorf("failed to get GOROOT from go env: %w", err) | 
|  | } | 
|  | goroot = strings.TrimSpace(string(slurp)) | 
|  | if goroot == "" { | 
|  | return "", errors.New("Failed to get $GOROOT from environment or go env") | 
|  | } | 
|  | } | 
|  | goroot = filepath.Clean(goroot) | 
|  | return goroot, nil | 
|  | } | 
|  |  | 
|  | func localFileExists(path string) bool { | 
|  | _, err := os.Stat(path) | 
|  | return !os.IsNotExist(err) | 
|  | } |