github.com/vsysoev/glide@v0.12.3/dependency/scan.go (about)

     1  package dependency
     2  
     3  import (
     4  	"bytes"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"text/scanner"
    10  
    11  	"github.com/Masterminds/glide/msg"
    12  	"github.com/Masterminds/glide/util"
    13  )
    14  
    15  var osList []string
    16  var archList []string
    17  
    18  func init() {
    19  	// The supported systems are listed in
    20  	// https://github.com/golang/go/blob/master/src/go/build/syslist.go
    21  	// The lists are not exported so we need to duplicate them here.
    22  	osListString := "android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris windows"
    23  	osList = strings.Split(osListString, " ")
    24  
    25  	archListString := "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc s390 s390x sparc sparc64"
    26  	archList = strings.Split(archListString, " ")
    27  }
    28  
    29  // IterativeScan attempts to obtain a list of imported dependencies from a
    30  // package. This scanning is different from ImportDir as part of the go/build
    31  // package. It looks over different permutations of the supported OS/Arch to
    32  // try and find all imports. This is different from setting UseAllFiles to
    33  // true on the build Context. It scopes down to just the supported OS/Arch.
    34  //
    35  // Note, there are cases where multiple packages are in the same directory. This
    36  // usually happens with an example that has a main package and a +build tag
    37  // of ignore. This is a bit of a hack. It causes UseAllFiles to have errors.
    38  func IterativeScan(path string) ([]string, []string, error) {
    39  
    40  	// TODO(mattfarina): Add support for release tags.
    41  
    42  	tgs, _ := readBuildTags(path)
    43  	// Handle the case of scanning with no tags
    44  	tgs = append(tgs, "")
    45  
    46  	var pkgs []string
    47  	var testPkgs []string
    48  	for _, tt := range tgs {
    49  
    50  		// split the tag combination to look at permutations.
    51  		ts := strings.Split(tt, ",")
    52  		var ttgs []string
    53  		var arch string
    54  		var ops string
    55  		for _, ttt := range ts {
    56  			dirty := false
    57  			if strings.HasPrefix(ttt, "!") {
    58  				dirty = true
    59  				ttt = strings.TrimPrefix(ttt, "!")
    60  			}
    61  			if isSupportedOs(ttt) {
    62  				if dirty {
    63  					ops = getOsValue(ttt)
    64  				} else {
    65  					ops = ttt
    66  				}
    67  			} else if isSupportedArch(ttt) {
    68  				if dirty {
    69  					arch = getArchValue(ttt)
    70  				} else {
    71  					arch = ttt
    72  				}
    73  			} else {
    74  				if !dirty {
    75  					ttgs = append(ttgs, ttt)
    76  				}
    77  			}
    78  		}
    79  
    80  		// Handle the case where there are no tags but we need to iterate
    81  		// on something.
    82  		if len(ttgs) == 0 {
    83  			ttgs = append(ttgs, "")
    84  		}
    85  
    86  		b, err := util.GetBuildContext()
    87  		if err != nil {
    88  			return []string{}, []string{}, err
    89  		}
    90  
    91  		// Make sure use all files is off
    92  		b.UseAllFiles = false
    93  
    94  		// Set the OS and Arch for this pass
    95  		b.GOARCH = arch
    96  		b.GOOS = ops
    97  		b.BuildTags = ttgs
    98  		msg.Debug("Scanning with Arch(%s), OS(%s), and Build Tags(%v)", arch, ops, ttgs)
    99  
   100  		pk, err := b.ImportDir(path, 0)
   101  
   102  		// If there are no buildable souce with this permutation we skip it.
   103  		if err != nil && strings.HasPrefix(err.Error(), "no buildable Go source files in") {
   104  			continue
   105  		} else if err != nil && strings.HasPrefix(err.Error(), "found packages ") {
   106  			// A permutation may cause multiple packages to appear. For example,
   107  			// an example file with an ignore build tag. If this happens we
   108  			// ignore it.
   109  			// TODO(mattfarina): Find a better way.
   110  			msg.Debug("Found multiple packages while scanning %s: %s", path, err)
   111  			continue
   112  		} else if err != nil {
   113  			msg.Debug("Problem parsing package at %s for %s %s", path, ops, arch)
   114  			return []string{}, []string{}, err
   115  		}
   116  
   117  		for _, dep := range pk.Imports {
   118  			found := false
   119  			for _, p := range pkgs {
   120  				if p == dep {
   121  					found = true
   122  				}
   123  			}
   124  			if !found {
   125  				pkgs = append(pkgs, dep)
   126  			}
   127  		}
   128  
   129  		for _, dep := range pk.TestImports {
   130  			found := false
   131  			for _, p := range pkgs {
   132  				if p == dep {
   133  					found = true
   134  				}
   135  			}
   136  			if !found {
   137  				testPkgs = append(testPkgs, dep)
   138  			}
   139  		}
   140  
   141  		for _, dep := range pk.XTestImports {
   142  			found := false
   143  			for _, p := range pkgs {
   144  				if p == dep {
   145  					found = true
   146  				}
   147  			}
   148  			if !found {
   149  				testPkgs = append(testPkgs, dep)
   150  			}
   151  		}
   152  	}
   153  
   154  	return pkgs, testPkgs, nil
   155  }
   156  
   157  func readBuildTags(p string) ([]string, error) {
   158  	_, err := os.Stat(p)
   159  	if err != nil {
   160  		return []string{}, err
   161  	}
   162  
   163  	d, err := os.Open(p)
   164  	if err != nil {
   165  		return []string{}, err
   166  	}
   167  
   168  	objects, err := d.Readdir(-1)
   169  	if err != nil {
   170  		return []string{}, err
   171  	}
   172  
   173  	var tags []string
   174  	for _, obj := range objects {
   175  
   176  		// only process Go files
   177  		if strings.HasSuffix(obj.Name(), ".go") {
   178  			fp := filepath.Join(p, obj.Name())
   179  
   180  			co, err := readGoContents(fp)
   181  			if err != nil {
   182  				return []string{}, err
   183  			}
   184  
   185  			// Only look at places where we had a code comment.
   186  			if len(co) > 0 {
   187  				t := findTags(co)
   188  				for _, tg := range t {
   189  					found := false
   190  					for _, tt := range tags {
   191  						if tt == tg {
   192  							found = true
   193  						}
   194  					}
   195  					if !found {
   196  						tags = append(tags, tg)
   197  					}
   198  				}
   199  			}
   200  		}
   201  	}
   202  
   203  	return tags, nil
   204  }
   205  
   206  // Read contents of a Go file up to the package declaration. This can be used
   207  // to find the the build tags.
   208  func readGoContents(fp string) ([]byte, error) {
   209  	f, err := os.Open(fp)
   210  	defer f.Close()
   211  	if err != nil {
   212  		return []byte{}, err
   213  	}
   214  
   215  	var s scanner.Scanner
   216  	s.Init(f)
   217  	var tok rune
   218  	var pos scanner.Position
   219  	for tok != scanner.EOF {
   220  		tok = s.Scan()
   221  
   222  		// Getting the token text will skip comments by default.
   223  		tt := s.TokenText()
   224  		// build tags will not be after the package declaration.
   225  		if tt == "package" {
   226  			pos = s.Position
   227  			break
   228  		}
   229  	}
   230  
   231  	buf := bytes.NewBufferString("")
   232  	f.Seek(0, 0)
   233  	_, err = io.CopyN(buf, f, int64(pos.Offset))
   234  	if err != nil {
   235  		return []byte{}, err
   236  	}
   237  
   238  	return buf.Bytes(), nil
   239  }
   240  
   241  // From a byte slice of a Go file find the tags.
   242  func findTags(co []byte) []string {
   243  	p := co
   244  	var tgs []string
   245  	for len(p) > 0 {
   246  		line := p
   247  		if i := bytes.IndexByte(line, '\n'); i >= 0 {
   248  			line, p = line[:i], p[i+1:]
   249  		} else {
   250  			p = p[len(p):]
   251  		}
   252  		line = bytes.TrimSpace(line)
   253  		// Only look at comment lines that are well formed in the Go style
   254  		if bytes.HasPrefix(line, []byte("//")) {
   255  			line = bytes.TrimSpace(line[len([]byte("//")):])
   256  			if len(line) > 0 && line[0] == '+' {
   257  				f := strings.Fields(string(line))
   258  
   259  				// We've found a +build tag line.
   260  				if f[0] == "+build" {
   261  					for _, tg := range f[1:] {
   262  						tgs = append(tgs, tg)
   263  					}
   264  				}
   265  			}
   266  		}
   267  	}
   268  
   269  	return tgs
   270  }
   271  
   272  // Get an OS value that's not the one passed in.
   273  func getOsValue(n string) string {
   274  	for _, o := range osList {
   275  		if o != n {
   276  			return o
   277  		}
   278  	}
   279  
   280  	return n
   281  }
   282  
   283  func isSupportedOs(n string) bool {
   284  	for _, o := range osList {
   285  		if o == n {
   286  			return true
   287  		}
   288  	}
   289  
   290  	return false
   291  }
   292  
   293  // Get an Arch value that's not the one passed in.
   294  func getArchValue(n string) string {
   295  	for _, o := range archList {
   296  		if o != n {
   297  			return o
   298  		}
   299  	}
   300  
   301  	return n
   302  }
   303  
   304  func isSupportedArch(n string) bool {
   305  	for _, o := range archList {
   306  		if o == n {
   307  			return true
   308  		}
   309  	}
   310  
   311  	return false
   312  }