cmd/buildlet/stage0: add untar mode
The new untar package will be shared by the buildlet later.
Change-Id: I7a33bf46e5cb6b7e5abd59613bb943039c84aa08
Reviewed-on: https://go-review.googlesource.com/41131
Reviewed-by: Jeff Johnson <jrjohnson@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/buildlet/stage0/stage0.go b/cmd/buildlet/stage0/stage0.go
index 05ac24a..2256c5e 100644
--- a/cmd/buildlet/stage0/stage0.go
+++ b/cmd/buildlet/stage0/stage0.go
@@ -28,6 +28,7 @@
"cloud.google.com/go/compute/metadata"
"golang.org/x/build/internal/httpdl"
+ "golang.org/x/build/internal/untar"
)
// This lets us be lazy and put the stage0 start-up in rc.local where
@@ -44,9 +45,20 @@
scalewayMeta scalewayMetadata
)
+// untar helper, for the Windows image prep script.
+var (
+ untarFile = flag.String("untar-file", "", "if non-empty, tar.gz to untar to --untar-dest-dir")
+ untarDestDir = flag.String("untar-dest-dir", "", "destination directory to untar --untar-file to")
+)
+
func main() {
flag.Parse()
+ if *untarFile != "" {
+ untarMode()
+ return
+ }
+
switch osArch {
case "linux/arm":
if os.Getenv("GO_BUILDER_ENV") == "linux-arm-arm5spacemonkey" {
@@ -441,3 +453,23 @@
}
return true
}
+
+func untarMode() {
+ if *untarDestDir == "" {
+ log.Fatal("--untar-dest-dir must not be empty")
+ }
+ if fi, err := os.Stat(*untarDestDir); err != nil || !fi.IsDir() {
+ if err != nil {
+ log.Fatalf("--untar-dest-dir %q: %v", *untarDestDir, err)
+ }
+ log.Fatalf("--untar-dest-dir %q not a directory.", *untarDestDir)
+ }
+ f, err := os.Open(*untarFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer f.Close()
+ if err := untar.Untar(f, *untarDestDir); err != nil {
+ log.Fatalf("Untarring %q to %q: %v", *untarFile, *untarDestDir)
+ }
+}
diff --git a/internal/untar/untar.go b/internal/untar/untar.go
new file mode 100644
index 0000000..1083395
--- /dev/null
+++ b/internal/untar/untar.go
@@ -0,0 +1,141 @@
+// Copyright 2017 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 untar untars a tarball to disk.
+package untar
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// TODO(bradfitz): this was copied from x/build/cmd/buildlet/buildlet.go
+// but there were some buildlet-specific bits in there, so the code is
+// forked for now. Unfork and add some opts arguments here, so the
+// buildlet can use this code somehow.
+
+// Untar reads the gzip-compressed tar file from r and writes it into dir.
+func Untar(r io.Reader, dir string) error {
+ return untar(r, dir)
+}
+
+func untar(r io.Reader, dir string) (err error) {
+ t0 := time.Now()
+ nFiles := 0
+ madeDir := map[string]bool{}
+ defer func() {
+ td := time.Since(t0)
+ if err == nil {
+ log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
+ } else {
+ log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
+ }
+ }()
+ zr, err := gzip.NewReader(r)
+ if err != nil {
+ return fmt.Errorf("requires gzip-compressed body: %v", err)
+ }
+ tr := tar.NewReader(zr)
+ loggedChtimesError := false
+ for {
+ f, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ log.Printf("tar reading error: %v", err)
+ return fmt.Errorf("tar error: %v", err)
+ }
+ if !validRelPath(f.Name) {
+ return fmt.Errorf("tar contained invalid name error %q", f.Name)
+ }
+ rel := filepath.FromSlash(f.Name)
+ abs := filepath.Join(dir, rel)
+
+ fi := f.FileInfo()
+ mode := fi.Mode()
+ switch {
+ case mode.IsRegular():
+ // Make the directory. This is redundant because it should
+ // already be made by a directory entry in the tar
+ // beforehand. Thus, don't check for errors; the next
+ // write will fail with the same error.
+ dir := filepath.Dir(abs)
+ if !madeDir[dir] {
+ if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
+ return err
+ }
+ madeDir[dir] = true
+ }
+ wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
+ if err != nil {
+ return err
+ }
+ n, err := io.Copy(wf, tr)
+ if closeErr := wf.Close(); closeErr != nil && err == nil {
+ err = closeErr
+ }
+ if err != nil {
+ return fmt.Errorf("error writing to %s: %v", abs, err)
+ }
+ if n != f.Size {
+ return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
+ }
+ modTime := f.ModTime
+ if modTime.After(t0) {
+ // Clamp modtimes at system time. See
+ // golang.org/issue/19062 when clock on
+ // buildlet was behind the gitmirror server
+ // doing the git-archive.
+ modTime = t0
+ }
+ if !modTime.IsZero() {
+ if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
+ // benign error. Gerrit doesn't even set the
+ // modtime in these, and we don't end up relying
+ // on it anywhere (the gomote push command relies
+ // on digests only), so this is a little pointless
+ // for now.
+ log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
+ loggedChtimesError = true // once is enough
+ }
+ }
+ nFiles++
+ case mode.IsDir():
+ if err := os.MkdirAll(abs, 0755); err != nil {
+ return err
+ }
+ madeDir[abs] = true
+ default:
+ return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
+ }
+ }
+ return nil
+}
+
+func validRelativeDir(dir string) bool {
+ if strings.Contains(dir, `\`) || path.IsAbs(dir) {
+ return false
+ }
+ dir = path.Clean(dir)
+ if strings.HasPrefix(dir, "../") || strings.HasSuffix(dir, "/..") || dir == ".." {
+ return false
+ }
+ return true
+}
+
+func validRelPath(p string) bool {
+ if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
+ return false
+ }
+ return true
+}