// 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 ( "bytes" "flag" "fmt" "io" "io/ioutil" "log" "net" "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "time" "golang.org/x/tools/go/vcs" ) const ( codeProject = "go" codePyScript = "misc/dashboard/googlecode_upload.py" gofrontendImportPath = "code.google.com/p/gofrontend" mkdirPerm = 0750 waitInterval = 30 * time.Second // time to wait before checking for new revs pkgBuildInterval = 24 * time.Hour // rebuild packages every 24 hours ) type Builder struct { goroot *Repo name string goos, goarch string key string env builderEnv // Last benchmarking workpath. We reuse it, if do successive benchmarks on the same commit. lastWorkpath string } var ( doBuild = flag.Bool("build", true, "Build and test packages") doBench = flag.Bool("bench", false, "Run benchmarks") buildroot = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build") dashboard = flag.String("dashboard", "https://build.golang.org", "Dashboard app base path") 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/)") buildTool = flag.String("tool", "go", "Tool to build.") gcPath = flag.String("gcpath", "go.googlesource.com/go", "Path to download gc from") gccPath = flag.String("gccpath", "https://github.com/mirrors/gcc.git", "Path to download gcc from") gccOpts = flag.String("gccopts", "", "Command-line options to pass to `make` when building gccgo") benchPath = flag.String("benchpath", "golang.org/x/benchmarks/bench", "Path to download benchmarks from") 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", 10*time.Minute, "Maximum time to wait for an external command") benchNum = flag.Int("benchnum", 5, "Run each benchmark that many times") benchTime = flag.Duration("benchtime", 5*time.Second, "Benchmarking time for a single benchmark run") benchMem = flag.Int("benchmem", 64, "Approx RSS value to aim at in benchmarks, in MB") fileLock = flag.String("filelock", "", "File to lock around benchmaring (synchronizes several builders)") verbose = flag.Bool("v", false, "verbose") report = flag.Bool("report", true, "whether to report results to the dashboard") ) var ( binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`) releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`) allCmd = "all" + suffix makeCmd = "make" + suffix raceCmd = "race" + suffix cleanCmd = "clean" + suffix suffix = defaultSuffix() exeExt = defaultExeExt() benchCPU = CpuList([]int{1}) benchAffinity = CpuList([]int{}) benchMutex *FileMutex // Isolates benchmarks from other activities ) // CpuList is used as flag.Value for -benchcpu flag. type CpuList []int func (cl *CpuList) String() string { str := "" for _, cpu := range *cl { if str == "" { str = strconv.Itoa(cpu) } else { str += fmt.Sprintf(",%v", cpu) } } return str } func (cl *CpuList) Set(str string) error { *cl = []int{} for _, val := range strings.Split(str, ",") { val = strings.TrimSpace(val) if val == "" { continue } cpu, err := strconv.Atoi(val) if err != nil || cpu <= 0 { return fmt.Errorf("%v is a bad value for GOMAXPROCS", val) } *cl = append(*cl, cpu) } if len(*cl) == 0 { *cl = append(*cl, 1) } return nil } func main() { flag.Var(&benchCPU, "benchcpu", "Comma-delimited list of GOMAXPROCS values for benchmarking") flag.Var(&benchAffinity, "benchaffinity", "Comma-delimited list of affinity values for benchmarking") 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() } vcs.ShowCmd = *verbose vcs.Verbose = *verbose benchMutex = MakeFileMutex(*fileLock) rr, err := repoForTool() if err != nil { log.Fatal("Error finding repository:", err) } rootPath := filepath.Join(*buildroot, "goroot") goroot := &Repo{ Path: rootPath, Master: rr, } // set up work environment, use existing environment if possible if goroot.Exists() || *failAll { 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) } var err error goroot, err = RemoteRepo(goroot.Master.Root, rootPath) if err != nil { log.Fatalf("Error creating repository with url (%s): %s", goroot.Master.Root, err) } goroot, err = goroot.Clone(goroot.Path, "") if err != nil { log.Fatal("Error cloning repository:", err) } } // 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) } builders[i] = b } if *failAll { failMode(builders) return } // if specified, build revision and return if *buildRevision != "" { hash, err := goroot.FullHash(*buildRevision) if err != nil { log.Fatal("Error finding revision: ", err) } var exitErr error for _, b := range builders { if err := b.buildHash(hash); err != nil { log.Println(err) exitErr = err } } if exitErr != nil && !*report { // This mode (-report=false) is used for // testing Docker images, making sure the // environment is correctly configured. For // testing, we want a non-zero exit status, as // returned by log.Fatal: log.Fatal("Build error.") } return } if !*doBuild && !*doBench { fmt.Fprintf(os.Stderr, "Nothing to do, exiting (specify either -build or -bench or both)\n") os.Exit(2) } // go continuous build mode // check for new commits and build them benchMutex.RLock() for { built := false t := time.Now() if *parallel { done := make(chan bool) for _, b := range builders { go func(b *Builder) { done <- b.buildOrBench() }(b) } for _ = range builders { built = <-done || built } } else { for _, b := range builders { built = b.buildOrBench() || built } } // sleep if there was nothing to build benchMutex.RUnlock() if !built { time.Sleep(waitInterval) } benchMutex.RLock() // sleep if we're looping too fast. 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(goroot *Repo, name string) (*Builder, error) { b := &Builder{ goroot: goroot, name: name, } // get builderEnv for this tool var err error if b.env, err = b.builderEnv(name); err != nil { return nil, err } if *report { err = b.setKey() } return b, err } func (b *Builder) setKey() error { // read keys from keyfile fn := "" switch runtime.GOOS { case "plan9": fn = os.Getenv("home") case "windows": fn = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") default: fn = os.Getenv("HOME") } fn = filepath.Join(fn, ".gobuildkey") if s := fn + "-" + b.name; isFile(s) { // builder-specific file fn = s } c, err := ioutil.ReadFile(fn) if err != nil { // If the on-disk file doesn't exist, also try the // Google Compute Engine metadata. if v := gceProjectMetadata("buildkey-" + b.name); v != "" { b.key = v return nil } return fmt.Errorf("readKeys %s (%s): %s", b.name, fn, err) } b.key = string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])) return nil } func gceProjectMetadata(attr string) string { client := &http.Client{ Transport: &http.Transport{ Dial: (&net.Dialer{ Timeout: 750 * time.Millisecond, KeepAlive: 30 * time.Second, }).Dial, ResponseHeaderTimeout: 750 * time.Millisecond, }, } req, _ := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/project/attributes/"+attr, nil) req.Header.Set("Metadata-Flavor", "Google") res, err := client.Do(req) if err != nil { return "" } defer res.Body.Close() if res.StatusCode != 200 { return "" } slurp, err := ioutil.ReadAll(res.Body) if err != nil { return "" } return string(bytes.TrimSpace(slurp)) } // builderEnv returns the builderEnv for this buildTool. func (b *Builder) builderEnv(name string) (builderEnv, error) { // get goos/goarch from builder string s := strings.SplitN(b.name, "-", 3) if len(s) < 2 { return nil, fmt.Errorf("unsupported builder form: %s", name) } b.goos = s[0] b.goarch = s[1] switch *buildTool { case "go": return &goEnv{ goos: s[0], goarch: s[1], }, nil case "gccgo": return &gccgoEnv{}, nil default: return nil, fmt.Errorf("unsupported build tool: %s", *buildTool) } } // buildCmd returns the build command to invoke. // Builders which contain the string '-race' in their // name will override *buildCmd and return raceCmd. func (b *Builder) buildCmd() string { if strings.Contains(b.name, "-race") { return raceCmd } return *buildCmd } // buildOrBench checks for a new commit for this builder // and builds or benchmarks it if one is found. // It returns true if a build/benchmark was attempted. func (b *Builder) buildOrBench() bool { var kinds []string if *doBuild { kinds = append(kinds, "build-go-commit") } if *doBench { kinds = append(kinds, "benchmark-go-commit") } kind, hash, benchs, err := b.todo(kinds, "", "") if err != nil { log.Println(err) return false } if hash == "" { return false } switch kind { case "build-go-commit": if err := b.buildHash(hash); err != nil { log.Println(err) } return true case "benchmark-go-commit": if err := b.benchHash(hash, benchs); err != nil { log.Println(err) } return true default: log.Printf("Unknown todo kind %v", kind) return false } } func (b *Builder) buildHash(hash string) error { log.Println(b.name, "building", hash) // create place in which to do work workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12]) if err := os.Mkdir(workpath, mkdirPerm); err != nil { if err2 := removePath(workpath); err2 != nil { return err } if err := os.Mkdir(workpath, mkdirPerm); err != nil { return err } } defer removePath(workpath) buildLog, runTime, err := b.buildRepoOnHash(workpath, hash, b.buildCmd()) if err != nil { // record failure return b.recordResult(false, "", hash, "", buildLog, runTime) } // record success if err = b.recordResult(true, "", hash, "", "", runTime); err != nil { return fmt.Errorf("recordResult: %s", err) } if *buildTool == "go" { // build sub-repositories goRoot := filepath.Join(workpath, *buildTool) goPath := workpath b.buildSubrepos(goRoot, goPath, hash) } return nil } // buildRepoOnHash clones repo into workpath and builds it. func (b *Builder) buildRepoOnHash(workpath, hash, cmd string) (buildLog string, runTime time.Duration, err error) { // Delete the previous workdir, if necessary // (benchmarking code can execute several benchmarks in the same workpath). if b.lastWorkpath != "" { if b.lastWorkpath == workpath { panic("workpath already exists: " + workpath) } removePath(b.lastWorkpath) b.lastWorkpath = "" } // pull before cloning to ensure we have the revision if err = b.goroot.Pull(); err != nil { buildLog = err.Error() return } // set up builder's environment. srcDir, err := b.env.setup(b.goroot, workpath, hash, b.envv()) if err != nil { buildLog = err.Error() return } // build var buildbuf bytes.Buffer logfile := filepath.Join(workpath, "build.log") f, err := os.Create(logfile) if err != nil { return err.Error(), 0, err } defer f.Close() w := io.MultiWriter(f, &buildbuf) // go's build command is a script relative to the srcDir, whereas // gccgo's build command is usually "make check-go" in the srcDir. if *buildTool == "go" { if !filepath.IsAbs(cmd) { cmd = filepath.Join(srcDir, cmd) } } // naive splitting of command from its arguments: args := strings.Split(cmd, " ") c := exec.Command(args[0], args[1:]...) c.Dir = srcDir c.Env = b.envv() if *verbose { c.Stdout = io.MultiWriter(os.Stdout, w) c.Stderr = io.MultiWriter(os.Stderr, w) } else { c.Stdout = w c.Stderr = w } startTime := time.Now() err = run(c, runTimeout(*buildTimeout)) runTime = time.Since(startTime) if err != nil { fmt.Fprintf(w, "Build complete, duration %v. Result: error: %v\n", runTime, err) } else { fmt.Fprintf(w, "Build complete, duration %v. Result: success\n", runTime) } return buildbuf.String(), runTime, err } // 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([]string{"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 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, goPath, goHash string) { for _, pkg := range dashboardPackages("subrepo") { // get the latest todo for this package _, hash, _, err := b.todo([]string{"build-package"}, pkg, goHash) if err != nil { log.Printf("buildSubrepos %s: %v", pkg, err) continue } if hash == "" { continue } // build the package if *verbose { log.Printf("buildSubrepos %s: building %q", pkg, hash) } buildLog, err := b.buildSubrepo(goRoot, goPath, 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 { log.Printf("buildSubrepos %s: %v", pkg, err) } } } // 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, goPath, pkg, hash string) (string, error) { goTool := filepath.Join(goRoot, "bin", "go") + exeExt env := append(b.envv(), "GOROOT="+goRoot, "GOPATH="+goPath) // add $GOROOT/bin and $GOPATH/bin to PATH for i, e := range env { const p = "PATH=" if !strings.HasPrefix(e, p) { continue } sep := string(os.PathListSeparator) env[i] = p + filepath.Join(goRoot, "bin") + sep + filepath.Join(goPath, "bin") + sep + e[len(p):] } // HACK: check out to new sub-repo location instead of old location. pkg = strings.Replace(pkg, "code.google.com/p/go.", "golang.org/x/", 1) // fetch package and dependencies var outbuf bytes.Buffer err := run(exec.Command(goTool, "get", "-d", pkg+"/..."), runEnv(env), allOutput(&outbuf), runDir(goPath)) if err != nil { return outbuf.String(), err } outbuf.Reset() // hg update to the specified hash pkgmaster, err := vcs.RepoRootForImportPath(pkg, *verbose) if err != nil { return "", fmt.Errorf("Error finding subrepo (%s): %s", pkg, err) } repo := &Repo{ Path: filepath.Join(goPath, "src", pkg), Master: pkgmaster, } if err := repo.UpdateTo(hash); err != nil { return "", err } // test the package err = run(exec.Command(goTool, "test", "-short", pkg+"/..."), runTimeout(*buildTimeout), runEnv(env), allOutput(&outbuf), runDir(goPath)) return outbuf.String(), err } // repoForTool returns the correct RepoRoot for the buildTool, or an error if // the tool is unknown. func repoForTool() (*vcs.RepoRoot, error) { switch *buildTool { case "go": return vcs.RepoRootForImportPath(*gcPath, *verbose) case "gccgo": return vcs.RepoRootForImportPath(gofrontendImportPath, *verbose) default: return nil, fmt.Errorf("unknown build tool: %s", *buildTool) } } func isDirectory(name string) bool { s, err := os.Stat(name) return err == nil && s.IsDir() } func isFile(name string) bool { s, err := os.Stat(name) return err == nil && !s.IsDir() } // defaultSuffix returns file extension used for command files in // current os environment. func defaultSuffix() string { switch runtime.GOOS { case "windows": return ".bat" case "plan9": return ".rc" default: return ".bash" } } func defaultExeExt() string { switch runtime.GOOS { case "windows": return ".exe" default: return "" } } // 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") } // removePath is a more robust version of os.RemoveAll. // On windows, if remove fails (which can happen if test/benchmark timeouts // and keeps some files open) it tries to rename the dir. func removePath(path string) error { if err := os.RemoveAll(path); err != nil { if runtime.GOOS == "windows" { err = os.Rename(path, filepath.Clean(path)+"_remove_me") } return err } return nil }