github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/analyzers/gradle/gradle.go (about) 1 // Package gradle implements analyzers for Gradle. 2 // 3 // A `BuildTarget` in Gradle is `$PROJECT:$CONFIGURATION`, where the Gradle 4 // module would list its dependencies by running `gradle $PROJECT:dependencies 5 // --configuration=$CONFIGURATION`. The directory of the `build.gradle` file is 6 // specified by `Dir`. 7 package gradle 8 9 import ( 10 "os" 11 "path/filepath" 12 "strings" 13 14 "github.com/apex/log" 15 "github.com/mitchellh/mapstructure" 16 "github.com/pkg/errors" 17 18 "github.com/fossas/fossa-cli/buildtools/gradle" 19 "github.com/fossas/fossa-cli/config" 20 "github.com/fossas/fossa-cli/files" 21 "github.com/fossas/fossa-cli/graph" 22 "github.com/fossas/fossa-cli/module" 23 "github.com/fossas/fossa-cli/pkg" 24 ) 25 26 type Analyzer struct { 27 Module module.Module 28 Options Options 29 Input gradle.Input 30 } 31 32 type Options struct { 33 Cmd string `mapstructure:"cmd"` 34 Task string `mapstructure:"task"` 35 Online bool `mapstructure:"online"` 36 AllSubmodules bool `mapstructure:"all-submodules"` 37 AllConfigurations bool `mapstructure:"all-configurations"` 38 Timeout string `mapstructure:"timeout"` 39 Retries int `mapstructure:"retries"` 40 // TODO: These are temporary until v2 configuration files (with proper BuildTarget) are implemented. 41 Project string `mapstructure:"project"` 42 Configuration string `mapstructure:"configuration"` 43 } 44 45 func New(m module.Module) (*Analyzer, error) { 46 log.Debugf("%#v", m.Options) 47 var options Options 48 err := mapstructure.Decode(m.Options, &options) 49 if err != nil { 50 return nil, err 51 } 52 53 binary := options.Cmd 54 if binary == "" { 55 binary, err = gradle.ValidBinary(m.Dir) 56 if err != nil { 57 log.Warnf("A build.gradle file has been found at %s, but Gradle could not be found. Ensure that Fossa can access `gradle`, `gradlew`, `gradlew.bat`, or set the `FOSSA_GRADLE_CMD` environment variable. Error: %s", m.Dir, err.Error()) 58 } 59 } 60 61 shellInput := gradle.NewShellInput(binary, m.Dir, options.Online, options.Timeout, options.Retries) 62 analyzer := Analyzer{ 63 Module: m, 64 Options: options, 65 Input: shellInput, 66 } 67 68 log.Debugf("Initialized Gradle analyzer: %#v", analyzer) 69 return &analyzer, nil 70 } 71 72 // Discover searches for `build.gradle` files and creates a module for each 73 // `*:dependencies` task in the output of `gradle tasks`. 74 // 75 // TODO: use the output of `gradle projects` and try `gradle 76 // <project>:dependencies` for each project? 77 func Discover(dir string, options map[string]interface{}) ([]module.Module, error) { 78 return DiscoverWithCommand(dir, options, gradle.Cmd) 79 } 80 81 func DiscoverWithCommand(dir string, userOptions map[string]interface{}, command func(string, string, int, ...string) (string, error)) ([]module.Module, error) { 82 var options Options 83 err := mapstructure.Decode(userOptions, &options) 84 if err != nil { 85 return nil, err 86 } 87 88 log.WithField("dir", dir).Debug("discovering gradle modules") 89 var modules []module.Module 90 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 91 if err != nil { 92 log.WithError(err).WithField("path", path).Debug("error while walking filepath discovering gradle modules") 93 return err 94 } 95 96 if info.IsDir() { 97 ok, err := files.Exists(path, "build.gradle") 98 if err != nil { 99 return err 100 } 101 if !ok { 102 return nil 103 } 104 105 name := filepath.Base(path) 106 bin, err := gradle.ValidBinary(path) 107 if err != nil { 108 return err 109 } 110 s := gradle.ShellCommand{ 111 Binary: bin, 112 Dir: path, 113 Cmd: command, 114 Timeout: options.Timeout, 115 Online: options.Online, 116 Retries: options.Retries, 117 } 118 119 projects, err := s.DependencyTasks() 120 if err != nil { 121 return filepath.SkipDir 122 } 123 124 for _, project := range projects { 125 modules = append(modules, module.Module{ 126 Name: filepath.Join(name, project), 127 Type: pkg.Gradle, 128 BuildTarget: project + ":", 129 Dir: path, 130 }) 131 } 132 if len(projects) == 0 { 133 modules = append(modules, module.Module{ 134 Name: filepath.Base(path), 135 Type: pkg.Gradle, 136 BuildTarget: ":", 137 Dir: path, 138 }) 139 } 140 // Don't continue recursing, because anything else is probably a 141 // subproject. 142 return filepath.SkipDir 143 } 144 return nil 145 }) 146 147 if err != nil { 148 return nil, errors.Wrap(err, "could not find Gradle projects") 149 } 150 151 return modules, nil 152 } 153 154 func (a *Analyzer) Clean() error { 155 return nil 156 } 157 158 func (a *Analyzer) Build() error { 159 return nil 160 } 161 162 func (a *Analyzer) IsBuilt() (bool, error) { 163 return true, nil 164 } 165 166 func (a *Analyzer) Analyze() (graph.Deps, error) { 167 log.Debugf("Running Gradle analysis: %#v", a.Module) 168 169 version := config.Version() 170 // cli v0.7.21 or earlier. This version takes a build target in the format of <project>:<configuration>. 171 if version > 0 && version <= 1 { 172 return parseModuleV1(a) 173 } 174 // cli v0.7.22 and later does not split project on `:` to account for deep sub-projects. 175 return parseModuleV2(a) 176 } 177 178 var defaultConfigurations = []string{"compile", "api", "implementation", "compileDependenciesMetadata", "apiDependenciesMetadata", "implementationDependenciesMetadata"} 179 180 func parseModuleV1(a *Analyzer) (graph.Deps, error) { 181 var configurations []string 182 var depsByConfig map[string]graph.Deps 183 var err error 184 targets := strings.Split(a.Module.BuildTarget, ":") 185 186 if a.Options.AllSubmodules { 187 submodules, err := a.Input.DependencyTasks() 188 if err != nil { 189 return graph.Deps{}, err 190 } 191 depsByConfig, err = gradle.MergeProjectsDependencies(a.Input, submodules) 192 if err != nil { 193 return graph.Deps{}, err 194 } 195 } else if a.Options.Task != "" { 196 depsByConfig, err = a.Input.ProjectDependencies(strings.Split(a.Options.Task, " ")...) 197 if err != nil { 198 return graph.Deps{}, err 199 } 200 } else { 201 project := a.Options.Project 202 if project == "" { 203 project = targets[0] 204 } 205 depsByConfig, err = gradle.Dependencies(project, a.Input) 206 if err != nil { 207 return graph.Deps{}, err 208 } 209 } 210 211 if a.Options.Configuration != "" { 212 configurations = strings.Split(a.Options.Configuration, ",") 213 } else if len(targets) > 1 && targets[1] != "" { 214 configurations = strings.Split(targets[1], ",") 215 } else if a.Options.AllConfigurations { 216 for config := range depsByConfig { 217 configurations = append(configurations, config) 218 } 219 } else { 220 configurations = defaultConfigurations 221 } 222 223 merged := graph.Deps{ 224 Direct: nil, 225 Transitive: make(map[pkg.ID]pkg.Package), 226 } 227 for _, config := range configurations { 228 merged = mergeGraphs(merged, depsByConfig[config]) 229 } 230 return merged, nil 231 } 232 233 func parseModuleV2(a *Analyzer) (graph.Deps, error) { 234 var configurations []string 235 var depsByConfig map[string]graph.Deps 236 var err error 237 238 if a.Options.AllSubmodules { 239 submodules, err := a.Input.DependencyTasks() 240 if err != nil { 241 return graph.Deps{}, err 242 } 243 depsByConfig, err = gradle.MergeProjectsDependencies(a.Input, submodules) 244 if err != nil { 245 return graph.Deps{}, err 246 } 247 } else if a.Options.Task != "" { 248 depsByConfig, err = a.Input.ProjectDependencies(strings.Split(a.Options.Task, " ")...) 249 if err != nil { 250 return graph.Deps{}, err 251 } 252 } else { 253 project := a.Options.Project 254 if project == "" { 255 project = a.Module.BuildTarget 256 } 257 depsByConfig, err = gradle.Dependencies(project, a.Input) 258 if err != nil { 259 return graph.Deps{}, err 260 } 261 } 262 263 if a.Options.Configuration != "" { 264 configurations = strings.Split(a.Options.Configuration, ",") 265 } else if a.Options.AllConfigurations { 266 for config := range depsByConfig { 267 configurations = append(configurations, config) 268 } 269 } else { 270 configurations = defaultConfigurations 271 } 272 273 merged := graph.Deps{ 274 Direct: nil, 275 Transitive: make(map[pkg.ID]pkg.Package), 276 } 277 for _, config := range configurations { 278 merged = mergeGraphs(merged, depsByConfig[config]) 279 } 280 return merged, nil 281 } 282 283 func mergeGraphs(gs ...graph.Deps) graph.Deps { 284 merged := graph.Deps{ 285 Direct: nil, 286 Transitive: make(map[pkg.ID]pkg.Package), 287 } 288 289 importSet := make(map[pkg.Import]bool) 290 for _, g := range gs { 291 for _, i := range g.Direct { 292 importSet[i] = true 293 } 294 for id, dep := range g.Transitive { 295 merged.Transitive[id] = dep 296 } 297 } 298 for i := range importSet { 299 merged.Direct = append(merged.Direct, i) 300 } 301 302 return merged 303 }