summaryrefslogtreecommitdiff
path: root/misc/dashboard/builder/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'misc/dashboard/builder/main.go')
-rw-r--r--misc/dashboard/builder/main.go487
1 files changed, 273 insertions, 214 deletions
diff --git a/misc/dashboard/builder/main.go b/misc/dashboard/builder/main.go
index 989965bc4..85bb7ad4b 100644
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -5,56 +5,54 @@
package main
import (
+ "bytes"
+ "encoding/xml"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
- "path"
+ "path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
- "xml"
)
const (
codeProject = "go"
codePyScript = "misc/dashboard/googlecode_upload.py"
- hgUrl = "https://go.googlecode.com/hg/"
- waitInterval = 30e9 // time to wait before checking for new revs
+ hgUrl = "https://code.google.com/p/go/"
mkdirPerm = 0750
- pkgBuildInterval = 1e9 * 60 * 60 * 24 // rebuild packages every 24 hours
+ waitInterval = 30 * time.Second // time to wait before checking for new revs
+ pkgBuildInterval = 24 * time.Hour // rebuild packages every 24 hours
)
// These variables are copied from the gobuilder's environment
// to the envv of its subprocesses.
var extraEnv = []string{
- "GOHOSTOS",
+ "GOARM",
"GOHOSTARCH",
+ "GOHOSTOS",
"PATH",
- "DISABLE_NET_TESTS",
- "MAKEFLAGS",
- "GOARM",
+ "TMPDIR",
}
type Builder struct {
name string
goos, goarch string
key string
- codeUsername string
- codePassword string
}
var (
- buildroot = flag.String("buildroot", path.Join(os.TempDir(), "gobuilder"), "Directory under which to build")
+ buildroot = flag.String("buildroot", filepath.Join(os.TempDir(), "gobuilder"), "Directory under which to build")
commitFlag = flag.Bool("commit", false, "upload information about new commits")
- dashboard = flag.String("dashboard", "godashboard.appspot.com", "Go Dashboard Host")
+ dashboard = flag.String("dashboard", "build.golang.org", "Go Dashboard Host")
buildRelease = flag.Bool("release", false, "Build and upload binary release archives")
buildRevision = flag.String("rev", "", "Build specified revision and exit")
- buildCmd = flag.String("cmd", "./all.bash", "Build command (specify absolute or relative to go/src/)")
- external = flag.Bool("external", false, "Build external packages")
+ buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)")
+ failAll = flag.Bool("fail", false, "fail all builds")
parallel = flag.Bool("parallel", false, "Build multiple targets in parallel")
verbose = flag.Bool("v", false, "verbose")
)
@@ -63,6 +61,9 @@ var (
goroot string
binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`)
releaseRe = regexp.MustCompile(`^release\.r[0-9\-.]+`)
+ allCmd = "all" + suffix
+ cleanCmd = "clean" + suffix
+ suffix = defaultSuffix()
)
func main() {
@@ -75,7 +76,7 @@ func main() {
if len(flag.Args()) == 0 && !*commitFlag {
flag.Usage()
}
- goroot = path.Join(*buildroot, "goroot")
+ goroot = filepath.Join(*buildroot, "goroot")
builders := make([]*Builder, len(flag.Args()))
for i, builder := range flag.Args() {
b, err := NewBuilder(builder)
@@ -85,15 +86,24 @@ func main() {
builders[i] = b
}
- // set up work environment
- if err := os.RemoveAll(*buildroot); err != nil {
- log.Fatalf("Error removing build root (%s): %s", *buildroot, err)
- }
- if err := os.Mkdir(*buildroot, mkdirPerm); err != nil {
- log.Fatalf("Error making build root (%s): %s", *buildroot, err)
+ if *failAll {
+ failMode(builders)
+ return
}
- if err := run(nil, *buildroot, "hg", "clone", hgUrl, goroot); err != nil {
- log.Fatal("Error cloning repository:", err)
+
+ // set up work environment, use existing enviroment if possible
+ if hgRepoExists(goroot) {
+ log.Print("Found old workspace, will use it")
+ } else {
+ if err := os.RemoveAll(*buildroot); err != nil {
+ log.Fatalf("Error removing build root (%s): %s", *buildroot, err)
+ }
+ if err := os.Mkdir(*buildroot, mkdirPerm); err != nil {
+ log.Fatalf("Error making build root (%s): %s", *buildroot, err)
+ }
+ if err := hgClone(hgUrl, goroot); err != nil {
+ log.Fatal("Error cloning repository:", err)
+ }
}
if *commitFlag {
@@ -106,7 +116,7 @@ func main() {
// if specified, build revision and return
if *buildRevision != "" {
- hash, err := fullHash(*buildRevision)
+ hash, err := fullHash(goroot, *buildRevision)
if err != nil {
log.Fatal("Error finding revision: ", err)
}
@@ -118,19 +128,11 @@ func main() {
return
}
- // external package build mode
- if *external {
- if len(builders) != 1 {
- log.Fatal("only one goos-goarch should be specified with -external")
- }
- builders[0].buildExternal()
- }
-
// go continuous build mode (default)
// check for new commits and build them
for {
built := false
- t := time.Nanoseconds()
+ t := time.Now()
if *parallel {
done := make(chan bool)
for _, b := range builders {
@@ -151,14 +153,29 @@ func main() {
time.Sleep(waitInterval)
}
// sleep if we're looping too fast.
- t1 := time.Nanoseconds() - t
- if t1 < waitInterval {
- time.Sleep(waitInterval - t1)
+ dt := time.Now().Sub(t)
+ if dt < waitInterval {
+ time.Sleep(waitInterval - dt)
+ }
+ }
+}
+
+// go continuous fail mode
+// check for new commits and FAIL them
+func failMode(builders []*Builder) {
+ for {
+ built := false
+ for _, b := range builders {
+ built = b.failBuild() || built
+ }
+ // stop if there was nothing to fail
+ if !built {
+ break
}
}
}
-func NewBuilder(builder string) (*Builder, os.Error) {
+func NewBuilder(builder string) (*Builder, error) {
b := &Builder{name: builder}
// get goos/goarch from builder string
@@ -170,7 +187,13 @@ func NewBuilder(builder string) (*Builder, os.Error) {
}
// read keys from keyfile
- fn := path.Join(os.Getenv("HOME"), ".gobuildkey")
+ fn := ""
+ if runtime.GOOS == "windows" {
+ fn = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
+ } else {
+ fn = os.Getenv("HOME")
+ }
+ fn = filepath.Join(fn, ".gobuildkey")
if s := fn + "-" + b.name; isFile(s) { // builder-specific file
fn = s
}
@@ -178,64 +201,15 @@ func NewBuilder(builder string) (*Builder, os.Error) {
if err != nil {
return nil, fmt.Errorf("readKeys %s (%s): %s", b.name, fn, err)
}
- v := strings.Split(string(c), "\n")
- b.key = v[0]
- if len(v) >= 3 {
- b.codeUsername, b.codePassword = v[1], v[2]
- }
-
+ b.key = string(bytes.TrimSpace(bytes.SplitN(c, []byte("\n"), 2)[0]))
return b, nil
}
-// buildExternal downloads and builds external packages, and
-// reports their build status to the dashboard.
-// It will re-build all packages after pkgBuildInterval nanoseconds or
-// a new release tag is found.
-func (b *Builder) buildExternal() {
- var prevTag string
- var nextBuild int64
- for {
- time.Sleep(waitInterval)
- err := run(nil, goroot, "hg", "pull", "-u")
- if err != nil {
- log.Println("hg pull failed:", err)
- continue
- }
- hash, tag, err := firstTag(releaseRe)
- if err != nil {
- log.Println(err)
- continue
- }
- if *verbose {
- log.Println("latest release:", tag)
- }
- // don't rebuild if there's no new release
- // and it's been less than pkgBuildInterval
- // nanoseconds since the last build.
- if tag == prevTag && time.Nanoseconds() < nextBuild {
- continue
- }
- // build will also build the packages
- if err := b.buildHash(hash); err != nil {
- log.Println(err)
- continue
- }
- prevTag = tag
- nextBuild = time.Nanoseconds() + pkgBuildInterval
- }
-}
-
// build checks for a new commit for this builder
// and builds it if one is found.
// It returns true if a build was attempted.
func (b *Builder) build() bool {
- defer func() {
- err := recover()
- if err != nil {
- log.Println(b.name, "build:", err)
- }
- }()
- hash, err := b.todo()
+ hash, err := b.todo("build-go-commit", "", "")
if err != nil {
log.Println(err)
return false
@@ -244,8 +218,7 @@ func (b *Builder) build() bool {
return false
}
// Look for hash locally before running hg pull.
-
- if _, err := fullHash(hash[:12]); err != nil {
+ if _, err := fullHash(goroot, hash[:12]); err != nil {
// Don't have hash, so run hg pull.
if err := run(nil, goroot, "hg", "pull"); err != nil {
log.Println("hg pull failed:", err)
@@ -259,92 +232,153 @@ func (b *Builder) build() bool {
return true
}
-func (b *Builder) buildHash(hash string) (err os.Error) {
- defer func() {
- if err != nil {
- err = fmt.Errorf("%s build: %s: %s", b.name, hash, err)
- }
- }()
-
+func (b *Builder) buildHash(hash string) error {
log.Println(b.name, "building", hash)
// create place in which to do work
- workpath := path.Join(*buildroot, b.name+"-"+hash[:12])
- err = os.Mkdir(workpath, mkdirPerm)
- if err != nil {
- return
+ workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12])
+ if err := os.Mkdir(workpath, mkdirPerm); err != nil {
+ return err
}
defer os.RemoveAll(workpath)
// clone repo
- err = run(nil, workpath, "hg", "clone", goroot, "go")
- if err != nil {
- return
+ if err := run(nil, workpath, "hg", "clone", goroot, "go"); err != nil {
+ return err
}
// update to specified revision
- err = run(nil, path.Join(workpath, "go"),
- "hg", "update", hash)
- if err != nil {
- return
+ if err := run(nil, filepath.Join(workpath, "go"), "hg", "update", hash); err != nil {
+ return err
}
- srcDir := path.Join(workpath, "go", "src")
+ srcDir := filepath.Join(workpath, "go", "src")
// build
- logfile := path.Join(workpath, "build.log")
- buildLog, status, err := runLog(b.envv(), logfile, srcDir, *buildCmd)
+ logfile := filepath.Join(workpath, "build.log")
+ cmd := *buildCmd
+ if !filepath.IsAbs(cmd) {
+ cmd = filepath.Join(srcDir, cmd)
+ }
+ startTime := time.Now()
+ buildLog, status, err := runLog(b.envv(), logfile, srcDir, cmd)
+ runTime := time.Now().Sub(startTime)
if err != nil {
return fmt.Errorf("%s: %s", *buildCmd, err)
}
- // if we're in external mode, build all packages and return
- if *external {
- if status != 0 {
- return os.NewError("go build failed")
- }
- return b.buildPackages(workpath, hash)
- }
-
if status != 0 {
// record failure
- return b.recordResult(buildLog, hash)
+ return b.recordResult(false, "", hash, "", buildLog, runTime)
}
// record success
- if err = b.recordResult("", hash); err != nil {
+ if err = b.recordResult(true, "", hash, "", "", runTime); err != nil {
return fmt.Errorf("recordResult: %s", err)
}
- // finish here if codeUsername and codePassword aren't set
- if b.codeUsername == "" || b.codePassword == "" || !*buildRelease {
- return
+ // build Go sub-repositories
+ b.buildSubrepos(filepath.Join(workpath, "go"), hash)
+
+ return nil
+}
+
+// failBuild checks for a new commit for this builder
+// and fails it if one is found.
+// It returns true if a build was "attempted".
+func (b *Builder) failBuild() bool {
+ hash, err := b.todo("build-go-commit", "", "")
+ if err != nil {
+ log.Println(err)
+ return false
}
+ if hash == "" {
+ return false
+ }
+
+ log.Printf("fail %s %s\n", b.name, hash)
- // if this is a release, create tgz and upload to google code
- releaseHash, release, err := firstTag(binaryTagRe)
- if hash == releaseHash {
- // clean out build state
- err = run(b.envv(), srcDir, "./clean.bash", "--nopkg")
+ if err := b.recordResult(false, "", hash, "", "auto-fail mode run by "+os.Getenv("USER"), 0); err != nil {
+ log.Print(err)
+ }
+ return true
+}
+
+func (b *Builder) buildSubrepos(goRoot, goHash string) {
+ for _, pkg := range dashboardPackages("subrepo") {
+ // get the latest todo for this package
+ hash, err := b.todo("build-package", pkg, goHash)
if err != nil {
- return fmt.Errorf("clean.bash: %s", err)
+ log.Printf("buildSubrepos %s: %v", pkg, err)
+ continue
}
- // upload binary release
- fn := fmt.Sprintf("go.%s.%s-%s.tar.gz", release, b.goos, b.goarch)
- err = run(nil, workpath, "tar", "czf", fn, "go")
+ if hash == "" {
+ continue
+ }
+
+ // build the package
+ if *verbose {
+ log.Printf("buildSubrepos %s: building %q", pkg, hash)
+ }
+ buildLog, err := b.buildSubrepo(goRoot, pkg, hash)
+ if err != nil {
+ if buildLog == "" {
+ buildLog = err.Error()
+ }
+ log.Printf("buildSubrepos %s: %v", pkg, err)
+ }
+
+ // record the result
+ err = b.recordResult(err == nil, pkg, hash, goHash, buildLog, 0)
if err != nil {
- return fmt.Errorf("tar: %s", err)
+ log.Printf("buildSubrepos %s: %v", pkg, err)
}
- err = run(nil, workpath, path.Join(goroot, codePyScript),
- "-s", release,
- "-p", codeProject,
- "-u", b.codeUsername,
- "-w", b.codePassword,
- "-l", fmt.Sprintf("%s,%s", b.goos, b.goarch),
- fn)
+ }
+}
+
+// buildSubrepo fetches the given package, updates it to the specified hash,
+// and runs 'go test -short pkg/...'. It returns the build log and any error.
+func (b *Builder) buildSubrepo(goRoot, pkg, hash string) (string, error) {
+ goBin := filepath.Join(goRoot, "bin")
+ goTool := filepath.Join(goBin, "go")
+ env := append(b.envv(), "GOROOT="+goRoot)
+
+ // add goBin to PATH
+ for i, e := range env {
+ const p = "PATH="
+ if !strings.HasPrefix(e, p) {
+ continue
+ }
+ env[i] = p + goBin + string(os.PathListSeparator) + e[len(p):]
}
- return
+ // fetch package and dependencies
+ log, status, err := runLog(env, "", goRoot, goTool, "get", "-d", pkg)
+ if err == nil && status != 0 {
+ err = fmt.Errorf("go exited with status %d", status)
+ }
+ if err != nil {
+ // 'go get -d' will fail for a subrepo because its top-level
+ // directory does not contain a go package. No matter, just
+ // check whether an hg directory exists and proceed.
+ hgDir := filepath.Join(goRoot, "src/pkg", pkg, ".hg")
+ if fi, e := os.Stat(hgDir); e != nil || !fi.IsDir() {
+ return log, err
+ }
+ }
+
+ // hg update to the specified hash
+ pkgPath := filepath.Join(goRoot, "src/pkg", pkg)
+ if err := run(nil, pkgPath, "hg", "update", hash); err != nil {
+ return "", err
+ }
+
+ // test the package
+ log, status, err = runLog(env, "", goRoot, goTool, "test", "-short", pkg+"/...")
+ if err == nil && status != 0 {
+ err = fmt.Errorf("go exited with status %d", status)
+ }
+ return log, err
}
// envv returns an environment for build/bench execution
@@ -358,8 +392,7 @@ func (b *Builder) envv() []string {
"GOROOT_FINAL=/usr/local/go",
}
for _, k := range extraEnv {
- s, err := os.Getenverror(k)
- if err == nil {
+ if s, ok := getenvOk(k); ok {
e = append(e, k+"="+s)
}
}
@@ -371,13 +404,11 @@ func (b *Builder) envvWindows() []string {
start := map[string]string{
"GOOS": b.goos,
"GOARCH": b.goarch,
- "GOROOT_FINAL": "/c/go",
- // TODO(brainman): remove once we find make that does not hang.
- "MAKEFLAGS": "-j1",
+ "GOROOT_FINAL": `c:\go`,
+ "GOBUILDEXIT": "1", // exit all.bat with completion status.
}
for _, name := range extraEnv {
- s, err := os.Getenverror(name)
- if err == nil {
+ if s, ok := getenvOk(name); ok {
start[name] = s
}
}
@@ -409,12 +440,12 @@ func (b *Builder) envvWindows() []string {
func isDirectory(name string) bool {
s, err := os.Stat(name)
- return err == nil && s.IsDirectory()
+ return err == nil && s.IsDir()
}
func isFile(name string) bool {
s, err := os.Stat(name)
- return err == nil && (s.IsRegular() || s.IsSymlink())
+ return err == nil && !s.IsDir()
}
// commitWatcher polls hg for new commits and tells the dashboard about them.
@@ -424,11 +455,18 @@ func commitWatcher() {
if err != nil {
log.Fatal(err)
}
+ key := b.key
+
for {
if *verbose {
log.Printf("poll...")
}
- commitPoll(b.key)
+ // Main Go repository.
+ commitPoll(key, "")
+ // Go sub-repositories.
+ for _, pkg := range dashboardPackages("subrepo") {
+ commitPoll(key, pkg)
+ }
if *verbose {
log.Printf("sleep...")
}
@@ -436,6 +474,18 @@ func commitWatcher() {
}
}
+func hgClone(url, path string) error {
+ return run(nil, *buildroot, "hg", "clone", url, path)
+}
+
+func hgRepoExists(path string) bool {
+ fi, err := os.Stat(filepath.Join(path, ".hg"))
+ if err != nil {
+ return false
+ }
+ return fi.IsDir()
+}
+
// HgLog represents a single Mercurial revision.
type HgLog struct {
Hash string
@@ -455,33 +505,41 @@ var logByHash = map[string]*HgLog{}
// xmlLogTemplate is a template to pass to Mercurial to make
// hg log print the log in valid XML for parsing with xml.Unmarshal.
const xmlLogTemplate = `
- <log>
- <hash>{node|escape}</hash>
- <parent>{parent|escape}</parent>
- <author>{author|escape}</author>
- <date>{date}</date>
- <desc>{desc|escape}</desc>
- </log>
+ <Log>
+ <Hash>{node|escape}</Hash>
+ <Parent>{parent|escape}</Parent>
+ <Author>{author|escape}</Author>
+ <Date>{date|rfc3339date}</Date>
+ <Desc>{desc|escape}</Desc>
+ </Log>
`
// commitPoll pulls any new revisions from the hg server
// and tells the server about them.
-func commitPoll(key string) {
- // Catch unexpected panics.
- defer func() {
- if err := recover(); err != nil {
- log.Printf("commitPoll panic: %s", err)
+func commitPoll(key, pkg string) {
+ pkgRoot := goroot
+
+ if pkg != "" {
+ pkgRoot = filepath.Join(*buildroot, pkg)
+ if !hgRepoExists(pkgRoot) {
+ if err := hgClone(repoURL(pkg), pkgRoot); err != nil {
+ log.Printf("%s: hg clone failed: %v", pkg, err)
+ if err := os.RemoveAll(pkgRoot); err != nil {
+ log.Printf("%s: %v", pkg, err)
+ }
+ return
+ }
}
- }()
+ }
- if err := run(nil, goroot, "hg", "pull"); err != nil {
+ if err := run(nil, pkgRoot, "hg", "pull"); err != nil {
log.Printf("hg pull: %v", err)
return
}
- const N = 20 // how many revisions to grab
+ const N = 50 // how many revisions to grab
- data, _, err := runLog(nil, "", goroot, "hg", "log",
+ data, _, err := runLog(nil, "", pkgRoot, "hg", "log",
"--encoding=utf-8",
"--limit="+strconv.Itoa(N),
"--template="+xmlLogTemplate,
@@ -494,7 +552,7 @@ func commitPoll(key string) {
var logStruct struct {
Log []HgLog
}
- err = xml.Unmarshal(strings.NewReader("<top>"+data+"</top>"), &logStruct)
+ err = xml.Unmarshal([]byte("<Top>"+data+"</Top>"), &logStruct)
if err != nil {
log.Printf("unmarshal hg log: %v", err)
return
@@ -507,17 +565,14 @@ func commitPoll(key string) {
// Non-empty parent has form 1234:hashhashhash; we want full hash.
for i := range logs {
l := &logs[i]
- log.Printf("hg log: %s < %s\n", l.Hash, l.Parent)
if l.Parent == "" && i+1 < len(logs) {
l.Parent = logs[i+1].Hash
} else if l.Parent != "" {
- l.Parent, _ = fullHash(l.Parent)
+ l.Parent, _ = fullHash(pkgRoot, l.Parent)
}
- if l.Parent == "" {
- // Can't create node without parent.
- continue
+ if *verbose {
+ log.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent)
}
-
if logByHash[l.Hash] == nil {
// Make copy to avoid pinning entire slice when only one entry is new.
t := *l
@@ -527,17 +582,14 @@ func commitPoll(key string) {
for i := range logs {
l := &logs[i]
- if l.Parent == "" {
- continue
- }
- addCommit(l.Hash, key)
+ addCommit(pkg, l.Hash, key)
}
}
// addCommit adds the commit with the named hash to the dashboard.
// key is the secret key for authentication to the dashboard.
// It avoids duplicate effort.
-func addCommit(hash, key string) bool {
+func addCommit(pkg, hash, key string) bool {
l := logByHash[hash]
if l == nil {
return false
@@ -547,7 +599,7 @@ func addCommit(hash, key string) bool {
}
// Check for already added, perhaps in an earlier run.
- if dashboardCommit(hash) {
+ if dashboardCommit(pkg, hash) {
log.Printf("%s already on dashboard\n", hash)
// Record that this hash is on the dashboard,
// as must be all its parents.
@@ -559,12 +611,14 @@ func addCommit(hash, key string) bool {
}
// Create parent first, to maintain some semblance of order.
- if !addCommit(l.Parent, key) {
- return false
+ if l.Parent != "" {
+ if !addCommit(pkg, l.Parent, key) {
+ return false
+ }
}
// Create commit.
- if err := postCommit(key, l); err != nil {
+ if err := postCommit(key, pkg, l); err != nil {
log.Printf("failed to add %s to dashboard: %v", key, err)
return false
}
@@ -572,13 +626,8 @@ func addCommit(hash, key string) bool {
}
// fullHash returns the full hash for the given Mercurial revision.
-func fullHash(rev string) (hash string, err os.Error) {
- defer func() {
- if err != nil {
- err = fmt.Errorf("fullHash: %s: %s", rev, err)
- }
- }()
- s, _, err := runLog(nil, "", goroot,
+func fullHash(root, rev string) (string, error) {
+ s, _, err := runLog(nil, "", root,
"hg", "log",
"--encoding=utf-8",
"--rev="+rev,
@@ -586,7 +635,7 @@ func fullHash(rev string) (hash string, err os.Error) {
"--template={node}",
)
if err != nil {
- return
+ return "", nil
}
s = strings.TrimSpace(s)
if s == "" {
@@ -598,27 +647,37 @@ func fullHash(rev string) (hash string, err os.Error) {
return s, nil
}
-var revisionRe = regexp.MustCompile(`^([^ ]+) +[0-9]+:([0-9a-f]+)$`)
+var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\-]+(\.[a-z0-9\-]+)?)(/[a-z0-9A-Z_.\-/]+)?$`)
-// firstTag returns the hash and tag of the most recent tag matching re.
-func firstTag(re *regexp.Regexp) (hash string, tag string, err os.Error) {
- o, _, err := runLog(nil, "", goroot, "hg", "tags")
- for _, l := range strings.Split(o, "\n") {
- if l == "" {
- continue
- }
- s := revisionRe.FindStringSubmatch(l)
- if s == nil {
- err = os.NewError("couldn't find revision number")
- return
- }
- if !re.MatchString(s[1]) {
- continue
+// repoURL returns the repository URL for the supplied import path.
+func repoURL(importPath string) string {
+ m := repoRe.FindStringSubmatch(importPath)
+ if len(m) < 2 {
+ log.Printf("repoURL: couldn't decipher %q", importPath)
+ return ""
+ }
+ return "https://code.google.com/p/" + m[1]
+}
+
+// defaultSuffix returns file extension used for command files in
+// current os environment.
+func defaultSuffix() string {
+ if runtime.GOOS == "windows" {
+ return ".bat"
+ }
+ return ".bash"
+}
+
+func getenvOk(k string) (v string, ok bool) {
+ v = os.Getenv(k)
+ if v != "" {
+ return v, true
+ }
+ keq := k + "="
+ for _, kv := range os.Environ() {
+ if kv == keq {
+ return "", true
}
- tag = s[1]
- hash, err = fullHash(s[2])
- return
}
- err = os.NewError("no matching tag found")
- return
+ return "", false
}