| // 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 build |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "go/ast" |
| "go/doc" |
| "go/parser" |
| "go/token" |
| "io/ioutil" |
| "log" |
| "os" |
| "path" |
| "path/filepath" |
| "runtime" |
| "sort" |
| "strconv" |
| "strings" |
| "unicode" |
| ) |
| |
| // A Context specifies the supporting context for a build. |
| type Context struct { |
| GOARCH string // target architecture |
| GOOS string // target operating system |
| CgoEnabled bool // whether cgo can be used |
| |
| // 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 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 error) |
| } |
| |
| func (ctxt *Context) readDir(dir string) ([]os.FileInfo, error) { |
| if f := ctxt.ReadDir; f != nil { |
| return f(dir) |
| } |
| return ioutil.ReadDir(dir) |
| } |
| |
| func (ctxt *Context) readFile(dir, file string) (string, []byte, 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 = defaultContext() |
| |
| var cgoEnabled = map[string]bool{ |
| "darwin/386": true, |
| "darwin/amd64": true, |
| "linux/386": true, |
| "linux/amd64": true, |
| "freebsd/386": true, |
| "freebsd/amd64": true, |
| "windows/386": true, |
| "windows/amd64": true, |
| } |
| |
| func defaultContext() Context { |
| var c Context |
| |
| c.GOARCH = envOr("GOARCH", runtime.GOARCH) |
| c.GOOS = envOr("GOOS", runtime.GOOS) |
| |
| s := os.Getenv("CGO_ENABLED") |
| switch s { |
| case "1": |
| c.CgoEnabled = true |
| case "0": |
| c.CgoEnabled = false |
| default: |
| c.CgoEnabled = cgoEnabled[c.GOOS+"/"+c.GOARCH] |
| } |
| |
| return c |
| } |
| |
| func envOr(name, def string) string { |
| s := os.Getenv(name) |
| if s == "" { |
| return def |
| } |
| return s |
| } |
| |
| type DirInfo struct { |
| 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) |
| HFiles []string // .h files in dir |
| CFiles []string // .c files in dir |
| SFiles []string // .s (and, when using cgo, .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 { |
| // TODO(rsc): This is at least a little bogus. |
| return d.Package == "main" |
| } |
| |
| // ScanDir calls DefaultContext.ScanDir. |
| func ScanDir(dir string) (info *DirInfo, err 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 no other package is found) |
| // - files in package documentation |
| // - files ending in _test.go |
| // - files starting with _ or . |
| // |
| func (ctxt *Context) ScanDir(dir string) (info *DirInfo, err error) { |
| dirs, err := ctxt.readDir(dir) |
| if err != nil { |
| return nil, err |
| } |
| |
| var Sfiles []string // files with ".S" (capital S) |
| var di DirInfo |
| imported := make(map[string]bool) |
| testImported := make(map[string]bool) |
| fset := token.NewFileSet() |
| for _, d := range dirs { |
| if d.IsDir() { |
| continue |
| } |
| name := d.Name() |
| if strings.HasPrefix(name, "_") || |
| strings.HasPrefix(name, ".") { |
| continue |
| } |
| if !ctxt.goodOSArchFile(name) { |
| continue |
| } |
| |
| ext := path.Ext(name) |
| switch ext { |
| case ".go", ".c", ".s", ".h", ".S": |
| // tentatively okay |
| default: |
| // skip |
| continue |
| } |
| |
| // Look for +build comments to accept or reject the file. |
| filename, data, err := ctxt.readFile(dir, name) |
| if err != nil { |
| return nil, err |
| } |
| if !ctxt.shouldBuild(data) { |
| continue |
| } |
| |
| // Going to save the file. For non-Go files, can stop here. |
| switch ext { |
| case ".c": |
| di.CFiles = append(di.CFiles, name) |
| continue |
| case ".h": |
| di.HFiles = append(di.HFiles, name) |
| continue |
| case ".s": |
| di.SFiles = append(di.SFiles, name) |
| continue |
| case ".S": |
| Sfiles = append(Sfiles, name) |
| continue |
| } |
| |
| pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly|parser.ParseComments) |
| if err != nil { |
| return nil, err |
| } |
| |
| pkg := string(pf.Name.Name) |
| if pkg == "main" && di.Package != "" && di.Package != "main" { |
| continue |
| } |
| if pkg == "documentation" { |
| continue |
| } |
| |
| isTest := strings.HasSuffix(name, "_test.go") |
| if isTest && strings.HasSuffix(pkg, "_test") { |
| pkg = pkg[:len(pkg)-len("_test")] |
| } |
| |
| 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 |
| } |
| } |
| |
| // Record imports and information about cgo. |
| isCgo := false |
| for _, decl := range pf.Decls { |
| d, ok := decl.(*ast.GenDecl) |
| if !ok { |
| continue |
| } |
| 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 { |
| 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 |
| } |
| } |
| } |
| if isCgo { |
| if ctxt.CgoEnabled { |
| di.CgoFiles = append(di.CgoFiles, name) |
| } |
| } else if isTest { |
| if pkg == string(pf.Name.Name) { |
| di.TestGoFiles = append(di.TestGoFiles, name) |
| } else { |
| di.XTestGoFiles = append(di.XTestGoFiles, name) |
| } |
| } else { |
| di.GoFiles = append(di.GoFiles, 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.Imports[i] = p |
| i++ |
| } |
| di.TestImports = make([]string, len(testImported)) |
| i = 0 |
| for p := range testImported { |
| di.TestImports[i] = p |
| i++ |
| } |
| |
| // add the .S files only if we are using cgo |
| // (which means gcc will compile them). |
| // The standard assemblers expect .s files. |
| if len(di.CgoFiles) > 0 { |
| di.SFiles = append(di.SFiles, Sfiles...) |
| sort.Strings(di.SFiles) |
| } |
| |
| // File name lists are sorted because ReadDir sorts. |
| sort.Strings(di.Imports) |
| sort.Strings(di.TestImports) |
| return &di, nil |
| } |
| |
| var slashslash = []byte("//") |
| |
| // shouldBuild reports whether it is okay to use this file, |
| // The rule is that in the file's leading run of // comments |
| // and blank lines, which must be followed by a blank line |
| // (to avoid including a Go package clause doc comment), |
| // lines beginning with '// +build' are taken as build directives. |
| // |
| // 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(content []byte) bool { |
| // Pass 1. Identify leading run of // comments and blank lines, |
| // which must be followed by a blank line. |
| end := 0 |
| p := content |
| for len(p) > 0 { |
| line := p |
| if i := bytes.IndexByte(line, '\n'); i >= 0 { |
| line, p = line[:i], p[i+1:] |
| } else { |
| p = p[len(p):] |
| } |
| line = bytes.TrimSpace(line) |
| if len(line) == 0 { // Blank line |
| end = cap(content) - cap(line) // &line[0] - &content[0] |
| continue |
| } |
| if !bytes.HasPrefix(line, slashslash) { // Not comment line |
| break |
| } |
| } |
| content = content[:end] |
| |
| // Pass 2. Process each line in the run. |
| p = content |
| for len(p) > 0 { |
| line := p |
| if i := bytes.IndexByte(line, '\n'); i >= 0 { |
| line, p = line[:i], p[i+1:] |
| } else { |
| p = p[len(p):] |
| } |
| line = bytes.TrimSpace(line) |
| if bytes.HasPrefix(line, slashslash) { |
| line = bytes.TrimSpace(line[len(slashslash):]) |
| if len(line) > 0 && line[0] == '+' { |
| // Looks like a comment +line. |
| f := strings.Fields(string(line)) |
| 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) 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 error) { |
| var args []string |
| arg := make([]rune, len(s)) |
| escaped := false |
| quoted := false |
| quote := '\x00' |
| i := 0 |
| for _, rune := range s { |
| switch { |
| case escaped: |
| escaped = false |
| case rune == '\\': |
| escaped = true |
| continue |
| case quote != '\x00': |
| if rune == quote { |
| quote = '\x00' |
| 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 = errors.New("unclosed quote") |
| } else if escaped { |
| err = errors.New("unfinished escaping") |
| } |
| return args, err |
| } |
| |
| // matchOSArch returns true if the name is one of: |
| // |
| // $GOOS |
| // $GOARCH |
| // cgo (if cgo is enabled) |
| // nocgo (if cgo is disabled) |
| // a slash-separated list of any of these |
| // |
| func (ctxt *Context) matchOSArch(name string) bool { |
| if ctxt.CgoEnabled && name == "cgo" { |
| return true |
| } |
| if !ctxt.CgoEnabled && name == "nocgo" { |
| return true |
| } |
| if name == ctxt.GOOS || name == ctxt.GOARCH { |
| return true |
| } |
| i := strings.Index(name, "/") |
| return i >= 0 && ctxt.matchOSArch(name[:i]) && ctxt.matchOSArch(name[i+1:]) |
| } |
| |
| // 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_$(GOOS).* |
| // name_$(GOARCH).* |
| // name_$(GOOS)_$(GOARCH).* |
| // name_$(GOOS)_test.* |
| // name_$(GOARCH)_test.* |
| // name_$(GOOS)_$(GOARCH)_test.* |
| // |
| func (ctxt *Context) goodOSArchFile(name string) bool { |
| if dot := strings.Index(name, "."); dot != -1 { |
| name = name[:dot] |
| } |
| l := strings.Split(name, "_") |
| if n := len(l); n > 0 && l[n-1] == "test" { |
| l = l[:n-1] |
| } |
| n := len(l) |
| if n >= 2 && knownOS[l[n-2]] && knownArch[l[n-1]] { |
| return l[n-2] == ctxt.GOOS && l[n-1] == ctxt.GOARCH |
| } |
| if n >= 1 && knownOS[l[n-1]] { |
| return l[n-1] == ctxt.GOOS |
| } |
| if n >= 1 && knownArch[l[n-1]] { |
| return l[n-1] == ctxt.GOARCH |
| } |
| return true |
| } |
| |
| var knownOS = make(map[string]bool) |
| var knownArch = make(map[string]bool) |
| |
| func init() { |
| for _, v := range strings.Fields(goosList) { |
| knownOS[v] = true |
| } |
| for _, v := range strings.Fields(goarchList) { |
| knownArch[v] = true |
| } |
| } |