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  }