cmd/release: build darwin packages and source tarballs

Change-Id: Idd1143cae097c3d19dcbdf72b8fdf1bc193e77b1
Reviewed-on: https://go-review.googlesource.com/12223
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/release/release.go b/cmd/release/release.go
index 45885d9..1c5b780 100644
--- a/cmd/release/release.go
+++ b/cmd/release/release.go
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// TODO(adg): build source release
 // TODO(adg): put windows release in a zip file
 // TODO(adg): put uploader in here
 
@@ -10,7 +9,9 @@
 package main
 
 import (
+	"archive/tar"
 	"bytes"
+	"compress/gzip"
 	"flag"
 	"fmt"
 	"io"
@@ -31,12 +32,13 @@
 var (
 	target = flag.String("target", "", "If specified, build specific target platform ('linux-amd64')")
 
-	rev      = flag.String("rev", "", "Go revision to build")
-	toolsRev = flag.String("tools", "", "Tools revision to build")
-	tourRev  = flag.String("tour", "master", "Tour revision to include")
-	blogRev  = flag.String("blog", "master", "Blog revision to include")
-	netRev   = flag.String("net", "master", "Net revision to include")
-	version  = flag.String("version", "", "Version string (go1.5.2)")
+	rev       = flag.String("rev", "", "Go revision to build")
+	toolsRev  = flag.String("tools", "", "Tools revision to build")
+	tourRev   = flag.String("tour", "master", "Tour revision to include")
+	blogRev   = flag.String("blog", "master", "Blog revision to include")
+	netRev    = flag.String("net", "master", "Net revision to include")
+	version   = flag.String("version", "", "Version string (go1.5.2)")
+	coordAddr = flag.String("coordinator", "", "Coordinator instance address (default is production)")
 
 	user = flag.String("user", username(), "coordinator username, appended to 'user-'")
 )
@@ -45,6 +47,7 @@
 
 type Build struct {
 	OS, Arch string
+	Source   bool
 
 	Race   bool // Build race detector.
 	Static bool // Statically-link binaries.
@@ -53,6 +56,9 @@
 }
 
 func (b *Build) String() string {
+	if b.Source {
+		return "src"
+	}
 	return fmt.Sprintf("%v-%v", b.OS, b.Arch)
 }
 
@@ -63,6 +69,10 @@
 
 var builds = []*Build{
 	{
+		Source:  true,
+		Builder: "linux-amd64",
+	},
+	{
 		OS:      "linux",
 		Arch:    "386",
 		Builder: "linux-386",
@@ -216,6 +226,9 @@
 		{"tour", *tourRev},
 		{"net", *netRev},
 	} {
+		if b.Source && r.repo != "go" {
+			continue
+		}
 		dir := goDir
 		if r.repo != "go" {
 			dir = goPath + "/src/golang.org/x/" + r.repo
@@ -226,7 +239,7 @@
 		}
 	}
 
-	if bc.Go14URL != "" {
+	if bc.Go14URL != "" && !b.Source {
 		b.logf("Installing go1.4.")
 		if err := client.PutTarFromURL(bc.Go14URL, go14); err != nil {
 			return err
@@ -238,6 +251,11 @@
 		return err
 	}
 
+	if b.Source {
+		b.logf("Skipping build.")
+		return b.fetchTarball(client)
+	}
+
 	// Set up build environment.
 	sep := "/"
 	if b.OS == "windows" {
@@ -246,8 +264,8 @@
 	env := append(bc.Env(),
 		"GOROOT_FINAL="+bc.GorootFinal(),
 		"GOROOT="+work+sep+goDir,
-		"GOBIN=",
 		"GOPATH="+work+sep+goPath,
+		"GOBIN=",
 	)
 	if b.Static {
 		env = append(env, "GO_DISTFLAGS=-s")
@@ -343,9 +361,19 @@
 
 	switch b.OS {
 	case "darwin":
-		// TODO(adg): fetch and remove pkg file
+		filename := *version + "." + b.String() + ".pkg"
+		if err := b.fetchFile(client, filename, "pkg"); err != nil {
+			return err
+		}
+		cleanFiles = append(cleanFiles, "pkg")
 	case "windows":
-		// TODO(adg): fetch and remove msi file
+		if false { // TODO(adg): implement this
+			filename := *version + "." + b.String() + ".msi"
+			if err := b.fetchFile(client, filename, "msi"); err != nil {
+				return err
+			}
+			cleanFiles = append(cleanFiles, "msi")
+		}
 	}
 
 	b.logf("Cleaning goroot (post-build).")
@@ -356,26 +384,61 @@
 		return err
 	}
 
-	// Download tarball
+	return b.fetchTarball(client)
+}
+
+func (b *Build) fetchTarball(client *buildlet.Client) error {
 	b.logf("Downloading tarball.")
 	tgz, err := client.GetTar(".")
 	if err != nil {
 		return err
 	}
 	filename := *version + "." + b.String() + ".tar.gz"
-	f, err = os.Create(filename)
+	return b.writeFile(filename, tgz)
+}
+
+// fetchFile fetches the specified directory from the given buildlet, and
+// writes the first file it finds in that directory to dest.
+func (b *Build) fetchFile(client *buildlet.Client, dest, dir string) error {
+	b.logf("Downloading file from %q.", dir)
+	tgz, err := client.GetTar(dir)
 	if err != nil {
 		return err
 	}
-	if _, err := io.Copy(f, tgz); err != nil {
+	defer tgz.Close()
+	zr, err := gzip.NewReader(tgz)
+	if err != nil {
+		return err
+	}
+	tr := tar.NewReader(zr)
+	for {
+		h, err := tr.Next()
+		if err == io.EOF {
+			return io.ErrUnexpectedEOF
+		}
+		if err != nil {
+			return err
+		}
+		if !h.FileInfo().IsDir() {
+			break
+		}
+	}
+	return b.writeFile(dest, tr)
+}
+
+func (b *Build) writeFile(name string, r io.Reader) error {
+	f, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	if _, err := io.Copy(f, r); err != nil {
 		f.Close()
 		return err
 	}
 	if err := f.Close(); err != nil {
 		return err
 	}
-	b.logf("Wrote %q.", filename)
-
+	b.logf("Wrote %q.", name)
 	return nil
 }
 
@@ -388,12 +451,16 @@
 }
 
 func coordinatorClient() *buildlet.CoordinatorClient {
+	inst := build.ProdCoordinator
+	if *coordAddr != "" {
+		inst = build.CoordinatorInstance(*coordAddr)
+	}
 	return &buildlet.CoordinatorClient{
 		Auth: buildlet.UserPass{
 			Username: "user-" + *user,
 			Password: userToken(),
 		},
-		Instance: build.ProdCoordinator,
+		Instance: inst,
 	}
 }