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 }