| // 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 ( |
| "container/vector" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "path" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| 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 |
| 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", |
| "GOARM", |
| } |
| |
| type Builder struct { |
| name string |
| goos, goarch string |
| key string |
| codeUsername string |
| 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") |
| 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/)") |
| external = flag.Bool("external", false, "Build external packages") |
| verbose = flag.Bool("v", false, "verbose") |
| ) |
| |
| var ( |
| goroot string |
| releaseRegexp = regexp.MustCompile(`^release\.[0-9\-.]+`) |
| benchRequests vector.Vector |
| ) |
| |
| 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 { |
| 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 specified, build revision and return |
| if *buildRevision != "" { |
| c, err := getCommit(*buildRevision) |
| if err != nil { |
| log.Fatal("Error finding revision: ", err) |
| } |
| for _, b := range builders { |
| if err := b.buildCommit(c); err != nil { |
| log.Println(err) |
| } |
| runQueuedBenchmark() |
| } |
| 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 |
| for _, b := range builders { |
| if b.build() { |
| built = true |
| } |
| } |
| // only run benchmarks if we didn't build anything |
| // so that they don't hold up the builder queue |
| 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. |
| } |
| } |
| } |
| |
| 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) |
| } |
| } |
| |
| 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 |
| } |
| c, tag, err := getTag(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 |
| } |
| // buildCommit will also build the packages |
| if err := b.buildCommit(c); 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) |
| } |
| }() |
| c, err := b.nextCommit() |
| if err != nil { |
| log.Println(err) |
| return false |
| } |
| if c == nil { |
| return false |
| } |
| err = b.buildCommit(*c) |
| if err != nil { |
| log.Println(err) |
| } |
| return true |
| } |
| |
| // 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) |
| } |
| }() |
| hw, err := b.getHighWater() |
| if err != nil { |
| return |
| } |
| c, err := getCommit(hw) |
| if err != nil { |
| return |
| } |
| next := c.num + 1 |
| c, err = getCommit(strconv.Itoa(next)) |
| if err == nil && c.num == next { |
| return &c, nil |
| } |
| return nil, nil |
| } |
| |
| func (b *Builder) buildCommit(c Commit) (err os.Error) { |
| defer func() { |
| if err != nil { |
| err = fmt.Errorf("%s buildCommit: %d: %s", b.name, c.num, err) |
| } |
| }() |
| |
| log.Println(b.name, "building", c.num) |
| |
| // create place in which to do work |
| workpath := path.Join(*buildroot, b.name+"-"+strconv.Itoa(c.num)) |
| err = os.Mkdir(workpath, mkdirPerm) |
| if err != nil { |
| return |
| } |
| benchRequested := false |
| defer func() { |
| if !benchRequested { |
| 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", "-r", strconv.Itoa(c.num)) |
| 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, c) |
| } |
| |
| if status != 0 { |
| // record failure |
| return b.recordResult(buildLog, c) |
| } |
| |
| // record success |
| if err = b.recordResult("", c); 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 != "" { |
| // 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()) |
| } |