github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/analyzers/ruby/ruby.go (about) 1 // Package ruby provides analysers for Ruby projects. 2 // 3 // A `BuildTarget` in Ruby is the directory of the Ruby project, generally 4 // containing `Gemfile` and `Gemfile.lock`. 5 package ruby 6 7 import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 12 "github.com/mitchellh/mapstructure" 13 14 "github.com/apex/log" 15 "github.com/fossas/fossa-cli/buildtools/bundler" 16 "github.com/fossas/fossa-cli/exec" 17 "github.com/fossas/fossa-cli/graph" 18 "github.com/fossas/fossa-cli/module" 19 "github.com/fossas/fossa-cli/pkg" 20 ) 21 22 // TODO: add a Ruby sidecar that evaluates `Gemfile` and `*.gemspec`. 23 24 type Analyzer struct { 25 RubyCmd string 26 RubyVersion string 27 28 BundlerCmd string 29 BundlerVersion string 30 31 Bundler bundler.Bundler 32 Module module.Module 33 Options Options 34 } 35 36 type Options struct { 37 Strategy string `mapstructure:"strategy"` 38 LockfilePath string `mapstructure:"gemfile-lock-path"` 39 } 40 41 func New(m module.Module) (*Analyzer, error) { 42 log.WithField("options", m.Options).Debug("constructing analyzer") 43 44 // Parse and validate options. 45 var options Options 46 err := mapstructure.Decode(m.Options, &options) 47 if err != nil { 48 return nil, err 49 } 50 log.WithField("options", options).Debug("parsed analyzer options") 51 52 // Construct analyzer. 53 rubyCmd, rubyVersion, err := exec.Which("--version", os.Getenv("FOSSA_RUBY_CMD"), "ruby") 54 if err != nil { 55 log.Warnf("Could not resolve Ruby") 56 } 57 bundlerCmd, bundlerVersion, err := exec.Which("--version", os.Getenv("FOSSA_BUNDLER_CMD"), "bundler", "bundle") 58 if err != nil { 59 log.Warnf("Could not resolve Bundler") 60 } 61 return &Analyzer{ 62 RubyCmd: rubyCmd, 63 RubyVersion: rubyVersion, 64 65 BundlerCmd: bundlerCmd, 66 BundlerVersion: bundlerVersion, 67 68 Bundler: bundler.Bundler{ 69 Cmd: bundlerCmd, 70 }, 71 Module: m, 72 Options: options, 73 }, nil 74 } 75 76 // Discover constructs modules in all directories with a `Gemfile`. 77 func Discover(dir string, options map[string]interface{}) ([]module.Module, error) { 78 var modules []module.Module 79 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 80 if err != nil { 81 log.WithError(err).WithField("path", path).Debug("error while walking for discovery") 82 } 83 84 if !info.IsDir() && info.Name() == "Gemfile" { 85 moduleName := filepath.Base(path) 86 87 log.WithFields(log.Fields{ 88 "path": path, 89 "name": moduleName, 90 }).Debug("found Ruby module") 91 relPath, _ := filepath.Rel(dir, path) 92 modules = append(modules, module.Module{ 93 Name: moduleName, 94 Type: pkg.Ruby, 95 BuildTarget: filepath.Dir(relPath), 96 Dir: filepath.Dir(relPath), 97 }) 98 } 99 100 return nil 101 }) 102 103 if err != nil { 104 return nil, fmt.Errorf("Could not find Ruby package manifests: %s", err.Error()) 105 } 106 return modules, nil 107 } 108 109 // Clean logs a warning and does nothing for Ruby. 110 func (a *Analyzer) Clean() error { 111 // TODO: maybe this should delete `vendor/` for `bundle --deployment` 112 // installations? How would we detect that? 113 log.Warnf("Clean is not implemented for Ruby") 114 return nil 115 } 116 117 func (a *Analyzer) Build() error { 118 return a.Bundler.Install() 119 } 120 121 func (a *Analyzer) IsBuilt() (bool, error) { 122 _, err := a.Bundler.List() 123 if err != nil { 124 return false, err 125 } 126 return true, nil 127 } 128 129 func (a *Analyzer) Analyze() (graph.Deps, error) { 130 strategy := "list-lockfile" 131 132 if a.Options.Strategy != "" { 133 strategy = a.Options.Strategy 134 } 135 136 lockfilePath := a.lockfilePath() 137 138 switch strategy { 139 case "list": 140 return a.bundlerListAnalyzerStrategy() 141 case "lockfile": 142 return a.lockfileAnalyzerStrategy(lockfilePath) 143 case "list-lockfile": 144 fallthrough 145 default: 146 return a.bundlerListLockfileAnalyzerStrategy(lockfilePath) 147 } 148 } 149 150 func (a *Analyzer) bundlerListLockfileAnalyzerStrategy(lockfilePath string) (graph.Deps, error) { 151 lockfile, err := bundler.FromLockfile(lockfilePath) 152 if err != nil { 153 if a.Options.Strategy != "" { 154 return graph.Deps{}, err 155 } 156 157 return a.lockfileAnalyzerStrategy(lockfilePath) 158 } 159 160 gems, err := a.Bundler.List() 161 if err == nil { 162 imports, deps := FilteredLockfile(gems, lockfile) 163 164 return graph.Deps{ 165 Direct: imports, 166 Transitive: deps, 167 }, nil 168 } 169 170 if a.Options.Strategy != "" { 171 return graph.Deps{}, err 172 } 173 174 deps, err := a.lockfileAnalyzerStrategy(lockfilePath) 175 176 if err == nil { 177 return deps, err 178 } 179 180 if a.Options.Strategy != "" { 181 return graph.Deps{}, err 182 } 183 184 return a.bundlerListAnalyzerStrategy() 185 } 186 187 func (a *Analyzer) bundlerListAnalyzerStrategy() (graph.Deps, error) { 188 gems, err := a.Bundler.List() 189 if err != nil { 190 return graph.Deps{}, err 191 } 192 193 imports, deps := FromGems(gems) 194 195 return graph.Deps{ 196 Direct: imports, 197 Transitive: deps, 198 }, nil 199 } 200 201 func (a *Analyzer) lockfileAnalyzerStrategy(lockfilePath string) (graph.Deps, error) { 202 lockfile, err := bundler.FromLockfile(lockfilePath) 203 if err != nil { 204 return graph.Deps{}, err 205 } 206 207 imports, deps := FromLockfile(lockfile) 208 209 return graph.Deps{ 210 Direct: imports, 211 Transitive: deps, 212 }, nil 213 } 214 215 func (a *Analyzer) lockfilePath() string { 216 if a.Options.LockfilePath != "" { 217 return a.Options.LockfilePath 218 } 219 return filepath.Join(a.Module.Dir, "Gemfile.lock") 220 }