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  }