diff options
Diffstat (limited to 'src/cmd/goinstall')
-rw-r--r-- | src/cmd/goinstall/Makefile | 19 | ||||
-rw-r--r-- | src/cmd/goinstall/doc.go | 196 | ||||
-rw-r--r-- | src/cmd/goinstall/download.go | 353 | ||||
-rw-r--r-- | src/cmd/goinstall/main.go | 334 | ||||
-rw-r--r-- | src/cmd/goinstall/make.go | 175 | ||||
-rw-r--r-- | src/cmd/goinstall/tag_test.go | 73 |
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) + } + } +} |