package build
import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/doc"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
+ "path"
"path/filepath"
+ "runtime"
"sort"
"strconv"
"strings"
- "runtime"
+ "unicode"
)
// A Context specifies the supporting context for a build.
GOARCH string // target architecture
GOOS string // target operating system
// TODO(rsc,adg): GOPATH
+
+ // By default, ScanDir uses the operating system's
+ // file system calls to read directories and files.
+ // Callers can override those calls to provide other
+ // ways to read data by setting ReadDir and ReadFile.
+ // ScanDir does not make any assumptions about the
+ // format of the strings dir and file: they can be
+ // slash-separated, backslash-separated, even URLs.
+
+ // ReadDir returns a slice of *os.FileInfo, sorted by Name,
+ // describing the content of the named directory.
+ // The dir argument is the argument to ScanDir.
+ // If ReadDir is nil, ScanDir uses io.ReadDir.
+ ReadDir func(dir string) (fi []*os.FileInfo, err os.Error)
+
+ // ReadFile returns the content of the file named file
+ // in the directory named dir. The dir argument is the
+ // argument to ScanDir, and the file argument is the
+ // Name field from an *os.FileInfo returned by ReadDir.
+ // The returned path is the full name of the file, to be
+ // used in error messages.
+ //
+ // If ReadFile is nil, ScanDir uses filepath.Join(dir, file)
+ // as the path and ioutil.ReadFile to read the data.
+ ReadFile func(dir, file string) (path string, content []byte, err os.Error)
+}
+
+func (ctxt *Context) readDir(dir string) ([]*os.FileInfo, os.Error) {
+ if f := ctxt.ReadDir; f != nil {
+ return f(dir)
+ }
+ return ioutil.ReadDir(dir)
+}
+
+func (ctxt *Context) readFile(dir, file string) (string, []byte, os.Error) {
+ if f := ctxt.ReadFile; f != nil {
+ return f(dir, file)
+ }
+ p := filepath.Join(dir, file)
+ content, err := ioutil.ReadFile(p)
+ return p, content, err
}
// The DefaultContext is the default Context for builds.
// It uses the GOARCH and GOOS environment variables
// if set, or else the compiled code's GOARCH and GOOS.
var DefaultContext = Context{
- envOr("GOARCH", runtime.GOARCH),
- envOr("GOOS", runtime.GOOS),
+ GOARCH: envOr("GOARCH", runtime.GOARCH),
+ GOOS: envOr("GOOS", runtime.GOOS),
}
func envOr(name, def string) string {
}
type DirInfo struct {
- GoFiles []string // .go files in dir (excluding CgoFiles)
- CgoFiles []string // .go files that import "C"
- CFiles []string // .c files in dir
- SFiles []string // .s files in dir
- Imports []string // All packages imported by GoFiles
- TestImports []string // All packages imported by (X)TestGoFiles
- PkgName string // Name of package in dir
+ Package string // Name of package in dir
+ PackageComment *ast.CommentGroup // Package comments from GoFiles
+ ImportPath string // Import path of package in dir
+ Imports []string // All packages imported by GoFiles
+
+ // Source files
+ GoFiles []string // .go files in dir (excluding CgoFiles)
+ CFiles []string // .c files in dir
+ SFiles []string // .s files in dir
+ CgoFiles []string // .go files that import "C"
+
+ // Cgo directives
+ CgoPkgConfig []string // Cgo pkg-config directives
+ CgoCFLAGS []string // Cgo CFLAGS directives
+ CgoLDFLAGS []string // Cgo LDFLAGS directives
+
+ // Test information
TestGoFiles []string // _test.go files in package
XTestGoFiles []string // _test.go files outside package
+ TestImports []string // All packages imported by (X)TestGoFiles
}
func (d *DirInfo) IsCommand() bool {
- return d.PkgName == "main"
+ // TODO(rsc): This is at least a little bogus.
+ return d.Package == "main"
}
// ScanDir calls DefaultContext.ScanDir.
-func ScanDir(dir string, allowMain bool) (info *DirInfo, err os.Error) {
- return DefaultContext.ScanDir(dir, allowMain)
+func ScanDir(dir string) (info *DirInfo, err os.Error) {
+ return DefaultContext.ScanDir(dir)
}
// ScanDir returns a structure with details about the Go content found
// in the given directory. The file lists exclude:
//
-// - files in package main (unless allowMain is true)
+// - files in package main (unless no other package is found)
// - files in package documentation
// - files ending in _test.go
-// - files starting with _ or .
+// - files starting with _ or .
//
-func (ctxt *Context) ScanDir(dir string, allowMain bool) (info *DirInfo, err os.Error) {
- dirs, err := ioutil.ReadDir(dir)
+func (ctxt *Context) ScanDir(dir string) (info *DirInfo, err os.Error) {
+ dirs, err := ctxt.readDir(dir)
if err != nil {
return nil, err
}
testImported := make(map[string]bool)
fset := token.NewFileSet()
for _, d := range dirs {
+ if !d.IsRegular() {
+ continue
+ }
if strings.HasPrefix(d.Name, "_") ||
strings.HasPrefix(d.Name, ".") {
continue
}
- if !ctxt.goodOSArch(d.Name) {
+ if !ctxt.goodOSArchFile(d.Name) {
continue
}
isTest := false
- switch filepath.Ext(d.Name) {
+ switch path.Ext(d.Name) {
case ".go":
isTest = strings.HasSuffix(d.Name, "_test.go")
case ".c":
continue
}
- filename := filepath.Join(dir, d.Name)
- pf, err := parser.ParseFile(fset, filename, nil, parser.ImportsOnly)
+ filename, data, err := ctxt.readFile(dir, d.Name)
if err != nil {
return nil, err
}
+ pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly|parser.ParseComments)
+ if err != nil {
+ return nil, err
+ }
+
+ // Skip if the //build comments don't match.
+ if !ctxt.shouldBuild(pf) {
+ continue
+ }
+
pkg := string(pf.Name.Name)
- if pkg == "main" && !allowMain {
+ if pkg == "main" && di.Package != "" && di.Package != "main" {
continue
}
if pkg == "documentation" {
if isTest && strings.HasSuffix(pkg, "_test") {
pkg = pkg[:len(pkg)-len("_test")]
}
- if di.PkgName == "" {
- di.PkgName = pkg
- } else if di.PkgName != pkg {
- // Only if all files in the directory are in package main
- // do we return PkgName=="main".
- // A mix of main and another package reverts
- // to the original (allowMain=false) behaviour.
- if pkg == "main" || di.PkgName == "main" {
- return ScanDir(dir, false)
+
+ if pkg != di.Package && di.Package == "main" {
+ // Found non-main package but was recording
+ // information about package main. Reset.
+ di = DirInfo{}
+ }
+ if di.Package == "" {
+ di.Package = pkg
+ } else if pkg != di.Package {
+ return nil, fmt.Errorf("%s: found packages %s and %s", dir, pkg, di.Package)
+ }
+ if pf.Doc != nil {
+ if di.PackageComment != nil {
+ di.PackageComment.List = append(di.PackageComment.List, pf.Doc.List...)
+ } else {
+ di.PackageComment = pf.Doc
}
- return nil, os.NewError("multiple package names in " + dir)
}
+
+ // Record imports and information about cgo.
isCgo := false
- for _, spec := range pf.Imports {
- quoted := string(spec.Path.Value)
- path, err := strconv.Unquote(quoted)
- if err != nil {
- log.Panicf("%s: parser returned invalid quoted string: <%s>", filename, quoted)
+ for _, decl := range pf.Decls {
+ d, ok := decl.(*ast.GenDecl)
+ if !ok {
+ continue
}
- if isTest {
- testImported[path] = true
- } else {
- imported[path] = true
- }
- if path == "C" {
+ for _, dspec := range d.Specs {
+ spec, ok := dspec.(*ast.ImportSpec)
+ if !ok {
+ continue
+ }
+ quoted := string(spec.Path.Value)
+ path, err := strconv.Unquote(quoted)
+ if err != nil {
+ log.Panicf("%s: parser returned invalid quoted string: <%s>", filename, quoted)
+ }
if isTest {
- return nil, os.NewError("use of cgo in test " + filename)
+ testImported[path] = true
+ } else {
+ imported[path] = true
+ }
+ if path == "C" {
+ if isTest {
+ return nil, fmt.Errorf("%s: use of cgo in test not supported", filename)
+ }
+ cg := spec.Doc
+ if cg == nil && len(d.Specs) == 1 {
+ cg = d.Doc
+ }
+ if cg != nil {
+ if err := ctxt.saveCgo(filename, &di, cg); err != nil {
+ return nil, err
+ }
+ }
+ isCgo = true
}
- isCgo = true
}
}
if isCgo {
di.GoFiles = append(di.GoFiles, d.Name)
}
}
+ if di.Package == "" {
+ return nil, fmt.Errorf("%s: no Go source files", dir)
+ }
di.Imports = make([]string, len(imported))
i := 0
for p := range imported {
di.TestImports[i] = p
i++
}
- // File name lists are sorted because ioutil.ReadDir sorts.
+ // File name lists are sorted because ReadDir sorts.
sort.Strings(di.Imports)
sort.Strings(di.TestImports)
return &di, nil
}
-// goodOSArch returns false if the name contains a $GOOS or $GOARCH
+// okayBuild reports whether it is okay to build this Go file,
+// based on the //build comments leading up to the package clause.
+//
+// The file is accepted only if each such line lists something
+// matching the file. For example:
+//
+// //build windows linux
+//
+// marks the file as applicable only on Windows and Linux.
+func (ctxt *Context) shouldBuild(pf *ast.File) bool {
+ for _, com := range pf.Comments {
+ if com.Pos() >= pf.Package {
+ break
+ }
+ for _, c := range com.List {
+ if strings.HasPrefix(c.Text, "//build") {
+ f := strings.Fields(c.Text)
+ if f[0] == "//build" {
+ ok := false
+ for _, tok := range f[1:] {
+ if ctxt.matchOSArch(tok) {
+ ok = true
+ break
+ }
+ }
+ if !ok {
+ return false // this one doesn't match
+ }
+ }
+ }
+ }
+ }
+ return true // everything matches
+}
+
+// saveCgo saves the information from the #cgo lines in the import "C" comment.
+// These lines set CFLAGS and LDFLAGS and pkg-config directives that affect
+// the way cgo's C code is built.
+//
+// TODO(rsc): This duplicates code in cgo.
+// Once the dust settles, remove this code from cgo.
+func (ctxt *Context) saveCgo(filename string, di *DirInfo, cg *ast.CommentGroup) os.Error {
+ text := doc.CommentText(cg)
+ for _, line := range strings.Split(text, "\n") {
+ orig := line
+
+ // Line is
+ // #cgo [GOOS/GOARCH...] LDFLAGS: stuff
+ //
+ line = strings.TrimSpace(line)
+ if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') {
+ continue
+ }
+
+ // Split at colon.
+ line = strings.TrimSpace(line[4:])
+ i := strings.Index(line, ":")
+ if i < 0 {
+ return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
+ }
+ line, argstr := line[:i], line[i+1:]
+
+ // Parse GOOS/GOARCH stuff.
+ f := strings.Fields(line)
+ if len(f) < 1 {
+ return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
+ }
+
+ cond, verb := f[:len(f)-1], f[len(f)-1]
+ if len(cond) > 0 {
+ ok := false
+ for _, c := range cond {
+ if ctxt.matchOSArch(c) {
+ ok = true
+ break
+ }
+ }
+ if !ok {
+ continue
+ }
+ }
+
+ args, err := splitQuoted(argstr)
+ if err != nil {
+ return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
+ }
+ for _, arg := range args {
+ if !safeName(arg) {
+ return fmt.Errorf("%s: malformed #cgo argument: %s", filename, arg)
+ }
+ }
+
+ switch verb {
+ case "CFLAGS":
+ di.CgoCFLAGS = append(di.CgoCFLAGS, args...)
+ case "LDFLAGS":
+ di.CgoLDFLAGS = append(di.CgoLDFLAGS, args...)
+ case "pkg-config":
+ di.CgoPkgConfig = append(di.CgoPkgConfig, args...)
+ default:
+ return fmt.Errorf("%s: invalid #cgo verb: %s", filename, orig)
+ }
+ }
+ return nil
+}
+
+var safeBytes = []byte("+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz")
+
+func safeName(s string) bool {
+ if 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
+}
+
+// splitQuoted splits the string s around each instance of one or more consecutive
+// white space characters while taking into account quotes and escaping, and
+// returns an array of substrings of s or an empty list if s contains only white space.
+// Single quotes and double quotes are recognized to prevent splitting within the
+// quoted region, and are removed from the resulting substrings. If a quote in s
+// isn't closed err will be set and r will have the unclosed argument as the
+// last element. The backslash is used for escaping.
+//
+// For example, the following string:
+//
+// a b:"c d" 'e''f' "g\""
+//
+// Would be parsed as:
+//
+// []string{"a", "b:c d", "ef", `g"`}
+//
+func splitQuoted(s string) (r []string, err os.Error) {
+ var args []string
+ arg := make([]int, len(s))
+ escaped := false
+ quoted := false
+ quote := 0
+ i := 0
+ for _, rune := range s {
+ switch {
+ case escaped:
+ escaped = false
+ case rune == '\\':
+ escaped = true
+ continue
+ case quote != 0:
+ if rune == quote {
+ quote = 0
+ continue
+ }
+ case rune == '"' || rune == '\'':
+ quoted = true
+ quote = rune
+ continue
+ case unicode.IsSpace(rune):
+ if quoted || i > 0 {
+ quoted = false
+ args = append(args, string(arg[:i]))
+ i = 0
+ }
+ continue
+ }
+ arg[i] = rune
+ i++
+ }
+ if quoted || i > 0 {
+ args = append(args, string(arg[:i]))
+ }
+ if quote != 0 {
+ err = os.NewError("unclosed quote")
+ } else if escaped {
+ err = os.NewError("unfinished escaping")
+ }
+ return args, err
+}
+
+// matchOSArch returns true if the name is one of:
+//
+// $GOOS
+// $GOARCH
+// $GOOS/$GOARCH
+//
+func (ctxt *Context) matchOSArch(name string) bool {
+ if name == ctxt.GOOS || name == ctxt.GOARCH {
+ return true
+ }
+ i := strings.Index(name, "/")
+ return i >= 0 && name[:i] == ctxt.GOOS && name[i+1:] == ctxt.GOARCH
+}
+
+// goodOSArchFile returns false if the name contains a $GOOS or $GOARCH
// suffix which does not match the current system.
// The recognized name formats are:
//
// name_$(GOARCH)_test.*
// name_$(GOOS)_$(GOARCH)_test.*
//
-func (ctxt *Context) goodOSArch(name string) bool {
+func (ctxt *Context) goodOSArchFile(name string) bool {
if dot := strings.Index(name, "."); dot != -1 {
name = name[:dot]
}