github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/pipenv/pipenv.go (about)

     1  package pipenv
     2  
     3  import (
     4  	"encoding/json"
     5  
     6  	"github.com/fossas/fossa-cli/errors"
     7  	"github.com/fossas/fossa-cli/exec"
     8  	"github.com/fossas/fossa-cli/graph"
     9  	"github.com/fossas/fossa-cli/pkg"
    10  )
    11  
    12  // Pipenv defines the interface for all pipenv tool implementations.
    13  type Pipenv interface {
    14  	Deps() (graph.Deps, error)
    15  }
    16  
    17  // Cmd implements Pipenv by parsing command output.
    18  type Cmd struct {
    19  	Dir   string
    20  	Graph func(dir string) (string, error)
    21  }
    22  
    23  // dependency is used to unmarshal the output from `pipenv graph --json-tree`.
    24  type dependency struct {
    25  	Package      string `json:"package_name"`
    26  	Resolved     string `json:"installed_version"`
    27  	Target       string `json:"required_version"`
    28  	Dependencies []dependency
    29  }
    30  
    31  // New constructs a Pipenv instance that calls the pipenv build tool.
    32  func New(dirname string) Pipenv {
    33  	return Cmd{
    34  		Dir:   dirname,
    35  		Graph: GraphJSON,
    36  	}
    37  }
    38  
    39  // Deps returns the dependencies of a pipenv project.
    40  func (p Cmd) Deps() (graph.Deps, error) {
    41  	depGraph := graph.Deps{}
    42  	rawJSON, err := p.Graph(p.Dir)
    43  	if err != nil {
    44  		return depGraph, err
    45  	}
    46  
    47  	deps, err := getDependencies(rawJSON)
    48  	if err != nil {
    49  		return depGraph, err
    50  	}
    51  
    52  	depGraph.Direct = getDirectDeps(deps)
    53  	depGraph.Transitive = getTransitiveDeps(deps)
    54  	return depGraph, nil
    55  }
    56  
    57  // GraphJSON returns the output from `pipenv graph --json-tree`.
    58  func GraphJSON(dirname string) (string, error) {
    59  	out, _, err := exec.Run(exec.Cmd{
    60  		Name: "pipenv",
    61  		Argv: []string{"graph", "--json-tree"},
    62  		Dir:  dirname,
    63  	})
    64  	if err != nil {
    65  		err = errors.Wrap(err, "Could not run `pipenv graph --json-tree` within the current directory")
    66  	}
    67  	return out, err
    68  }
    69  
    70  func getDependencies(graphJSON string) ([]dependency, error) {
    71  	var depList []dependency
    72  	err := json.Unmarshal([]byte(graphJSON), &depList)
    73  	if err != nil {
    74  		return nil, errors.Wrap(err, "Could not unmarshal JSON into dependency list")
    75  	}
    76  	return depList, nil
    77  }
    78  
    79  func getDirectDeps(depList []dependency) []pkg.Import {
    80  	var imports []pkg.Import
    81  	for _, dep := range depList {
    82  		imports = append(imports, pkg.Import{
    83  			Target: dep.Target,
    84  			Resolved: pkg.ID{
    85  				Type:     pkg.Python,
    86  				Name:     dep.Package,
    87  				Revision: dep.Resolved,
    88  			},
    89  		})
    90  	}
    91  	return imports
    92  }
    93  
    94  func getTransitiveDeps(directDeps []dependency) map[pkg.ID]pkg.Package {
    95  	graph := make(map[pkg.ID]pkg.Package)
    96  	for _, dep := range directDeps {
    97  		id := pkg.ID{
    98  			Type:     pkg.Python,
    99  			Name:     dep.Package,
   100  			Revision: dep.Resolved,
   101  		}
   102  
   103  		graph[id] = pkg.Package{
   104  			ID:      id,
   105  			Imports: packageImports(dep.Dependencies),
   106  		}
   107  
   108  		flattenDeepDependencies(graph, dep)
   109  	}
   110  
   111  	return graph
   112  }
   113  
   114  func flattenDeepDependencies(graph map[pkg.ID]pkg.Package, transDep dependency) {
   115  	for _, dep := range transDep.Dependencies {
   116  		id := pkg.ID{
   117  			Type:     pkg.Python,
   118  			Name:     dep.Package,
   119  			Revision: dep.Resolved,
   120  		}
   121  		// Don't process duplicate transitive dependencies.
   122  		_, ok := graph[id]
   123  		if ok {
   124  			continue
   125  		}
   126  
   127  		graph[id] = pkg.Package{
   128  			ID:      id,
   129  			Imports: packageImports(dep.Dependencies),
   130  		}
   131  		flattenDeepDependencies(graph, dep)
   132  	}
   133  }
   134  
   135  func packageImports(packageDeps []dependency) []pkg.Import {
   136  	var imports []pkg.Import
   137  	for _, i := range packageDeps {
   138  		imports = append(imports, pkg.Import{
   139  			Resolved: pkg.ID{
   140  				Type:     pkg.Python,
   141  				Name:     i.Package,
   142  				Revision: i.Resolved,
   143  			},
   144  		})
   145  	}
   146  	return imports
   147  }