Parse build tags and scan a package based on those
diff --git a/dependency/scan.go b/dependency/scan.go index 5b99858..1b5ac65 100644 --- a/dependency/scan.go +++ b/dependency/scan.go
@@ -1,7 +1,12 @@ package dependency import ( + "bytes" + "io" + "os" + "path/filepath" "strings" + "text/scanner" "github.com/Masterminds/glide/msg" "github.com/Masterminds/glide/util" @@ -32,40 +37,242 @@ // of ignore. This is a bit of a hack. It causes UseAllFiles to have errors. func IterativeScan(path string) ([]string, error) { + // TODO(mattfarina): Add support for release tags. + + tgs, _ := readBuildTags(path) + // Handle the case of scanning with no tags + tgs = append(tgs, "") + var pkgs []string - for _, o := range osList { - for _, a := range archList { - b, err := util.GetBuildContext() - if err != nil { - return []string{}, err + for _, tt := range tgs { + + // split the tag combination to look at permutations. + ts := strings.Split(tt, ",") + var ttgs []string + var arch string + var ops string + for _, ttt := range ts { + dirty := false + if strings.HasPrefix(ttt, "!") { + dirty = true + ttt = strings.TrimPrefix(ttt, "!") } - - // Make sure use all files is off - b.UseAllFiles = false - - // Set the OS and Arch for this pass - b.GOARCH = a - b.GOOS = o - - pk, err := b.ImportDir(path, 0) - if err != nil { - msg.Debug("Problem parsing package at %s for %s %s", path, o, a) - return []string{}, err + if isSupportedOs(ttt) { + if dirty { + ops = getOsValue(ttt) + } else { + ops = ttt + } + } else if isSupportedArch(ttt) { + if dirty { + arch = getArchValue(ttt) + } else { + arch = ttt + } + } else { + if !dirty { + ttgs = append(ttgs, ttt) + } } + } - for _, dep := range pk.Imports { - found := false - for _, p := range pkgs { - if p == dep { - found = true - } + // Handle the case where there are no tags but we need to iterate + // on something. + if len(ttgs) == 0 { + ttgs = append(ttgs, "") + } + + b, err := util.GetBuildContext() + if err != nil { + return []string{}, err + } + + // Make sure use all files is off + b.UseAllFiles = false + + // Set the OS and Arch for this pass + b.GOARCH = arch + b.GOOS = ops + b.BuildTags = ttgs + msg.Debug("Scanning with Arch(%s), OS(%s), and Build Tags(%v)", arch, ops, ttgs) + + pk, err := b.ImportDir(path, 0) + if err != nil && strings.HasPrefix(err.Error(), "no buildable Go source files in") { + continue + } else if err != nil { + msg.Debug("Problem parsing package at %s for %s %s", path, ops, arch) + return []string{}, err + } + + for _, dep := range pk.Imports { + found := false + for _, p := range pkgs { + if p == dep { + found = true } - if !found { - pkgs = append(pkgs, dep) - } + } + if !found { + pkgs = append(pkgs, dep) } } } return pkgs, nil } + +func readBuildTags(p string) ([]string, error) { + _, err := os.Stat(p) + if err != nil { + return []string{}, err + } + + d, err := os.Open(p) + if err != nil { + return []string{}, err + } + + objects, err := d.Readdir(-1) + if err != nil { + return []string{}, err + } + + var tags []string + for _, obj := range objects { + + // only process Go files + if strings.HasSuffix(obj.Name(), ".go") { + fp := filepath.Join(p, obj.Name()) + + co, err := readGoContents(fp) + if err != nil { + return []string{}, err + } + + // Only look at places where we had a code comment. + if len(co) > 0 { + t := findTags(co) + for _, tg := range t { + found := false + for _, tt := range tags { + if tt == tg { + found = true + } + } + if !found { + tags = append(tags, tg) + } + } + } + } + } + + return tags, nil +} + +// Read contents of a Go file up to the package declaration. This can be used +// to find the the build tags. +func readGoContents(fp string) ([]byte, error) { + f, err := os.Open(fp) + defer f.Close() + if err != nil { + return []byte{}, err + } + + var s scanner.Scanner + s.Init(f) + var tok rune + var pos scanner.Position + for tok != scanner.EOF { + tok = s.Scan() + + // Getting the token text will skip comments by default. + tt := s.TokenText() + // build tags will not be after the package declaration. + if tt == "package" { + pos = s.Position + break + } + } + + buf := bytes.NewBufferString("") + f.Seek(0, 0) + _, err = io.CopyN(buf, f, int64(pos.Offset)) + if err != nil { + return []byte{}, err + } + + return buf.Bytes(), nil +} + +// From a byte slice of a Go file find the tags. +func findTags(co []byte) []string { + p := co + var tgs []string + 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) + // Only look at comment lines that are well formed in the Go style + if bytes.HasPrefix(line, []byte("//")) { + line = bytes.TrimSpace(line[len([]byte("//")):]) + if len(line) > 0 && line[0] == '+' { + f := strings.Fields(string(line)) + + // We've found a +build tag line. + if f[0] == "+build" { + for _, tg := range f[1:] { + tgs = append(tgs, tg) + } + } + } + } + } + + return tgs +} + +// Get an OS value that's not the one passed in. +func getOsValue(n string) string { + for _, o := range osList { + if o != n { + return o + } + } + + return n +} + +func isSupportedOs(n string) bool { + for _, o := range osList { + if o == n { + return true + } + } + + return false +} + +// Get an Arch value that's not the one passed in. +func getArchValue(n string) string { + for _, o := range archList { + if o != n { + return o + } + } + + return n +} + +func isSupportedArch(n string) bool { + for _, o := range archList { + if o == n { + return true + } + } + + return false +}