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  }