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

     1  // Package python provides analysers for Python projects.
     2  //
     3  // A `BuildTarget` in Python is the directory of the Python project, generally
     4  // containing `requirements.txt` or `setup.py`.
     5  package python
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/apex/log"
    13  	"github.com/mitchellh/mapstructure"
    14  
    15  	"github.com/fossas/fossa-cli/buildtools/pip"
    16  	"github.com/fossas/fossa-cli/buildtools/pipenv"
    17  	"github.com/fossas/fossa-cli/exec"
    18  	"github.com/fossas/fossa-cli/graph"
    19  	"github.com/fossas/fossa-cli/module"
    20  	"github.com/fossas/fossa-cli/pkg"
    21  )
    22  
    23  type Analyzer struct {
    24  	PythonCmd     string
    25  	PythonVersion string
    26  
    27  	Pipenv  pipenv.Pipenv
    28  	Pip     pip.Pip
    29  	Module  module.Module
    30  	Options Options
    31  }
    32  
    33  type Options struct {
    34  	Strategy         string `mapstructure:"strategy"`
    35  	RequirementsPath string `mapstructure:"requirements"`
    36  	VirtualEnv       string `mapstructure:"venv"`
    37  }
    38  
    39  func New(m module.Module) (*Analyzer, error) {
    40  	log.WithField("options", m.Options).Debug("constructing analyzer")
    41  
    42  	// Parse and validate options.
    43  	var options Options
    44  	err := mapstructure.Decode(m.Options, &options)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	log.WithField("options", options).Debug("parsed analyzer options")
    49  
    50  	// Construct analyzer.
    51  	pythonCmd, pythonVersion, err := exec.Which("--version", os.Getenv("FOSSA_PYTHON_CMD"), "python", "python3", "python2.7")
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	// TODO: this should be fatal depending on the configured strategy.
    56  	pipCmd, _, err := exec.Which("--version", os.Getenv("FOSSA_PIP_CMD"), "pip3", "pip")
    57  	if err != nil {
    58  		log.Warn("`pip` command not detected")
    59  	}
    60  	return &Analyzer{
    61  		PythonCmd:     pythonCmd,
    62  		PythonVersion: pythonVersion,
    63  
    64  		Pipenv: pipenv.New(m.Dir),
    65  		Pip: pip.Pip{
    66  			Cmd:       pipCmd,
    67  			PythonCmd: pythonCmd,
    68  		},
    69  		Module:  m,
    70  		Options: options,
    71  	}, nil
    72  }
    73  
    74  // Discover constructs modules in all directories with a `requirements.txt` or
    75  // `setup.py`.
    76  func Discover(dir string, options map[string]interface{}) ([]module.Module, error) {
    77  	// A map of directory to module. This is to avoid multiple modules in one
    78  	// directory e.g. if we find _both_ a `requirements.txt` and `setup.py`.
    79  	modules := make(map[string]module.Module)
    80  
    81  	err := filepath.Walk(dir, func(filename string, info os.FileInfo, err error) error {
    82  		if err != nil {
    83  			log.WithError(err).WithField("filename", filename).Debug("failed to access path")
    84  			return err
    85  		}
    86  
    87  		if !info.IsDir() && (info.Name() == "requirements.txt") {
    88  			moduleDir := filepath.Dir(filename)
    89  			_, ok := modules[moduleDir]
    90  			if ok {
    91  				// We've already constructed a module for this directory.
    92  				return nil
    93  			}
    94  
    95  			moduleName := filepath.Base(moduleDir)
    96  
    97  			log.WithFields(log.Fields{
    98  				"path": filename,
    99  				"name": moduleName,
   100  			}).Debug("constructing Python module")
   101  			relPath, _ := filepath.Rel(dir, filename)
   102  			modules[moduleDir] = module.Module{
   103  				Name:        moduleName,
   104  				Type:        pkg.Python,
   105  				BuildTarget: filepath.Dir(relPath),
   106  				Dir:         filepath.Dir(relPath),
   107  			}
   108  		}
   109  
   110  		return nil
   111  	})
   112  
   113  	if err != nil {
   114  		return nil, fmt.Errorf("Could not find Python package manifests: %s", err.Error())
   115  	}
   116  
   117  	var moduleList []module.Module
   118  	for _, m := range modules {
   119  		moduleList = append(moduleList, m)
   120  	}
   121  	return moduleList, nil
   122  }
   123  
   124  // Clean logs a warning and does nothing for Python.
   125  func (a *Analyzer) Clean() error {
   126  	log.Warnf("Clean is not implemented for Python")
   127  	return nil
   128  }
   129  
   130  func (a *Analyzer) Build() error {
   131  	return a.Pip.Install(a.requirementsFile(a.Module))
   132  }
   133  
   134  func (a *Analyzer) IsBuilt() (bool, error) {
   135  	return true, nil
   136  }
   137  
   138  func (a *Analyzer) Analyze() (graph.Deps, error) {
   139  	switch a.Options.Strategy {
   140  	case "deptree":
   141  		tree, err := a.Pip.DepTree()
   142  		if err != nil {
   143  			return graph.Deps{}, err
   144  		}
   145  		imports, deps := FromTree(tree)
   146  		return graph.Deps{
   147  			Direct:     imports,
   148  			Transitive: deps,
   149  		}, nil
   150  	case "pip":
   151  		reqs, err := a.Pip.List()
   152  		if err != nil {
   153  			return graph.Deps{}, err
   154  		}
   155  		imports := FromRequirements(reqs)
   156  		return graph.Deps{
   157  			Direct:     imports,
   158  			Transitive: fromImports(imports),
   159  		}, nil
   160  	case "pipenv":
   161  		depGraph, err := a.Pipenv.Deps()
   162  		return depGraph, err
   163  	case "requirements":
   164  		fallthrough
   165  	default:
   166  		reqs, err := pip.FromFile(a.requirementsFile(a.Module))
   167  		if err != nil {
   168  			return graph.Deps{}, err
   169  		}
   170  		imports := FromRequirements(reqs)
   171  		return graph.Deps{
   172  			Direct:     imports,
   173  			Transitive: fromImports(imports),
   174  		}, nil
   175  	}
   176  }
   177  
   178  func (a *Analyzer) requirementsFile(m module.Module) string {
   179  	reqFilename := filepath.Join(m.Dir, "requirements.txt")
   180  	if a.Options.RequirementsPath != "" {
   181  		reqFilename = a.Options.RequirementsPath
   182  	}
   183  	log.WithField("path", reqFilename).Debug("requirements.txt filepath")
   184  	return reqFilename
   185  }
   186  
   187  func fromImports(imports []pkg.Import) map[pkg.ID]pkg.Package {
   188  	deps := make(map[pkg.ID]pkg.Package)
   189  	for _, i := range imports {
   190  		deps[i.Resolved] = pkg.Package{
   191  			ID: i.Resolved,
   192  		}
   193  	}
   194  	return deps
   195  }