diff options
Diffstat (limited to 'misc/dashboard/builder/main.go')
-rw-r--r-- | misc/dashboard/builder/main.go | 313 |
1 files changed, 129 insertions, 184 deletions
diff --git a/misc/dashboard/builder/main.go b/misc/dashboard/builder/main.go index 85bb7ad4b..b2b8f43a6 100644 --- a/misc/dashboard/builder/main.go +++ b/misc/dashboard/builder/main.go @@ -6,16 +6,15 @@ package main import ( "bytes" - "encoding/xml" "flag" "fmt" + "io" "io/ioutil" "log" "os" "path/filepath" "regexp" "runtime" - "strconv" "strings" "time" ) @@ -32,6 +31,7 @@ const ( // These variables are copied from the gobuilder's environment // to the envv of its subprocesses. var extraEnv = []string{ + "CC", "GOARM", "GOHOSTARCH", "GOHOSTOS", @@ -40,25 +40,27 @@ var extraEnv = []string{ } type Builder struct { + goroot *Repo name string goos, goarch string key string } var ( - 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", "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", 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") + buildroot = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build") + 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", 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") + buildTimeout = flag.Duration("buildTimeout", 60*time.Minute, "Maximum time to wait for builds and tests") + cmdTimeout = flag.Duration("cmdTimeout", 5*time.Minute, "Maximum time to wait for an external command") + commitInterval = flag.Duration("commitInterval", 1*time.Minute, "Time to wait between polling for new commits (0 disables commit poller)") + verbose = flag.Bool("v", false, "verbose") ) var ( - goroot string binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`) releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`) allCmd = "all" + suffix @@ -73,26 +75,15 @@ func main() { os.Exit(2) } flag.Parse() - if len(flag.Args()) == 0 && !*commitFlag { + if len(flag.Args()) == 0 { flag.Usage() } - goroot = filepath.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 - } - - if *failAll { - failMode(builders) - return + goroot := &Repo{ + Path: filepath.Join(*buildroot, "goroot"), } // set up work environment, use existing enviroment if possible - if hgRepoExists(goroot) { + if goroot.Exists() || *failAll { log.Print("Found old workspace, will use it") } else { if err := os.RemoveAll(*buildroot); err != nil { @@ -101,22 +92,31 @@ func main() { 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 { + var err error + goroot, err = RemoteRepo(hgUrl).Clone(goroot.Path, "tip") + if err != nil { log.Fatal("Error cloning repository:", err) } } - if *commitFlag { - if len(flag.Args()) == 0 { - commitWatcher() - return + // set up builders + builders := make([]*Builder, len(flag.Args())) + for i, name := range flag.Args() { + b, err := NewBuilder(goroot, name) + if err != nil { + log.Fatal(err) } - go commitWatcher() + builders[i] = b + } + + if *failAll { + failMode(builders) + return } // if specified, build revision and return if *buildRevision != "" { - hash, err := fullHash(goroot, *buildRevision) + hash, err := goroot.FullHash(*buildRevision) if err != nil { log.Fatal("Error finding revision: ", err) } @@ -128,7 +128,10 @@ func main() { return } - // go continuous build mode (default) + // Start commit watcher + go commitWatcher(goroot) + + // go continuous build mode // check for new commits and build them for { built := false @@ -175,15 +178,18 @@ func failMode(builders []*Builder) { } } -func NewBuilder(builder string) (*Builder, error) { - b := &Builder{name: builder} +func NewBuilder(goroot *Repo, name string) (*Builder, error) { + b := &Builder{ + goroot: goroot, + name: name, + } // get goos/goarch from builder string - s := strings.SplitN(builder, "-", 3) + s := strings.SplitN(b.name, "-", 3) if len(s) >= 2 { b.goos, b.goarch = s[0], s[1] } else { - return nil, fmt.Errorf("unsupported builder form: %s", builder) + return nil, fmt.Errorf("unsupported builder form: %s", name) } // read keys from keyfile @@ -206,7 +212,7 @@ func NewBuilder(builder string) (*Builder, error) { } // build checks for a new commit for this builder -// and builds it if one is found. +// and builds it if one is found. // It returns true if a build was attempted. func (b *Builder) build() bool { hash, err := b.todo("build-go-commit", "", "") @@ -217,16 +223,8 @@ func (b *Builder) build() bool { if hash == "" { return false } - // Look for hash locally before running hg pull. - 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) - return false - } - } - err = b.buildHash(hash) - if err != nil { + + if err := b.buildHash(hash); err != nil { log.Println(err) } return true @@ -242,34 +240,49 @@ func (b *Builder) buildHash(hash string) error { } defer os.RemoveAll(workpath) - // clone repo - if err := run(nil, workpath, "hg", "clone", goroot, "go"); err != nil { + // pull before cloning to ensure we have the revision + if err := b.goroot.Pull(); err != nil { return err } - // update to specified revision - if err := run(nil, filepath.Join(workpath, "go"), "hg", "update", hash); err != nil { + // clone repo at specified revision + if _, err := b.goroot.Clone(filepath.Join(workpath, "go"), hash); err != nil { return err } srcDir := filepath.Join(workpath, "go", "src") // build + var buildlog bytes.Buffer logfile := filepath.Join(workpath, "build.log") + f, err := os.Create(logfile) + if err != nil { + return err + } + defer f.Close() + w := io.MultiWriter(f, &buildlog) + cmd := *buildCmd if !filepath.IsAbs(cmd) { cmd = filepath.Join(srcDir, cmd) } startTime := time.Now() - buildLog, status, err := runLog(b.envv(), logfile, srcDir, cmd) + ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd) runTime := time.Now().Sub(startTime) - if err != nil { - return fmt.Errorf("%s: %s", *buildCmd, err) + errf := func() string { + if err != nil { + return fmt.Sprintf("error: %v", err) + } + if !ok { + return "failed" + } + return "success" } + fmt.Fprintf(w, "Build complete, duration %v. Result: %v\n", runTime, errf()) - if status != 0 { + if err != nil || !ok { // record failure - return b.recordResult(false, "", hash, "", buildLog, runTime) + return b.recordResult(false, "", hash, "", buildlog.String(), runTime) } // record success @@ -278,13 +291,15 @@ func (b *Builder) buildHash(hash string) error { } // build Go sub-repositories - b.buildSubrepos(filepath.Join(workpath, "go"), hash) + goRoot := filepath.Join(workpath, "go") + goPath := workpath + b.buildSubrepos(goRoot, goPath, hash) return nil } // failBuild checks for a new commit for this builder -// and fails it if one is found. +// 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", "", "") @@ -304,7 +319,7 @@ func (b *Builder) failBuild() bool { return true } -func (b *Builder) buildSubrepos(goRoot, goHash string) { +func (b *Builder) buildSubrepos(goRoot, goPath, goHash string) { for _, pkg := range dashboardPackages("subrepo") { // get the latest todo for this package hash, err := b.todo("build-package", pkg, goHash) @@ -320,7 +335,7 @@ func (b *Builder) buildSubrepos(goRoot, goHash string) { if *verbose { log.Printf("buildSubrepos %s: building %q", pkg, hash) } - buildLog, err := b.buildSubrepo(goRoot, pkg, hash) + buildLog, err := b.buildSubrepo(goRoot, goPath, pkg, hash) if err != nil { if buildLog == "" { buildLog = err.Error() @@ -338,45 +353,39 @@ func (b *Builder) buildSubrepos(goRoot, goHash string) { // 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) +func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error) { + goTool := filepath.Join(goRoot, "bin", "go") + env := append(b.envv(), "GOROOT="+goRoot, "GOPATH="+goPath) - // add goBin to PATH + // add $GOROOT/bin and $GOPATH/bin 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):] + sep := string(os.PathListSeparator) + env[i] = p + filepath.Join(goRoot, "bin") + sep + filepath.Join(goPath, "bin") + sep + e[len(p):] } // 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) + log, ok, err := runLog(*cmdTimeout, env, goPath, goTool, "get", "-d", pkg+"/...") + if err == nil && !ok { + err = fmt.Errorf("go exited with status 1") } 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 - } + 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 { + repo := Repo{Path: filepath.Join(goPath, "src", pkg)} + if err := repo.UpdateTo(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) + log, ok, err = runLog(*buildTimeout, env, goPath, goTool, "test", "-short", pkg+"/...") + if err == nil && !ok { + err = fmt.Errorf("go exited with status 1") } return log, err } @@ -449,9 +458,13 @@ func isFile(name string) bool { } // commitWatcher polls hg for new commits and tells the dashboard about them. -func commitWatcher() { +func commitWatcher(goroot *Repo) { + if *commitInterval == 0 { + log.Printf("commitInterval is %s, disabling commitWatcher", *commitInterval) + return + } // Create builder just to get master key. - b, err := NewBuilder("mercurial-commit") + b, err := NewBuilder(goroot, "mercurial-commit") if err != nil { log.Fatal(err) } @@ -462,104 +475,46 @@ func commitWatcher() { log.Printf("poll...") } // Main Go repository. - commitPoll(key, "") + commitPoll(goroot, "", key) // Go sub-repositories. for _, pkg := range dashboardPackages("subrepo") { - commitPoll(key, pkg) + pkgroot := &Repo{ + Path: filepath.Join(*buildroot, pkg), + } + commitPoll(pkgroot, pkg, key) } if *verbose { log.Printf("sleep...") } - time.Sleep(60e9) - } -} - -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 + time.Sleep(*commitInterval) } - return fi.IsDir() -} - -// 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|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, 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 +func commitPoll(repo *Repo, pkg, key string) { + if !repo.Exists() { + var err error + repo, err = RemoteRepo(repoURL(pkg)).Clone(repo.Path, "tip") + if err != nil { + log.Printf("%s: hg clone failed: %v", pkg, err) + if err := os.RemoveAll(repo.Path); err != nil { + log.Printf("%s: %v", pkg, err) } } - } - - if err := run(nil, pkgRoot, "hg", "pull"); err != nil { - log.Printf("hg pull: %v", err) return } - const N = 50 // how many revisions to grab - - data, _, err := runLog(nil, "", pkgRoot, "hg", "log", - "--encoding=utf-8", - "--limit="+strconv.Itoa(N), - "--template="+xmlLogTemplate, - ) + logs, err := repo.Log() // repo.Log calls repo.Pull internally if err != nil { log.Printf("hg log: %v", err) return } - var logStruct struct { - Log []HgLog - } - err = xml.Unmarshal([]byte("<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. @@ -568,7 +523,7 @@ func commitPoll(key, pkg string) { if l.Parent == "" && i+1 < len(logs) { l.Parent = logs[i+1].Hash } else if l.Parent != "" { - l.Parent, _ = fullHash(pkgRoot, l.Parent) + l.Parent, _ = repo.FullHash(l.Parent) } if *verbose { log.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent) @@ -580,8 +535,7 @@ func commitPoll(key, pkg string) { } } - for i := range logs { - l := &logs[i] + for _, l := range logs { addCommit(pkg, l.Hash, key) } } @@ -625,28 +579,6 @@ func addCommit(pkg, hash, key string) bool { return true } -// fullHash returns the full hash for the given Mercurial revision. -func fullHash(root, rev string) (string, error) { - s, _, err := runLog(nil, "", root, - "hg", "log", - "--encoding=utf-8", - "--rev="+rev, - "--limit=1", - "--template={node}", - ) - if err != nil { - return "", nil - } - s = strings.TrimSpace(s) - if s == "" { - return "", fmt.Errorf("cannot find revision") - } - if len(s) != 40 { - return "", fmt.Errorf("hg returned invalid hash " + s) - } - return s, nil -} - var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+(\.[a-z0-9\-]+)?)(/[a-z0-9A-Z_.\-/]+)?$`) // repoURL returns the repository URL for the supplied import path. @@ -668,6 +600,19 @@ func defaultSuffix() string { return ".bash" } +// defaultBuildRoot returns default buildroot directory. +func defaultBuildRoot() string { + var d string + if runtime.GOOS == "windows" { + // will use c:\, otherwise absolute paths become too long + // during builder run, see http://golang.org/issue/3358. + d = `c:\` + } else { + d = os.TempDir() + } + return filepath.Join(d, "gobuilder") +} + func getenvOk(k string) (v string, ok bool) { v = os.Getenv(k) if v != "" { |