diff options
Diffstat (limited to 'misc/dashboard/builder/main.go')
-rw-r--r-- | misc/dashboard/builder/main.go | 487 |
1 files changed, 273 insertions, 214 deletions
diff --git a/misc/dashboard/builder/main.go b/misc/dashboard/builder/main.go index 989965bc4..85bb7ad4b 100644 --- a/misc/dashboard/builder/main.go +++ b/misc/dashboard/builder/main.go @@ -5,56 +5,54 @@ package main import ( + "bytes" + "encoding/xml" "flag" "fmt" "io/ioutil" "log" "os" - "path" + "path/filepath" "regexp" "runtime" "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 + hgUrl = "https://code.google.com/p/go/" mkdirPerm = 0750 - pkgBuildInterval = 1e9 * 60 * 60 * 24 // rebuild packages every 24 hours + waitInterval = 30 * time.Second // time to wait before checking for new revs + pkgBuildInterval = 24 * time.Hour // rebuild packages every 24 hours ) // These variables are copied from the gobuilder's environment // to the envv of its subprocesses. var extraEnv = []string{ - "GOHOSTOS", + "GOARM", "GOHOSTARCH", + "GOHOSTOS", "PATH", - "DISABLE_NET_TESTS", - "MAKEFLAGS", - "GOARM", + "TMPDIR", } 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") + buildroot = flag.String("buildroot", filepath.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") + dashboard = flag.String("dashboard", "build.golang.org", "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") + buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)") + failAll = flag.Bool("fail", false, "fail all builds") parallel = flag.Bool("parallel", false, "Build multiple targets in parallel") verbose = flag.Bool("v", false, "verbose") ) @@ -63,6 +61,9 @@ var ( goroot string binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`) releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`) + allCmd = "all" + suffix + cleanCmd = "clean" + suffix + suffix = defaultSuffix() ) func main() { @@ -75,7 +76,7 @@ func main() { if len(flag.Args()) == 0 && !*commitFlag { flag.Usage() } - goroot = path.Join(*buildroot, "goroot") + goroot = filepath.Join(*buildroot, "goroot") builders := make([]*Builder, len(flag.Args())) for i, builder := range flag.Args() { b, err := NewBuilder(builder) @@ -85,15 +86,24 @@ func main() { 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 *failAll { + failMode(builders) + return } - if err := run(nil, *buildroot, "hg", "clone", hgUrl, goroot); err != nil { - log.Fatal("Error cloning repository:", err) + + // set up work environment, use existing enviroment if possible + if hgRepoExists(goroot) { + log.Print("Found old workspace, will use it") + } else { + 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 := hgClone(hgUrl, goroot); err != nil { + log.Fatal("Error cloning repository:", err) + } } if *commitFlag { @@ -106,7 +116,7 @@ func main() { // if specified, build revision and return if *buildRevision != "" { - hash, err := fullHash(*buildRevision) + hash, err := fullHash(goroot, *buildRevision) if err != nil { log.Fatal("Error finding revision: ", err) } @@ -118,19 +128,11 @@ func main() { 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 { built := false - t := time.Nanoseconds() + t := time.Now() if *parallel { done := make(chan bool) for _, b := range builders { @@ -151,14 +153,29 @@ func main() { time.Sleep(waitInterval) } // sleep if we're looping too fast. - t1 := time.Nanoseconds() - t - if t1 < waitInterval { - time.Sleep(waitInterval - t1) + dt := time.Now().Sub(t) + if dt < waitInterval { + time.Sleep(waitInterval - dt) + } + } +} + +// go continuous fail mode +// check for new commits and FAIL them +func failMode(builders []*Builder) { + for { + built := false + for _, b := range builders { + built = b.failBuild() || built + } + // stop if there was nothing to fail + if !built { + break } } } -func NewBuilder(builder string) (*Builder, os.Error) { +func NewBuilder(builder string) (*Builder, error) { b := &Builder{name: builder} // get goos/goarch from builder string @@ -170,7 +187,13 @@ func NewBuilder(builder string) (*Builder, os.Error) { } // read keys from keyfile - fn := path.Join(os.Getenv("HOME"), ".gobuildkey") + fn := "" + if runtime.GOOS == "windows" { + fn = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + } else { + fn = os.Getenv("HOME") + } + fn = filepath.Join(fn, ".gobuildkey") if s := fn + "-" + b.name; isFile(s) { // builder-specific file fn = s } @@ -178,64 +201,15 @@ func NewBuilder(builder string) (*Builder, os.Error) { if err != nil { return nil, fmt.Errorf("readKeys %s (%s): %s", b.name, fn, err) } - v := strings.Split(string(c), "\n") - b.key = v[0] - if len(v) >= 3 { - b.codeUsername, b.codePassword = v[1], v[2] - } - + b.key = string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])) 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(releaseRe) - 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() + hash, err := b.todo("build-go-commit", "", "") if err != nil { log.Println(err) return false @@ -244,8 +218,7 @@ func (b *Builder) build() bool { return false } // Look for hash locally before running hg pull. - - if _, err := fullHash(hash[:12]); err != nil { + if _, err := fullHash(goroot, 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) @@ -259,92 +232,153 @@ func (b *Builder) build() bool { 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) - } - }() - +func (b *Builder) buildHash(hash string) error { 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 + workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12]) + if err := os.Mkdir(workpath, mkdirPerm); err != nil { + return err } defer os.RemoveAll(workpath) // clone repo - err = run(nil, workpath, "hg", "clone", goroot, "go") - if err != nil { - return + if err := run(nil, workpath, "hg", "clone", goroot, "go"); err != nil { + return err } // update to specified revision - err = run(nil, path.Join(workpath, "go"), - "hg", "update", hash) - if err != nil { - return + if err := run(nil, filepath.Join(workpath, "go"), "hg", "update", hash); err != nil { + return err } - srcDir := path.Join(workpath, "go", "src") + srcDir := filepath.Join(workpath, "go", "src") // build - logfile := path.Join(workpath, "build.log") - buildLog, status, err := runLog(b.envv(), logfile, srcDir, *buildCmd) + logfile := filepath.Join(workpath, "build.log") + cmd := *buildCmd + if !filepath.IsAbs(cmd) { + cmd = filepath.Join(srcDir, cmd) + } + startTime := time.Now() + buildLog, status, err := runLog(b.envv(), logfile, srcDir, cmd) + runTime := time.Now().Sub(startTime) if err != nil { return fmt.Errorf("%s: %s", *buildCmd, 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) + return b.recordResult(false, "", hash, "", buildLog, runTime) } // record success - if err = b.recordResult("", hash); err != nil { + if err = b.recordResult(true, "", hash, "", "", runTime); err != nil { return fmt.Errorf("recordResult: %s", err) } - // finish here if codeUsername and codePassword aren't set - if b.codeUsername == "" || b.codePassword == "" || !*buildRelease { - return + // build Go sub-repositories + b.buildSubrepos(filepath.Join(workpath, "go"), hash) + + return nil +} + +// failBuild checks for a new commit for this builder +// and fails it if one is found. +// It returns true if a build was "attempted". +func (b *Builder) failBuild() bool { + hash, err := b.todo("build-go-commit", "", "") + if err != nil { + log.Println(err) + return false } + if hash == "" { + return false + } + + log.Printf("fail %s %s\n", b.name, hash) - // if this is a release, create tgz and upload to google code - releaseHash, release, err := firstTag(binaryTagRe) - if hash == releaseHash { - // clean out build state - err = run(b.envv(), srcDir, "./clean.bash", "--nopkg") + if err := b.recordResult(false, "", hash, "", "auto-fail mode run by "+os.Getenv("USER"), 0); err != nil { + log.Print(err) + } + return true +} + +func (b *Builder) buildSubrepos(goRoot, goHash string) { + for _, pkg := range dashboardPackages("subrepo") { + // get the latest todo for this package + hash, err := b.todo("build-package", pkg, goHash) if err != nil { - return fmt.Errorf("clean.bash: %s", err) + log.Printf("buildSubrepos %s: %v", pkg, err) + continue } - // 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 hash == "" { + continue + } + + // build the package + if *verbose { + log.Printf("buildSubrepos %s: building %q", pkg, hash) + } + buildLog, err := b.buildSubrepo(goRoot, pkg, hash) + if err != nil { + if buildLog == "" { + buildLog = err.Error() + } + log.Printf("buildSubrepos %s: %v", pkg, err) + } + + // record the result + err = b.recordResult(err == nil, pkg, hash, goHash, buildLog, 0) if err != nil { - return fmt.Errorf("tar: %s", err) + log.Printf("buildSubrepos %s: %v", pkg, 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) + } +} + +// buildSubrepo fetches the given package, updates it to the specified hash, +// and runs 'go test -short pkg/...'. It returns the build log and any error. +func (b *Builder) buildSubrepo(goRoot, pkg, hash string) (string, error) { + goBin := filepath.Join(goRoot, "bin") + goTool := filepath.Join(goBin, "go") + env := append(b.envv(), "GOROOT="+goRoot) + + // add goBin to PATH + for i, e := range env { + const p = "PATH=" + if !strings.HasPrefix(e, p) { + continue + } + env[i] = p + goBin + string(os.PathListSeparator) + e[len(p):] } - return + // fetch package and dependencies + log, status, err := runLog(env, "", goRoot, goTool, "get", "-d", pkg) + if err == nil && status != 0 { + err = fmt.Errorf("go exited with status %d", status) + } + if err != nil { + // 'go get -d' will fail for a subrepo because its top-level + // directory does not contain a go package. No matter, just + // check whether an hg directory exists and proceed. + hgDir := filepath.Join(goRoot, "src/pkg", pkg, ".hg") + if fi, e := os.Stat(hgDir); e != nil || !fi.IsDir() { + return log, err + } + } + + // hg update to the specified hash + pkgPath := filepath.Join(goRoot, "src/pkg", pkg) + if err := run(nil, pkgPath, "hg", "update", hash); err != nil { + return "", err + } + + // test the package + log, status, err = runLog(env, "", goRoot, goTool, "test", "-short", pkg+"/...") + if err == nil && status != 0 { + err = fmt.Errorf("go exited with status %d", status) + } + return log, err } // envv returns an environment for build/bench execution @@ -358,8 +392,7 @@ func (b *Builder) envv() []string { "GOROOT_FINAL=/usr/local/go", } for _, k := range extraEnv { - s, err := os.Getenverror(k) - if err == nil { + if s, ok := getenvOk(k); ok { e = append(e, k+"="+s) } } @@ -371,13 +404,11 @@ func (b *Builder) envvWindows() []string { start := map[string]string{ "GOOS": b.goos, "GOARCH": b.goarch, - "GOROOT_FINAL": "/c/go", - // TODO(brainman): remove once we find make that does not hang. - "MAKEFLAGS": "-j1", + "GOROOT_FINAL": `c:\go`, + "GOBUILDEXIT": "1", // exit all.bat with completion status. } for _, name := range extraEnv { - s, err := os.Getenverror(name) - if err == nil { + if s, ok := getenvOk(name); ok { start[name] = s } } @@ -409,12 +440,12 @@ func (b *Builder) envvWindows() []string { func isDirectory(name string) bool { s, err := os.Stat(name) - return err == nil && s.IsDirectory() + return err == nil && s.IsDir() } func isFile(name string) bool { s, err := os.Stat(name) - return err == nil && (s.IsRegular() || s.IsSymlink()) + return err == nil && !s.IsDir() } // commitWatcher polls hg for new commits and tells the dashboard about them. @@ -424,11 +455,18 @@ func commitWatcher() { if err != nil { log.Fatal(err) } + key := b.key + for { if *verbose { log.Printf("poll...") } - commitPoll(b.key) + // Main Go repository. + commitPoll(key, "") + // Go sub-repositories. + for _, pkg := range dashboardPackages("subrepo") { + commitPoll(key, pkg) + } if *verbose { log.Printf("sleep...") } @@ -436,6 +474,18 @@ func commitWatcher() { } } +func hgClone(url, path string) error { + return run(nil, *buildroot, "hg", "clone", url, path) +} + +func hgRepoExists(path string) bool { + fi, err := os.Stat(filepath.Join(path, ".hg")) + if err != nil { + return false + } + return fi.IsDir() +} + // HgLog represents a single Mercurial revision. type HgLog struct { Hash string @@ -455,33 +505,41 @@ 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> + <Log> + <Hash>{node|escape}</Hash> + <Parent>{parent|escape}</Parent> + <Author>{author|escape}</Author> + <Date>{date|rfc3339date}</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) +func commitPoll(key, pkg string) { + pkgRoot := goroot + + if pkg != "" { + pkgRoot = filepath.Join(*buildroot, pkg) + if !hgRepoExists(pkgRoot) { + if err := hgClone(repoURL(pkg), pkgRoot); err != nil { + log.Printf("%s: hg clone failed: %v", pkg, err) + if err := os.RemoveAll(pkgRoot); err != nil { + log.Printf("%s: %v", pkg, err) + } + return + } } - }() + } - if err := run(nil, goroot, "hg", "pull"); err != nil { + if err := run(nil, pkgRoot, "hg", "pull"); err != nil { log.Printf("hg pull: %v", err) return } - const N = 20 // how many revisions to grab + const N = 50 // how many revisions to grab - data, _, err := runLog(nil, "", goroot, "hg", "log", + data, _, err := runLog(nil, "", pkgRoot, "hg", "log", "--encoding=utf-8", "--limit="+strconv.Itoa(N), "--template="+xmlLogTemplate, @@ -494,7 +552,7 @@ func commitPoll(key string) { var logStruct struct { Log []HgLog } - err = xml.Unmarshal(strings.NewReader("<top>"+data+"</top>"), &logStruct) + err = xml.Unmarshal([]byte("<Top>"+data+"</Top>"), &logStruct) if err != nil { log.Printf("unmarshal hg log: %v", err) return @@ -507,17 +565,14 @@ func commitPoll(key string) { // 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) + l.Parent, _ = fullHash(pkgRoot, l.Parent) } - if l.Parent == "" { - // Can't create node without parent. - continue + if *verbose { + log.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent) } - if logByHash[l.Hash] == nil { // Make copy to avoid pinning entire slice when only one entry is new. t := *l @@ -527,17 +582,14 @@ func commitPoll(key string) { for i := range logs { l := &logs[i] - if l.Parent == "" { - continue - } - addCommit(l.Hash, key) + addCommit(pkg, 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 { +func addCommit(pkg, hash, key string) bool { l := logByHash[hash] if l == nil { return false @@ -547,7 +599,7 @@ func addCommit(hash, key string) bool { } // Check for already added, perhaps in an earlier run. - if dashboardCommit(hash) { + if dashboardCommit(pkg, hash) { log.Printf("%s already on dashboard\n", hash) // Record that this hash is on the dashboard, // as must be all its parents. @@ -559,12 +611,14 @@ func addCommit(hash, key string) bool { } // Create parent first, to maintain some semblance of order. - if !addCommit(l.Parent, key) { - return false + if l.Parent != "" { + if !addCommit(pkg, l.Parent, key) { + return false + } } // Create commit. - if err := postCommit(key, l); err != nil { + if err := postCommit(key, pkg, l); err != nil { log.Printf("failed to add %s to dashboard: %v", key, err) return false } @@ -572,13 +626,8 @@ func addCommit(hash, key string) bool { } // 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, +func fullHash(root, rev string) (string, error) { + s, _, err := runLog(nil, "", root, "hg", "log", "--encoding=utf-8", "--rev="+rev, @@ -586,7 +635,7 @@ func fullHash(rev string) (hash string, err os.Error) { "--template={node}", ) if err != nil { - return + return "", nil } s = strings.TrimSpace(s) if s == "" { @@ -598,27 +647,37 @@ func fullHash(rev string) (hash string, err os.Error) { return s, nil } -var revisionRe = regexp.MustCompile(`^([^ ]+) +[0-9]+:([0-9a-f]+)$`) +var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+(\.[a-z0-9\-]+)?)(/[a-z0-9A-Z_.\-/]+)?$`) -// 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") { - 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 +// repoURL returns the repository URL for the supplied import path. +func repoURL(importPath string) string { + m := repoRe.FindStringSubmatch(importPath) + if len(m) < 2 { + log.Printf("repoURL: couldn't decipher %q", importPath) + return "" + } + return "https://code.google.com/p/" + m[1] +} + +// defaultSuffix returns file extension used for command files in +// current os environment. +func defaultSuffix() string { + if runtime.GOOS == "windows" { + return ".bat" + } + return ".bash" +} + +func getenvOk(k string) (v string, ok bool) { + v = os.Getenv(k) + if v != "" { + return v, true + } + keq := k + "=" + for _, kv := range os.Environ() { + if kv == keq { + return "", true } - tag = s[1] - hash, err = fullHash(s[2]) - return } - err = os.NewError("no matching tag found") - return + return "", false } |