github.com/anthonymayer/glide@v0.0.0-20160224162501-bff8b50d232e/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, 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  	for _, tt := range tgs {
    48  
    49  		// split the tag combination to look at permutations.
    50  		ts := strings.Split(tt, ",")
    51  		var ttgs []string
    52  		var arch string
    53  		var ops string
    54  		for _, ttt := range ts {
    55  			dirty := false
    56  			if strings.HasPrefix(ttt, "!") {
    57  				dirty = true
    58  				ttt = strings.TrimPrefix(ttt, "!")
    59  			}
    60  			if isSupportedOs(ttt) {
    61  				if dirty {
    62  					ops = getOsValue(ttt)
    63  				} else {
    64  					ops = ttt
    65  				}
    66  			} else if isSupportedArch(ttt) {
    67  				if dirty {
    68  					arch = getArchValue(ttt)
    69  				} else {
    70  					arch = ttt
    71  				}
    72  			} else {
    73  				if !dirty {
    74  					ttgs = append(ttgs, ttt)
    75  				}
    76  			}
    77  		}
    78  
    79  		// Handle the case where there are no tags but we need to iterate
    80  		// on something.
    81  		if len(ttgs) == 0 {
    82  			ttgs = append(ttgs, "")
    83  		}
    84  
    85  		b, err := util.GetBuildContext()
    86  		if err != nil {
    87  			return []string{}, err
    88  		}
    89  
    90  		// Make sure use all files is off
    91  		b.UseAllFiles = false
    92  
    93  		// Set the OS and Arch for this pass
    94  		b.GOARCH = arch
    95  		b.GOOS = ops
    96  		b.BuildTags = ttgs
    97  		msg.Debug("Scanning with Arch(%s), OS(%s), and Build Tags(%v)", arch, ops, ttgs)
    98  
    99  		pk, err := b.ImportDir(path, 0)
   100  
   101  		// If there are no buildable souce with this permutation we skip it.
   102  		if err != nil && strings.HasPrefix(err.Error(), "no buildable Go source files in") {
   103  			continue
   104  		} else if err != nil && strings.HasPrefix(err.Error(), "found packages ") {
   105  			// A permutation may cause multiple packages to appear. For example,
   106  			// an example file with an ignore build tag. If this happens we
   107  			// ignore it.
   108  			// TODO(mattfarina): Find a better way.
   109  			msg.Debug("Found multiple packages while scanning %s: %s", path, err)
   110  			continue
   111  		} else if err != nil {
   112  			msg.Debug("Problem parsing package at %s for %s %s", path, ops, arch)
   113  			return []string{}, err
   114  		}
   115  
   116  		for _, dep := range pk.Imports {
   117  			found := false
   118  			for _, p := range pkgs {
   119  				if p == dep {
   120  					found = true
   121  				}
   122  			}
   123  			if !found {
   124  				pkgs = append(pkgs, dep)
   125  			}
   126  		}
   127  	}
   128  
   129  	return pkgs, nil
   130  }
   131  
   132  func readBuildTags(p string) ([]string, error) {
   133  	_, err := os.Stat(p)
   134  	if err != nil {
   135  		return []string{}, err
   136  	}
   137  
   138  	d, err := os.Open(p)
   139  	if err != nil {
   140  		return []string{}, err
   141  	}
   142  
   143  	objects, err := d.Readdir(-1)
   144  	if err != nil {
   145  		return []string{}, err
   146  	}
   147  
   148  	var tags []string
   149  	for _, obj := range objects {
   150  
   151  		// only process Go files
   152  		if strings.HasSuffix(obj.Name(), ".go") {
   153  			fp := filepath.Join(p, obj.Name())
   154  
   155  			co, err := readGoContents(fp)
   156  			if err != nil {
   157  				return []string{}, err
   158  			}
   159  
   160  			// Only look at places where we had a code comment.
   161  			if len(co) > 0 {
   162  				t := findTags(co)
   163  				for _, tg := range t {
   164  					found := false
   165  					for _, tt := range tags {
   166  						if tt == tg {
   167  							found = true
   168  						}
   169  					}
   170  					if !found {
   171  						tags = append(tags, tg)
   172  					}
   173  				}
   174  			}
   175  		}
   176  	}
   177  
   178  	return tags, nil
   179  }
   180  
   181  // Read contents of a Go file up to the package declaration. This can be used
   182  // to find the the build tags.
   183  func readGoContents(fp string) ([]byte, error) {
   184  	f, err := os.Open(fp)
   185  	defer f.Close()
   186  	if err != nil {
   187  		return []byte{}, err
   188  	}
   189  
   190  	var s scanner.Scanner
   191  	s.Init(f)
   192  	var tok rune
   193  	var pos scanner.Position
   194  	for tok != scanner.EOF {
   195  		tok = s.Scan()
   196  
   197  		// Getting the token text will skip comments by default.
   198  		tt := s.TokenText()
   199  		// build tags will not be after the package declaration.
   200  		if tt == "package" {
   201  			pos = s.Position
   202  			break
   203  		}
   204  	}
   205  
   206  	buf := bytes.NewBufferString("")
   207  	f.Seek(0, 0)
   208  	_, err = io.CopyN(buf, f, int64(pos.Offset))
   209  	if err != nil {
   210  		return []byte{}, err
   211  	}
   212  
   213  	return buf.Bytes(), nil
   214  }
   215  
   216  // From a byte slice of a Go file find the tags.
   217  func findTags(co []byte) []string {
   218  	p := co
   219  	var tgs []string
   220  	for len(p) > 0 {
   221  		line := p
   222  		if i := bytes.IndexByte(line, '\n'); i >= 0 {
   223  			line, p = line[:i], p[i+1:]
   224  		} else {
   225  			p = p[len(p):]
   226  		}
   227  		line = bytes.TrimSpace(line)
   228  		// Only look at comment lines that are well formed in the Go style
   229  		if bytes.HasPrefix(line, []byte("//")) {
   230  			line = bytes.TrimSpace(line[len([]byte("//")):])
   231  			if len(line) > 0 && line[0] == '+' {
   232  				f := strings.Fields(string(line))
   233  
   234  				// We've found a +build tag line.
   235  				if f[0] == "+build" {
   236  					for _, tg := range f[1:] {
   237  						tgs = append(tgs, tg)
   238  					}
   239  				}
   240  			}
   241  		}
   242  	}
   243  
   244  	return tgs
   245  }
   246  
   247  // Get an OS value that's not the one passed in.
   248  func getOsValue(n string) string {
   249  	for _, o := range osList {
   250  		if o != n {
   251  			return o
   252  		}
   253  	}
   254  
   255  	return n
   256  }
   257  
   258  func isSupportedOs(n string) bool {
   259  	for _, o := range osList {
   260  		if o == n {
   261  			return true
   262  		}
   263  	}
   264  
   265  	return false
   266  }
   267  
   268  // Get an Arch value that's not the one passed in.
   269  func getArchValue(n string) string {
   270  	for _, o := range archList {
   271  		if o != n {
   272  			return o
   273  		}
   274  	}
   275  
   276  	return n
   277  }
   278  
   279  func isSupportedArch(n string) bool {
   280  	for _, o := range archList {
   281  		if o == n {
   282  			return true
   283  		}
   284  	}
   285  
   286  	return false
   287  }