blob: bff3b2cbb5e4f836932c774e17b049c580b49f80 [file] [log] [blame]
// 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.
// TODO(adg): add flag so that we can choose to run make.bash only
// Command release builds a Go release.
package main
import (
"archive/tar"
"archive/zip"
"bufio"
"bytes"
"compress/gzip"
"context"
"flag"
"fmt"
gobuild "go/build"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"golang.org/x/build"
"golang.org/x/build/buildenv"
"golang.org/x/build/buildlet"
"golang.org/x/build/dashboard"
)
var (
target = flag.String("target", "", "If specified, build specific target platform ('linux-amd64')")
watch = flag.Bool("watch", false, "Watch the build. Only compatible with -target")
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)")
user = flag.String("user", username(), "coordinator username, appended to 'user-'")
skipTests = flag.Bool("skip_tests", false, "skip tests; run make.bash instead of all.bash (only use if you ran trybots first)")
uploadMode = flag.Bool("upload", false, "Upload files (exclusive to all other flags)")
)
var (
coordClient *buildlet.CoordinatorClient
buildEnv *buildenv.Environment
)
func main() {
flag.Parse()
if *uploadMode {
if err := upload(flag.Args()); err != nil {
log.Fatal(err)
}
return
}
if err := findReleaselet(); err != nil {
log.Fatalf("couldn't find releaselet source: %v", err)
}
if *rev == "" {
log.Fatal("must specify -rev flag")
}
if *toolsRev == "" {
log.Fatal("must specify -tools flag")
}
if *version == "" {
log.Fatal("must specify -version flag")
}
coordClient = coordinatorClient()
buildEnv = buildenv.Production
var wg sync.WaitGroup
matches := 0
for _, b := range builds {
b := b
if *target != "" && b.String() != *target {
continue
}
matches++
b.logf("Start.")
wg.Add(1)
go func() {
defer wg.Done()
if err := b.make(); err != nil {
b.logf("Error: %v", err)
} else {
b.logf("Done.")
}
}()
}
if *target != "" && matches == 0 {
log.Fatalf("no targets matched %q", *target)
}
wg.Wait()
}
var releaselet = "releaselet.go"
func findReleaselet() error {
// First try the working directory.
if _, err := os.Stat(releaselet); err == nil {
return nil
}
// Then, try to locate the release command in the workspace.
const importPath = "golang.org/x/build/cmd/release"
pkg, err := gobuild.Import(importPath, "", gobuild.FindOnly)
if err != nil {
return fmt.Errorf("finding %q: %v", importPath, err)
}
r := filepath.Join(pkg.Dir, releaselet)
if _, err := os.Stat(r); err != nil {
return err
}
releaselet = r
return nil
}
type Build struct {
OS, Arch string
Source bool
Race bool // Build race detector.
Builder string // Key for dashboard.Builders.
Goarm int // GOARM value if set.
MakeOnly bool // don't run tests; needed by cross-compile builders (s390x)
}
func (b *Build) String() string {
if b.Source {
return "src"
}
if b.Goarm != 0 {
return fmt.Sprintf("%v-%vv%vl", b.OS, b.Arch, b.Goarm)
}
return fmt.Sprintf("%v-%v", b.OS, b.Arch)
}
func (b *Build) toolDir() string { return "go/pkg/tool/" + b.OS + "_" + b.Arch }
func (b *Build) logf(format string, args ...interface{}) {
format = fmt.Sprintf("%v: %s", b, format)
log.Printf(format, args...)
}
var builds = []*Build{
{
Source: true,
Builder: "linux-amd64",
},
{
OS: "linux",
Arch: "386",
Builder: "linux-386",
},
{
OS: "linux",
Arch: "arm",
Builder: "linux-arm",
Goarm: 6, // for compatibility with all Raspberry Pi models.
},
{
OS: "linux",
Arch: "amd64",
Race: true,
Builder: "linux-amd64",
},
{
OS: "linux",
Arch: "arm64",
Builder: "linux-arm64-packet",
},
{
OS: "freebsd",
Arch: "386",
Builder: "freebsd-386-gce101",
},
{
OS: "freebsd",
Arch: "amd64",
Race: true,
Builder: "freebsd-amd64-gce101",
},
{
OS: "windows",
Arch: "386",
Builder: "windows-386-gce",
},
{
OS: "windows",
Arch: "amd64",
Race: true,
Builder: "windows-amd64-2008",
},
{
OS: "darwin",
Arch: "amd64",
Race: true,
Builder: "darwin-amd64-10_11",
},
{
OS: "linux",
Arch: "s390x",
MakeOnly: true,
Builder: "linux-s390x-crosscompile",
},
// TODO(bradfitz): switch this ppc64 builder to a Kubernetes
// container cross-compiling ppc64 like the s390x one? For
// now, the ppc64le builders (5) are back, so let's see if we
// can just depend on them not going away.
{
OS: "linux",
Arch: "ppc64le",
MakeOnly: true,
Builder: "linux-ppc64le-buildlet",
},
}
const (
toolsRepo = "golang.org/x/tools"
blogRepo = "golang.org/x/blog"
tourRepo = "golang.org/x/tour"
)
var toolPaths = []string{
"golang.org/x/tools/cmd/godoc",
"golang.org/x/tour/gotour",
}
var preBuildCleanFiles = []string{
".gitattributes",
".github",
".gitignore",
".hgignore",
".hgtags",
"misc/dashboard",
"misc/makerelease",
}
var postBuildCleanFiles = []string{
"VERSION.cache",
"pkg/bootstrap",
}
func (b *Build) buildlet() (*buildlet.Client, error) {
b.logf("Creating buildlet.")
bc, err := coordClient.CreateBuildlet(b.Builder)
if err != nil {
return nil, err
}
bc.SetReleaseMode(true) // disable pargzip; golang.org/issue/19052
return bc, nil
}
func (b *Build) make() error {
bc, ok := dashboard.Builders[b.Builder]
if !ok {
return fmt.Errorf("unknown builder: %v", bc)
}
var hostArch string // non-empty if we're cross-compiling (s390x)
if b.MakeOnly && bc.IsKube() && (bc.GOARCH() != "amd64" && bc.GOARCH() != "386") {
hostArch = "amd64"
}
client, err := b.buildlet()
if err != nil {
return err
}
defer client.Close()
work, err := client.WorkDir()
if err != nil {
return err
}
// Push source to buildlet
b.logf("Pushing source to buildlet.")
const (
goDir = "go"
goPath = "gopath"
go14 = "go1.4"
)
for _, r := range []struct {
repo, rev string
}{
{"go", *rev},
{"tools", *toolsRev},
{"blog", *blogRev},
{"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
}
tar := "https://go.googlesource.com/" + r.repo + "/+archive/" + r.rev + ".tar.gz"
if err := client.PutTarFromURL(tar, dir); err != nil {
b.logf("failed to put tarball %q into dir %q: %v", tar, dir, err)
return err
}
}
if u := bc.GoBootstrapURL(buildEnv); u != "" && !b.Source {
b.logf("Installing go1.4.")
if err := client.PutTarFromURL(u, go14); err != nil {
return err
}
}
// Write out version file.
b.logf("Writing VERSION file.")
if err := client.Put(strings.NewReader(*version), "go/VERSION", 0644); err != nil {
return err
}
b.logf("Cleaning goroot (pre-build).")
if err := client.RemoveAll(addPrefix(goDir, preBuildCleanFiles)...); err != nil {
return err
}
if b.Source {
b.logf("Skipping build.")
return b.fetchTarball(client)
}
// Set up build environment.
sep := "/"
if b.OS == "windows" {
sep = "\\"
}
env := append(bc.Env(),
"GOROOT_FINAL="+bc.GorootFinal(),
"GOROOT="+work+sep+goDir,
"GOPATH="+work+sep+goPath,
"GOBIN=",
)
if b.Goarm > 0 {
env = append(env, fmt.Sprintf("GOARM=%d", b.Goarm))
env = append(env, fmt.Sprintf("CGO_CFLAGS=-march=armv%d", b.Goarm))
env = append(env, fmt.Sprintf("CGO_LDFLAGS=-march=armv%d", b.Goarm))
}
// Execute build
b.logf("Building.")
out := new(bytes.Buffer)
script := bc.AllScript()
scriptArgs := bc.AllScriptArgs()
if *skipTests || b.MakeOnly {
script = bc.MakeScript()
scriptArgs = bc.MakeScriptArgs()
}
all := filepath.Join(goDir, script)
var execOut io.Writer = out
if *watch && *target != "" {
execOut = io.MultiWriter(out, os.Stdout)
}
remoteErr, err := client.Exec(all, buildlet.ExecOpts{
Output: execOut,
ExtraEnv: env,
Args: scriptArgs,
})
if err != nil {
return err
}
if remoteErr != nil {
return fmt.Errorf("Build failed: %v\nOutput:\n%v", remoteErr, out)
}
goCmd := path.Join(goDir, "bin/go")
if b.OS == "windows" {
goCmd += ".exe"
}
runGo := func(args ...string) error {
out := new(bytes.Buffer)
var execOut io.Writer = out
if *watch && *target != "" {
execOut = io.MultiWriter(out, os.Stdout)
}
cmdEnv := append([]string(nil), env...)
if len(args) > 0 && args[0] == "run" && hostArch != "" {
cmdEnv = setGOARCH(cmdEnv, hostArch)
}
remoteErr, err := client.Exec(goCmd, buildlet.ExecOpts{
Output: execOut,
Dir: ".", // root of buildlet work directory
Args: args,
ExtraEnv: cmdEnv,
})
if err != nil {
return err
}
if remoteErr != nil {
return fmt.Errorf("go %v: %v\n%s", strings.Join(args, " "), remoteErr, out)
}
return nil
}
if b.Race {
b.logf("Building race detector.")
if err := runGo("install", "-race", "std"); err != nil {
return err
}
}
b.logf("Building %v.", strings.Join(toolPaths, ", "))
if err := runGo(append([]string{"install"}, toolPaths...)...); err != nil {
return err
}
b.logf("Cleaning goroot (post-build).")
if err := client.RemoveAll(addPrefix(goDir, postBuildCleanFiles)...); err != nil {
return err
}
if err := client.RemoveAll(b.toolDir() + "/api"); err != nil {
return err
}
b.logf("Pushing and running releaselet.")
f, err := os.Open(releaselet)
if err != nil {
return err
}
err = client.Put(f, "releaselet.go", 0666)
f.Close()
if err != nil {
return err
}
if err := runGo("run", "releaselet.go"); err != nil {
log.Printf("releaselet failed: %v", err)
client.ListDir(".", buildlet.ListDirOpts{Recursive: true}, func(ent buildlet.DirEntry) {
log.Printf("remote: %v", ent)
})
return err
}
cleanFiles := []string{"releaselet.go", goPath, go14}
switch b.OS {
case "darwin":
filename := *version + "." + b.String() + ".pkg"
if err := b.fetchFile(client, filename, "pkg"); err != nil {
return err
}
cleanFiles = append(cleanFiles, "pkg")
case "windows":
filename := *version + "." + b.String() + ".msi"
if err := b.fetchFile(client, filename, "msi"); err != nil {
return err
}
cleanFiles = append(cleanFiles, "msi")
}
// Need to delete everything except the final "go" directory,
// as we make the tarball relative to workdir.
b.logf("Cleaning workdir.")
if err := client.RemoveAll(cleanFiles...); err != nil {
return err
}
if b.OS == "windows" {
return b.fetchZip(client)
}
return b.fetchTarball(client)
}
func (b *Build) fetchTarball(client *buildlet.Client) error {
b.logf("Downloading tarball.")
tgz, err := client.GetTar(context.Background(), ".")
if err != nil {
return err
}
filename := *version + "." + b.String() + ".tar.gz"
return b.writeFile(filename, tgz)
}
func (b *Build) fetchZip(client *buildlet.Client) error {
b.logf("Downloading tarball and re-compressing as zip.")
tgz, err := client.GetTar(context.Background(), ".")
if err != nil {
return err
}
defer tgz.Close()
filename := *version + "." + b.String() + ".zip"
f, err := os.Create(filename)
if err != nil {
return err
}
if err := tgzToZip(f, tgz); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
b.logf("Wrote %q.", filename)
return nil
}
func tgzToZip(w io.Writer, r io.Reader) error {
zr, err := gzip.NewReader(r)
if err != nil {
return err
}
tr := tar.NewReader(zr)
zw := zip.NewWriter(w)
for {
th, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
fi := th.FileInfo()
zh, err := zip.FileInfoHeader(fi)
if err != nil {
return err
}
zh.Name = th.Name // for the full path
switch strings.ToLower(path.Ext(zh.Name)) {
case ".jpg", ".jpeg", ".png", ".gif":
// Don't re-compress already compressed files.
zh.Method = zip.Store
default:
zh.Method = zip.Deflate
}
if fi.IsDir() {
zh.Method = zip.Store
}
w, err := zw.CreateHeader(zh)
if err != nil {
return err
}
if fi.IsDir() {
continue
}
if _, err := io.Copy(w, tr); err != nil {
return err
}
}
return zw.Close()
}
// 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(context.Background(), dir)
if err != nil {
return err
}
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
}
if strings.HasSuffix(name, ".gz") {
if err := verifyGzipSingleStream(name); err != nil {
return fmt.Errorf("error verifying that %s is a single-stream gzip: %v", name, err)
}
}
b.logf("Wrote %q.", name)
return nil
}
// verifyGzipSingleStream verifies that the named gzip file is not
// a multi-stream file. See golang.org/issue/19052
func verifyGzipSingleStream(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
br := bufio.NewReader(f)
zr, err := gzip.NewReader(br)
if err != nil {
return err
}
zr.Multistream(false)
if _, err := io.Copy(ioutil.Discard, zr); err != nil {
return fmt.Errorf("reading first stream: %v", err)
}
peek, err := br.Peek(1)
if len(peek) > 0 || err != io.EOF {
return fmt.Errorf("unexpected peek of %d, %v after first gzip stream", len(peek), err)
}
return nil
}
func addPrefix(prefix string, in []string) []string {
var out []string
for _, s := range in {
out = append(out, path.Join(prefix, s))
}
return out
}
func coordinatorClient() *buildlet.CoordinatorClient {
return &buildlet.CoordinatorClient{
Auth: buildlet.UserPass{
Username: "user-" + *user,
Password: userToken(),
},
Instance: build.ProdCoordinator,
}
}
func homeDir() string {
if runtime.GOOS == "windows" {
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
}
return os.Getenv("HOME")
}
func configDir() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("APPDATA"), "Gomote")
}
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "gomote")
}
return filepath.Join(homeDir(), ".config", "gomote")
}
func username() string {
if runtime.GOOS == "windows" {
return os.Getenv("USERNAME")
}
return os.Getenv("USER")
}
func userToken() string {
if *user == "" {
panic("userToken called with user flag empty")
}
keyDir := configDir()
baseFile := "user-" + *user + ".token"
tokenFile := filepath.Join(keyDir, baseFile)
slurp, err := ioutil.ReadFile(tokenFile)
if os.IsNotExist(err) {
log.Printf("Missing file %s for user %q. Change --user or obtain a token and place it there.",
tokenFile, *user)
}
if err != nil {
log.Fatal(err)
}
return strings.TrimSpace(string(slurp))
}
func setGOARCH(env []string, goarch string) []string {
wantKV := "GOARCH=" + goarch
existing := false
for i, kv := range env {
if strings.HasPrefix(kv, "GOARCH=") && kv != wantKV {
env[i] = wantKV
existing = true
}
}
if existing {
return env
}
return append(env, wantKV)
}