mirror of
https://github.com/golang/go
synced 2024-11-19 02:04:42 -07:00
12c3afc1ae
In the new world, one builder runs gobuilder -commit which uploads information about commits to the dashboard, which then hands the work out to the builders by hash. There is no assumption anymore that the commit numbers are consistent across builders. New builders will need to be deployed. For now darwin-amd64 is running the new builder to test the code. The new JSON-based protocol for handing out work via /todo should be easy to extend if we want to add support for sending trial CLs to the builders. This code is already running on godashboard.appspot.com. R=adg, dave CC=golang-dev https://golang.org/cl/4519047
573 lines
13 KiB
Go
573 lines
13 KiB
Go
// Copyright 2011 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 (
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"xml"
|
|
)
|
|
|
|
const (
|
|
codeProject = "go"
|
|
codePyScript = "misc/dashboard/googlecode_upload.py"
|
|
hgUrl = "https://go.googlecode.com/hg/"
|
|
waitInterval = 30e9 // time to wait before checking for new revs
|
|
mkdirPerm = 0750
|
|
pkgBuildInterval = 1e9 * 60 * 60 * 24 // rebuild packages every 24 hours
|
|
)
|
|
|
|
// These variables are copied from the gobuilder's environment
|
|
// to the envv of its subprocesses.
|
|
var extraEnv = []string{
|
|
"GOHOSTOS",
|
|
"GOHOSTARCH",
|
|
"PATH",
|
|
"DISABLE_NET_TESTS",
|
|
"MAKEFLAGS",
|
|
"GOARM",
|
|
}
|
|
|
|
type Builder struct {
|
|
name string
|
|
goos, goarch string
|
|
key string
|
|
codeUsername string
|
|
codePassword string
|
|
}
|
|
|
|
var (
|
|
buildroot = flag.String("buildroot", path.Join(os.TempDir(), "gobuilder"), "Directory under which to build")
|
|
commitFlag = flag.Bool("commit", false, "upload information about new commits")
|
|
dashboard = flag.String("dashboard", "godashboard.appspot.com", "Go Dashboard Host")
|
|
buildRelease = flag.Bool("release", false, "Build and upload binary release archives")
|
|
buildRevision = flag.String("rev", "", "Build specified revision and exit")
|
|
buildCmd = flag.String("cmd", "./all.bash", "Build command (specify absolute or relative to go/src/)")
|
|
external = flag.Bool("external", false, "Build external packages")
|
|
parallel = flag.Bool("parallel", false, "Build multiple targets in parallel")
|
|
verbose = flag.Bool("v", false, "verbose")
|
|
)
|
|
|
|
var (
|
|
goroot string
|
|
releaseRegexp = regexp.MustCompile(`^(release|weekly)\.[0-9\-.]+`)
|
|
)
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "usage: %s goos-goarch...\n", os.Args[0])
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
flag.Parse()
|
|
if len(flag.Args()) == 0 && !*commitFlag {
|
|
flag.Usage()
|
|
}
|
|
goroot = path.Join(*buildroot, "goroot")
|
|
builders := make([]*Builder, len(flag.Args()))
|
|
for i, builder := range flag.Args() {
|
|
b, err := NewBuilder(builder)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
builders[i] = b
|
|
}
|
|
|
|
// set up work environment
|
|
if err := os.RemoveAll(*buildroot); err != nil {
|
|
log.Fatalf("Error removing build root (%s): %s", *buildroot, err)
|
|
}
|
|
if err := os.Mkdir(*buildroot, mkdirPerm); err != nil {
|
|
log.Fatalf("Error making build root (%s): %s", *buildroot, err)
|
|
}
|
|
if err := run(nil, *buildroot, "hg", "clone", hgUrl, goroot); err != nil {
|
|
log.Fatal("Error cloning repository:", err)
|
|
}
|
|
|
|
if *commitFlag {
|
|
if len(flag.Args()) == 0 {
|
|
commitWatcher()
|
|
return
|
|
}
|
|
go commitWatcher()
|
|
}
|
|
|
|
// if specified, build revision and return
|
|
if *buildRevision != "" {
|
|
hash, err := fullHash(*buildRevision)
|
|
if err != nil {
|
|
log.Fatal("Error finding revision: ", err)
|
|
}
|
|
for _, b := range builders {
|
|
if err := b.buildHash(hash); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// external package build mode
|
|
if *external {
|
|
if len(builders) != 1 {
|
|
log.Fatal("only one goos-goarch should be specified with -external")
|
|
}
|
|
builders[0].buildExternal()
|
|
}
|
|
|
|
// go continuous build mode (default)
|
|
// check for new commits and build them
|
|
for {
|
|
err := run(nil, goroot, "hg", "pull", "-u")
|
|
if err != nil {
|
|
log.Println("hg pull failed:", err)
|
|
time.Sleep(waitInterval)
|
|
continue
|
|
}
|
|
built := false
|
|
t := time.Nanoseconds()
|
|
if *parallel {
|
|
done := make(chan bool)
|
|
for _, b := range builders {
|
|
go func(b *Builder) {
|
|
done <- b.build()
|
|
}(b)
|
|
}
|
|
for _ = range builders {
|
|
built = <-done || built
|
|
}
|
|
} else {
|
|
for _, b := range builders {
|
|
built = b.build() || built
|
|
}
|
|
}
|
|
// sleep if there was nothing to build
|
|
if !built {
|
|
time.Sleep(waitInterval)
|
|
}
|
|
// sleep if we're looping too fast.
|
|
t1 := time.Nanoseconds() - t
|
|
if t1 < waitInterval {
|
|
time.Sleep(waitInterval - t1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewBuilder(builder string) (*Builder, os.Error) {
|
|
b := &Builder{name: builder}
|
|
|
|
// get goos/goarch from builder string
|
|
s := strings.Split(builder, "-", 3)
|
|
if len(s) >= 2 {
|
|
b.goos, b.goarch = s[0], s[1]
|
|
} else {
|
|
return nil, fmt.Errorf("unsupported builder form: %s", builder)
|
|
}
|
|
|
|
// read keys from keyfile
|
|
fn := path.Join(os.Getenv("HOME"), ".gobuildkey")
|
|
if s := fn + "-" + b.name; isFile(s) { // builder-specific file
|
|
fn = s
|
|
}
|
|
c, err := ioutil.ReadFile(fn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("readKeys %s (%s): %s", b.name, fn, err)
|
|
}
|
|
v := strings.Split(string(c), "\n", -1)
|
|
b.key = v[0]
|
|
if len(v) >= 3 {
|
|
b.codeUsername, b.codePassword = v[1], v[2]
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// buildExternal downloads and builds external packages, and
|
|
// reports their build status to the dashboard.
|
|
// It will re-build all packages after pkgBuildInterval nanoseconds or
|
|
// a new release tag is found.
|
|
func (b *Builder) buildExternal() {
|
|
var prevTag string
|
|
var nextBuild int64
|
|
for {
|
|
time.Sleep(waitInterval)
|
|
err := run(nil, goroot, "hg", "pull", "-u")
|
|
if err != nil {
|
|
log.Println("hg pull failed:", err)
|
|
continue
|
|
}
|
|
hash, tag, err := firstTag(releaseRegexp)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
if *verbose {
|
|
log.Println("latest release:", tag)
|
|
}
|
|
// don't rebuild if there's no new release
|
|
// and it's been less than pkgBuildInterval
|
|
// nanoseconds since the last build.
|
|
if tag == prevTag && time.Nanoseconds() < nextBuild {
|
|
continue
|
|
}
|
|
// build will also build the packages
|
|
if err := b.buildHash(hash); err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
prevTag = tag
|
|
nextBuild = time.Nanoseconds() + pkgBuildInterval
|
|
}
|
|
}
|
|
|
|
// build checks for a new commit for this builder
|
|
// and builds it if one is found.
|
|
// It returns true if a build was attempted.
|
|
func (b *Builder) build() bool {
|
|
defer func() {
|
|
err := recover()
|
|
if err != nil {
|
|
log.Println(b.name, "build:", err)
|
|
}
|
|
}()
|
|
hash, err := b.todo()
|
|
if err != nil {
|
|
log.Println(err)
|
|
return false
|
|
}
|
|
if hash == "" {
|
|
return false
|
|
}
|
|
err = b.buildHash(hash)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (b *Builder) buildHash(hash string) (err os.Error) {
|
|
defer func() {
|
|
if err != nil {
|
|
err = fmt.Errorf("%s build: %s: %s", b.name, hash, err)
|
|
}
|
|
}()
|
|
|
|
log.Println(b.name, "building", hash)
|
|
|
|
// create place in which to do work
|
|
workpath := path.Join(*buildroot, b.name+"-"+hash[:12])
|
|
err = os.Mkdir(workpath, mkdirPerm)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer os.RemoveAll(workpath)
|
|
|
|
// clone repo
|
|
err = run(nil, workpath, "hg", "clone", goroot, "go")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// update to specified revision
|
|
err = run(nil, path.Join(workpath, "go"),
|
|
"hg", "update", hash)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
srcDir := path.Join(workpath, "go", "src")
|
|
|
|
// build
|
|
logfile := path.Join(workpath, "build.log")
|
|
buildLog, status, err := runLog(b.envv(), logfile, srcDir, *buildCmd)
|
|
if err != nil {
|
|
return fmt.Errorf("all.bash: %s", err)
|
|
}
|
|
|
|
// if we're in external mode, build all packages and return
|
|
if *external {
|
|
if status != 0 {
|
|
return os.NewError("go build failed")
|
|
}
|
|
return b.buildPackages(workpath, hash)
|
|
}
|
|
|
|
if status != 0 {
|
|
// record failure
|
|
return b.recordResult(buildLog, hash)
|
|
}
|
|
|
|
// record success
|
|
if err = b.recordResult("", hash); err != nil {
|
|
return fmt.Errorf("recordResult: %s", err)
|
|
}
|
|
|
|
// finish here if codeUsername and codePassword aren't set
|
|
if b.codeUsername == "" || b.codePassword == "" || !*buildRelease {
|
|
return
|
|
}
|
|
|
|
// if this is a release, create tgz and upload to google code
|
|
releaseHash, release, err := firstTag(releaseRegexp)
|
|
if hash == releaseHash {
|
|
// clean out build state
|
|
err = run(b.envv(), srcDir, "./clean.bash", "--nopkg")
|
|
if err != nil {
|
|
return fmt.Errorf("clean.bash: %s", err)
|
|
}
|
|
// upload binary release
|
|
fn := fmt.Sprintf("go.%s.%s-%s.tar.gz", release, b.goos, b.goarch)
|
|
err = run(nil, workpath, "tar", "czf", fn, "go")
|
|
if err != nil {
|
|
return fmt.Errorf("tar: %s", err)
|
|
}
|
|
err = run(nil, workpath, path.Join(goroot, codePyScript),
|
|
"-s", release,
|
|
"-p", codeProject,
|
|
"-u", b.codeUsername,
|
|
"-w", b.codePassword,
|
|
"-l", fmt.Sprintf("%s,%s", b.goos, b.goarch),
|
|
fn)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// envv returns an environment for build/bench execution
|
|
func (b *Builder) envv() []string {
|
|
e := []string{
|
|
"GOOS=" + b.goos,
|
|
"GOARCH=" + b.goarch,
|
|
"GOROOT_FINAL=/usr/local/go",
|
|
}
|
|
for _, k := range extraEnv {
|
|
e = append(e, k+"="+os.Getenv(k))
|
|
}
|
|
return e
|
|
}
|
|
|
|
func isDirectory(name string) bool {
|
|
s, err := os.Stat(name)
|
|
return err == nil && s.IsDirectory()
|
|
}
|
|
|
|
func isFile(name string) bool {
|
|
s, err := os.Stat(name)
|
|
return err == nil && (s.IsRegular() || s.IsSymlink())
|
|
}
|
|
|
|
// commitWatcher polls hg for new commits and tells the dashboard about them.
|
|
func commitWatcher() {
|
|
// Create builder just to get master key.
|
|
b, err := NewBuilder("mercurial-commit")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
for {
|
|
if *verbose {
|
|
log.Printf("poll...")
|
|
}
|
|
commitPoll(b.key)
|
|
if *verbose {
|
|
log.Printf("sleep...")
|
|
}
|
|
time.Sleep(60e9)
|
|
}
|
|
}
|
|
|
|
// HgLog represents a single Mercurial revision.
|
|
type HgLog struct {
|
|
Hash string
|
|
Author string
|
|
Date string
|
|
Desc string
|
|
Parent string
|
|
|
|
// Internal metadata
|
|
added bool
|
|
}
|
|
|
|
// logByHash is a cache of all Mercurial revisions we know about,
|
|
// indexed by full hash.
|
|
var logByHash = map[string]*HgLog{}
|
|
|
|
// xmlLogTemplate is a template to pass to Mercurial to make
|
|
// hg log print the log in valid XML for parsing with xml.Unmarshal.
|
|
const xmlLogTemplate = `
|
|
<log>
|
|
<hash>{node|escape}</hash>
|
|
<parent>{parent|escape}</parent>
|
|
<author>{author|escape}</author>
|
|
<date>{date}</date>
|
|
<desc>{desc|escape}</desc>
|
|
</log>
|
|
`
|
|
|
|
// commitPoll pulls any new revisions from the hg server
|
|
// and tells the server about them.
|
|
func commitPoll(key string) {
|
|
// Catch unexpected panics.
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
log.Printf("commitPoll panic: %s", err)
|
|
}
|
|
}()
|
|
|
|
if err := run(nil, goroot, "hg", "pull"); err != nil {
|
|
log.Printf("hg pull: %v", err)
|
|
return
|
|
}
|
|
|
|
const N = 20 // how many revisions to grab
|
|
|
|
data, _, err := runLog(nil, "", goroot, "hg", "log",
|
|
"--encoding=utf-8",
|
|
"--limit=" + strconv.Itoa(N),
|
|
"--template=" + xmlLogTemplate,
|
|
)
|
|
if err != nil {
|
|
log.Printf("hg log: %v", err)
|
|
return
|
|
}
|
|
|
|
var logStruct struct {
|
|
Log []HgLog
|
|
}
|
|
err = xml.Unmarshal(strings.NewReader("<top>" + data + "</top>"), &logStruct)
|
|
if err != nil {
|
|
log.Printf("unmarshal hg log: %v", err)
|
|
return
|
|
}
|
|
|
|
logs := logStruct.Log
|
|
|
|
// Pass 1. Fill in parents and add new log entries to logsByHash.
|
|
// Empty parent means take parent from next log entry.
|
|
// Non-empty parent has form 1234:hashhashhash; we weant full hash.
|
|
for i := range logs {
|
|
l := &logs[i]
|
|
log.Printf("hg log: %s < %s\n", l.Hash, l.Parent)
|
|
if l.Parent == "" && i+1 < len(logs) {
|
|
l.Parent = logs[i+1].Hash
|
|
} else if l.Parent != "" {
|
|
l.Parent, _ = fullHash(l.Parent)
|
|
}
|
|
if l.Parent == "" {
|
|
// Can't create node without parent.
|
|
continue
|
|
}
|
|
|
|
if logByHash[l.Hash] == nil {
|
|
// Make copy to avoid pinning entire slice when only one entry is new.
|
|
t := *l
|
|
logByHash[t.Hash] = &t
|
|
}
|
|
}
|
|
|
|
for i := range logs {
|
|
l := &logs[i]
|
|
if l.Parent == "" {
|
|
continue
|
|
}
|
|
addCommit(l.Hash, key)
|
|
}
|
|
}
|
|
|
|
// addCommit adds the commit with the named hash to the dashboard.
|
|
// key is the secret key for authentication to the dashboard.
|
|
// It avoids duplicate effort.
|
|
func addCommit(hash, key string) bool {
|
|
l := logByHash[hash]
|
|
if l == nil {
|
|
return false
|
|
}
|
|
if l.added {
|
|
return true
|
|
}
|
|
|
|
// Check for already added, perhaps in an earlier run.
|
|
if dashboardCommit(hash) {
|
|
log.Printf("%s already on dashboard\n", hash)
|
|
// Record that this hash is on the dashboard,
|
|
// as must be all its parents.
|
|
for l != nil {
|
|
l.added = true
|
|
l = logByHash[l.Parent]
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Create parent first, to maintain some semblance of order.
|
|
if !addCommit(l.Parent, key) {
|
|
return false
|
|
}
|
|
|
|
// Create commit.
|
|
if err := postCommit(key, l); err != nil {
|
|
log.Printf("faield to add %s to dashboard: %v", err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// fullHash returns the full hash for the given Mercurial revision.
|
|
func fullHash(rev string) (hash string, err os.Error) {
|
|
defer func() {
|
|
if err != nil {
|
|
err = fmt.Errorf("fullHash: %s: %s", rev, err)
|
|
}
|
|
}()
|
|
s, _, err := runLog(nil, "", goroot,
|
|
"hg", "log",
|
|
"--encoding=utf-8",
|
|
"--rev="+rev,
|
|
"--limit=1",
|
|
"--template={node}",
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return "", fmt.Errorf("cannot find revision")
|
|
}
|
|
if len(s) != 20 {
|
|
return "", fmt.Errorf("hg returned invalid hash " + s)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
var revisionRe = regexp.MustCompile(`^([^ ]+) +[0-9]+:([0-9a-f]+)$`)
|
|
|
|
// firstTag returns the hash and tag of the most recent tag matching re.
|
|
func firstTag(re *regexp.Regexp) (hash string, tag string, err os.Error) {
|
|
o, _, err := runLog(nil, "", goroot, "hg", "tags")
|
|
for _, l := range strings.Split(o, "\n", -1) {
|
|
if l == "" {
|
|
continue
|
|
}
|
|
s := revisionRe.FindStringSubmatch(l)
|
|
if s == nil {
|
|
err = os.NewError("couldn't find revision number")
|
|
return
|
|
}
|
|
if !re.MatchString(s[1]) {
|
|
continue
|
|
}
|
|
tag = s[1]
|
|
hash, err = fullHash(s[3])
|
|
return
|
|
}
|
|
err = os.NewError("no matching tag found")
|
|
return
|
|
}
|