cmd/release: add new program to build releases
The release command will replace misc/makerelease from the main repo.
The contents of makerelease make their way into this program.
Also add GorootFinal and MakeScript methods to dashboard.BuildConfig.
Change-Id: Id20f3f7a85a21f040d8cf47e438d299d57360556
Reviewed-on: https://go-review.googlesource.com/3693
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/release/release.go b/cmd/release/release.go
new file mode 100644
index 0000000..82599f2
--- /dev/null
+++ b/cmd/release/release.go
@@ -0,0 +1,236 @@
+// 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.
+
+// Command release builds a Go release.
+package main
+
+import (
+ "bytes"
+ "crypto/rand"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "golang.org/x/build/auth"
+ "golang.org/x/build/buildlet"
+ "golang.org/x/build/dashboard"
+ "golang.org/x/oauth2"
+ "google.golang.org/api/compute/v1"
+)
+
+var (
+ revision = flag.String("rev", "", "Go revision to build")
+ project = flag.String("project", "symbolic-datum-552", "Google Cloud Project")
+ zone = flag.String("zone", "us-central1-a", "Compute Engine zone")
+)
+
+var builders = []string{
+ "darwin-386",
+ "darwin-amd64",
+ "freebsd-386",
+ "freebsd-amd64",
+ "linux-386",
+ "linux-amd64",
+ "windows-386",
+ "windows-amd64",
+}
+
+const timeout = time.Hour
+
+var preBuildCleanFiles = []string{
+ ".gitattributes",
+ ".gitignore",
+ ".hgignore",
+ ".hgtags",
+ "misc/dashboard",
+}
+
+var postBuildCleanFiles = []string{
+ "VERSION.cache",
+}
+
+func main() {
+ flag.Parse()
+
+ if *revision == "" {
+ log.Fatal("must specify -rev flag")
+ }
+
+ var wg sync.WaitGroup
+ for _, name := range builders {
+ b, ok := dashboard.Builders[name]
+ if !ok {
+ log.Printf("unknown builder %q", name)
+ continue
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if err := makeRelease(b); err != nil {
+ log.Printf("makeRelease(%q): %v", b.Name, err)
+ }
+ }()
+ }
+ // TODO(adg): show progress of running builders
+ wg.Wait()
+}
+
+func makeRelease(bc dashboard.BuildConfig) (err error) {
+ // Start VM
+ keypair, err := buildlet.NewKeyPair()
+ if err != nil {
+ return err
+ }
+ instance := fmt.Sprintf("release-%v-%v-rn%v", os.Getenv("USER"), bc.Name, randHex(6))
+ client, err := buildlet.StartNewVM(projTokenSource(), instance, bc.Name, buildlet.VMOpts{
+ Zone: *zone,
+ ProjectID: *project,
+ TLS: keypair,
+ DeleteIn: timeout,
+ Description: fmt.Sprintf("release buildlet for %s", os.Getenv("USER")),
+ OnInstanceRequested: func() {
+ log.Printf("%v: Sent create request. Waiting for operation.", instance)
+ },
+ OnInstanceCreated: func() {
+ log.Printf("%v: Instance created.", instance)
+ },
+ })
+ if err != nil {
+ return err
+ }
+ log.Printf("%v: Instance up.", instance)
+
+ defer func() {
+ log.Printf("%v: Destroying VM.", instance)
+ haltAndDestroy(client, instance)
+ }()
+
+ // Push source to VM
+ const dir = "go"
+ tar := "https://go.googlesource.com/go/+archive/" + *revision + ".tar.gz"
+ if err := client.PutTarFromURL(tar, dir); err != nil {
+ return err
+ }
+ log.Printf("%v: Pushed source to VM.", instance)
+
+ if err := client.RemoveAll(preBuildCleanFiles...); err != nil {
+ return err
+ }
+ log.Printf("%v: Cleaned repo (pre-build).", instance)
+
+ // Execute build
+ out := new(bytes.Buffer)
+ mk := filepath.Join(dir, bc.MakeScript())
+ remoteErr, err := client.Exec(mk, buildlet.ExecOpts{
+ Output: out,
+ ExtraEnv: []string{"GOROOT_FINAL=" + bc.GorootFinal()},
+ })
+ if err != nil {
+ return err
+ }
+ if remoteErr != nil {
+ // TODO(adg): write log to file instead?
+ return fmt.Errorf("Build failed: %v\nOutput:\n%v", remoteErr, out)
+ }
+ log.Printf("%v: Build complete.", instance)
+
+ // TODO: build race-enabled tool chain
+
+ // TODO: check out tools
+ // TODO: build godoc, vet, cover
+
+ // TODO: check out blog, add to misc
+ // TODO: check out tour, add to misc
+
+ if err := client.RemoveAll(postBuildCleanFiles...); err != nil {
+ return err
+ }
+ log.Printf("%v: Cleaned repo (post-build).", instance)
+
+ // Download tarball
+ tgz, err := client.GetTar(dir)
+ if err != nil {
+ return err
+ }
+ filename := "go-version-" + bc.Name + ".tar.gz"
+ f, err := os.Create(filename)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(f, tgz); err != nil {
+ f.Close()
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ log.Printf("%v: Wrote %q.", instance, filename)
+
+ return nil
+}
+
+func haltAndDestroy(bc *buildlet.Client, instance string) {
+ // TODO(adg): move this to buildlet library.
+
+ // Ask buildlet to kill itself, and tell GCE to kill it too.
+ gceErrc := make(chan error, 1)
+ buildletErrc := make(chan error, 1)
+ go func() {
+ gceErrc <- buildlet.DestroyVM(projTokenSource(), *project, *zone, instance)
+ }()
+ go func() {
+ buildletErrc <- bc.Destroy()
+ }()
+ timeout := time.NewTimer(5 * time.Second)
+ defer timeout.Stop()
+
+ var gceDone, buildletDone bool
+ for !gceDone || !buildletDone {
+ select {
+ case err := <-gceErrc:
+ if err != nil {
+ log.Printf("%v: GCE: %v", instance, err)
+ } else {
+ log.Printf("%v: Requested GCE delete.", instance)
+ }
+ gceDone = true
+ case err := <-buildletErrc:
+ if err != nil {
+ log.Printf("%v: Buildlet: %v", instance, err)
+ } else {
+ log.Printf("%v: Requested buildlet to shut down.", instance)
+ }
+ buildletDone = true
+ case <-timeout.C:
+ if !buildletDone {
+ log.Printf("%v: timeout asking buildlet to shut down", instance)
+ }
+ if !gceDone {
+ log.Printf("%v: timeout asking GCE to delete builder VM", instance)
+ }
+ }
+ }
+}
+
+func projTokenSource() oauth2.TokenSource {
+ ts, err := auth.ProjectTokenSource(*project, compute.ComputeScope)
+ if err != nil {
+ log.Fatalf("Failed to get OAuth2 token source for project %s: %v", *project, err)
+ }
+ return ts
+}
+
+func randHex(n int) string {
+ buf := make([]byte, n/2)
+ _, err := rand.Read(buf)
+ if err != nil {
+ panic("Failed to get randomness: " + err.Error())
+ }
+ return fmt.Sprintf("%x", buf)
+}