Skip to content

Commit

Permalink
feat(python): Python analyser
Browse files Browse the repository at this point in the history
  • Loading branch information
elldritch committed Jun 21, 2018
1 parent 82397b2 commit 0de97ca
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 207 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ test: docker-test
.PHONY: dev
dev: docker-test-base
sudo docker run -it \
-v $$GOPATH/github.com/fossas/fossa-cli:/home/fossa/go/src/github.com/fossas/fossa-cli \
-v $$GOPATH/src/github.com/fossas/fossa-cli:/home/fossa/go/src/github.com/fossas/fossa-cli \
-v $$GOPATH/bin:/home/fossa/go/bin \
quay.io/fossa/fossa-cli-test-base /bin/bash

Expand Down
3 changes: 2 additions & 1 deletion analyzers/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/fossas/fossa-cli/analyzers/golang"
"github.com/fossas/fossa-cli/analyzers/nodejs"
"github.com/fossas/fossa-cli/analyzers/python"

"github.com/fossas/fossa-cli/module"
"github.com/fossas/fossa-cli/pkg"
Expand Down Expand Up @@ -51,7 +52,7 @@ func New(key pkg.Type, options map[string]interface{}) (Analyzer, error) {
case pkg.NuGet:
return nil, ErrAnalyzerNotImplemented
case pkg.Python:
return nil, ErrAnalyzerNotImplemented
return python.New(options)
case pkg.Ruby:
return nil, ErrAnalyzerNotImplemented
case pkg.Scala:
Expand Down
6 changes: 2 additions & 4 deletions analyzers/nodejs/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (a *Analyzer) Discover(dir string) ([]module.Module, error) {
if !info.IsDir() && info.Name() == "package.json" {
name := filepath.Base(filepath.Dir(path))
// Parse from project name from `package.json` if possible
if manifest, err := npm.FromManifest(path); err == nil {
if manifest, err := npm.FromManifest(path); err == nil && manifest.Name != "" {
name = manifest.Name
}

Expand Down Expand Up @@ -255,7 +255,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
return m, nil
}

func recurseDeps(pkgMap map[pkg.ID]pkg.Package, p npm.Output) map[pkg.ID]pkg.Package {
func recurseDeps(pkgMap map[pkg.ID]pkg.Package, p npm.Output) {
for name, dep := range p.Dependencies {
// Construct ID.
id := pkg.ID{
Expand Down Expand Up @@ -290,6 +290,4 @@ func recurseDeps(pkgMap map[pkg.ID]pkg.Package, p npm.Output) map[pkg.ID]pkg.Pac
// Recurse in imports.
recurseDeps(pkgMap, dep)
}

return pkgMap
}
241 changes: 54 additions & 187 deletions analyzers/python/pip.go
Original file line number Diff line number Diff line change
@@ -1,207 +1,74 @@
package python

//go:generate go-bindata -pkg bindata -o ./bindata/bindata.go ./bindata/pipdeptree.py
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/pkg/errors"

"github.com/fossas/fossa-cli/builders/builderutil"
"github.com/fossas/fossa-cli/builders/python/bindata"
"github.com/fossas/fossa-cli/exec"
"github.com/fossas/fossa-cli/files"
"github.com/fossas/fossa-cli/log"
"github.com/fossas/fossa-cli/module"
"github.com/fossas/fossa-cli/buildtools/pip"
"github.com/fossas/fossa-cli/pkg"
)

// PipBuilder implements Builder for Pip.
// These properties are public for the sake of serialization.
type PipBuilder struct {
PythonCmd string
PythonVersion string

PipCmd string
PipVersion string

Virtualenv string
}

// Initialize collects metadata on Python and Pip binaries
func (builder *PipBuilder) Initialize() error {
log.Logger.Debug("Initializing Pip builder...")

// Set Python context variables
pythonCmd, pythonVersion, err := exec.Which("--version", os.Getenv("PYTHON_BINARY"), "python", "python3", "python27", "python2")
if err != nil {
log.Logger.Warningf("Could not find Python binary (try setting $PYTHON_BINARY): %s", err.Error())
}
builder.PythonCmd = pythonCmd
builder.PythonVersion = pythonVersion

// Set Pip context variables
pipCmd, pipVersion, pipErr := exec.Which("--version", os.Getenv("PIP_BINARY"), "pip")
builder.PipCmd = pipCmd
builder.PipVersion = pipVersion
if pipErr != nil {
log.Logger.Warningf("No supported Python build tools detected (try setting $PIP_BINARY): %#v", pipErr)
}

// Check virtualenv
builder.Virtualenv = os.Getenv("VIRTUAL_ENV")

log.Logger.Debugf("Initialized Pip builder: %#v", builder)
return nil
}

// Build runs `pip install -r requirements.txt`
func (builder *PipBuilder) Build(m module.Module, force bool) error {
log.Logger.Debugf("Running Pip build: %#v %#v", m, force)

_, _, err := exec.Run(exec.Cmd{
Name: builder.PipCmd,
Argv: []string{"install", "-r", "requirements"},
Dir: m.Dir,
})
if err != nil {
return fmt.Errorf("could not run Pip build: %s", err.Error())
}

log.Logger.Debug("Done running Pip build.")
return nil
}

type pipDepTreeDep struct {
PackageName string `json:"package_name"`
InstalledVersion string `json:"installed_version"`
Dependencies []pipDepTreeDep
}

func flattenPipDepTree(pkg pipDepTreeDep, from module.ImportPath) []builderutil.Imported {
var imports []builderutil.Imported
locator := module.Locator{
Fetcher: "pip",
Project: pkg.PackageName,
Revision: pkg.InstalledVersion,
}
for _, dep := range pkg.Dependencies {
imports = append(imports, flattenPipDepTree(dep, append(from, locator))...)
func FromRequirements(reqs []pip.Requirement) []pkg.Import {
var imports []pkg.Import
for _, req := range reqs {
imports = append(imports, pkg.Import{
Target: req.String(),
Resolved: pkg.ID{
Type: pkg.Python,
Name: req.Name,
Revision: req.Revision,
},
})
}
imports = append(imports, builderutil.Imported{
Locator: locator,
From: append(module.ImportPath{}, from...),
})
return imports
}

// Analyze runs `pipdeptree.py` in the current environment
func (builder *PipBuilder) Analyze(m module.Module, allowUnresolved bool) ([]module.Dependency, error) {
log.Logger.Debugf("Running Pip analysis: %#v %#v", m, allowUnresolved)

// Write helper to disk.
pipdeptreeSrc, err := bindata.Asset("builders/python/bindata/pipdeptree.py")
if err != nil {
return nil, errors.Wrap(err, "could not read `pipdeptree` helper")
}

pipdeptreeFile, err := ioutil.TempFile("", "fossa-cli-pipdeptree")
if err != nil {
return nil, errors.Wrap(err, "could not create temp file to write `pipdeptree` helper")
}
defer pipdeptreeFile.Close()
defer os.Remove(pipdeptreeFile.Name())

n, err := pipdeptreeFile.Write(pipdeptreeSrc)
if len(pipdeptreeSrc) != n || err != nil {
return nil, errors.Wrap(err, "could not write `pipdeptree` helper")
}
err = pipdeptreeFile.Sync()
if err != nil {
return nil, errors.Wrap(err, "could not flush `pipdeptree` helper to storage")
}

// Run helper.
out, _, err := exec.Run(exec.Cmd{
Name: builder.PythonCmd,
Argv: []string{pipdeptreeFile.Name(), "--local-only", "--json-tree"},
Dir: m.Dir,
})
if err != nil {
return nil, errors.Wrap(err, "could not run `pipdeptree`")
}

// Parse output.
var pipdeptreeOutput []pipDepTreeDep
err = json.Unmarshal([]byte(out), &pipdeptreeOutput)
if err != nil {
return nil, errors.Wrap(err, "could not parse `pipdeptree` output")
}

// Flatten tree.
var imports []builderutil.Imported
for _, dep := range pipdeptreeOutput {
imports = append(imports, flattenPipDepTree(dep, module.ImportPath{
module.Locator{
Fetcher: "root",
Project: "root",
Revision: "",
func FromTree(tree pip.DepTree) ([]pkg.Import, map[pkg.ID]pkg.Package) {
var imports []pkg.Import
for _, dep := range tree.Dependencies {
imports = append(imports, pkg.Import{
Target: dep.Target,
Resolved: pkg.ID{
Type: pkg.Python,
Name: dep.Package,
Revision: dep.Resolved,
},
})...)
})
}
deps := builderutil.ComputeImportPaths(imports)

log.Logger.Debugf("Done running Pip analysis: %#v", deps)
return deps, nil
}
graph := make(map[pkg.ID]pkg.Package)
flattenTree(graph, tree)

// IsBuilt checks for the existence of `requirements.txt`
func (builder *PipBuilder) IsBuilt(m module.Module, allowUnresolved bool) (bool, error) {
log.Logger.Debugf("Checking Pip build: %#v %#v", m, allowUnresolved)

// TODO: support `pip freeze` output and other alternative methods?
isBuilt, err := files.Exists(m.Dir, "requirements.txt")
if err != nil {
return false, err
}

log.Logger.Debugf("Done checking Pip build: %#v", isBuilt)
return isBuilt, nil
return imports, graph
}

// IsModule is not implemented
func (builder *PipBuilder) IsModule(target string) (bool, error) {
return false, errors.New("IsModule is not implemented for PipBuilder")
}

// DiscoverModules builds ModuleConfigs for any `requirements.txt` files
func (builder *PipBuilder) DiscoverModules(dir string) ([]module.Config, error) {
var moduleConfigs []module.Config
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Logger.Debugf("Failed to access path %s: %s\n", path, err.Error())
return err
func flattenTree(graph map[pkg.ID]pkg.Package, tree pip.DepTree) {
for _, dep := range tree.Dependencies {
// Construct ID.
id := pkg.ID{
Type: pkg.NodeJS,
Name: dep.Package,
Revision: dep.Resolved,
}

if !info.IsDir() && info.Name() == "requirements.txt" {
moduleName := filepath.Base(filepath.Dir(path))

log.Logger.Debugf("Found Python package: %s (%s)", path, moduleName)
path, _ = filepath.Rel(dir, path)
moduleConfigs = append(moduleConfigs, module.Config{
Name: moduleName,
Path: path,
Type: string(module.Pip),
// Don't process duplicates.
_, ok := graph[id]
if ok {
continue
}
// Get direct imports.
var imports []pkg.Import
for _, i := range tree.Dependencies {
imports = append(imports, pkg.Import{
Resolved: pkg.ID{
Type: pkg.NodeJS,
Name: i.Package,
Revision: i.Resolved,
},
})
}
return nil
})

if err != nil {
return nil, fmt.Errorf("Could not find Python package manifests: %s", err.Error())
// Update map.
graph[id] = pkg.Package{
ID: id,
Imports: imports,
}
// Recurse in imports.
flattenTree(graph, dep)
}

return moduleConfigs, nil
}

0 comments on commit 0de97ca

Please sign in to comment.