github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/misc/buildbot/builder/builder.go (about)

     1  /*
     2  Copyright 2012 The Camlistore Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // The buildbot is Camlistore's continuous builder.
    18  // This builder program is started by the master. It then rebuilds
    19  // Go 1, GoTip, Camlistore, and runs a battery of tests for Camlistore.
    20  // It then sends a report to the master and terminates.
    21  // It can also respond to progress requests from the master.
    22  package main
    23  
    24  import (
    25  	"bufio"
    26  	"bytes"
    27  	"crypto/tls"
    28  	"encoding/json"
    29  	"errors"
    30  	"flag"
    31  	"fmt"
    32  	"io"
    33  	"io/ioutil"
    34  	"log"
    35  	"net/http"
    36  	"net/url"
    37  	"os"
    38  	"os/exec"
    39  	"os/signal"
    40  	"path/filepath"
    41  	"regexp"
    42  	"runtime"
    43  	"strings"
    44  	"sync"
    45  	"syscall"
    46  	"time"
    47  
    48  	"camlistore.org/pkg/osutil"
    49  )
    50  
    51  const (
    52  	interval = 60 * time.Second // polling frequency
    53  	warmup   = 30 * time.Second // duration before we test if devcam server has started properly
    54  )
    55  
    56  var (
    57  	// TODO(mpl): use that one, same as in master.
    58  	altCamliRevURL = flag.String("camlirevurl", "", "alternative URL to query about the latest camlistore revision hash (e.g camlistore.org/latesthash), to alleviate hitting too often the Camlistore git repo.")
    59  	arch           = flag.String("arch", "", "The arch we report the master(s). Defaults to runtime.GOARCH.")
    60  	fakeTests      = flag.Bool("faketests", false, "Run fast fake tests instead of the real ones, for faster debugging.")
    61  	help           = flag.Bool("h", false, "show this help")
    62  	host           = flag.String("host", "0.0.0.0:8081", "listening hostname and port")
    63  	masterHosts    = flag.String("masterhosts", "localhost:8080", "listening hostname and port of the master bots, i.e where to send the test suite reports. Comma separated list.")
    64  	ourOS          = flag.String("os", "", "The OS we report the master(s). Defaults to runtime.GOOS.")
    65  	skipGo1Build   = flag.Bool("skipgo1build", false, "skip initial go1 build, for debugging and quickly going to the next steps.")
    66  	verbose        = flag.Bool("verbose", false, "print what's going on")
    67  	skipTLSCheck   = flag.Bool("skiptlscheck", false, "accept any certificate presented by server when uploading results.")
    68  )
    69  
    70  var (
    71  	testFile       = []string{"AUTHORS", "CONTRIBUTORS"}
    72  	cacheDir       string
    73  	camliHeadHash  string
    74  	camliRoot      string
    75  	camputCacheDir string
    76  	client         = http.DefaultClient
    77  	dbg            *debugger
    78  	defaultPATH    string
    79  	go1Dir         string
    80  	goTipDir       string
    81  	goTipHash      string
    82  
    83  	biSuitelk        sync.Mutex
    84  	currentTestSuite *testSuite
    85  	currentBiSuite   *biTestSuite
    86  
    87  	// Process of the camlistore server, so we can kill it when
    88  	// we get killed ourselves.
    89  	camliProc *os.Process
    90  
    91  	// For "If-Modified-Since" requests asking for progress.
    92  	// Updated every time a new test task/run is added to the test suite.
    93  	lastModified time.Time
    94  )
    95  
    96  var devcamBin = filepath.Join("bin", "devcam")
    97  var (
    98  	hgCloneGo1Cmd      = newTask("hg", "clone", "-u", "release", "https://code.google.com/p/go")
    99  	hgCloneGoTipCmd    = newTask("hg", "clone", "-u", "tip", "https://code.google.com/p/go")
   100  	hgPullCmd          = newTask("hg", "pull")
   101  	hgUpdateCmd        = newTask("hg", "update", "-C", "default")
   102  	hgLogCmd           = newTask("hg", "log", "-r", "tip", "--template", "{node}")
   103  	hgConfigCmd        = newTask("hg", "--config", "extensions.purge=", "purge", "--all")
   104  	gitCloneCmd        = newTask("git", "clone", "https://camlistore.googlesource.com/camlistore")
   105  	gitResetCmd        = newTask("git", "reset", "--hard")
   106  	gitCleanCmd        = newTask("git", "clean", "-Xdf")
   107  	gitPullCmd         = newTask("git", "pull")
   108  	gitRevCmd          = newTask("git", "rev-parse", "HEAD")
   109  	buildGoCmd         = newTask("./make.bash")
   110  	buildCamliCmd      = newTask("go", "run", "make.go", "-v")
   111  	runTestsCmd        = newTask(devcamBin, "test")
   112  	runCamliCmd        = newTask(devcamBin, "server", "--wipe", "--mysql")
   113  	camgetCmd          = newTask(devcamBin, "get")
   114  	camputCmd          = newTask(devcamBin, "put", "file", "--permanode", testFile[0])
   115  	camputVivifyCmd    = newTask(devcamBin, "put", "file", "--vivify", testFile[1])
   116  	camputFilenodesCmd = newTask(devcamBin, "put", "file", "--filenodes", "pkg")
   117  )
   118  
   119  func usage() {
   120  	fmt.Fprintf(os.Stderr, "\t builderBot \n")
   121  	flag.PrintDefaults()
   122  	os.Exit(2)
   123  }
   124  
   125  type debugger struct {
   126  	lg *log.Logger
   127  }
   128  
   129  func (dbg *debugger) Printf(format string, v ...interface{}) {
   130  	if dbg != nil && *verbose {
   131  		dbg.lg.Printf(format, v...)
   132  	}
   133  }
   134  
   135  func (dbg *debugger) Println(v ...interface{}) {
   136  	if v == nil {
   137  		return
   138  	}
   139  	if dbg != nil && *verbose {
   140  		dbg.lg.Println(v...)
   141  	}
   142  }
   143  
   144  type task struct {
   145  	Program  string
   146  	Args     []string
   147  	Start    time.Time
   148  	Duration time.Duration
   149  	Err      string
   150  	hidden   bool
   151  }
   152  
   153  func newTask(program string, args ...string) *task {
   154  	return &task{Program: program, Args: args}
   155  }
   156  
   157  // because sometimes we do not want to modify the tsk template
   158  // so we make a copy of it
   159  func newTaskFrom(tsk *task) *task {
   160  	return newTask(tsk.Program, tsk.Args...)
   161  }
   162  
   163  func (t *task) String() string {
   164  	return fmt.Sprintf("%v %v", t.Program, t.Args)
   165  }
   166  
   167  func (t *task) run() (string, error) {
   168  	var err error
   169  	defer func() {
   170  		t.Duration = time.Now().Sub(t.Start)
   171  		if !t.hidden {
   172  			biSuitelk.Lock()
   173  			currentTestSuite.addRun(t)
   174  			biSuitelk.Unlock()
   175  		}
   176  	}()
   177  	dbg.Println(t.String())
   178  	cmd := exec.Command(t.Program, t.Args...)
   179  	var stdout, stderr bytes.Buffer
   180  	cmd.Stdout = &stdout
   181  	cmd.Stderr = &stderr
   182  	t.Start = time.Now()
   183  	err = cmd.Run()
   184  	if err != nil {
   185  		var sout, serr string
   186  		if sout = stdout.String(); sout == "" {
   187  			sout = "(empty)"
   188  		}
   189  		if serr = stderr.String(); serr == "" {
   190  			serr = "(empty)"
   191  		}
   192  		t.Err = fmt.Sprintf("%v\n\nStdout:\n%s\n\nStderr:\n%s\n", err, sout, serr)
   193  		return "", errors.New(t.Err)
   194  	}
   195  	return stdout.String(), nil
   196  }
   197  
   198  type testSuite struct {
   199  	Run       []*task
   200  	CamliHash string
   201  	GoHash    string
   202  	Err       string
   203  	Start     time.Time
   204  	IsTip     bool
   205  }
   206  
   207  func (ts *testSuite) addRun(tsk *task) {
   208  	if ts == nil {
   209  		panic("Tried adding a run to a nil testSuite")
   210  	}
   211  	if ts.Start.IsZero() && len(ts.Run) == 0 {
   212  		ts.Start = tsk.Start
   213  	}
   214  	if tsk.Err != "" && ts.Err == "" {
   215  		ts.Err = tsk.Err
   216  	}
   217  	ts.Run = append(ts.Run, tsk)
   218  	lastModified = time.Now()
   219  }
   220  
   221  type biTestSuite struct {
   222  	Local bool
   223  	Go1   testSuite
   224  	GoTip testSuite
   225  }
   226  
   227  func main() {
   228  	flag.Usage = usage
   229  	flag.Parse()
   230  	if *help {
   231  		usage()
   232  	}
   233  
   234  	if *skipTLSCheck {
   235  		tr := &http.Transport{
   236  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   237  		}
   238  		client = &http.Client{Transport: tr}
   239  	}
   240  
   241  	go handleSignals()
   242  	http.HandleFunc("/progress", progressHandler)
   243  	go func() {
   244  		log.Printf("Now starting to listen on %v", *host)
   245  		if err := http.ListenAndServe(*host, nil); err != nil {
   246  			log.Fatalf("Could not start listening on %v: %v", *host, err)
   247  		}
   248  	}()
   249  	setup()
   250  
   251  	biSuitelk.Lock()
   252  	currentBiSuite = &biTestSuite{}
   253  	biSuitelk.Unlock()
   254  	for _, isTip := range [2]bool{false, true} {
   255  		currentTestSuite = &testSuite{
   256  			Run:   make([]*task, 0, 1),
   257  			IsTip: isTip,
   258  			Start: time.Now(),
   259  		}
   260  		// We prepare the Go tip tree as soon as in the Go 1 run, so
   261  		// we can set GoTipHash in the test suite.
   262  		if !isTip {
   263  			if err := prepGoTipTree(); err != nil {
   264  				endOfSuite(err)
   265  				// If we failed with that in the Go 1 run, we just restart
   266  				// from scratch instead of trying to cope with it in the Gotip run.
   267  				// Same for buildGoTip and prepCamliTree.
   268  				break
   269  			}
   270  		}
   271  
   272  		biSuitelk.Lock()
   273  		currentTestSuite.GoHash = goTipHash
   274  		biSuitelk.Unlock()
   275  		if isTip && !*fakeTests {
   276  			if err := buildGoTip(); err != nil {
   277  				endOfSuite(err)
   278  				break
   279  			}
   280  		}
   281  		if err := prepCamliTree(isTip); err != nil {
   282  			endOfSuite(err)
   283  			break
   284  		}
   285  
   286  		biSuitelk.Lock()
   287  		currentTestSuite.CamliHash = camliHeadHash
   288  		biSuitelk.Unlock()
   289  		restorePATH()
   290  		goDir := go1Dir
   291  		if isTip {
   292  			goDir = goTipDir
   293  		}
   294  		switchGo(goDir)
   295  		if *fakeTests {
   296  			if err := fakeRun(); err != nil {
   297  				endOfSuite(err)
   298  				continue
   299  			}
   300  			endOfSuite(nil)
   301  			if isTip {
   302  				break
   303  			}
   304  			continue
   305  		}
   306  		if err := buildCamli(); err != nil {
   307  			endOfSuite(err)
   308  			continue
   309  		}
   310  		if err := runTests(); err != nil {
   311  			endOfSuite(err)
   312  			continue
   313  		}
   314  		if err := runCamli(); err != nil {
   315  			endOfSuite(err)
   316  			continue
   317  		}
   318  		if err := hitCamliUi(); err != nil {
   319  			endOfSuite(err)
   320  			continue
   321  		}
   322  		doVivify := false
   323  		if err := camputOne(doVivify); err != nil {
   324  			endOfSuite(err)
   325  			continue
   326  		}
   327  		doVivify = true
   328  		if err := camputOne(doVivify); err != nil {
   329  			endOfSuite(err)
   330  			continue
   331  		}
   332  		if err := camputMany(); err != nil {
   333  			endOfSuite(err)
   334  			continue
   335  		}
   336  		endOfSuite(nil)
   337  	}
   338  	sanitizeRevs()
   339  	sendReport()
   340  }
   341  
   342  func sanitizeRevs() {
   343  	if currentBiSuite == nil {
   344  		return
   345  	}
   346  	if currentBiSuite.GoTip.Start.IsZero() {
   347  		return
   348  	}
   349  	if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" {
   350  		dbg.Printf("CamliHash not set in both Go1 and GoTip test suites")
   351  		return
   352  	}
   353  	if currentBiSuite.GoTip.CamliHash == "" && currentBiSuite.Go1.CamliHash == "" {
   354  		dbg.Printf("GoHash not set in both Go1 and GoTip test suites")
   355  		return
   356  	}
   357  	if currentBiSuite.GoTip.CamliHash != "" && currentBiSuite.Go1.CamliHash != "" &&
   358  		currentBiSuite.GoTip.CamliHash != currentBiSuite.Go1.CamliHash {
   359  		panic("CamliHash in GoTip suite and in Go1 suite differ; should not happen.")
   360  	}
   361  	if currentBiSuite.GoTip.GoHash != "" && currentBiSuite.Go1.GoHash != "" &&
   362  		currentBiSuite.GoTip.GoHash != currentBiSuite.Go1.GoHash {
   363  		panic("GoHash in GoTip suite and in Go1 suite differ; should not happen.")
   364  	}
   365  	if currentBiSuite.GoTip.GoHash == "" {
   366  		currentBiSuite.GoTip.GoHash = currentBiSuite.Go1.GoHash
   367  	}
   368  	if currentBiSuite.Go1.GoHash == "" {
   369  		currentBiSuite.Go1.GoHash = currentBiSuite.GoTip.GoHash
   370  	}
   371  	if currentBiSuite.GoTip.CamliHash == "" {
   372  		currentBiSuite.GoTip.CamliHash = currentBiSuite.Go1.CamliHash
   373  	}
   374  	if currentBiSuite.Go1.CamliHash == "" {
   375  		currentBiSuite.Go1.CamliHash = currentBiSuite.GoTip.CamliHash
   376  	}
   377  }
   378  
   379  func endOfSuite(err error) {
   380  	biSuitelk.Lock()
   381  	defer biSuitelk.Unlock()
   382  	if currentTestSuite.IsTip {
   383  		currentBiSuite.GoTip = *currentTestSuite
   384  	} else {
   385  		currentBiSuite.Go1 = *currentTestSuite
   386  	}
   387  	killCamli()
   388  	if err != nil {
   389  		log.Printf("%v", err)
   390  	} else {
   391  		dbg.Println("All good.")
   392  	}
   393  }
   394  
   395  func masterHostsReader(r io.Reader) ([]string, error) {
   396  	hosts := []string{}
   397  	scanner := bufio.NewScanner(r)
   398  	for scanner.Scan() {
   399  		l := scanner.Text()
   400  		u, err := url.Parse(l)
   401  		if err != nil {
   402  			return nil, err
   403  		}
   404  		if u.Host == "" {
   405  			return nil, fmt.Errorf("URL missing Host: %q", l)
   406  		}
   407  		hosts = append(hosts, u.String())
   408  	}
   409  	if err := scanner.Err(); err != nil {
   410  		return nil, err
   411  	}
   412  	return hosts, nil
   413  }
   414  
   415  var masterHostsFile = filepath.Join(osutil.CamliConfigDir(), "builderbot-config")
   416  
   417  func loadMasterHosts() error {
   418  	r, err := os.Open(masterHostsFile)
   419  	if err != nil {
   420  		return err
   421  	}
   422  	defer r.Close()
   423  
   424  	hosts, err := masterHostsReader(r)
   425  	if err != nil {
   426  		return err
   427  	}
   428  	if *masterHosts != "" {
   429  		*masterHosts += ","
   430  	}
   431  	log.Println("Additional host(s) to send our build reports:", hosts)
   432  	*masterHosts += strings.Join(hosts, ",")
   433  	return nil
   434  }
   435  
   436  func setup() {
   437  	var err error
   438  	defaultPATH = os.Getenv("PATH")
   439  	if defaultPATH == "" {
   440  		log.Fatal("PATH not set")
   441  	}
   442  	log.SetPrefix("BUILDER: ")
   443  	dbg = &debugger{log.New(os.Stderr, "BUILDER: ", log.LstdFlags)}
   444  
   445  	err = loadMasterHosts()
   446  	if err != nil {
   447  		if os.IsNotExist(err) {
   448  			log.Printf("%q missing. No additional remote master(s) will receive build report.", masterHostsFile)
   449  		} else {
   450  			log.Println("Error parsing master hosts file %q: %v",
   451  				masterHostsFile, err)
   452  		}
   453  	}
   454  
   455  	// the OS we run on
   456  	if *ourOS == "" {
   457  		*ourOS = runtime.GOOS
   458  		if *ourOS == "" {
   459  			// Can this happen? I don't think so, but just in case...
   460  			panic("runtime.GOOS was not set")
   461  		}
   462  	}
   463  	// the arch we run on
   464  	if *arch == "" {
   465  		*arch = runtime.GOARCH
   466  		if *arch == "" {
   467  			panic("runtime.GOARCH was not set")
   468  		}
   469  	}
   470  
   471  	// cacheDir
   472  	cacheDir = filepath.Join(os.TempDir(), "camlibot-cache")
   473  	if err := os.MkdirAll(cacheDir, 0755); err != nil {
   474  		log.Fatalf("Could not create cache dir %v: %v", cacheDir, err)
   475  	}
   476  
   477  	// get go1 and gotip source
   478  	if err := os.Chdir(cacheDir); err != nil {
   479  		log.Fatalf("Could not cd to %v: %v", cacheDir, err)
   480  	}
   481  	go1Dir, err = filepath.Abs("go1")
   482  	if err != nil {
   483  		log.Fatalf("Problem with Go 1 dir: %v", err)
   484  	}
   485  	goTipDir, err = filepath.Abs("gotip")
   486  	if err != nil {
   487  		log.Fatalf("Problem with Go tip dir: %v", err)
   488  	}
   489  	for _, goDir := range []string{go1Dir, goTipDir} {
   490  		// if go dirs exist, just reuse them
   491  		if _, err := os.Stat(goDir); err != nil {
   492  			if !os.IsNotExist(err) {
   493  				log.Fatalf("Could not stat %v: %v", goDir, err)
   494  			}
   495  			// go1/gotip dir not here, let's clone it.
   496  			hgCloneCmd := hgCloneGo1Cmd
   497  			if goDir == goTipDir {
   498  				hgCloneCmd = hgCloneGoTipCmd
   499  			}
   500  			tsk := newTask(hgCloneCmd.Program, hgCloneCmd.Args...)
   501  			tsk.hidden = true
   502  			if _, err := tsk.run(); err != nil {
   503  				log.Fatalf("Could not hg clone %v: %v", goDir, err)
   504  			}
   505  			if err := os.Rename("go", goDir); err != nil {
   506  				log.Fatalf("Could not rename go dir into %v: %v", goDir, err)
   507  			}
   508  		}
   509  	}
   510  
   511  	if !*skipGo1Build {
   512  		// build Go1
   513  		if err := buildGo1(); err != nil {
   514  			log.Fatal(err)
   515  		}
   516  	}
   517  
   518  	// get camlistore source
   519  	if err := os.Chdir(cacheDir); err != nil {
   520  		log.Fatal("Could not cd to %v: %v", cacheDir, err)
   521  	}
   522  	camliRoot, err = filepath.Abs("camlistore.org")
   523  	if err != nil {
   524  		log.Fatal(err)
   525  	}
   526  	// if camlistore dir already exists, reuse it
   527  	if _, err := os.Stat(camliRoot); err != nil {
   528  		if !os.IsNotExist(err) {
   529  			log.Fatalf("Could not stat %v: %v", camliRoot, err)
   530  		}
   531  		cloneCmd := newTask(gitCloneCmd.Program, append(gitCloneCmd.Args, camliRoot)...)
   532  		cloneCmd.hidden = true
   533  		if _, err := cloneCmd.run(); err != nil {
   534  			log.Fatalf("Could not git clone into %v: %v", camliRoot, err)
   535  		}
   536  	}
   537  
   538  	// recording camput cache dir, so we can clean it up fast everytime
   539  	homeDir := os.Getenv("HOME")
   540  	if homeDir == "" {
   541  		log.Fatal("HOME not set")
   542  	}
   543  	camputCacheDir = filepath.Join(homeDir, ".cache", "camlistore")
   544  }
   545  
   546  func buildGo1() error {
   547  	if err := os.Chdir(filepath.Join(go1Dir, "src")); err != nil {
   548  		log.Fatalf("Could not cd to %v: %v", go1Dir, err)
   549  	}
   550  	tsk := newTask(buildGoCmd.Program, buildGoCmd.Args...)
   551  	tsk.hidden = true
   552  	if _, err := tsk.run(); err != nil {
   553  		return err
   554  	}
   555  	return nil
   556  }
   557  
   558  func handleSignals() {
   559  	c := make(chan os.Signal)
   560  	sigs := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT}
   561  	signal.Notify(c, sigs...)
   562  	for {
   563  		sig := <-c
   564  		sysSig, ok := sig.(syscall.Signal)
   565  		if !ok {
   566  			log.Fatal("Not a unix signal")
   567  		}
   568  		switch sysSig {
   569  		case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
   570  			log.Printf("Received %v signal, terminating.", sig)
   571  			killCamli()
   572  			os.Exit(0)
   573  		default:
   574  			panic("should not get other signals here")
   575  		}
   576  	}
   577  }
   578  
   579  var plausibleHashRx = regexp.MustCompile(`^[a-f0-9]{40}$`)
   580  
   581  func prepGoTipTree() error {
   582  	if err := os.Chdir(goTipDir); err != nil {
   583  		return fmt.Errorf("Could not cd to %v: %v", goTipDir, err)
   584  	}
   585  	tasks := []*task{
   586  		newTaskFrom(hgPullCmd),
   587  		newTaskFrom(hgUpdateCmd),
   588  		newTaskFrom(hgLogCmd),
   589  		newTaskFrom(hgConfigCmd),
   590  	}
   591  	hash := ""
   592  	for _, t := range tasks {
   593  		out, err := t.run()
   594  		if err != nil {
   595  			return fmt.Errorf("Could not prepare the Go tip tree with %v: %v", t.String(), err)
   596  		}
   597  		if t.String() == hgLogCmd.String() {
   598  			hash = strings.TrimRight(out, "\n")
   599  		}
   600  	}
   601  	if !plausibleHashRx.MatchString(hash) {
   602  		return fmt.Errorf("Go rev %q does not look like an hg hash.", hash)
   603  	}
   604  	goTipHash = hash
   605  	dbg.Println("current head in go tree: " + goTipHash)
   606  	return nil
   607  }
   608  
   609  func buildGoTip() error {
   610  	srcDir := filepath.Join(goTipDir, "src")
   611  	if err := os.Chdir(srcDir); err != nil {
   612  		return fmt.Errorf("Could not cd to %v: %v", srcDir, err)
   613  	}
   614  	if _, err := newTaskFrom(buildGoCmd).run(); err != nil {
   615  		return err
   616  	}
   617  	return nil
   618  }
   619  
   620  func prepCamliTree(isTip bool) error {
   621  	if err := os.Chdir(camliRoot); err != nil {
   622  		return fmt.Errorf("Could not cd to %v: %v", camliRoot, err)
   623  	}
   624  	rev := "HEAD"
   625  	if isTip {
   626  		if !plausibleHashRx.MatchString(camliHeadHash) {
   627  			// the run with Go 1 should have taken care of setting camliHeadHash
   628  			return errors.New("camliHeadHash hasn't been set properly in the Go 1 run")
   629  		}
   630  		// we reset to the rev that was noted at the previous run with Go 1
   631  		// because we want to do both runs at the same rev
   632  		rev = camliHeadHash
   633  	}
   634  	resetCmd := newTask(gitResetCmd.Program, append(gitResetCmd.Args, rev)...)
   635  	tasks := []*task{
   636  		resetCmd,
   637  		newTaskFrom(gitCleanCmd),
   638  	}
   639  	for _, t := range tasks {
   640  		_, err := t.run()
   641  		if err != nil {
   642  			return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
   643  		}
   644  	}
   645  	if isTip {
   646  		// We only need to pull and get the camli head hash when in the Go 1 run
   647  		return nil
   648  	}
   649  	tasks = []*task{
   650  		newTaskFrom(gitPullCmd),
   651  		newTaskFrom(gitRevCmd),
   652  	}
   653  	hash := ""
   654  	for _, t := range tasks {
   655  		out, err := t.run()
   656  		if err != nil {
   657  			return fmt.Errorf("Could not prepare the Camli tree with %v: %v\n", t.String(), err)
   658  		}
   659  		hash = strings.TrimRight(out, "\n")
   660  	}
   661  	if !plausibleHashRx.MatchString(hash) {
   662  		return fmt.Errorf("Camlistore rev %q does not look like a git hash.", hash)
   663  	}
   664  	camliHeadHash = hash
   665  	return nil
   666  }
   667  
   668  func restorePATH() {
   669  	err := os.Setenv("PATH", defaultPATH)
   670  	if err != nil {
   671  		log.Fatalf("Could not set PATH to %v: %v", defaultPATH, err)
   672  	}
   673  }
   674  
   675  func switchGo(goDir string) {
   676  	if runtime.GOOS == "plan9" {
   677  		panic("plan 9 not unsupported")
   678  	}
   679  	gobin := filepath.Join(goDir, "bin", "go")
   680  	if _, err := os.Stat(gobin); err != nil {
   681  		log.Fatalf("Could not stat 'go' bin at %q: %v", gobin, err)
   682  	}
   683  	p := filepath.Join(goDir, "bin") + string(filepath.ListSeparator) + defaultPATH
   684  	if err := os.Setenv("PATH", p); err != nil {
   685  		log.Fatalf("Could not set PATH to %v: %v", p, err)
   686  	}
   687  	if err := os.Setenv("GOROOT", goDir); err != nil {
   688  		log.Fatalf("Could not set GOROOT to %v: %v", goDir, err)
   689  	}
   690  }
   691  
   692  func cleanBuildGopaths() {
   693  	tmpDir := filepath.Join(camliRoot, "tmp")
   694  	if _, err := os.Stat(tmpDir); err != nil {
   695  		if !os.IsNotExist(err) {
   696  			log.Fatalf("Could not stat %v: %v", tmpDir, err)
   697  		}
   698  		// Does not exist, we only have to recreate it
   699  		// TODO(mpl): hmm maybe it should be an error that
   700  		// it does not exist, since it also contains the
   701  		// closure stuff?
   702  		if err := os.MkdirAll(tmpDir, 0755); err != nil {
   703  			log.Fatalf("Could not mkdir %v: %v", tmpDir, err)
   704  		}
   705  		return
   706  	}
   707  	f, err := os.Open(tmpDir)
   708  	if err != nil {
   709  		log.Fatalf("Could not open %v: %v", tmpDir, err)
   710  	}
   711  	defer f.Close()
   712  	names, err := f.Readdirnames(-1)
   713  	if err != nil {
   714  		log.Fatal("Could not read %v: %v", tmpDir, err)
   715  	}
   716  	for _, v := range names {
   717  		if strings.HasPrefix(v, "build-gopath") {
   718  			if err := os.RemoveAll(filepath.Join(tmpDir, v)); err != nil {
   719  				log.Fatalf("Could not remove %v: %v", v, err)
   720  			}
   721  		}
   722  	}
   723  }
   724  
   725  func fakeRun() error {
   726  	if _, err := newTask("sleep", "1").run(); err != nil {
   727  		return err
   728  	}
   729  	return nil
   730  }
   731  
   732  func buildCamli() error {
   733  	if err := os.Chdir(camliRoot); err != nil {
   734  		log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   735  	}
   736  	// Clean up Camlistore's hermetic gopaths
   737  	cleanBuildGopaths()
   738  
   739  	if *verbose {
   740  		tsk := newTask("go", "version")
   741  		out, err := tsk.run()
   742  		tsk.hidden = true
   743  		if err != nil {
   744  			return fmt.Errorf("failed to run 'go version': %v", err)
   745  		}
   746  		out = strings.TrimRight(out, "\n")
   747  		dbg.Printf("Building Camlistore with: %v\n", out)
   748  	}
   749  	if _, err := newTaskFrom(buildCamliCmd).run(); err != nil {
   750  		return err
   751  	}
   752  	return nil
   753  }
   754  
   755  func runCamli() error {
   756  	if err := os.Chdir(camliRoot); err != nil {
   757  		log.Fatal(err)
   758  	}
   759  
   760  	t := newTaskFrom(runCamliCmd)
   761  	dbg.Println(t.String())
   762  	cmd := exec.Command(t.Program, t.Args...)
   763  	var output []byte
   764  	errc := make(chan error, 1)
   765  	t.Start = time.Now()
   766  	go func() {
   767  		var err error
   768  		output, err = cmd.CombinedOutput()
   769  		if err != nil {
   770  			err = fmt.Errorf("%v: %v", err, string(output))
   771  		}
   772  		errc <- err
   773  	}()
   774  	select {
   775  	case err := <-errc:
   776  		t.Err = fmt.Sprintf("%v terminated early:\n%v\n", t.String(), err)
   777  		biSuitelk.Lock()
   778  		currentTestSuite.addRun(t)
   779  		biSuitelk.Unlock()
   780  		log.Println(t.Err)
   781  		return errors.New(t.Err)
   782  	case <-time.After(warmup):
   783  		biSuitelk.Lock()
   784  		currentTestSuite.addRun(t)
   785  		camliProc = cmd.Process
   786  		biSuitelk.Unlock()
   787  		dbg.Printf("%v running OK so far\n", t.String())
   788  	}
   789  	return nil
   790  }
   791  
   792  func killCamli() {
   793  	if camliProc == nil {
   794  		return
   795  	}
   796  	dbg.Println("killing Camlistore server")
   797  	if err := camliProc.Kill(); err != nil {
   798  		log.Fatalf("Could not kill server with pid %v: %v", camliProc.Pid, err)
   799  	}
   800  	camliProc = nil
   801  	dbg.Println("")
   802  }
   803  
   804  func hitCamliUi() error {
   805  	return hitURL("http://localhost:3179/ui/")
   806  }
   807  
   808  func hitURL(url string) (err error) {
   809  	tsk := newTask("http.Get", url)
   810  	defer func() {
   811  		if err != nil {
   812  			tsk.Err = fmt.Sprintf("%v", err)
   813  		}
   814  		biSuitelk.Lock()
   815  		currentTestSuite.addRun(tsk)
   816  		biSuitelk.Unlock()
   817  	}()
   818  	dbg.Println(tsk.String())
   819  	tsk.Start = time.Now()
   820  	var resp *http.Response
   821  	resp, err = http.Get(url)
   822  	if err != nil {
   823  		return fmt.Errorf("%v: %v\n", tsk.String(), err)
   824  	}
   825  	defer resp.Body.Close()
   826  	if resp.StatusCode != 200 {
   827  		return fmt.Errorf("%v, got StatusCode: %d\n", tsk.String(), resp.StatusCode)
   828  	}
   829  	return nil
   830  }
   831  
   832  func camputOne(vivify bool) error {
   833  	if err := os.Chdir(camliRoot); err != nil {
   834  		log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   835  	}
   836  
   837  	// clean up camput caches
   838  	if err := os.RemoveAll(camputCacheDir); err != nil {
   839  		log.Fatalf("Problem cleaning up camputCacheDir %v: %v", camputCacheDir, err)
   840  	}
   841  
   842  	// push the file to camli
   843  	tsk := newTaskFrom(camputCmd)
   844  	if vivify {
   845  		tsk = newTaskFrom(camputVivifyCmd)
   846  	}
   847  	out, err := tsk.run()
   848  	if err != nil {
   849  		return err
   850  	}
   851  	// TODO(mpl): parsing camput output is kinda weak.
   852  	firstSHA1 := regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\nsha1-[a-zA-Z0-9]+\nsha1-[a-zA-Z0-9]+\n.*`)
   853  	if vivify {
   854  		firstSHA1 = regexp.MustCompile(`.*(sha1-[a-zA-Z0-9]+)\n.*`)
   855  	}
   856  	m := firstSHA1.FindStringSubmatch(out)
   857  	if m == nil {
   858  		return fmt.Errorf("%v: unexpected camput output\n", tsk.String())
   859  	}
   860  	blobref := m[1]
   861  
   862  	// get the file's json to find out the file's blobref
   863  	tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...)
   864  	out, err = tsk.run()
   865  	if err != nil {
   866  		return err
   867  	}
   868  	blobrefPattern := regexp.MustCompile(`"blobRef": "(sha1-[a-zA-Z0-9]+)",\n.*`)
   869  	m = blobrefPattern.FindStringSubmatch(out)
   870  	if m == nil {
   871  		return fmt.Errorf("%v: unexpected camget output\n", tsk.String())
   872  	}
   873  	blobref = m[1]
   874  
   875  	// finally, get the file back
   876  	tsk = newTask(camgetCmd.Program, append(camgetCmd.Args, blobref)...)
   877  	out, err = tsk.run()
   878  	if err != nil {
   879  		return err
   880  	}
   881  
   882  	// and compare it with the original
   883  	wantFile := testFile[0]
   884  	if vivify {
   885  		wantFile = testFile[1]
   886  	}
   887  	fileContents, err := ioutil.ReadFile(wantFile)
   888  	if err != nil {
   889  		log.Fatalf("Could not read %v: %v", wantFile, err)
   890  	}
   891  	if string(fileContents) != out {
   892  		return fmt.Errorf("%v: contents fetched with camget differ from %v contents", tsk.String(), wantFile)
   893  	}
   894  	return nil
   895  }
   896  
   897  func camputMany() error {
   898  	err := os.Chdir(camliRoot)
   899  	if err != nil {
   900  		log.Fatalf("Could not cd to %v: %v", camliRoot, err)
   901  	}
   902  
   903  	// upload the full camli pkg tree
   904  	if _, err := newTaskFrom(camputFilenodesCmd).run(); err != nil {
   905  		return err
   906  	}
   907  	return nil
   908  }
   909  
   910  func runTests() error {
   911  	if err := os.Chdir(camliRoot); err != nil {
   912  		log.Fatal(err)
   913  	}
   914  	if _, err := newTaskFrom(runTestsCmd).run(); err != nil {
   915  		return err
   916  	}
   917  	return nil
   918  }
   919  
   920  const reportPrefix = "/report"
   921  
   922  func postToURL(u string, r io.Reader) (*http.Response, error) {
   923  	// Default to plain HTTP.
   924  	if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) {
   925  		u = "http://" + u
   926  	}
   927  	uri, err := url.Parse(u)
   928  	if err != nil {
   929  		return nil, err
   930  	}
   931  
   932  	// If the URL explicitly specifies "/" or something else, we'll POST to
   933  	// that, otherwise default to build-time default.
   934  	if uri.Path == "" {
   935  		uri.Path = reportPrefix
   936  	}
   937  
   938  	// Save user/pass if specified in the URL.
   939  	user := uri.User
   940  	// But don't send user/pass in URL to server.
   941  	uri.User = nil
   942  
   943  	req, err := http.NewRequest("POST", uri.String(), r)
   944  	if err != nil {
   945  		return nil, err
   946  	}
   947  	req.Header.Set("Content-Type", "text/javascript")
   948  	// If user/pass set on original URL, set the auth header for the request.
   949  	if user != nil {
   950  		pass, ok := user.Password()
   951  		if !ok {
   952  			log.Println("Password not set for", user.Username(), "in", u)
   953  		}
   954  		req.SetBasicAuth(user.Username(), pass)
   955  	}
   956  	return client.Do(req)
   957  }
   958  
   959  func sendReport() {
   960  	biSuitelk.Lock()
   961  	// we make a copy so we can release the lock quickly enough
   962  	currentBiSuiteCpy := &biTestSuite{
   963  		Go1:   currentBiSuite.Go1,
   964  		GoTip: currentBiSuite.GoTip,
   965  	}
   966  	biSuitelk.Unlock()
   967  	masters := strings.Split(*masterHosts, ",")
   968  	OSArch := *ourOS + "_" + *arch
   969  	toReport := struct {
   970  		OSArch string
   971  		Ts     *biTestSuite
   972  	}{
   973  		OSArch: OSArch,
   974  		Ts:     currentBiSuiteCpy,
   975  	}
   976  	for _, v := range masters {
   977  		// TODO(mpl): ipv6 too I suppose. just make a IsLocalhost func or whatever.
   978  		// probably can borrow something from camli code for that.
   979  		if strings.HasPrefix(v, "localhost") || strings.HasPrefix(v, "127.0.0.1") {
   980  			toReport.Ts.Local = true
   981  		} else {
   982  			toReport.Ts.Local = false
   983  		}
   984  		report, err := json.MarshalIndent(toReport, "", "  ")
   985  		if err != nil {
   986  			log.Printf("JSON serialization error: %v", err)
   987  			return
   988  		}
   989  		r := bytes.NewReader(report)
   990  		resp, err := postToURL(v, r)
   991  		if err != nil {
   992  			log.Printf("Could not send report: %v", err)
   993  			continue
   994  		}
   995  		resp.Body.Close()
   996  	}
   997  }
   998  
   999  func progressHandler(w http.ResponseWriter, r *http.Request) {
  1000  	if r.Method != "GET" {
  1001  		log.Printf("Invalid method in progress handler: %v, want GET", r.Method)
  1002  		http.Error(w, "Invalid method", http.StatusMethodNotAllowed)
  1003  		return
  1004  	}
  1005  	if checkLastModified(w, r, lastModified) {
  1006  		return
  1007  	}
  1008  	biSuitelk.Lock()
  1009  	if currentBiSuite != nil {
  1010  		if currentTestSuite.IsTip {
  1011  			currentBiSuite.GoTip = *currentTestSuite
  1012  		} else {
  1013  			currentBiSuite.Go1 = *currentTestSuite
  1014  		}
  1015  	}
  1016  	sanitizeRevs()
  1017  	report, err := json.MarshalIndent(currentBiSuite, "", "  ")
  1018  	if err != nil {
  1019  		biSuitelk.Unlock()
  1020  		log.Printf("JSON serialization error: %v", err)
  1021  		http.Error(w, "internal error", http.StatusInternalServerError)
  1022  		return
  1023  	}
  1024  	biSuitelk.Unlock()
  1025  	_, err = io.Copy(w, bytes.NewReader(report))
  1026  	if err != nil {
  1027  		log.Printf("Could not send progress report: %v", err)
  1028  	}
  1029  }
  1030  
  1031  // modtime is the modification time of the resource to be served, or IsZero().
  1032  // return value is whether this request is now complete.
  1033  func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {
  1034  	if modtime.IsZero() {
  1035  		return false
  1036  	}
  1037  
  1038  	// The Date-Modified header truncates sub-second precision, so
  1039  	// use mtime < t+1s instead of mtime <= t to check for unmodified.
  1040  	if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
  1041  		h := w.Header()
  1042  		delete(h, "Content-Type")
  1043  		delete(h, "Content-Length")
  1044  		w.WriteHeader(http.StatusNotModified)
  1045  		return true
  1046  	}
  1047  	w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
  1048  	return false
  1049  }