summaryrefslogtreecommitdiff
path: root/src/cmd/goinstall
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/goinstall')
-rw-r--r--src/cmd/goinstall/Makefile19
-rw-r--r--src/cmd/goinstall/doc.go196
-rw-r--r--src/cmd/goinstall/download.go353
-rw-r--r--src/cmd/goinstall/main.go334
-rw-r--r--src/cmd/goinstall/make.go175
-rw-r--r--src/cmd/goinstall/tag_test.go73
6 files changed, 1150 insertions, 0 deletions
diff --git a/src/cmd/goinstall/Makefile b/src/cmd/goinstall/Makefile
new file mode 100644
index 000000000..b90646973
--- /dev/null
+++ b/src/cmd/goinstall/Makefile
@@ -0,0 +1,19 @@
+# Copyright 2009 The Go Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+include ../../Make.inc
+
+TARG=goinstall
+GOFILES=\
+ download.go\
+ main.go\
+ make.go\
+
+include ../../Make.cmd
+
+test:
+ gotest
+
+testshort:
+ gotest -test.short
diff --git a/src/cmd/goinstall/doc.go b/src/cmd/goinstall/doc.go
new file mode 100644
index 000000000..47c615364
--- /dev/null
+++ b/src/cmd/goinstall/doc.go
@@ -0,0 +1,196 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Goinstall is an experiment in automatic package installation.
+It installs packages, possibly downloading them from the internet.
+It maintains a list of public Go packages at
+http://godashboard.appspot.com/package.
+
+Usage:
+ goinstall [flags] importpath...
+ goinstall [flags] -a
+
+Flags and default settings:
+ -a=false install all previously installed packages
+ -clean=false clean the package directory before installing
+ -dashboard=true tally public packages on godashboard.appspot.com
+ -install=true build and install the package and its dependencies
+ -nuke=false remove the target object and clean before installing
+ -u=false update already-downloaded packages
+ -v=false verbose operation
+
+Goinstall installs each of the packages identified on the command line. It
+installs a package's prerequisites before trying to install the package
+itself. Unless -log=false is specified, goinstall logs the import path of each
+installed package to $GOROOT/goinstall.log for use by goinstall -a.
+
+If the -a flag is given, goinstall reinstalls all previously installed
+packages, reading the list from $GOROOT/goinstall.log. After updating to a
+new Go release, which deletes all package binaries, running
+
+ goinstall -a
+
+will recompile and reinstall goinstalled packages.
+
+Another common idiom is to use
+
+ goinstall -a -u
+
+to update, recompile, and reinstall all goinstalled packages.
+
+The source code for a package with import path foo/bar is expected
+to be in the directory $GOROOT/src/pkg/foo/bar/ or $GOPATH/src/foo/bar/.
+See "The GOPATH Environment Variable" for more about GOPATH.
+
+By default, goinstall prints output only when it encounters an error.
+The -v flag causes goinstall to print information about packages
+being considered and installed.
+
+Goinstall ignores Makefiles.
+
+
+Remote Repositories
+
+If a package import path refers to a remote repository, goinstall will
+download the code if necessary.
+
+Goinstall recognizes packages from a few common code hosting sites:
+
+ BitBucket (Mercurial)
+
+ import "bitbucket.org/user/project"
+ import "bitbucket.org/user/project/sub/directory"
+
+ GitHub (Git)
+
+ import "github.com/user/project"
+ import "github.com/user/project/sub/directory"
+
+ Google Code Project Hosting (Git, Mercurial, Subversion)
+
+ import "project.googlecode.com/git"
+ import "project.googlecode.com/git/sub/directory"
+
+ import "project.googlecode.com/hg"
+ import "project.googlecode.com/hg/sub/directory"
+
+ import "project.googlecode.com/svn/trunk"
+ import "project.googlecode.com/svn/trunk/sub/directory"
+
+ Launchpad (Bazaar)
+
+ import "launchpad.net/project"
+ import "launchpad.net/project/series"
+ import "launchpad.net/project/series/sub/directory"
+
+ import "launchpad.net/~user/project/branch"
+ import "launchpad.net/~user/project/branch/sub/directory"
+
+If the destination directory (e.g., $GOROOT/src/pkg/bitbucket.org/user/project)
+already exists and contains an appropriate checkout, goinstall will not
+attempt to fetch updates. The -u flag changes this behavior,
+causing goinstall to update all remote packages encountered during
+the installation.
+
+When downloading or updating, goinstall looks for a tag with the "go." prefix
+that corresponds to the local Go version. For Go "release.r58" it looks for a
+tag named "go.r58". For "weekly.2011-06-03" it looks for "go.weekly.2011-06-03".
+If the specific "go.X" tag is not found, it chooses the closest earlier version.
+If an appropriate tag is found, goinstall uses that version of the code.
+Otherwise it uses the default version selected by the version control
+system, typically HEAD for git, tip for Mercurial.
+
+After a successful download and installation of one of these import paths,
+goinstall reports the installation to godashboard.appspot.com, which
+increments a count associated with the package and the time of its most
+recent installation. This mechanism powers the package list at
+http://godashboard.appspot.com/package, allowing Go programmers to learn about
+popular packages that might be worth looking at.
+The -dashboard=false flag disables this reporting.
+
+For code hosted on other servers, goinstall recognizes the general form
+
+ repository.vcs/path
+
+as denoting the given repository, with or without the .vcs suffix, using
+the named version control system, and then the path inside that repository.
+The supported version control systems are:
+
+ Bazaar .bzr
+ Git .git
+ Mercurial .hg
+ Subversion .svn
+
+For example,
+
+ import "example.org/user/foo.hg"
+
+denotes the root directory of the Mercurial repository at example.org/user/foo
+or foo.hg, and
+
+ import "example.org/repo.git/foo/bar"
+
+denotes the foo/bar directory of the Git repository at example.com/repo or
+repo.git.
+
+When a version control system supports multiple protocols, goinstall tries each
+in turn.
+For example, for Git it tries git://, then https://, then http://.
+
+
+The GOPATH Environment Variable
+
+GOPATH may be set to a colon-separated list of paths inside which Go code,
+package objects, and executables may be found.
+
+Set a GOPATH to use goinstall to build and install your own code and
+external libraries outside of the Go tree (and to avoid writing Makefiles).
+
+The top-level directory structure of a GOPATH is prescribed:
+
+The 'src' directory is for source code. The directory naming inside 'src'
+determines the package import path or executable name.
+
+The 'pkg' directory is for package objects. Like the Go tree, package objects
+are stored inside a directory named after the target operating system and
+processor architecture ('pkg/$GOOS_$GOARCH').
+A package whose source is located at '$GOPATH/src/foo/bar' would be imported
+as 'foo/bar' and installed as '$GOPATH/pkg/$GOOS_$GOARCH/foo/bar.a'.
+
+The 'bin' directory is for executable files.
+Goinstall installs program binaries using the name of the source folder.
+A binary whose source is at 'src/foo/qux' would be built and installed to
+'$GOPATH/bin/qux'. (Note 'bin/qux', not 'bin/foo/qux' - this is such that
+you can put the bin directory in your PATH.)
+
+Here's an example directory layout:
+
+ GOPATH=/home/user/gocode
+
+ /home/user/gocode/
+ src/foo/
+ bar/ (go code in package bar)
+ qux/ (go code in package main)
+ bin/qux (executable file)
+ pkg/linux_amd64/foo/bar.a (object file)
+
+Run 'goinstall foo/bar' to build and install the package 'foo/bar'
+(and its dependencies).
+Goinstall will search each GOPATH (in order) for 'src/foo/bar'.
+If the directory cannot be found, goinstall will attempt to fetch the
+source from a remote repository and write it to the 'src' directory of the
+first GOPATH (or $GOROOT/src/pkg if GOPATH is not set).
+
+Goinstall recognizes relative and absolute paths (paths beginning with / or .).
+The following commands would build our example packages:
+
+ goinstall /home/user/gocode/src/foo/bar # build and install foo/bar
+ cd /home/user/gocode/src/foo
+ goinstall ./bar # build and install foo/bar (again)
+ cd qux
+ goinstall . # build and install foo/qux
+
+*/
+package documentation
diff --git a/src/cmd/goinstall/download.go b/src/cmd/goinstall/download.go
new file mode 100644
index 000000000..cc873150a
--- /dev/null
+++ b/src/cmd/goinstall/download.go
@@ -0,0 +1,353 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Download remote packages.
+
+package main
+
+import (
+ "bytes"
+ "exec"
+ "fmt"
+ "http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+const dashboardURL = "http://godashboard.appspot.com/package"
+
+// maybeReportToDashboard reports path to dashboard unless
+// -dashboard=false is on command line. It ignores errors.
+func maybeReportToDashboard(path string) {
+ // if -dashboard=false was on command line, do nothing
+ if !*reportToDashboard {
+ return
+ }
+
+ // otherwise lob url to dashboard
+ r, _ := http.Post(dashboardURL, "application/x-www-form-urlencoded", strings.NewReader("path="+path))
+ if r != nil && r.Body != nil {
+ r.Body.Close()
+ }
+}
+
+// a vcs represents a version control system
+// like Mercurial, Git, or Subversion.
+type vcs struct {
+ name string
+ cmd string
+ metadir string
+ checkout string
+ clone string
+ update string
+ updateRevFlag string
+ pull string
+ pullForceFlag string
+ tagList string
+ tagListRe *regexp.Regexp
+ check string
+ protocols []string
+ suffix string
+ defaultHosts []host
+}
+
+type host struct {
+ pattern *regexp.Regexp
+ protocol string
+ suffix string
+}
+
+var hg = vcs{
+ name: "Mercurial",
+ cmd: "hg",
+ metadir: ".hg",
+ checkout: "checkout",
+ clone: "clone",
+ update: "update",
+ pull: "pull",
+ tagList: "tags",
+ tagListRe: regexp.MustCompile("([^ ]+)[^\n]+\n"),
+ check: "identify",
+ protocols: []string{"https", "http"},
+ suffix: ".hg",
+ defaultHosts: []host{
+ {regexp.MustCompile(`^([a-z0-9\-]+\.googlecode\.com/hg)(/[a-z0-9A-Z_.\-/]*)?$`), "https", ""},
+ {regexp.MustCompile(`^(bitbucket\.org/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(/[a-z0-9A-Z_.\-/]*)?$`), "http", ""},
+ },
+}
+
+var git = vcs{
+ name: "Git",
+ cmd: "git",
+ metadir: ".git",
+ checkout: "checkout",
+ clone: "clone",
+ update: "pull",
+ pull: "fetch",
+ tagList: "tag",
+ tagListRe: regexp.MustCompile("([^\n]+)\n"),
+ check: "ls-remote",
+ protocols: []string{"git", "https", "http"},
+ suffix: ".git",
+ defaultHosts: []host{
+ {regexp.MustCompile(`^([a-z0-9\-]+\.googlecode\.com/git)(/[a-z0-9A-Z_.\-/]*)?$`), "https", ""},
+ {regexp.MustCompile(`^(github\.com/[a-z0-9A-Z_.\-]+/[a-z0-9A-Z_.\-]+)(/[a-z0-9A-Z_.\-/]*)?$`), "http", ".git"},
+ },
+}
+
+var svn = vcs{
+ name: "Subversion",
+ cmd: "svn",
+ metadir: ".svn",
+ checkout: "checkout",
+ clone: "checkout",
+ update: "update",
+ check: "info",
+ protocols: []string{"https", "http", "svn"},
+ suffix: ".svn",
+ defaultHosts: []host{
+ {regexp.MustCompile(`^([a-z0-9\-]+\.googlecode\.com/svn)(/[a-z0-9A-Z_.\-/]*)?$`), "https", ""},
+ },
+}
+
+var bzr = vcs{
+ name: "Bazaar",
+ cmd: "bzr",
+ metadir: ".bzr",
+ checkout: "update",
+ clone: "branch",
+ update: "update",
+ updateRevFlag: "-r",
+ pull: "pull",
+ pullForceFlag: "--overwrite",
+ tagList: "tags",
+ tagListRe: regexp.MustCompile("([^ ]+)[^\n]+\n"),
+ check: "info",
+ protocols: []string{"https", "http", "bzr"},
+ suffix: ".bzr",
+ defaultHosts: []host{
+ {regexp.MustCompile(`^(launchpad\.net/([a-z0-9A-Z_.\-]+(/[a-z0-9A-Z_.\-]+)?|~[a-z0-9A-Z_.\-]+/(\+junk|[a-z0-9A-Z_.\-]+)/[a-z0-9A-Z_.\-]+))(/[a-z0-9A-Z_.\-/]+)?$`), "https", ""},
+ },
+}
+
+var vcsList = []*vcs{&git, &hg, &bzr, &svn}
+
+type vcsMatch struct {
+ *vcs
+ prefix, repo string
+}
+
+// findPublicRepo checks whether pkg is located at one of
+// the supported code hosting sites and, if so, returns a match.
+func findPublicRepo(pkg string) (*vcsMatch, os.Error) {
+ for _, v := range vcsList {
+ for _, host := range v.defaultHosts {
+ if hm := host.pattern.FindStringSubmatch(pkg); hm != nil {
+ if host.suffix != "" && strings.HasSuffix(hm[1], host.suffix) {
+ return nil, os.NewError("repository " + pkg + " should not have " + v.suffix + " suffix")
+ }
+ repo := host.protocol + "://" + hm[1] + host.suffix
+ return &vcsMatch{v, hm[1], repo}, nil
+ }
+ }
+ }
+ return nil, nil
+}
+
+// findAnyRepo looks for a vcs suffix in pkg (.git, etc) and returns a match.
+func findAnyRepo(pkg string) (*vcsMatch, os.Error) {
+ for _, v := range vcsList {
+ i := strings.Index(pkg+"/", v.suffix+"/")
+ if i < 0 {
+ continue
+ }
+ if !strings.Contains(pkg[:i], "/") {
+ continue // don't match vcs suffix in the host name
+ }
+ if m := v.find(pkg[:i]); m != nil {
+ return m, nil
+ }
+ return nil, fmt.Errorf("couldn't find %s repository", v.name)
+ }
+ return nil, nil
+}
+
+func (v *vcs) find(pkg string) *vcsMatch {
+ for _, proto := range v.protocols {
+ for _, suffix := range []string{"", v.suffix} {
+ repo := proto + "://" + pkg + suffix
+ out, err := exec.Command(v.cmd, v.check, repo).CombinedOutput()
+ if err == nil {
+ printf("find %s: found %s\n", pkg, repo)
+ return &vcsMatch{v, pkg + v.suffix, repo}
+ }
+ printf("find %s: %s %s %s: %v\n%s\n", pkg, v.cmd, v.check, repo, err, out)
+ }
+ }
+ return nil
+}
+
+// isRemote returns true if the first part of the package name looks like a
+// hostname - i.e. contains at least one '.' and the last part is at least 2
+// characters.
+func isRemote(pkg string) bool {
+ parts := strings.SplitN(pkg, "/", 2)
+ if len(parts) != 2 {
+ return false
+ }
+ parts = strings.Split(parts[0], ".")
+ if len(parts) < 2 || len(parts[len(parts)-1]) < 2 {
+ return false
+ }
+ return true
+}
+
+// download checks out or updates pkg from the remote server.
+func download(pkg, srcDir string) (public bool, err os.Error) {
+ if strings.Contains(pkg, "..") {
+ err = os.NewError("invalid path (contains ..)")
+ return
+ }
+ m, err := findPublicRepo(pkg)
+ if err != nil {
+ return
+ }
+ if m != nil {
+ public = true
+ } else {
+ m, err = findAnyRepo(pkg)
+ if err != nil {
+ return
+ }
+ }
+ if m == nil {
+ err = os.NewError("cannot download: " + pkg)
+ return
+ }
+ err = m.checkoutRepo(srcDir, m.prefix, m.repo)
+ return
+}
+
+// updateRepo gets a list of tags in the repository and
+// checks out the tag closest to the current runtime.Version.
+// If no matching tag is found, it just updates to tip.
+func (v *vcs) updateRepo(dst string) os.Error {
+ if v.tagList == "" || v.tagListRe == nil {
+ // TODO(adg): fix for svn
+ return run(dst, nil, v.cmd, v.update)
+ }
+
+ // Get tag list.
+ stderr := new(bytes.Buffer)
+ cmd := exec.Command(v.cmd, v.tagList)
+ cmd.Dir = dst
+ cmd.Stderr = stderr
+ b, err := cmd.Output()
+ if err != nil {
+ errorf("%s %s: %s\n", v.cmd, v.tagList, stderr)
+ return err
+ }
+ var tags []string
+ for _, m := range v.tagListRe.FindAllStringSubmatch(string(b), -1) {
+ tags = append(tags, m[1])
+ }
+
+ // Only use the tag component of runtime.Version.
+ ver := strings.Split(runtime.Version(), " ")[0]
+
+ // Select tag.
+ if tag := selectTag(ver, tags); tag != "" {
+ printf("selecting revision %q\n", tag)
+ return run(dst, nil, v.cmd, v.checkout, v.updateRevFlag+tag)
+ }
+
+ // No matching tag found, make default selection.
+ printf("selecting tip\n")
+ return run(dst, nil, v.cmd, v.update)
+}
+
+// selectTag returns the closest matching tag for a given version.
+// Closest means the latest one that is not after the current release.
+// Version "release.rN" matches tags of the form "go.rN" (N being a decimal).
+// Version "weekly.YYYY-MM-DD" matches tags like "go.weekly.YYYY-MM-DD".
+func selectTag(goVersion string, tags []string) (match string) {
+ const rPrefix = "release.r"
+ if strings.HasPrefix(goVersion, rPrefix) {
+ p := "go.r"
+ v, err := strconv.Atof64(goVersion[len(rPrefix):])
+ if err != nil {
+ return ""
+ }
+ var matchf float64
+ for _, t := range tags {
+ if !strings.HasPrefix(t, p) {
+ continue
+ }
+ tf, err := strconv.Atof64(t[len(p):])
+ if err != nil {
+ continue
+ }
+ if matchf < tf && tf <= v {
+ match, matchf = t, tf
+ }
+ }
+ }
+ const wPrefix = "weekly."
+ if strings.HasPrefix(goVersion, wPrefix) {
+ p := "go.weekly."
+ v := goVersion[len(wPrefix):]
+ for _, t := range tags {
+ if !strings.HasPrefix(t, p) {
+ continue
+ }
+ if match < t && t[len(p):] <= v {
+ match = t
+ }
+ }
+ }
+ return match
+}
+
+// checkoutRepo checks out repo into dst using vcs.
+// It tries to check out (or update, if the dst already
+// exists and -u was specified on the command line)
+// the repository at tag/branch "release". If there is no
+// such tag or branch, it falls back to the repository tip.
+func (vcs *vcs) checkoutRepo(srcDir, pkgprefix, repo string) os.Error {
+ dst := filepath.Join(srcDir, filepath.FromSlash(pkgprefix))
+ dir, err := os.Stat(filepath.Join(dst, vcs.metadir))
+ if err == nil && !dir.IsDirectory() {
+ return os.NewError("not a directory: " + dst)
+ }
+ if err != nil {
+ parent, _ := filepath.Split(dst)
+ if err = os.MkdirAll(parent, 0777); err != nil {
+ return err
+ }
+ if err = run(string(filepath.Separator), nil, vcs.cmd, vcs.clone, repo, dst); err != nil {
+ return err
+ }
+ return vcs.updateRepo(dst)
+ }
+ if *update {
+ // Retrieve new revisions from the remote branch, if the VCS
+ // supports this operation independently (e.g. svn doesn't)
+ if vcs.pull != "" {
+ if vcs.pullForceFlag != "" {
+ if err = run(dst, nil, vcs.cmd, vcs.pull, vcs.pullForceFlag); err != nil {
+ return err
+ }
+ } else if err = run(dst, nil, vcs.cmd, vcs.pull); err != nil {
+ return err
+ }
+ }
+ // Update to release or latest revision
+ return vcs.updateRepo(dst)
+ }
+ return nil
+}
diff --git a/src/cmd/goinstall/main.go b/src/cmd/goinstall/main.go
new file mode 100644
index 000000000..acda6efbb
--- /dev/null
+++ b/src/cmd/goinstall/main.go
@@ -0,0 +1,334 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "exec"
+ "flag"
+ "fmt"
+ "go/build"
+ "go/token"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+)
+
+func usage() {
+ fmt.Fprint(os.Stderr, "usage: goinstall importpath...\n")
+ fmt.Fprintf(os.Stderr, "\tgoinstall -a\n")
+ flag.PrintDefaults()
+ os.Exit(2)
+}
+
+const logfile = "goinstall.log"
+
+var (
+ fset = token.NewFileSet()
+ argv0 = os.Args[0]
+ errors = false
+ parents = make(map[string]string)
+ visit = make(map[string]status)
+ installedPkgs = make(map[string]map[string]bool)
+ schemeRe = regexp.MustCompile(`^[a-z]+://`)
+
+ allpkg = flag.Bool("a", false, "install all previously installed packages")
+ reportToDashboard = flag.Bool("dashboard", true, "report public packages at "+dashboardURL)
+ update = flag.Bool("u", false, "update already-downloaded packages")
+ doInstall = flag.Bool("install", true, "build and install")
+ clean = flag.Bool("clean", false, "clean the package directory before installing")
+ nuke = flag.Bool("nuke", false, "clean the package directory and target before installing")
+ useMake = flag.Bool("make", true, "use make to build and install")
+ verbose = flag.Bool("v", false, "verbose")
+)
+
+type status int // status for visited map
+const (
+ unvisited status = iota
+ visiting
+ done
+)
+
+func logf(format string, args ...interface{}) {
+ format = "%s: " + format
+ args = append([]interface{}{argv0}, args...)
+ fmt.Fprintf(os.Stderr, format, args...)
+}
+
+func printf(format string, args ...interface{}) {
+ if *verbose {
+ logf(format, args...)
+ }
+}
+
+func errorf(format string, args ...interface{}) {
+ errors = true
+ logf(format, args...)
+}
+
+func terrorf(tree *build.Tree, format string, args ...interface{}) {
+ if tree != nil && tree.Goroot && os.Getenv("GOPATH") == "" {
+ format = strings.TrimRight(format, "\n") + " ($GOPATH not set)\n"
+ }
+ errorf(format, args...)
+}
+
+func main() {
+ flag.Usage = usage
+ flag.Parse()
+ if runtime.GOROOT() == "" {
+ fmt.Fprintf(os.Stderr, "%s: no $GOROOT\n", argv0)
+ os.Exit(1)
+ }
+ readPackageList()
+
+ // special case - "unsafe" is already installed
+ visit["unsafe"] = done
+
+ args := flag.Args()
+ if *allpkg {
+ if len(args) != 0 {
+ usage() // -a and package list both provided
+ }
+ // install all packages that were ever installed
+ n := 0
+ for _, pkgs := range installedPkgs {
+ for pkg := range pkgs {
+ args = append(args, pkg)
+ n++
+ }
+ }
+ if n == 0 {
+ logf("no installed packages\n")
+ os.Exit(1)
+ }
+ }
+ if len(args) == 0 {
+ usage()
+ }
+ for _, path := range args {
+ if s := schemeRe.FindString(path); s != "" {
+ errorf("%q used in import path, try %q\n", s, path[len(s):])
+ continue
+ }
+
+ install(path, "")
+ }
+ if errors {
+ os.Exit(1)
+ }
+}
+
+// printDeps prints the dependency path that leads to pkg.
+func printDeps(pkg string) {
+ if pkg == "" {
+ return
+ }
+ if visit[pkg] != done {
+ printDeps(parents[pkg])
+ }
+ fmt.Fprintf(os.Stderr, "\t%s ->\n", pkg)
+}
+
+// readPackageList reads the list of installed packages from the
+// goinstall.log files in GOROOT and the GOPATHs and initalizes
+// the installedPkgs variable.
+func readPackageList() {
+ for _, t := range build.Path {
+ installedPkgs[t.Path] = make(map[string]bool)
+ name := filepath.Join(t.Path, logfile)
+ pkglistdata, err := ioutil.ReadFile(name)
+ if err != nil {
+ printf("%s\n", err)
+ continue
+ }
+ pkglist := strings.Fields(string(pkglistdata))
+ for _, pkg := range pkglist {
+ installedPkgs[t.Path][pkg] = true
+ }
+ }
+}
+
+// logPackage logs the named package as installed in the goinstall.log file
+// in the given tree if the package is not already in that file.
+func logPackage(pkg string, tree *build.Tree) (logged bool) {
+ if installedPkgs[tree.Path][pkg] {
+ return false
+ }
+ name := filepath.Join(tree.Path, logfile)
+ fout, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ terrorf(tree, "package log: %s\n", err)
+ return false
+ }
+ fmt.Fprintf(fout, "%s\n", pkg)
+ fout.Close()
+ return true
+}
+
+// install installs the package named by path, which is needed by parent.
+func install(pkg, parent string) {
+ // Make sure we're not already trying to install pkg.
+ switch visit[pkg] {
+ case done:
+ return
+ case visiting:
+ fmt.Fprintf(os.Stderr, "%s: package dependency cycle\n", argv0)
+ printDeps(parent)
+ fmt.Fprintf(os.Stderr, "\t%s\n", pkg)
+ os.Exit(2)
+ }
+ parents[pkg] = parent
+ visit[pkg] = visiting
+ defer func() {
+ visit[pkg] = done
+ }()
+
+ // Don't allow trailing '/'
+ if _, f := filepath.Split(pkg); f == "" {
+ errorf("%s should not have trailing '/'\n", pkg)
+ return
+ }
+
+ // Check whether package is local or remote.
+ // If remote, download or update it.
+ tree, pkg, err := build.FindTree(pkg)
+ // Don't build the standard library.
+ if err == nil && tree.Goroot && isStandardPath(pkg) {
+ if parent == "" {
+ errorf("%s: can not goinstall the standard library\n", pkg)
+ } else {
+ printf("%s: skipping standard library\n", pkg)
+ }
+ return
+ }
+ // Download remote packages if not found or forced with -u flag.
+ remote, public := isRemote(pkg), false
+ if remote {
+ if err == build.ErrNotFound || (err == nil && *update) {
+ // Download remote package.
+ printf("%s: download\n", pkg)
+ public, err = download(pkg, tree.SrcDir())
+ } else {
+ // Test if this is a public repository
+ // (for reporting to dashboard).
+ m, _ := findPublicRepo(pkg)
+ public = m != nil
+ }
+ }
+ if err != nil {
+ terrorf(tree, "%s: %v\n", pkg, err)
+ return
+ }
+ dir := filepath.Join(tree.SrcDir(), pkg)
+
+ // Install prerequisites.
+ dirInfo, err := build.ScanDir(dir, parent == "")
+ if err != nil {
+ terrorf(tree, "%s: %v\n", pkg, err)
+ return
+ }
+ if len(dirInfo.GoFiles)+len(dirInfo.CgoFiles) == 0 {
+ terrorf(tree, "%s: package has no files\n", pkg)
+ return
+ }
+ for _, p := range dirInfo.Imports {
+ if p != "C" {
+ install(p, pkg)
+ }
+ }
+ if errors {
+ return
+ }
+
+ // Install this package.
+ if *useMake {
+ err := domake(dir, pkg, tree, dirInfo.IsCommand())
+ if err != nil {
+ terrorf(tree, "%s: install: %v\n", pkg, err)
+ return
+ }
+ } else {
+ script, err := build.Build(tree, pkg, dirInfo)
+ if err != nil {
+ terrorf(tree, "%s: install: %v\n", pkg, err)
+ return
+ }
+ if *nuke {
+ printf("%s: nuke\n", pkg)
+ script.Nuke()
+ } else if *clean {
+ printf("%s: clean\n", pkg)
+ script.Clean()
+ }
+ if *doInstall {
+ if script.Stale() {
+ printf("%s: install\n", pkg)
+ if err := script.Run(); err != nil {
+ terrorf(tree, "%s: install: %v\n", pkg, err)
+ return
+ }
+ } else {
+ printf("%s: up-to-date\n", pkg)
+ }
+ }
+ }
+
+ if remote {
+ // mark package as installed in goinstall.log
+ logged := logPackage(pkg, tree)
+
+ // report installation to the dashboard if this is the first
+ // install from a public repository.
+ if logged && public {
+ maybeReportToDashboard(pkg)
+ }
+ }
+}
+
+// Is this a standard package path? strings container/vector etc.
+// Assume that if the first element has a dot, it's a domain name
+// and is not the standard package path.
+func isStandardPath(s string) bool {
+ dot := strings.Index(s, ".")
+ slash := strings.Index(s, "/")
+ return dot < 0 || 0 < slash && slash < dot
+}
+
+// run runs the command cmd in directory dir with standard input stdin.
+// If the command fails, run prints the command and output on standard error
+// in addition to returning a non-nil os.Error.
+func run(dir string, stdin []byte, cmd ...string) os.Error {
+ return genRun(dir, stdin, cmd, false)
+}
+
+// quietRun is like run but prints nothing on failure unless -v is used.
+func quietRun(dir string, stdin []byte, cmd ...string) os.Error {
+ return genRun(dir, stdin, cmd, true)
+}
+
+// genRun implements run and quietRun.
+func genRun(dir string, stdin []byte, arg []string, quiet bool) os.Error {
+ cmd := exec.Command(arg[0], arg[1:]...)
+ cmd.Stdin = bytes.NewBuffer(stdin)
+ cmd.Dir = dir
+ printf("%s: %s %s\n", dir, cmd.Path, strings.Join(arg[1:], " "))
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ if !quiet || *verbose {
+ if dir != "" {
+ dir = "cd " + dir + "; "
+ }
+ fmt.Fprintf(os.Stderr, "%s: === %s%s\n", cmd.Path, dir, strings.Join(cmd.Args, " "))
+ os.Stderr.Write(out)
+ fmt.Fprintf(os.Stderr, "--- %s\n", err)
+ }
+ return os.NewError("running " + arg[0] + ": " + err.String())
+ }
+ return nil
+}
diff --git a/src/cmd/goinstall/make.go b/src/cmd/goinstall/make.go
new file mode 100644
index 000000000..38a70ddfd
--- /dev/null
+++ b/src/cmd/goinstall/make.go
@@ -0,0 +1,175 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Run "make install" to build package.
+
+package main
+
+import (
+ "bytes"
+ "go/build"
+ "os"
+ "path/filepath"
+ "strings"
+ "template"
+)
+
+// domake builds the package in dir.
+// domake generates a standard Makefile and passes it
+// to make on standard input.
+func domake(dir, pkg string, tree *build.Tree, isCmd bool) (err os.Error) {
+ makefile, err := makeMakefile(dir, pkg, tree, isCmd)
+ if err != nil {
+ return err
+ }
+ cmd := []string{"bash", "gomake", "-f-"}
+ if *nuke {
+ cmd = append(cmd, "nuke")
+ } else if *clean {
+ cmd = append(cmd, "clean")
+ }
+ cmd = append(cmd, "install")
+ return run(dir, makefile, cmd...)
+}
+
+// makeMakefile computes the standard Makefile for the directory dir
+// installing as package pkg. It includes all *.go files in the directory
+// except those in package main and those ending in _test.go.
+func makeMakefile(dir, pkg string, tree *build.Tree, isCmd bool) ([]byte, os.Error) {
+ if !safeName(pkg) {
+ return nil, os.NewError("unsafe name: " + pkg)
+ }
+ targ := pkg
+ targDir := tree.PkgDir()
+ if isCmd {
+ // use the last part of the package name for targ
+ _, targ = filepath.Split(pkg)
+ targDir = tree.BinDir()
+ }
+ dirInfo, err := build.ScanDir(dir, isCmd)
+ if err != nil {
+ return nil, err
+ }
+
+ cgoFiles := dirInfo.CgoFiles
+ isCgo := make(map[string]bool, len(cgoFiles))
+ for _, file := range cgoFiles {
+ if !safeName(file) {
+ return nil, os.NewError("bad name: " + file)
+ }
+ isCgo[file] = true
+ }
+
+ goFiles := make([]string, 0, len(dirInfo.GoFiles))
+ for _, file := range dirInfo.GoFiles {
+ if !safeName(file) {
+ return nil, os.NewError("unsafe name: " + file)
+ }
+ if !isCgo[file] {
+ goFiles = append(goFiles, file)
+ }
+ }
+
+ oFiles := make([]string, 0, len(dirInfo.CFiles)+len(dirInfo.SFiles))
+ cgoOFiles := make([]string, 0, len(dirInfo.CFiles))
+ for _, file := range dirInfo.CFiles {
+ if !safeName(file) {
+ return nil, os.NewError("unsafe name: " + file)
+ }
+ // When cgo is in use, C files are compiled with gcc,
+ // otherwise they're compiled with gc.
+ if len(cgoFiles) > 0 {
+ cgoOFiles = append(cgoOFiles, file[:len(file)-2]+".o")
+ } else {
+ oFiles = append(oFiles, file[:len(file)-2]+".$O")
+ }
+ }
+
+ for _, file := range dirInfo.SFiles {
+ if !safeName(file) {
+ return nil, os.NewError("unsafe name: " + file)
+ }
+ oFiles = append(oFiles, file[:len(file)-2]+".$O")
+ }
+
+ var imports []string
+ for _, t := range build.Path {
+ imports = append(imports, t.PkgDir())
+ }
+
+ var buf bytes.Buffer
+ md := makedata{targ, targDir, "pkg", goFiles, oFiles, cgoFiles, cgoOFiles, imports}
+ if isCmd {
+ md.Type = "cmd"
+ }
+ if err := makefileTemplate.Execute(&buf, &md); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+var safeBytes = []byte("+-./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")
+
+func safeName(s string) bool {
+ if s == "" {
+ return false
+ }
+ if strings.Contains(s, "..") {
+ return false
+ }
+ for i := 0; i < len(s); i++ {
+ if c := s[i]; c < 0x80 && bytes.IndexByte(safeBytes, c) < 0 {
+ return false
+ }
+ }
+ return true
+}
+
+// makedata is the data type for the makefileTemplate.
+type makedata struct {
+ Targ string // build target
+ TargDir string // build target directory
+ Type string // build type: "pkg" or "cmd"
+ GoFiles []string // list of non-cgo .go files
+ OFiles []string // list of .$O files
+ CgoFiles []string // list of cgo .go files
+ CgoOFiles []string // list of cgo .o files, without extension
+ Imports []string // gc/ld import paths
+}
+
+var makefileTemplate = template.Must(template.New("Makefile").Parse(`
+include $(GOROOT)/src/Make.inc
+
+TARG={{.Targ}}
+TARGDIR={{.TargDir}}
+
+{{with .GoFiles}}
+GOFILES=\
+{{range .}} {{.}}\
+{{end}}
+
+{{end}}
+{{with .OFiles}}
+OFILES=\
+{{range .}} {{.}}\
+{{end}}
+
+{{end}}
+{{with .CgoFiles}}
+CGOFILES=\
+{{range .}} {{.}}\
+{{end}}
+
+{{end}}
+{{with .CgoOFiles}}
+CGO_OFILES=\
+{{range .}} {{.}}\
+{{end}}
+
+{{end}}
+GCIMPORTS={{range .Imports}}-I "{{.}}" {{end}}
+LDIMPORTS={{range .Imports}}-L "{{.}}" {{end}}
+
+include $(GOROOT)/src/Make.{{.Type}}
+`))
diff --git a/src/cmd/goinstall/tag_test.go b/src/cmd/goinstall/tag_test.go
new file mode 100644
index 000000000..a23a7ea82
--- /dev/null
+++ b/src/cmd/goinstall/tag_test.go
@@ -0,0 +1,73 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import "testing"
+
+var selectTagTestTags = []string{
+ "go.r58",
+ "go.r58.1",
+ "go.r59",
+ "go.r59.1",
+ "go.r61",
+ "go.r61.1",
+ "go.weekly.2010-01-02",
+ "go.weekly.2011-10-12",
+ "go.weekly.2011-10-12.1",
+ "go.weekly.2011-10-14",
+ "go.weekly.2011-11-01",
+ // these should be ignored:
+ "release.r59",
+ "release.r59.1",
+ "release",
+ "weekly.2011-10-12",
+ "weekly.2011-10-12.1",
+ "weekly",
+ "foo",
+ "bar",
+ "go.f00",
+ "go!r60",
+ "go.1999-01-01",
+}
+
+var selectTagTests = []struct {
+ version string
+ selected string
+}{
+ {"release.r57", ""},
+ {"release.r58.2", "go.r58.1"},
+ {"release.r59", "go.r59"},
+ {"release.r59.1", "go.r59.1"},
+ {"release.r60", "go.r59.1"},
+ {"release.r60.1", "go.r59.1"},
+ {"release.r61", "go.r61"},
+ {"release.r66", "go.r61.1"},
+ {"weekly.2010-01-01", ""},
+ {"weekly.2010-01-02", "go.weekly.2010-01-02"},
+ {"weekly.2010-01-02.1", "go.weekly.2010-01-02"},
+ {"weekly.2010-01-03", "go.weekly.2010-01-02"},
+ {"weekly.2011-10-12", "go.weekly.2011-10-12"},
+ {"weekly.2011-10-12.1", "go.weekly.2011-10-12.1"},
+ {"weekly.2011-10-13", "go.weekly.2011-10-12.1"},
+ {"weekly.2011-10-14", "go.weekly.2011-10-14"},
+ {"weekly.2011-10-14.1", "go.weekly.2011-10-14"},
+ {"weekly.2011-11-01", "go.weekly.2011-11-01"},
+ {"weekly.2014-01-01", "go.weekly.2011-11-01"},
+ {"weekly.3000-01-01", "go.weekly.2011-11-01"},
+ // faulty versions:
+ {"release.f00", ""},
+ {"weekly.1999-01-01", ""},
+ {"junk", ""},
+ {"", ""},
+}
+
+func TestSelectTag(t *testing.T) {
+ for _, c := range selectTagTests {
+ selected := selectTag(c.version, selectTagTestTags)
+ if selected != c.selected {
+ t.Errorf("selectTag(%q) = %q, want %q", c.version, selected, c.selected)
+ }
+ }
+}