summaryrefslogtreecommitdiff
path: root/misc/dashboard/builder/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'misc/dashboard/builder/main.go')
-rw-r--r--misc/dashboard/builder/main.go406
1 files changed, 294 insertions, 112 deletions
diff --git a/misc/dashboard/builder/main.go b/misc/dashboard/builder/main.go
index d11cbb133..9377fbe32 100644
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -5,7 +5,6 @@
package main
import (
- "container/vector"
"flag"
"fmt"
"io/ioutil"
@@ -13,16 +12,18 @@ import (
"os"
"path"
"regexp"
+ "runtime"
"strconv"
"strings"
"time"
+ "xml"
)
const (
codeProject = "go"
codePyScript = "misc/dashboard/googlecode_upload.py"
hgUrl = "https://go.googlecode.com/hg/"
- waitInterval = 10e9 // time to wait before checking for new revs
+ waitInterval = 30e9 // time to wait before checking for new revs
mkdirPerm = 0750
pkgBuildInterval = 1e9 * 60 * 60 * 24 // rebuild packages every 24 hours
)
@@ -46,16 +47,10 @@ type Builder struct {
codePassword string
}
-type BenchRequest struct {
- builder *Builder
- commit Commit
- path 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")
- runBenchmarks = flag.Bool("bench", false, "Run benchmarks")
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/)")
@@ -67,7 +62,6 @@ var (
var (
goroot string
releaseRegexp = regexp.MustCompile(`^(release|weekly)\.[0-9\-.]+`)
- benchRequests vector.Vector
)
func main() {
@@ -77,7 +71,7 @@ func main() {
os.Exit(2)
}
flag.Parse()
- if len(flag.Args()) == 0 {
+ if len(flag.Args()) == 0 && !*commitFlag {
flag.Usage()
}
goroot = path.Join(*buildroot, "goroot")
@@ -101,17 +95,24 @@ func main() {
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 != "" {
- c, err := getCommit(*buildRevision)
+ hash, err := fullHash(*buildRevision)
if err != nil {
log.Fatal("Error finding revision: ", err)
}
for _, b := range builders {
- if err := b.buildCommit(c); err != nil {
+ if err := b.buildHash(hash); err != nil {
log.Println(err)
}
- runQueuedBenchmark()
}
return
}
@@ -127,13 +128,8 @@ func main() {
// 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 {
@@ -149,46 +145,15 @@ func main() {
built = b.build() || built
}
}
- // only run benchmarks if we didn't build anything
- // so that they don't hold up the builder queue
+ // sleep if there was nothing to build
if !built {
- if !runQueuedBenchmark() {
- // if we have no benchmarks to do, pause
- time.Sleep(waitInterval)
- }
- // after running one benchmark,
- // continue to find and build new revisions.
+ time.Sleep(waitInterval)
+ }
+ // sleep if we're looping too fast.
+ t1 := time.Nanoseconds() - t
+ if t1 < waitInterval {
+ time.Sleep(waitInterval - t1)
}
- }
-}
-
-func runQueuedBenchmark() bool {
- if benchRequests.Len() == 0 {
- return false
- }
- runBenchmark(benchRequests.Pop().(BenchRequest))
- return true
-}
-
-func runBenchmark(r BenchRequest) {
- // run benchmarks and send to dashboard
- log.Println(r.builder.name, "benchmarking", r.commit.num)
- defer os.RemoveAll(r.path)
- pkg := path.Join(r.path, "go", "src", "pkg")
- bin := path.Join(r.path, "go", "bin")
- env := []string{
- "GOOS=" + r.builder.goos,
- "GOARCH=" + r.builder.goarch,
- "PATH=" + bin + ":" + os.Getenv("PATH"),
- }
- logfile := path.Join(r.path, "bench.log")
- benchLog, _, err := runLog(env, logfile, pkg, "gomake", "bench")
- if err != nil {
- log.Println(r.builder.name, "gomake bench:", err)
- return
- }
- if err = r.builder.recordBenchmarks(benchLog, r.commit); err != nil {
- log.Println("recordBenchmarks:", err)
}
}
@@ -235,7 +200,7 @@ func (b *Builder) buildExternal() {
log.Println("hg pull failed:", err)
continue
}
- c, tag, err := getTag(releaseRegexp)
+ hash, tag, err := firstTag(releaseRegexp)
if err != nil {
log.Println(err)
continue
@@ -249,8 +214,8 @@ func (b *Builder) buildExternal() {
if tag == prevTag && time.Nanoseconds() < nextBuild {
continue
}
- // buildCommit will also build the packages
- if err := b.buildCommit(c); err != nil {
+ // build will also build the packages
+ if err := b.buildHash(hash); err != nil {
log.Println(err)
continue
}
@@ -269,65 +234,46 @@ func (b *Builder) build() bool {
log.Println(b.name, "build:", err)
}
}()
- c, err := b.nextCommit()
+ hash, err := b.todo()
if err != nil {
log.Println(err)
return false
}
- if c == nil {
+ if hash == "" {
return false
}
- err = b.buildCommit(*c)
- if err != nil {
- log.Println(err)
- }
- return true
-}
+ // Look for hash locally before running hg pull.
-// nextCommit returns the next unbuilt Commit for this builder
-func (b *Builder) nextCommit() (nextC *Commit, err os.Error) {
- defer func() {
- if err != nil {
- err = fmt.Errorf("%s nextCommit: %s", b.name, err)
+ if _, err := fullHash(hash[:12]); err != nil {
+ // Don't have hash, so run hg pull.
+ if err := run(nil, goroot, "hg", "pull"); err != nil {
+ log.Println("hg pull failed:", err)
+ return false
}
- }()
- hw, err := b.getHighWater()
- if err != nil {
- return
}
- c, err := getCommit(hw)
+ err = b.buildHash(hash)
if err != nil {
- return
- }
- next := c.num + 1
- c, err = getCommit(strconv.Itoa(next))
- if err == nil && c.num == next {
- return &c, nil
+ log.Println(err)
}
- return nil, nil
+ return true
}
-func (b *Builder) buildCommit(c Commit) (err os.Error) {
+func (b *Builder) buildHash(hash string) (err os.Error) {
defer func() {
if err != nil {
- err = fmt.Errorf("%s buildCommit: %d: %s", b.name, c.num, err)
+ err = fmt.Errorf("%s build: %s: %s", b.name, hash, err)
}
}()
- log.Println(b.name, "building", c.num)
+ log.Println(b.name, "building", hash)
// create place in which to do work
- workpath := path.Join(*buildroot, b.name+"-"+strconv.Itoa(c.num))
+ workpath := path.Join(*buildroot, b.name+"-"+hash[:12])
err = os.Mkdir(workpath, mkdirPerm)
if err != nil {
return
}
- benchRequested := false
- defer func() {
- if !benchRequested {
- os.RemoveAll(workpath)
- }
- }()
+ defer os.RemoveAll(workpath)
// clone repo
err = run(nil, workpath, "hg", "clone", goroot, "go")
@@ -337,7 +283,7 @@ func (b *Builder) buildCommit(c Commit) (err os.Error) {
// update to specified revision
err = run(nil, path.Join(workpath, "go"),
- "hg", "update", "-r", strconv.Itoa(c.num))
+ "hg", "update", hash)
if err != nil {
return
}
@@ -348,7 +294,7 @@ func (b *Builder) buildCommit(c Commit) (err os.Error) {
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)
+ return fmt.Errorf("%s: %s", *buildCmd, err)
}
// if we're in external mode, build all packages and return
@@ -356,36 +302,27 @@ func (b *Builder) buildCommit(c Commit) (err os.Error) {
if status != 0 {
return os.NewError("go build failed")
}
- return b.buildPackages(workpath, c)
+ return b.buildPackages(workpath, hash)
}
if status != 0 {
// record failure
- return b.recordResult(buildLog, c)
+ return b.recordResult(buildLog, hash)
}
// record success
- if err = b.recordResult("", c); err != nil {
+ if err = b.recordResult("", hash); err != nil {
return fmt.Errorf("recordResult: %s", err)
}
- // send benchmark request if benchmarks are enabled
- if *runBenchmarks {
- benchRequests.Insert(0, BenchRequest{
- builder: b,
- commit: c,
- path: workpath,
- })
- benchRequested = true
- }
-
// 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
- if release := releaseRegexp.FindString(c.desc); release != "" {
+ releaseHash, release, err := firstTag(releaseRegexp)
+ if hash == releaseHash {
// clean out build state
err = run(b.envv(), srcDir, "./clean.bash", "--nopkg")
if err != nil {
@@ -411,6 +348,9 @@ func (b *Builder) buildCommit(c Commit) (err os.Error) {
// envv returns an environment for build/bench execution
func (b *Builder) envv() []string {
+ if runtime.GOOS == "windows" {
+ return b.envvWindows()
+ }
e := []string{
"GOOS=" + b.goos,
"GOARCH=" + b.goarch,
@@ -422,6 +362,42 @@ func (b *Builder) envv() []string {
return e
}
+// windows version of envv
+func (b *Builder) envvWindows() []string {
+ start := map[string]string{
+ "GOOS": b.goos,
+ "GOARCH": b.goarch,
+ "GOROOT_FINAL": "/c/go",
+ }
+ for _, name := range extraEnv {
+ start[name] = os.Getenv(name)
+ }
+ skip := map[string]bool{
+ "GOBIN": true,
+ "GOROOT": true,
+ "INCLUDE": true,
+ "LIB": true,
+ }
+ var e []string
+ for name, v := range start {
+ e = append(e, name+"="+v)
+ skip[name] = true
+ }
+ for _, kv := range os.Environ() {
+ s := strings.Split(kv, "=", 2)
+ name := strings.ToUpper(s[0])
+ switch {
+ case name == "":
+ // variables, like "=C:=C:\", just copy them
+ e = append(e, kv)
+ case !skip[name]:
+ e = append(e, kv)
+ skip[name] = true
+ }
+ }
+ return e
+}
+
func isDirectory(name string) bool {
s, err := os.Stat(name)
return err == nil && s.IsDirectory()
@@ -431,3 +407,209 @@ 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 want 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("failed to add %s to dashboard: %v", key, 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
+}