github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/misc/dashboard/builder/main.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "flag" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "log" 14 "os" 15 "path/filepath" 16 "regexp" 17 "runtime" 18 "strings" 19 "time" 20 ) 21 22 const ( 23 codeProject = "go" 24 codePyScript = "misc/dashboard/googlecode_upload.py" 25 hgUrl = "https://code.google.com/p/go/" 26 mkdirPerm = 0750 27 waitInterval = 30 * time.Second // time to wait before checking for new revs 28 pkgBuildInterval = 24 * time.Hour // rebuild packages every 24 hours 29 ) 30 31 // These variables are copied from the gobuilder's environment 32 // to the envv of its subprocesses. 33 var extraEnv = []string{ 34 "CC", 35 "GOARM", 36 "PATH", 37 "TMPDIR", 38 "USER", 39 } 40 41 type Builder struct { 42 goroot *Repo 43 name string 44 goos, goarch string 45 key string 46 } 47 48 var ( 49 buildroot = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build") 50 dashboard = flag.String("dashboard", "build.golang.org", "Go Dashboard Host") 51 buildRelease = flag.Bool("release", false, "Build and upload binary release archives") 52 buildRevision = flag.String("rev", "", "Build specified revision and exit") 53 buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)") 54 failAll = flag.Bool("fail", false, "fail all builds") 55 parallel = flag.Bool("parallel", false, "Build multiple targets in parallel") 56 buildTimeout = flag.Duration("buildTimeout", 60*time.Minute, "Maximum time to wait for builds and tests") 57 cmdTimeout = flag.Duration("cmdTimeout", 5*time.Minute, "Maximum time to wait for an external command") 58 commitInterval = flag.Duration("commitInterval", 1*time.Minute, "Time to wait between polling for new commits (0 disables commit poller)") 59 verbose = flag.Bool("v", false, "verbose") 60 ) 61 62 var ( 63 binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`) 64 releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`) 65 allCmd = "all" + suffix 66 raceCmd = "race" + suffix 67 cleanCmd = "clean" + suffix 68 suffix = defaultSuffix() 69 ) 70 71 func main() { 72 flag.Usage = func() { 73 fmt.Fprintf(os.Stderr, "usage: %s goos-goarch...\n", os.Args[0]) 74 flag.PrintDefaults() 75 os.Exit(2) 76 } 77 flag.Parse() 78 if len(flag.Args()) == 0 { 79 flag.Usage() 80 } 81 goroot := &Repo{ 82 Path: filepath.Join(*buildroot, "goroot"), 83 } 84 85 // set up work environment, use existing enviroment if possible 86 if goroot.Exists() || *failAll { 87 log.Print("Found old workspace, will use it") 88 } else { 89 if err := os.RemoveAll(*buildroot); err != nil { 90 log.Fatalf("Error removing build root (%s): %s", *buildroot, err) 91 } 92 if err := os.Mkdir(*buildroot, mkdirPerm); err != nil { 93 log.Fatalf("Error making build root (%s): %s", *buildroot, err) 94 } 95 var err error 96 goroot, err = RemoteRepo(hgUrl).Clone(goroot.Path, "tip") 97 if err != nil { 98 log.Fatal("Error cloning repository:", err) 99 } 100 } 101 102 // set up builders 103 builders := make([]*Builder, len(flag.Args())) 104 for i, name := range flag.Args() { 105 b, err := NewBuilder(goroot, name) 106 if err != nil { 107 log.Fatal(err) 108 } 109 builders[i] = b 110 } 111 112 if *failAll { 113 failMode(builders) 114 return 115 } 116 117 // if specified, build revision and return 118 if *buildRevision != "" { 119 hash, err := goroot.FullHash(*buildRevision) 120 if err != nil { 121 log.Fatal("Error finding revision: ", err) 122 } 123 for _, b := range builders { 124 if err := b.buildHash(hash); err != nil { 125 log.Println(err) 126 } 127 } 128 return 129 } 130 131 // Start commit watcher 132 go commitWatcher(goroot) 133 134 // go continuous build mode 135 // check for new commits and build them 136 for { 137 built := false 138 t := time.Now() 139 if *parallel { 140 done := make(chan bool) 141 for _, b := range builders { 142 go func(b *Builder) { 143 done <- b.build() 144 }(b) 145 } 146 for _ = range builders { 147 built = <-done || built 148 } 149 } else { 150 for _, b := range builders { 151 built = b.build() || built 152 } 153 } 154 // sleep if there was nothing to build 155 if !built { 156 time.Sleep(waitInterval) 157 } 158 // sleep if we're looping too fast. 159 dt := time.Now().Sub(t) 160 if dt < waitInterval { 161 time.Sleep(waitInterval - dt) 162 } 163 } 164 } 165 166 // go continuous fail mode 167 // check for new commits and FAIL them 168 func failMode(builders []*Builder) { 169 for { 170 built := false 171 for _, b := range builders { 172 built = b.failBuild() || built 173 } 174 // stop if there was nothing to fail 175 if !built { 176 break 177 } 178 } 179 } 180 181 func NewBuilder(goroot *Repo, name string) (*Builder, error) { 182 b := &Builder{ 183 goroot: goroot, 184 name: name, 185 } 186 187 // get goos/goarch from builder string 188 s := strings.SplitN(b.name, "-", 3) 189 if len(s) >= 2 { 190 b.goos, b.goarch = s[0], s[1] 191 } else { 192 return nil, fmt.Errorf("unsupported builder form: %s", name) 193 } 194 195 // read keys from keyfile 196 fn := "" 197 if runtime.GOOS == "windows" { 198 fn = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 199 } else { 200 fn = os.Getenv("HOME") 201 } 202 fn = filepath.Join(fn, ".gobuildkey") 203 if s := fn + "-" + b.name; isFile(s) { // builder-specific file 204 fn = s 205 } 206 c, err := ioutil.ReadFile(fn) 207 if err != nil { 208 return nil, fmt.Errorf("readKeys %s (%s): %s", b.name, fn, err) 209 } 210 b.key = string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])) 211 return b, nil 212 } 213 214 // buildCmd returns the build command to invoke. 215 // Builders which contain the string '-race' in their 216 // name will override *buildCmd and return raceCmd. 217 func (b *Builder) buildCmd() string { 218 if strings.Contains(b.name, "-race") { 219 return raceCmd 220 } 221 return *buildCmd 222 } 223 224 // build checks for a new commit for this builder 225 // and builds it if one is found. 226 // It returns true if a build was attempted. 227 func (b *Builder) build() bool { 228 hash, err := b.todo("build-go-commit", "", "") 229 if err != nil { 230 log.Println(err) 231 return false 232 } 233 if hash == "" { 234 return false 235 } 236 237 if err := b.buildHash(hash); err != nil { 238 log.Println(err) 239 } 240 return true 241 } 242 243 func (b *Builder) buildHash(hash string) error { 244 log.Println(b.name, "building", hash) 245 246 // create place in which to do work 247 workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12]) 248 if err := os.Mkdir(workpath, mkdirPerm); err != nil { 249 return err 250 } 251 defer os.RemoveAll(workpath) 252 253 // pull before cloning to ensure we have the revision 254 if err := b.goroot.Pull(); err != nil { 255 return err 256 } 257 258 // clone repo at specified revision 259 if _, err := b.goroot.Clone(filepath.Join(workpath, "go"), hash); err != nil { 260 return err 261 } 262 263 srcDir := filepath.Join(workpath, "go", "src") 264 265 // build 266 var buildlog bytes.Buffer 267 logfile := filepath.Join(workpath, "build.log") 268 f, err := os.Create(logfile) 269 if err != nil { 270 return err 271 } 272 defer f.Close() 273 w := io.MultiWriter(f, &buildlog) 274 275 cmd := b.buildCmd() 276 if !filepath.IsAbs(cmd) { 277 cmd = filepath.Join(srcDir, cmd) 278 } 279 startTime := time.Now() 280 ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd) 281 runTime := time.Now().Sub(startTime) 282 errf := func() string { 283 if err != nil { 284 return fmt.Sprintf("error: %v", err) 285 } 286 if !ok { 287 return "failed" 288 } 289 return "success" 290 } 291 fmt.Fprintf(w, "Build complete, duration %v. Result: %v\n", runTime, errf()) 292 293 if err != nil || !ok { 294 // record failure 295 return b.recordResult(false, "", hash, "", buildlog.String(), runTime) 296 } 297 298 // record success 299 if err = b.recordResult(true, "", hash, "", "", runTime); err != nil { 300 return fmt.Errorf("recordResult: %s", err) 301 } 302 303 // build Go sub-repositories 304 goRoot := filepath.Join(workpath, "go") 305 goPath := workpath 306 b.buildSubrepos(goRoot, goPath, hash) 307 308 return nil 309 } 310 311 // failBuild checks for a new commit for this builder 312 // and fails it if one is found. 313 // It returns true if a build was "attempted". 314 func (b *Builder) failBuild() bool { 315 hash, err := b.todo("build-go-commit", "", "") 316 if err != nil { 317 log.Println(err) 318 return false 319 } 320 if hash == "" { 321 return false 322 } 323 324 log.Printf("fail %s %s\n", b.name, hash) 325 326 if err := b.recordResult(false, "", hash, "", "auto-fail mode run by "+os.Getenv("USER"), 0); err != nil { 327 log.Print(err) 328 } 329 return true 330 } 331 332 func (b *Builder) buildSubrepos(goRoot, goPath, goHash string) { 333 for _, pkg := range dashboardPackages("subrepo") { 334 // get the latest todo for this package 335 hash, err := b.todo("build-package", pkg, goHash) 336 if err != nil { 337 log.Printf("buildSubrepos %s: %v", pkg, err) 338 continue 339 } 340 if hash == "" { 341 continue 342 } 343 344 // build the package 345 if *verbose { 346 log.Printf("buildSubrepos %s: building %q", pkg, hash) 347 } 348 buildLog, err := b.buildSubrepo(goRoot, goPath, pkg, hash) 349 if err != nil { 350 if buildLog == "" { 351 buildLog = err.Error() 352 } 353 log.Printf("buildSubrepos %s: %v", pkg, err) 354 } 355 356 // record the result 357 err = b.recordResult(err == nil, pkg, hash, goHash, buildLog, 0) 358 if err != nil { 359 log.Printf("buildSubrepos %s: %v", pkg, err) 360 } 361 } 362 } 363 364 // buildSubrepo fetches the given package, updates it to the specified hash, 365 // and runs 'go test -short pkg/...'. It returns the build log and any error. 366 func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error) { 367 goTool := filepath.Join(goRoot, "bin", "go") 368 env := append(b.envv(), "GOROOT="+goRoot, "GOPATH="+goPath) 369 370 // add $GOROOT/bin and $GOPATH/bin to PATH 371 for i, e := range env { 372 const p = "PATH=" 373 if !strings.HasPrefix(e, p) { 374 continue 375 } 376 sep := string(os.PathListSeparator) 377 env[i] = p + filepath.Join(goRoot, "bin") + sep + filepath.Join(goPath, "bin") + sep + e[len(p):] 378 } 379 380 // fetch package and dependencies 381 log, ok, err := runLog(*cmdTimeout, env, goPath, goTool, "get", "-d", pkg+"/...") 382 if err == nil && !ok { 383 err = fmt.Errorf("go exited with status 1") 384 } 385 if err != nil { 386 return log, err 387 } 388 389 // hg update to the specified hash 390 repo := Repo{Path: filepath.Join(goPath, "src", pkg)} 391 if err := repo.UpdateTo(hash); err != nil { 392 return "", err 393 } 394 395 // test the package 396 log, ok, err = runLog(*buildTimeout, env, goPath, goTool, "test", "-short", pkg+"/...") 397 if err == nil && !ok { 398 err = fmt.Errorf("go exited with status 1") 399 } 400 return log, err 401 } 402 403 // envv returns an environment for build/bench execution 404 func (b *Builder) envv() []string { 405 if runtime.GOOS == "windows" { 406 return b.envvWindows() 407 } 408 e := []string{ 409 "GOOS=" + b.goos, 410 "GOHOSTOS=" + b.goos, 411 "GOARCH=" + b.goarch, 412 "GOHOSTARCH=" + b.goarch, 413 "GOROOT_FINAL=/usr/local/go", 414 } 415 for _, k := range extraEnv { 416 if s, ok := getenvOk(k); ok { 417 e = append(e, k+"="+s) 418 } 419 } 420 return e 421 } 422 423 // windows version of envv 424 func (b *Builder) envvWindows() []string { 425 start := map[string]string{ 426 "GOOS": b.goos, 427 "GOHOSTOS": b.goos, 428 "GOARCH": b.goarch, 429 "GOHOSTARCH": b.goarch, 430 "GOROOT_FINAL": `c:\go`, 431 "GOBUILDEXIT": "1", // exit all.bat with completion status. 432 } 433 for _, name := range extraEnv { 434 if s, ok := getenvOk(name); ok { 435 start[name] = s 436 } 437 } 438 skip := map[string]bool{ 439 "GOBIN": true, 440 "GOROOT": true, 441 "INCLUDE": true, 442 "LIB": true, 443 } 444 var e []string 445 for name, v := range start { 446 e = append(e, name+"="+v) 447 skip[name] = true 448 } 449 for _, kv := range os.Environ() { 450 s := strings.SplitN(kv, "=", 2) 451 name := strings.ToUpper(s[0]) 452 switch { 453 case name == "": 454 // variables, like "=C:=C:\", just copy them 455 e = append(e, kv) 456 case !skip[name]: 457 e = append(e, kv) 458 skip[name] = true 459 } 460 } 461 return e 462 } 463 464 func isDirectory(name string) bool { 465 s, err := os.Stat(name) 466 return err == nil && s.IsDir() 467 } 468 469 func isFile(name string) bool { 470 s, err := os.Stat(name) 471 return err == nil && !s.IsDir() 472 } 473 474 // commitWatcher polls hg for new commits and tells the dashboard about them. 475 func commitWatcher(goroot *Repo) { 476 if *commitInterval == 0 { 477 log.Printf("commitInterval is %s, disabling commitWatcher", *commitInterval) 478 return 479 } 480 // Create builder just to get master key. 481 b, err := NewBuilder(goroot, "mercurial-commit") 482 if err != nil { 483 log.Fatal(err) 484 } 485 key := b.key 486 487 for { 488 if *verbose { 489 log.Printf("poll...") 490 } 491 // Main Go repository. 492 commitPoll(goroot, "", key) 493 // Go sub-repositories. 494 for _, pkg := range dashboardPackages("subrepo") { 495 pkgroot := &Repo{ 496 Path: filepath.Join(*buildroot, pkg), 497 } 498 commitPoll(pkgroot, pkg, key) 499 } 500 if *verbose { 501 log.Printf("sleep...") 502 } 503 time.Sleep(*commitInterval) 504 } 505 } 506 507 // logByHash is a cache of all Mercurial revisions we know about, 508 // indexed by full hash. 509 var logByHash = map[string]*HgLog{} 510 511 // commitPoll pulls any new revisions from the hg server 512 // and tells the server about them. 513 func commitPoll(repo *Repo, pkg, key string) { 514 if !repo.Exists() { 515 var err error 516 repo, err = RemoteRepo(repoURL(pkg)).Clone(repo.Path, "tip") 517 if err != nil { 518 log.Printf("%s: hg clone failed: %v", pkg, err) 519 if err := os.RemoveAll(repo.Path); err != nil { 520 log.Printf("%s: %v", pkg, err) 521 } 522 } 523 return 524 } 525 526 logs, err := repo.Log() // repo.Log calls repo.Pull internally 527 if err != nil { 528 log.Printf("hg log: %v", err) 529 return 530 } 531 532 // Pass 1. Fill in parents and add new log entries to logsByHash. 533 // Empty parent means take parent from next log entry. 534 // Non-empty parent has form 1234:hashhashhash; we want full hash. 535 for i := range logs { 536 l := &logs[i] 537 if l.Parent == "" && i+1 < len(logs) { 538 l.Parent = logs[i+1].Hash 539 } else if l.Parent != "" { 540 l.Parent, _ = repo.FullHash(l.Parent) 541 } 542 if *verbose { 543 log.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent) 544 } 545 if logByHash[l.Hash] == nil { 546 // Make copy to avoid pinning entire slice when only one entry is new. 547 t := *l 548 logByHash[t.Hash] = &t 549 } 550 } 551 552 for _, l := range logs { 553 addCommit(pkg, l.Hash, key) 554 } 555 } 556 557 // addCommit adds the commit with the named hash to the dashboard. 558 // key is the secret key for authentication to the dashboard. 559 // It avoids duplicate effort. 560 func addCommit(pkg, hash, key string) bool { 561 l := logByHash[hash] 562 if l == nil { 563 return false 564 } 565 if l.added { 566 return true 567 } 568 569 // Check for already added, perhaps in an earlier run. 570 if dashboardCommit(pkg, hash) { 571 log.Printf("%s already on dashboard\n", hash) 572 // Record that this hash is on the dashboard, 573 // as must be all its parents. 574 for l != nil { 575 l.added = true 576 l = logByHash[l.Parent] 577 } 578 return true 579 } 580 581 // Create parent first, to maintain some semblance of order. 582 if l.Parent != "" { 583 if !addCommit(pkg, l.Parent, key) { 584 return false 585 } 586 } 587 588 // Create commit. 589 if err := postCommit(key, pkg, l); err != nil { 590 log.Printf("failed to add %s to dashboard: %v", key, err) 591 return false 592 } 593 return true 594 } 595 596 var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+(\.[a-z0-9\-]+)?)(/[a-z0-9A-Z_.\-/]+)?$`) 597 598 // repoURL returns the repository URL for the supplied import path. 599 func repoURL(importPath string) string { 600 m := repoRe.FindStringSubmatch(importPath) 601 if len(m) < 2 { 602 log.Printf("repoURL: couldn't decipher %q", importPath) 603 return "" 604 } 605 return "https://code.google.com/p/" + m[1] 606 } 607 608 // defaultSuffix returns file extension used for command files in 609 // current os environment. 610 func defaultSuffix() string { 611 if runtime.GOOS == "windows" { 612 return ".bat" 613 } 614 return ".bash" 615 } 616 617 // defaultBuildRoot returns default buildroot directory. 618 func defaultBuildRoot() string { 619 var d string 620 if runtime.GOOS == "windows" { 621 // will use c:\, otherwise absolute paths become too long 622 // during builder run, see http://golang.org/issue/3358. 623 d = `c:\` 624 } else { 625 d = os.TempDir() 626 } 627 return filepath.Join(d, "gobuilder") 628 } 629 630 func getenvOk(k string) (v string, ok bool) { 631 v = os.Getenv(k) 632 if v != "" { 633 return v, true 634 } 635 keq := k + "=" 636 for _, kv := range os.Environ() { 637 if kv == keq { 638 return "", true 639 } 640 } 641 return "", false 642 }