github.com/golang/dep@v0.5.4/cmd/dep/gopath_scanner.go (about)

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/golang/dep"
    15  	"github.com/golang/dep/gps"
    16  	"github.com/golang/dep/gps/paths"
    17  	"github.com/golang/dep/gps/pkgtree"
    18  	fb "github.com/golang/dep/internal/feedback"
    19  	"github.com/golang/dep/internal/fs"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  // gopathScanner supplies manifest/lock data by scanning the contents of GOPATH
    24  // It uses its results to fill-in any missing details left by the rootAnalyzer.
    25  type gopathScanner struct {
    26  	ctx        *dep.Ctx
    27  	directDeps map[gps.ProjectRoot]bool
    28  	sm         gps.SourceManager
    29  
    30  	pd    projectData
    31  	origM *dep.Manifest
    32  	origL *dep.Lock
    33  }
    34  
    35  func newGopathScanner(ctx *dep.Ctx, directDeps map[gps.ProjectRoot]bool, sm gps.SourceManager) *gopathScanner {
    36  	return &gopathScanner{
    37  		ctx:        ctx,
    38  		directDeps: directDeps,
    39  		sm:         sm,
    40  	}
    41  }
    42  
    43  // InitializeRootManifestAndLock performs analysis of the filesystem tree rooted
    44  // at path, with the root import path importRoot, to determine the project's
    45  // constraints. Respect any initial constraints defined in the root manifest and
    46  // lock.
    47  func (g *gopathScanner) InitializeRootManifestAndLock(rootM *dep.Manifest, rootL *dep.Lock) error {
    48  	var err error
    49  
    50  	g.ctx.Err.Println("Searching GOPATH for projects...")
    51  	g.pd, err = g.scanGopathForDependencies()
    52  	if err != nil {
    53  		return err
    54  	}
    55  
    56  	g.origM = dep.NewManifest()
    57  	g.origM.Constraints = g.pd.constraints
    58  
    59  	g.origL = &dep.Lock{
    60  		P: make([]gps.LockedProject, 0, len(g.pd.ondisk)),
    61  	}
    62  
    63  	for pr, v := range g.pd.ondisk {
    64  		// That we have to chop off these path prefixes is a symptom of
    65  		// a problem in gps itself
    66  		pkgs := make([]string, 0, len(g.pd.dependencies[pr]))
    67  		prslash := string(pr) + "/"
    68  		for _, pkg := range g.pd.dependencies[pr] {
    69  			if pkg == string(pr) {
    70  				pkgs = append(pkgs, ".")
    71  			} else {
    72  				pkgs = append(pkgs, trimPathPrefix(pkg, prslash))
    73  			}
    74  		}
    75  
    76  		g.origL.P = append(g.origL.P, gps.NewLockedProject(
    77  			gps.ProjectIdentifier{ProjectRoot: pr}, v, pkgs),
    78  		)
    79  	}
    80  
    81  	g.overlay(rootM, rootL)
    82  
    83  	return nil
    84  }
    85  
    86  // Fill in gaps in the root manifest/lock with data found from the GOPATH.
    87  func (g *gopathScanner) overlay(rootM *dep.Manifest, rootL *dep.Lock) {
    88  	for pkg, prj := range g.origM.Constraints {
    89  		if _, has := rootM.Constraints[pkg]; has {
    90  			continue
    91  		}
    92  		rootM.Constraints[pkg] = prj
    93  		v := g.pd.ondisk[pkg]
    94  
    95  		pi := gps.ProjectIdentifier{ProjectRoot: pkg, Source: prj.Source}
    96  		f := fb.NewConstraintFeedback(gps.ProjectConstraint{Ident: pi, Constraint: v}, fb.DepTypeDirect)
    97  		f.LogFeedback(g.ctx.Err)
    98  		f = fb.NewLockedProjectFeedback(gps.NewLockedProject(pi, v, nil), fb.DepTypeDirect)
    99  		f.LogFeedback(g.ctx.Err)
   100  	}
   101  
   102  	// Keep track of which projects have been locked
   103  	lockedProjects := map[gps.ProjectRoot]bool{}
   104  	for _, lp := range rootL.P {
   105  		lockedProjects[lp.Ident().ProjectRoot] = true
   106  	}
   107  
   108  	for _, lp := range g.origL.P {
   109  		pkg := lp.Ident().ProjectRoot
   110  		if _, isLocked := lockedProjects[pkg]; isLocked {
   111  			continue
   112  		}
   113  		rootL.P = append(rootL.P, lp)
   114  		lockedProjects[pkg] = true
   115  
   116  		if _, isDirect := g.directDeps[pkg]; !isDirect {
   117  			f := fb.NewLockedProjectFeedback(lp, fb.DepTypeTransitive)
   118  			f.LogFeedback(g.ctx.Err)
   119  		}
   120  	}
   121  
   122  	// Identify projects whose version is unknown and will have to be solved for
   123  	var missing []string    // all project roots missing from GOPATH
   124  	var missingVCS []string // all project roots missing VCS information
   125  	for pr := range g.pd.notondisk {
   126  		if _, isLocked := lockedProjects[pr]; isLocked {
   127  			continue
   128  		}
   129  		if g.pd.invalidSVC[pr] {
   130  			missingVCS = append(missingVCS, string(pr))
   131  		} else {
   132  			missing = append(missing, string(pr))
   133  		}
   134  	}
   135  
   136  	missingStr := ""
   137  	missingVCSStr := ""
   138  	if len(missing) > 0 {
   139  		missingStr = fmt.Sprintf("The following dependencies were not found in GOPATH:\n  %s\n\n",
   140  			strings.Join(missing, "\n  "))
   141  	}
   142  	if len(missingVCS) > 0 {
   143  		missingVCSStr = fmt.Sprintf("The following dependencies found in GOPATH were missing VCS information (a remote source is required):\n  %s\n\n",
   144  			strings.Join(missingVCS, "\n  "))
   145  	}
   146  	if len(missingVCS)+len(missing) > 0 {
   147  		g.ctx.Err.Printf("\n%s%sThe most recent version of these projects will be used.\n\n", missingStr, missingVCSStr)
   148  	}
   149  }
   150  
   151  func trimPathPrefix(p1, p2 string) string {
   152  	if isPrefix, _ := fs.HasFilepathPrefix(p1, p2); isPrefix {
   153  		return p1[len(p2):]
   154  	}
   155  	return p1
   156  }
   157  
   158  // contains checks if a array of strings contains a value
   159  func contains(a []string, b string) bool {
   160  	for _, v := range a {
   161  		if b == v {
   162  			return true
   163  		}
   164  	}
   165  	return false
   166  }
   167  
   168  // getProjectPropertiesFromVersion takes a Version and returns a proper
   169  // ProjectProperties with Constraint value based on the provided version.
   170  func getProjectPropertiesFromVersion(v gps.Version) gps.ProjectProperties {
   171  	pp := gps.ProjectProperties{}
   172  
   173  	// extract version and ignore if it's revision only
   174  	switch tv := v.(type) {
   175  	case gps.PairedVersion:
   176  		v = tv.Unpair()
   177  	case gps.Revision:
   178  		return pp
   179  	}
   180  
   181  	switch v.Type() {
   182  	case gps.IsBranch, gps.IsVersion:
   183  		pp.Constraint = v
   184  	case gps.IsSemver:
   185  		c, err := gps.NewSemverConstraintIC(v.String())
   186  		if err != nil {
   187  			panic(err)
   188  		}
   189  		pp.Constraint = c
   190  	}
   191  
   192  	return pp
   193  }
   194  
   195  type projectData struct {
   196  	constraints  gps.ProjectConstraints          // constraints that could be found
   197  	dependencies map[gps.ProjectRoot][]string    // all dependencies (imports) found by project root
   198  	notondisk    map[gps.ProjectRoot]bool        // projects that were not found on disk
   199  	invalidSVC   map[gps.ProjectRoot]bool        // projects that were found on disk but SVC data could not be read
   200  	ondisk       map[gps.ProjectRoot]gps.Version // projects that were found on disk
   201  }
   202  
   203  func (g *gopathScanner) scanGopathForDependencies() (projectData, error) {
   204  	constraints := make(gps.ProjectConstraints)
   205  	dependencies := make(map[gps.ProjectRoot][]string)
   206  	packages := make(map[string]bool)
   207  	notondisk := make(map[gps.ProjectRoot]bool)
   208  	invalidSVC := make(map[gps.ProjectRoot]bool)
   209  	ondisk := make(map[gps.ProjectRoot]gps.Version)
   210  
   211  	var syncDepGroup sync.WaitGroup
   212  	syncDep := func(pr gps.ProjectRoot, sm gps.SourceManager) {
   213  		if err := sm.SyncSourceFor(gps.ProjectIdentifier{ProjectRoot: pr}); err != nil {
   214  			g.ctx.Err.Printf("%+v", errors.Wrapf(err, "Unable to cache %s", pr))
   215  		}
   216  		syncDepGroup.Done()
   217  	}
   218  
   219  	if len(g.directDeps) == 0 {
   220  		return projectData{}, nil
   221  	}
   222  
   223  	for ippr := range g.directDeps {
   224  		// TODO(sdboyer) these are not import paths by this point, they've
   225  		// already been worked down to project roots.
   226  		ip := string(ippr)
   227  		pr, err := g.sm.DeduceProjectRoot(ip)
   228  		if err != nil {
   229  			return projectData{}, errors.Wrap(err, "sm.DeduceProjectRoot")
   230  		}
   231  
   232  		packages[ip] = true
   233  		if _, has := dependencies[pr]; has {
   234  			dependencies[pr] = append(dependencies[pr], ip)
   235  			continue
   236  		}
   237  		syncDepGroup.Add(1)
   238  		go syncDep(pr, g.sm)
   239  
   240  		dependencies[pr] = []string{ip}
   241  		abs, err := g.ctx.AbsForImport(string(pr))
   242  		if err != nil {
   243  			notondisk[pr] = true
   244  			continue
   245  		}
   246  		v, err := gps.VCSVersion(abs)
   247  		if err != nil {
   248  			invalidSVC[pr] = true
   249  			notondisk[pr] = true
   250  			continue
   251  		}
   252  
   253  		ondisk[pr] = v
   254  		pp := getProjectPropertiesFromVersion(v)
   255  		if pp.Constraint != nil || pp.Source != "" {
   256  			constraints[pr] = pp
   257  		}
   258  	}
   259  
   260  	// Explore the packages we've found for transitive deps, either
   261  	// completing the lock or identifying (more) missing projects that we'll
   262  	// need to ask gps to solve for us.
   263  	colors := make(map[string]uint8)
   264  	const (
   265  		white uint8 = iota
   266  		grey
   267  		black
   268  	)
   269  
   270  	// cache of PackageTrees, so we don't parse projects more than once
   271  	ptrees := make(map[gps.ProjectRoot]pkgtree.PackageTree)
   272  
   273  	// depth-first traverser
   274  	var dft func(string) error
   275  	dft = func(pkg string) error {
   276  		switch colors[pkg] {
   277  		case white:
   278  			colors[pkg] = grey
   279  
   280  			pr, err := g.sm.DeduceProjectRoot(pkg)
   281  			if err != nil {
   282  				return errors.Wrap(err, "could not deduce project root for "+pkg)
   283  			}
   284  
   285  			// We already visited this project root earlier via some other
   286  			// pkg within it, and made the decision that it's not on disk.
   287  			// Respect that decision, and pop the stack.
   288  			if notondisk[pr] {
   289  				colors[pkg] = black
   290  				return nil
   291  			}
   292  
   293  			ptree, has := ptrees[pr]
   294  			if !has {
   295  				// It's fine if the root does not exist - it indicates that this
   296  				// project is not present in the workspace, and so we need to
   297  				// solve to deal with this dep.
   298  				r := filepath.Join(g.ctx.GOPATH, "src", string(pr))
   299  				fi, err := os.Stat(r)
   300  				if os.IsNotExist(err) || !fi.IsDir() {
   301  					colors[pkg] = black
   302  					notondisk[pr] = true
   303  					return nil
   304  				}
   305  
   306  				// We know the project is on disk; the question is whether we're
   307  				// first seeing it here, in the transitive exploration, or if it
   308  				// was found in the initial pass on direct imports. We know it's
   309  				// the former if there's no entry for it in the ondisk map.
   310  				if _, in := ondisk[pr]; !in {
   311  					abs, err := g.ctx.AbsForImport(string(pr))
   312  					if err != nil {
   313  						colors[pkg] = black
   314  						notondisk[pr] = true
   315  						return nil
   316  					}
   317  					v, err := gps.VCSVersion(abs)
   318  					if err != nil {
   319  						// Even if we know it's on disk, errors are still
   320  						// possible when trying to deduce version. If we
   321  						// encounter such an error, just treat the project as
   322  						// not being on disk; the solver will work it out.
   323  						colors[pkg] = black
   324  						notondisk[pr] = true
   325  						return nil
   326  					}
   327  					ondisk[pr] = v
   328  				}
   329  
   330  				ptree, err = pkgtree.ListPackages(r, string(pr))
   331  				if err != nil {
   332  					// Any error here other than an a nonexistent dir (which
   333  					// can't happen because we covered that case above) is
   334  					// probably critical, so bail out.
   335  					return errors.Wrap(err, "gps.ListPackages")
   336  				}
   337  				ptrees[pr] = ptree
   338  			}
   339  
   340  			// Get a reachmap that includes main pkgs (even though importing
   341  			// them is an error, what we're checking right now is simply whether
   342  			// there's a package with go code present on disk), and does not
   343  			// backpropagate errors (again, because our only concern right now
   344  			// is package existence).
   345  			rm, errmap := ptree.ToReachMap(true, false, false, nil)
   346  			reached, ok := rm[pkg]
   347  			if !ok {
   348  				colors[pkg] = black
   349  				// not on disk...
   350  				notondisk[pr] = true
   351  				return nil
   352  			}
   353  			if _, ok := errmap[pkg]; ok {
   354  				// The package is on disk, but contains some errors.
   355  				colors[pkg] = black
   356  				return nil
   357  			}
   358  
   359  			if deps, has := dependencies[pr]; has {
   360  				if !contains(deps, pkg) {
   361  					dependencies[pr] = append(deps, pkg)
   362  				}
   363  			} else {
   364  				dependencies[pr] = []string{pkg}
   365  				syncDepGroup.Add(1)
   366  				go syncDep(pr, g.sm)
   367  			}
   368  
   369  			// recurse
   370  			for _, rpkg := range reached.External {
   371  				if paths.IsStandardImportPath(rpkg) {
   372  					continue
   373  				}
   374  
   375  				err := dft(rpkg)
   376  				if err != nil {
   377  					// Bubble up any errors we encounter
   378  					return err
   379  				}
   380  			}
   381  
   382  			colors[pkg] = black
   383  		case grey:
   384  			return errors.Errorf("Import cycle detected on %s", pkg)
   385  		}
   386  		return nil
   387  	}
   388  
   389  	// run the depth-first traversal from the set of immediate external
   390  	// package imports we found in the current project
   391  	for pkg := range packages {
   392  		err := dft(pkg)
   393  		if err != nil {
   394  			return projectData{}, err // already errors.Wrap()'d internally
   395  		}
   396  	}
   397  
   398  	syncDepGroup.Wait()
   399  
   400  	pd := projectData{
   401  		constraints:  constraints,
   402  		dependencies: dependencies,
   403  		invalidSVC:   invalidSVC,
   404  		notondisk:    notondisk,
   405  		ondisk:       ondisk,
   406  	}
   407  	return pd, nil
   408  }