| package dependency |
| |
| import ( |
| "bytes" |
| "io" |
| "os" |
| "path/filepath" |
| "strings" |
| "text/scanner" |
| |
| "github.com/Masterminds/glide/msg" |
| "github.com/Masterminds/glide/util" |
| ) |
| |
| var osList []string |
| var archList []string |
| |
| func init() { |
| // The supported systems are listed in |
| // https://github.com/golang/go/blob/master/src/go/build/syslist.go |
| // The lists are not exported so we need to duplicate them here. |
| osListString := "android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris windows" |
| osList = strings.Split(osListString, " ") |
| |
| archListString := "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc s390 s390x sparc sparc64" |
| archList = strings.Split(archListString, " ") |
| } |
| |
| // IterativeScan attempts to obtain a list of imported dependencies from a |
| // package. This scanning is different from ImportDir as part of the go/build |
| // package. It looks over different permutations of the supported OS/Arch to |
| // try and find all imports. This is different from setting UseAllFiles to |
| // true on the build Context. It scopes down to just the supported OS/Arch. |
| // |
| // Note, there are cases where multiple packages are in the same directory. This |
| // usually happens with an example that has a main package and a +build tag |
| // of ignore. This is a bit of a hack. It causes UseAllFiles to have errors. |
| func IterativeScan(path string) ([]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 |
| var testPkgs []string |
| 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, "!") |
| } |
| 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) |
| } |
| } |
| } |
| |
| // 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{}, []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 there are no buildable souce with this permutation we skip it. |
| if err != nil && strings.HasPrefix(err.Error(), "no buildable Go source files in") { |
| continue |
| } else if err != nil && strings.HasPrefix(err.Error(), "found packages ") { |
| // A permutation may cause multiple packages to appear. For example, |
| // an example file with an ignore build tag. If this happens we |
| // ignore it. |
| // TODO(mattfarina): Find a better way. |
| msg.Debug("Found multiple packages while scanning %s: %s", path, err) |
| continue |
| } else if err != nil { |
| msg.Debug("Problem parsing package at %s for %s %s", path, ops, arch) |
| return []string{}, []string{}, err |
| } |
| |
| for _, dep := range pk.Imports { |
| found := false |
| for _, p := range pkgs { |
| if p == dep { |
| found = true |
| } |
| } |
| if !found { |
| pkgs = append(pkgs, dep) |
| } |
| } |
| |
| for _, dep := range pk.TestImports { |
| found := false |
| for _, p := range pkgs { |
| if p == dep { |
| found = true |
| } |
| } |
| if !found { |
| testPkgs = append(testPkgs, dep) |
| } |
| } |
| } |
| |
| return pkgs, testPkgs, 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 |
| } |