github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/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 "net" 15 "net/http" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "regexp" 20 "runtime" 21 "strconv" 22 "strings" 23 "time" 24 25 "golang.org/x/tools/go/vcs" 26 ) 27 28 const ( 29 codeProject = "go" 30 codePyScript = "misc/dashboard/googlecode_upload.py" 31 gofrontendImportPath = "code.google.com/p/gofrontend" 32 mkdirPerm = 0750 33 waitInterval = 30 * time.Second // time to wait before checking for new revs 34 pkgBuildInterval = 24 * time.Hour // rebuild packages every 24 hours 35 ) 36 37 type Builder struct { 38 goroot *Repo 39 name string 40 goos, goarch string 41 key string 42 env builderEnv 43 // Last benchmarking workpath. We reuse it, if do successive benchmarks on the same commit. 44 lastWorkpath string 45 } 46 47 var ( 48 doBuild = flag.Bool("build", true, "Build and test packages") 49 doBench = flag.Bool("bench", false, "Run benchmarks") 50 buildroot = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build") 51 dashboard = flag.String("dashboard", "https://build.golang.org", "Dashboard app base path") 52 buildRelease = flag.Bool("release", false, "Build and upload binary release archives") 53 buildRevision = flag.String("rev", "", "Build specified revision and exit") 54 buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)") 55 buildTool = flag.String("tool", "go", "Tool to build.") 56 gcPath = flag.String("gcpath", "go.googlesource.com/go", "Path to download gc from") 57 gccPath = flag.String("gccpath", "https://github.com/mirrors/gcc.git", "Path to download gcc from") 58 gccOpts = flag.String("gccopts", "", "Command-line options to pass to `make` when building gccgo") 59 benchPath = flag.String("benchpath", "golang.org/x/benchmarks/bench", "Path to download benchmarks from") 60 failAll = flag.Bool("fail", false, "fail all builds") 61 parallel = flag.Bool("parallel", false, "Build multiple targets in parallel") 62 buildTimeout = flag.Duration("buildTimeout", 60*time.Minute, "Maximum time to wait for builds and tests") 63 cmdTimeout = flag.Duration("cmdTimeout", 10*time.Minute, "Maximum time to wait for an external command") 64 benchNum = flag.Int("benchnum", 5, "Run each benchmark that many times") 65 benchTime = flag.Duration("benchtime", 5*time.Second, "Benchmarking time for a single benchmark run") 66 benchMem = flag.Int("benchmem", 64, "Approx RSS value to aim at in benchmarks, in MB") 67 fileLock = flag.String("filelock", "", "File to lock around benchmaring (synchronizes several builders)") 68 verbose = flag.Bool("v", false, "verbose") 69 report = flag.Bool("report", true, "whether to report results to the dashboard") 70 ) 71 72 var ( 73 binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`) 74 releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`) 75 allCmd = "all" + suffix 76 makeCmd = "make" + suffix 77 raceCmd = "race" + suffix 78 cleanCmd = "clean" + suffix 79 suffix = defaultSuffix() 80 exeExt = defaultExeExt() 81 82 benchCPU = CpuList([]int{1}) 83 benchAffinity = CpuList([]int{}) 84 benchMutex *FileMutex // Isolates benchmarks from other activities 85 ) 86 87 // CpuList is used as flag.Value for -benchcpu flag. 88 type CpuList []int 89 90 func (cl *CpuList) String() string { 91 str := "" 92 for _, cpu := range *cl { 93 if str == "" { 94 str = strconv.Itoa(cpu) 95 } else { 96 str += fmt.Sprintf(",%v", cpu) 97 } 98 } 99 return str 100 } 101 102 func (cl *CpuList) Set(str string) error { 103 *cl = []int{} 104 for _, val := range strings.Split(str, ",") { 105 val = strings.TrimSpace(val) 106 if val == "" { 107 continue 108 } 109 cpu, err := strconv.Atoi(val) 110 if err != nil || cpu <= 0 { 111 return fmt.Errorf("%v is a bad value for GOMAXPROCS", val) 112 } 113 *cl = append(*cl, cpu) 114 } 115 if len(*cl) == 0 { 116 *cl = append(*cl, 1) 117 } 118 return nil 119 } 120 121 func main() { 122 flag.Var(&benchCPU, "benchcpu", "Comma-delimited list of GOMAXPROCS values for benchmarking") 123 flag.Var(&benchAffinity, "benchaffinity", "Comma-delimited list of affinity values for benchmarking") 124 flag.Usage = func() { 125 fmt.Fprintf(os.Stderr, "usage: %s goos-goarch...\n", os.Args[0]) 126 flag.PrintDefaults() 127 os.Exit(2) 128 } 129 flag.Parse() 130 if len(flag.Args()) == 0 { 131 flag.Usage() 132 } 133 134 vcs.ShowCmd = *verbose 135 vcs.Verbose = *verbose 136 137 benchMutex = MakeFileMutex(*fileLock) 138 139 rr, err := repoForTool() 140 if err != nil { 141 log.Fatal("Error finding repository:", err) 142 } 143 rootPath := filepath.Join(*buildroot, "goroot") 144 goroot := &Repo{ 145 Path: rootPath, 146 Master: rr, 147 } 148 149 // set up work environment, use existing environment if possible 150 if goroot.Exists() || *failAll { 151 log.Print("Found old workspace, will use it") 152 } else { 153 if err := os.RemoveAll(*buildroot); err != nil { 154 log.Fatalf("Error removing build root (%s): %s", *buildroot, err) 155 } 156 if err := os.Mkdir(*buildroot, mkdirPerm); err != nil { 157 log.Fatalf("Error making build root (%s): %s", *buildroot, err) 158 } 159 var err error 160 goroot, err = RemoteRepo(goroot.Master.Root, rootPath) 161 if err != nil { 162 log.Fatalf("Error creating repository with url (%s): %s", goroot.Master.Root, err) 163 } 164 165 goroot, err = goroot.Clone(goroot.Path, "") 166 if err != nil { 167 log.Fatal("Error cloning repository:", err) 168 } 169 } 170 171 // set up builders 172 builders := make([]*Builder, len(flag.Args())) 173 for i, name := range flag.Args() { 174 b, err := NewBuilder(goroot, name) 175 if err != nil { 176 log.Fatal(err) 177 } 178 builders[i] = b 179 } 180 181 if *failAll { 182 failMode(builders) 183 return 184 } 185 186 // if specified, build revision and return 187 if *buildRevision != "" { 188 hash, err := goroot.FullHash(*buildRevision) 189 if err != nil { 190 log.Fatal("Error finding revision: ", err) 191 } 192 var exitErr error 193 for _, b := range builders { 194 if err := b.buildHash(hash); err != nil { 195 log.Println(err) 196 exitErr = err 197 } 198 } 199 if exitErr != nil && !*report { 200 // This mode (-report=false) is used for 201 // testing Docker images, making sure the 202 // environment is correctly configured. For 203 // testing, we want a non-zero exit status, as 204 // returned by log.Fatal: 205 log.Fatal("Build error.") 206 } 207 return 208 } 209 210 if !*doBuild && !*doBench { 211 fmt.Fprintf(os.Stderr, "Nothing to do, exiting (specify either -build or -bench or both)\n") 212 os.Exit(2) 213 } 214 215 // go continuous build mode 216 // check for new commits and build them 217 benchMutex.RLock() 218 for { 219 built := false 220 t := time.Now() 221 if *parallel { 222 done := make(chan bool) 223 for _, b := range builders { 224 go func(b *Builder) { 225 done <- b.buildOrBench() 226 }(b) 227 } 228 for _ = range builders { 229 built = <-done || built 230 } 231 } else { 232 for _, b := range builders { 233 built = b.buildOrBench() || built 234 } 235 } 236 // sleep if there was nothing to build 237 benchMutex.RUnlock() 238 if !built { 239 time.Sleep(waitInterval) 240 } 241 benchMutex.RLock() 242 // sleep if we're looping too fast. 243 dt := time.Now().Sub(t) 244 if dt < waitInterval { 245 time.Sleep(waitInterval - dt) 246 } 247 } 248 } 249 250 // go continuous fail mode 251 // check for new commits and FAIL them 252 func failMode(builders []*Builder) { 253 for { 254 built := false 255 for _, b := range builders { 256 built = b.failBuild() || built 257 } 258 // stop if there was nothing to fail 259 if !built { 260 break 261 } 262 } 263 } 264 265 func NewBuilder(goroot *Repo, name string) (*Builder, error) { 266 b := &Builder{ 267 goroot: goroot, 268 name: name, 269 } 270 271 // get builderEnv for this tool 272 var err error 273 if b.env, err = b.builderEnv(name); err != nil { 274 return nil, err 275 } 276 if *report { 277 err = b.setKey() 278 } 279 return b, err 280 } 281 282 func (b *Builder) setKey() error { 283 // read keys from keyfile 284 fn := "" 285 switch runtime.GOOS { 286 case "plan9": 287 fn = os.Getenv("home") 288 case "windows": 289 fn = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 290 default: 291 fn = os.Getenv("HOME") 292 } 293 fn = filepath.Join(fn, ".gobuildkey") 294 if s := fn + "-" + b.name; isFile(s) { // builder-specific file 295 fn = s 296 } 297 c, err := ioutil.ReadFile(fn) 298 if err != nil { 299 // If the on-disk file doesn't exist, also try the 300 // Google Compute Engine metadata. 301 if v := gceProjectMetadata("buildkey-" + b.name); v != "" { 302 b.key = v 303 return nil 304 } 305 return fmt.Errorf("readKeys %s (%s): %s", b.name, fn, err) 306 } 307 b.key = string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0])) 308 return nil 309 } 310 311 func gceProjectMetadata(attr string) string { 312 client := &http.Client{ 313 Transport: &http.Transport{ 314 Dial: (&net.Dialer{ 315 Timeout: 750 * time.Millisecond, 316 KeepAlive: 30 * time.Second, 317 }).Dial, 318 ResponseHeaderTimeout: 750 * time.Millisecond, 319 }, 320 } 321 req, _ := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/project/attributes/"+attr, nil) 322 req.Header.Set("Metadata-Flavor", "Google") 323 res, err := client.Do(req) 324 if err != nil { 325 return "" 326 } 327 defer res.Body.Close() 328 if res.StatusCode != 200 { 329 return "" 330 } 331 slurp, err := ioutil.ReadAll(res.Body) 332 if err != nil { 333 return "" 334 } 335 return string(bytes.TrimSpace(slurp)) 336 } 337 338 // builderEnv returns the builderEnv for this buildTool. 339 func (b *Builder) builderEnv(name string) (builderEnv, error) { 340 // get goos/goarch from builder string 341 s := strings.SplitN(b.name, "-", 3) 342 if len(s) < 2 { 343 return nil, fmt.Errorf("unsupported builder form: %s", name) 344 } 345 b.goos = s[0] 346 b.goarch = s[1] 347 348 switch *buildTool { 349 case "go": 350 return &goEnv{ 351 goos: s[0], 352 goarch: s[1], 353 }, nil 354 case "gccgo": 355 return &gccgoEnv{}, nil 356 default: 357 return nil, fmt.Errorf("unsupported build tool: %s", *buildTool) 358 } 359 } 360 361 // buildCmd returns the build command to invoke. 362 // Builders which contain the string '-race' in their 363 // name will override *buildCmd and return raceCmd. 364 func (b *Builder) buildCmd() string { 365 if strings.Contains(b.name, "-race") { 366 return raceCmd 367 } 368 return *buildCmd 369 } 370 371 // buildOrBench checks for a new commit for this builder 372 // and builds or benchmarks it if one is found. 373 // It returns true if a build/benchmark was attempted. 374 func (b *Builder) buildOrBench() bool { 375 var kinds []string 376 if *doBuild { 377 kinds = append(kinds, "build-go-commit") 378 } 379 if *doBench { 380 kinds = append(kinds, "benchmark-go-commit") 381 } 382 kind, hash, benchs, err := b.todo(kinds, "", "") 383 if err != nil { 384 log.Println(err) 385 return false 386 } 387 if hash == "" { 388 return false 389 } 390 switch kind { 391 case "build-go-commit": 392 if err := b.buildHash(hash); err != nil { 393 log.Println(err) 394 } 395 return true 396 case "benchmark-go-commit": 397 if err := b.benchHash(hash, benchs); err != nil { 398 log.Println(err) 399 } 400 return true 401 default: 402 log.Printf("Unknown todo kind %v", kind) 403 return false 404 } 405 } 406 407 func (b *Builder) buildHash(hash string) error { 408 log.Println(b.name, "building", hash) 409 410 // create place in which to do work 411 workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12]) 412 if err := os.Mkdir(workpath, mkdirPerm); err != nil { 413 if err2 := removePath(workpath); err2 != nil { 414 return err 415 } 416 if err := os.Mkdir(workpath, mkdirPerm); err != nil { 417 return err 418 } 419 } 420 defer removePath(workpath) 421 422 buildLog, runTime, err := b.buildRepoOnHash(workpath, hash, b.buildCmd()) 423 if err != nil { 424 // record failure 425 return b.recordResult(false, "", hash, "", buildLog, runTime) 426 } 427 428 // record success 429 if err = b.recordResult(true, "", hash, "", "", runTime); err != nil { 430 return fmt.Errorf("recordResult: %s", err) 431 } 432 433 if *buildTool == "go" { 434 // build sub-repositories 435 goRoot := filepath.Join(workpath, *buildTool) 436 goPath := workpath 437 b.buildSubrepos(goRoot, goPath, hash) 438 } 439 440 return nil 441 } 442 443 // buildRepoOnHash clones repo into workpath and builds it. 444 func (b *Builder) buildRepoOnHash(workpath, hash, cmd string) (buildLog string, runTime time.Duration, err error) { 445 // Delete the previous workdir, if necessary 446 // (benchmarking code can execute several benchmarks in the same workpath). 447 if b.lastWorkpath != "" { 448 if b.lastWorkpath == workpath { 449 panic("workpath already exists: " + workpath) 450 } 451 removePath(b.lastWorkpath) 452 b.lastWorkpath = "" 453 } 454 455 // pull before cloning to ensure we have the revision 456 if err = b.goroot.Pull(); err != nil { 457 buildLog = err.Error() 458 return 459 } 460 461 // set up builder's environment. 462 srcDir, err := b.env.setup(b.goroot, workpath, hash, b.envv()) 463 if err != nil { 464 buildLog = err.Error() 465 return 466 } 467 468 // build 469 var buildbuf bytes.Buffer 470 logfile := filepath.Join(workpath, "build.log") 471 f, err := os.Create(logfile) 472 if err != nil { 473 return err.Error(), 0, err 474 } 475 defer f.Close() 476 w := io.MultiWriter(f, &buildbuf) 477 478 // go's build command is a script relative to the srcDir, whereas 479 // gccgo's build command is usually "make check-go" in the srcDir. 480 if *buildTool == "go" { 481 if !filepath.IsAbs(cmd) { 482 cmd = filepath.Join(srcDir, cmd) 483 } 484 } 485 486 // naive splitting of command from its arguments: 487 args := strings.Split(cmd, " ") 488 c := exec.Command(args[0], args[1:]...) 489 c.Dir = srcDir 490 c.Env = b.envv() 491 if *verbose { 492 c.Stdout = io.MultiWriter(os.Stdout, w) 493 c.Stderr = io.MultiWriter(os.Stderr, w) 494 } else { 495 c.Stdout = w 496 c.Stderr = w 497 } 498 499 startTime := time.Now() 500 err = run(c, runTimeout(*buildTimeout)) 501 runTime = time.Since(startTime) 502 if err != nil { 503 fmt.Fprintf(w, "Build complete, duration %v. Result: error: %v\n", runTime, err) 504 } else { 505 fmt.Fprintf(w, "Build complete, duration %v. Result: success\n", runTime) 506 } 507 return buildbuf.String(), runTime, err 508 } 509 510 // failBuild checks for a new commit for this builder 511 // and fails it if one is found. 512 // It returns true if a build was "attempted". 513 func (b *Builder) failBuild() bool { 514 _, hash, _, err := b.todo([]string{"build-go-commit"}, "", "") 515 if err != nil { 516 log.Println(err) 517 return false 518 } 519 if hash == "" { 520 return false 521 } 522 523 log.Printf("fail %s %s\n", b.name, hash) 524 525 if err := b.recordResult(false, "", hash, "", "auto-fail mode run by "+os.Getenv("USER"), 0); err != nil { 526 log.Print(err) 527 } 528 return true 529 } 530 531 func (b *Builder) buildSubrepos(goRoot, goPath, goHash string) { 532 for _, pkg := range dashboardPackages("subrepo") { 533 // get the latest todo for this package 534 _, hash, _, err := b.todo([]string{"build-package"}, pkg, goHash) 535 if err != nil { 536 log.Printf("buildSubrepos %s: %v", pkg, err) 537 continue 538 } 539 if hash == "" { 540 continue 541 } 542 543 // build the package 544 if *verbose { 545 log.Printf("buildSubrepos %s: building %q", pkg, hash) 546 } 547 buildLog, err := b.buildSubrepo(goRoot, goPath, pkg, hash) 548 if err != nil { 549 if buildLog == "" { 550 buildLog = err.Error() 551 } 552 log.Printf("buildSubrepos %s: %v", pkg, err) 553 } 554 555 // record the result 556 err = b.recordResult(err == nil, pkg, hash, goHash, buildLog, 0) 557 if err != nil { 558 log.Printf("buildSubrepos %s: %v", pkg, err) 559 } 560 } 561 } 562 563 // buildSubrepo fetches the given package, updates it to the specified hash, 564 // and runs 'go test -short pkg/...'. It returns the build log and any error. 565 func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error) { 566 goTool := filepath.Join(goRoot, "bin", "go") + exeExt 567 env := append(b.envv(), "GOROOT="+goRoot, "GOPATH="+goPath) 568 569 // add $GOROOT/bin and $GOPATH/bin to PATH 570 for i, e := range env { 571 const p = "PATH=" 572 if !strings.HasPrefix(e, p) { 573 continue 574 } 575 sep := string(os.PathListSeparator) 576 env[i] = p + filepath.Join(goRoot, "bin") + sep + filepath.Join(goPath, "bin") + sep + e[len(p):] 577 } 578 579 // HACK: check out to new sub-repo location instead of old location. 580 pkg = strings.Replace(pkg, "code.google.com/p/go.", "golang.org/x/", 1) 581 582 // fetch package and dependencies 583 var outbuf bytes.Buffer 584 err := run(exec.Command(goTool, "get", "-d", pkg+"/..."), runEnv(env), allOutput(&outbuf), runDir(goPath)) 585 if err != nil { 586 return outbuf.String(), err 587 } 588 outbuf.Reset() 589 590 // hg update to the specified hash 591 pkgmaster, err := vcs.RepoRootForImportPath(pkg, *verbose) 592 if err != nil { 593 return "", fmt.Errorf("Error finding subrepo (%s): %s", pkg, err) 594 } 595 repo := &Repo{ 596 Path: filepath.Join(goPath, "src", pkg), 597 Master: pkgmaster, 598 } 599 if err := repo.UpdateTo(hash); err != nil { 600 return "", err 601 } 602 603 // test the package 604 err = run(exec.Command(goTool, "test", "-short", pkg+"/..."), 605 runTimeout(*buildTimeout), runEnv(env), allOutput(&outbuf), runDir(goPath)) 606 return outbuf.String(), err 607 } 608 609 // repoForTool returns the correct RepoRoot for the buildTool, or an error if 610 // the tool is unknown. 611 func repoForTool() (*vcs.RepoRoot, error) { 612 switch *buildTool { 613 case "go": 614 return vcs.RepoRootForImportPath(*gcPath, *verbose) 615 case "gccgo": 616 return vcs.RepoRootForImportPath(gofrontendImportPath, *verbose) 617 default: 618 return nil, fmt.Errorf("unknown build tool: %s", *buildTool) 619 } 620 } 621 622 func isDirectory(name string) bool { 623 s, err := os.Stat(name) 624 return err == nil && s.IsDir() 625 } 626 627 func isFile(name string) bool { 628 s, err := os.Stat(name) 629 return err == nil && !s.IsDir() 630 } 631 632 // defaultSuffix returns file extension used for command files in 633 // current os environment. 634 func defaultSuffix() string { 635 switch runtime.GOOS { 636 case "windows": 637 return ".bat" 638 case "plan9": 639 return ".rc" 640 default: 641 return ".bash" 642 } 643 } 644 645 func defaultExeExt() string { 646 switch runtime.GOOS { 647 case "windows": 648 return ".exe" 649 default: 650 return "" 651 } 652 } 653 654 // defaultBuildRoot returns default buildroot directory. 655 func defaultBuildRoot() string { 656 var d string 657 if runtime.GOOS == "windows" { 658 // will use c:\, otherwise absolute paths become too long 659 // during builder run, see http://golang.org/issue/3358. 660 d = `c:\` 661 } else { 662 d = os.TempDir() 663 } 664 return filepath.Join(d, "gobuilder") 665 } 666 667 // removePath is a more robust version of os.RemoveAll. 668 // On windows, if remove fails (which can happen if test/benchmark timeouts 669 // and keeps some files open) it tries to rename the dir. 670 func removePath(path string) error { 671 if err := os.RemoveAll(path); err != nil { 672 if runtime.GOOS == "windows" { 673 err = os.Rename(path, filepath.Clean(path)+"_remove_me") 674 } 675 return err 676 } 677 return nil 678 }