github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/gradle/gradle.go (about) 1 package gradle 2 3 import ( 4 "os" 5 "path/filepath" 6 "regexp" 7 "strings" 8 9 "github.com/apex/log" 10 "github.com/pkg/errors" 11 12 "github.com/fossas/fossa-cli/exec" 13 "github.com/fossas/fossa-cli/files" 14 "github.com/fossas/fossa-cli/graph" 15 "github.com/fossas/fossa-cli/pkg" 16 ) 17 18 // ShellCommand controls the information needed to run a gradle command. 19 type ShellCommand struct { 20 Binary string 21 Dir string 22 Online bool 23 Timeout string 24 Retries int 25 Cmd func(command string, timeout string, retries int, arguments ...string) (string, error) 26 } 27 28 // Dependency models a gradle dependency. 29 type Dependency struct { 30 Name string 31 RequestedVersion string 32 ResolvedVersion string 33 IsProject bool 34 } 35 36 // Input is an interface for any point where gradle analysis requires input from 37 // a file, shell command, or other source. 38 type Input interface { 39 ProjectDependencies(...string) (map[string]graph.Deps, error) 40 DependencyTasks() ([]string, error) 41 } 42 43 // NewShellInput creates a new ShellCommand and returns it as an Input. 44 func NewShellInput(binary, dir string, online bool, timeout string, retries int) Input { 45 return ShellCommand{ 46 Binary: binary, 47 Dir: dir, 48 Online: online, 49 Timeout: timeout, 50 Retries: retries, 51 Cmd: Cmd, 52 } 53 } 54 55 // MergeProjectsDependencies creates a complete configuration to dep graph map by 56 // looping through a given list of projects and merging their dependencies by configuration. 57 func MergeProjectsDependencies(i Input, projects []string) (map[string]graph.Deps, error) { 58 configurationMap := make(map[string]graph.Deps) 59 for _, project := range projects { 60 depGraph, err := Dependencies(project, i) 61 if err != nil { 62 return configurationMap, err 63 } 64 for configuration, configGraph := range depGraph { 65 if configurationMap[configuration].Direct == nil { 66 configurationMap[configuration] = configGraph 67 } else { 68 for id, transitivePackage := range configGraph.Transitive { 69 configurationMap[configuration].Transitive[id] = transitivePackage 70 } 71 tempDirect := configurationMap[configuration].Direct 72 for _, dep := range configGraph.Direct { 73 if !contains(tempDirect, dep) { 74 tempDirect = append(tempDirect, dep) 75 } 76 } 77 configurationMap[configuration] = graph.Deps{ 78 Direct: tempDirect, 79 Transitive: configurationMap[configuration].Transitive, 80 } 81 } 82 } 83 } 84 85 return configurationMap, nil 86 } 87 88 // Dependencies returns the dependencies of a gradle project 89 func Dependencies(project string, i Input) (map[string]graph.Deps, error) { 90 args := []string{ 91 project + ":dependencies", 92 "--quiet", 93 } 94 95 return i.ProjectDependencies(args...) 96 } 97 98 // ProjectDependencies returns the dependencies of a given gradle project 99 func (s ShellCommand) ProjectDependencies(taskArgs ...string) (map[string]graph.Deps, error) { 100 arguments := taskArgs 101 if s.Dir != "" { 102 arguments = append(arguments, "-p", s.Dir) 103 } 104 if !s.Online { 105 arguments = append(arguments, "--offline") 106 } 107 stdout, err := s.Cmd(s.Binary, s.Timeout, s.Retries, arguments...) 108 if err != nil { 109 return nil, err 110 } 111 112 // Parse individual configurations. 113 configurations := make(map[string]graph.Deps) 114 // Divide output into configurations. Each configuration is separated by an 115 // empty line, started with a configuration name (and optional description), 116 // and has a dependency as its second line. 117 splitReg := regexp.MustCompile("\r?\n\r?\n") 118 sections := splitReg.Split(stdout, -1) 119 for _, section := range sections { 120 lines := strings.Split(section, "\n") 121 if len(lines) < 2 { 122 continue 123 } 124 config := strings.Split(lines[0], " - ")[0] 125 if lines[1] == "No dependencies" { 126 configurations[config] = graph.Deps{} 127 } else if strings.HasPrefix(lines[1], "\\--- ") || strings.HasPrefix(lines[1], "+--- ") { 128 imports, deps, err := ParseDependencies(section) 129 if err != nil { 130 return nil, err 131 } 132 configurations[config] = NormalizeDependencies(imports, deps) 133 } 134 } 135 136 return configurations, nil 137 } 138 139 // DependencyTasks returns a list of gradle projects by analyzing a list of gradle tasks. 140 func (s ShellCommand) DependencyTasks() ([]string, error) { 141 arguments := []string{"tasks", "--all", "--quiet"} 142 if s.Dir != "" { 143 arguments = append(arguments, "-p", s.Dir) 144 } 145 stdout, err := s.Cmd(s.Binary, s.Timeout, s.Retries, arguments...) 146 if err != nil { 147 log.Warnf("Error found running `%s %s`: %s", s.Binary, arguments, err) 148 return nil, err 149 } 150 var projects []string 151 lines := strings.Split(stdout, "\n") 152 for _, line := range lines { 153 if i := strings.Index(line, ":dependencies -"); i != -1 { 154 projects = append(projects, line[:i]) 155 } 156 } 157 return projects, nil 158 } 159 160 //go:generate bash -c "genny -in=$GOPATH/src/github.com/fossas/fossa-cli/graph/readtree.go gen 'Generic=Dependency' | sed -e 's/package graph/package gradle/' > readtree_generated.go" 161 162 func ParseDependencies(stdout string) ([]Dependency, map[Dependency][]Dependency, error) { 163 r := regexp.MustCompile("^([ `+\\\\|-]+)([^ `+\\\\|-].+)$") 164 splitReg := regexp.MustCompile("\r?\n") 165 // Skip non-dependency lines. 166 var filteredLines []string 167 for _, line := range splitReg.Split(stdout, -1) { 168 if r.MatchString(line) { 169 filteredLines = append(filteredLines, line) 170 } 171 } 172 173 return ReadDependencyTree(filteredLines, func(line string) (int, Dependency, error) { 174 // Match line. 175 matches := r.FindStringSubmatch(line) 176 depth := len(matches[1]) 177 if depth%5 != 0 { 178 // Sanity check. 179 return -1, Dependency{}, errors.Errorf("bad depth: %#v %s %#v", depth, line, matches) 180 } 181 182 // Parse dependency. 183 dep := matches[2] 184 withoutAnnotations := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(dep, " (*)"), " (n)"), " FAILED") 185 var parsed Dependency 186 if strings.HasPrefix(withoutAnnotations, "project ") { 187 // TODO: the desired method for handling this might be to recurse into the subproject. 188 parsed = Dependency{ 189 // The project name may or may not have a leading colon. 190 Name: strings.TrimPrefix(strings.TrimPrefix(withoutAnnotations, "project "), ":"), 191 IsProject: true, 192 } 193 } else { 194 // The line withoutAnnotations can have the form: 195 // 1. group:project:requestedVersion 196 // 2. group:project:requestedVersion -> resolvedVersion 197 // 3. group:project -> resolvedVersion 198 199 var name, requestedVer, resolvedVer string 200 201 sections := strings.Split(withoutAnnotations, " -> ") 202 requestedIsNotResolved := len(sections) == 2 203 204 idSections := strings.Split(sections[0], ":") 205 name = idSections[0] 206 if len(idSections) > 1 { 207 name += ":" + idSections[1] 208 if len(idSections) > 2 { 209 requestedVer = idSections[2] 210 } 211 } 212 213 if requestedIsNotResolved { 214 resolvedVer = sections[1] 215 } else { 216 resolvedVer = requestedVer 217 } 218 parsed = Dependency{ 219 Name: name, 220 RequestedVersion: requestedVer, 221 ResolvedVersion: resolvedVer, 222 } 223 } 224 225 log.Debugf("%#v %#v", depth/5, parsed) 226 return depth / 5, parsed, nil 227 }) 228 } 229 230 // Cmd executes the gradle shell command. 231 func Cmd(command string, timeout string, retries int, taskArgs ...string) (string, error) { 232 tempcmd := exec.Cmd{ 233 Name: command, 234 Argv: taskArgs, 235 Timeout: timeout, 236 Retries: retries, 237 } 238 239 stdout, stderr, err := exec.Run(tempcmd) 240 if stderr != "" { 241 return stdout, errors.Errorf("%s", stderr) 242 } 243 244 return stdout, err 245 } 246 247 // ValidBinary finds the best possible gradle command to run for 248 // shell commands. 249 func ValidBinary(dir string) (string, error) { 250 gradleEnv := os.Getenv("FOSSA_GRADLE_CMD") 251 if gradleEnv != "" { 252 return gradleEnv, nil 253 } 254 255 ok, err := files.Exists(dir, "gradlew") 256 if err != nil { 257 return "", err 258 } 259 if ok { 260 return filepath.Abs(filepath.Join(dir, "gradlew")) 261 } 262 263 ok, err = files.Exists(dir, "gradlew.bat") 264 if err != nil { 265 return "", err 266 } 267 if ok { 268 return filepath.Abs(filepath.Join(dir, "gradlew.bat")) 269 } 270 271 return "gradle", nil 272 } 273 274 // NormalizeDependencies turns a dependency map into a FOSSA recognized dependency graph. 275 func NormalizeDependencies(imports []Dependency, deps map[Dependency][]Dependency) graph.Deps { 276 // Set direct dependencies. 277 var i []pkg.Import 278 for _, dep := range imports { 279 i = append(i, pkg.Import{ 280 Target: dep.RequestedVersion, 281 Resolved: pkg.ID{ 282 Type: pkg.Gradle, 283 Name: dep.Name, 284 Revision: dep.ResolvedVersion, 285 }, 286 }) 287 } 288 289 // Set transitive dependencies. 290 d := make(map[pkg.ID]pkg.Package) 291 for parent, children := range deps { 292 id := pkg.ID{ 293 Type: pkg.Gradle, 294 Name: parent.Name, 295 Revision: parent.ResolvedVersion, 296 } 297 var imports []pkg.Import 298 for _, child := range children { 299 imports = append(imports, pkg.Import{ 300 Target: child.ResolvedVersion, 301 Resolved: pkg.ID{ 302 Type: pkg.Gradle, 303 Name: child.Name, 304 Revision: child.ResolvedVersion, 305 }, 306 }) 307 } 308 d[id] = pkg.Package{ 309 ID: id, 310 Imports: imports, 311 } 312 } 313 314 return graph.Deps{ 315 Direct: i, 316 Transitive: d, 317 } 318 } 319 320 func contains(array []pkg.Import, check pkg.Import) bool { 321 for _, val := range array { 322 if val == check { 323 return true 324 } 325 } 326 return false 327 }