Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/support gradle root deps #116

Merged
merged 11 commits into from
Mar 21, 2018
34 changes: 27 additions & 7 deletions builders/gradle.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,25 @@ func (builder *GradleBuilder) Analyze(m module.Module, allowUnresolved bool) ([]
gradleLogger.Debugf("Running Gradle analysis: %#v %#v in %s", m, allowUnresolved, m.Dir)

moduleConfigurationKey := strings.Split(m.Name, ":")

taskName := "dependencies" // defaults to root gradle dependencies task

if moduleConfigurationKey[0] != "" {
// a subtask was configured
taskName = moduleConfigurationKey[0] + ":dependencies"
}

taskConfiguration := "compile"
taskName := moduleConfigurationKey[0]
if len(moduleConfigurationKey) == 2 {
if len(moduleConfigurationKey) == 2 && moduleConfigurationKey[1] != "" {
taskConfiguration = moduleConfigurationKey[1]
}

// TODO: We need to let the user configure the right configurations
// NOTE: we are intentionally using exec.Command over runLogged here, due to path issues with defining cmd.Dir
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the issue described in this comment?

// TODO: set TERM=dumb
dependenciesOutput, err := exec.Command(builder.GradleCmd, taskName+":dependencies", "-q", "--configuration="+taskConfiguration, "--offline", "-a").Output()
dependenciesCmd := exec.Command(builder.GradleCmd, taskName, "-q", "--configuration="+taskConfiguration, "--offline", "-a")
dependenciesCmd.Env = os.Environ()
dependenciesCmd.Env = append(dependenciesCmd.Env, "TERM=dumb")

dependenciesOutput, err := dependenciesCmd.Output()
if len(dependenciesOutput) == 0 || err != nil {
return nil, fmt.Errorf("could not run Gradle task %s:dependencies", taskName)
}
Expand Down Expand Up @@ -139,10 +148,21 @@ func (builder *GradleBuilder) DiscoverModules(dir string) ([]module.Config, erro
}
}

return moduleConfigurations, nil
if len(moduleConfigurations) > 0 {
return moduleConfigurations, nil
}

// If task list succeeds but returns no subproject dependencies tasks, fall back to the root `dependencies` task
return []module.Config{
{
Name: ":compile",
Path: "build.gradle",
Type: "gradle",
},
}, nil
}

// Fall back to "app" as default task, even though technically it would be "" (root)
// If task list fails, fall back to "app" as default task, even though technically it would be "" (root)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this behaviour correct? Should we expect app:compile to work even when gradle tasks fails, or should this be a fatal error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 50/50 on having this fallback vs erroring if gradle tasks fails, but:

  1. I believe app:compile is a sane default if a build.gradle is found
  2. Checking for a build.gradle is a valid way of identifying a module, and much simpler/more reliable then parsing output of gradle tasks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the question is whether app:compile is likely to work. If it works, then this is great. If it doesn't work, then we've kicked a configuration error down the line into a much more obscure runtime error.

I don't have much experience with the Gradle ecosystem, so I trust your intention on this.

return []module.Config{
{
Name: "app:compile",
Expand Down
7 changes: 7 additions & 0 deletions cmd/fossa/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"os"
"regexp"
"time"

"github.com/briandowns/spinner"
logging "github.com/op/go-logging"
"github.com/urfave/cli"

Expand Down Expand Up @@ -35,6 +37,10 @@ func initCmd(c *cli.Context) {
}

func doInit(conf *config.CLIConfig, overwrite bool, includeAll bool) error {
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
s.Writer = os.Stderr
s.Suffix = " Initializing..."
s.Start()
findDir := "."
if len(conf.Modules) == 0 || overwrite {
if cwd, err := os.Getwd(); err == nil {
Expand All @@ -61,6 +67,7 @@ func doInit(conf *config.CLIConfig, overwrite bool, includeAll bool) error {
} else {
initLogger.Warningf("%d module(s) found in config file (`%s`); skipping initialization.", len(conf.Modules), conf.ConfigFilePath)
}
s.Stop()
return nil
}

Expand Down
93 changes: 85 additions & 8 deletions docs/integrations/gradle.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,101 @@ Gradle support in FOSSA CLI depends on the following tools existing in your envi

### Automatic Configuration

When running `fossa init`, FOSSA will look for a root Gradle build (`build.gradle`) and interrogate it for build-able tasks. From there, it will write module configurations into your `.fossa.yml` for every task that supports a `:dependency` sub-task.
`fossa init` will attempt a "best-effort" strategy to look through all available Gradle tasks/configurations and elect the most likely ones used for a production build.

1. Look for a root Gradle build (`build.gradle`)
2. Run and parse the output of `gradle tasks --all -q -a --offline` to find all available sub-tasks that support a `:dependencies` command
3. If task list succeeds but no sub-tasks are available, fallback to the root `dependencies` task in `build.gradle`
4. Filter out any suspicious-looking tasks (i.e. labeled `test` or `testCompile`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should really mention that there's a flag to disable this.

5. Write tasks to configuration (`fossa.yml`)

If a gradle wrapper was provided in the same directory (`./gradlew`), `fossa` will prefer to use that over the local `gradle` binary.

### Manual Configuration

Add a `gradle` module with the path to the `pom.xml` in your project.
`fossa init` will automatically generate this configuration for you, but if you'd like to manually configure, add a `gradle` module to your `fossa.yml`:

```yaml
analyze:
modules:
- name: {task}:{configuration}
path: {path-to-build.gradle}
type: gradle
```

For Gradle modules, `fossa` requires a few peices of information to run dependency analysis:

- `task` - the name of the task/subtask used to generate your build (most often this is `app` or empty string `''` for root)
- `configuration` - the configuration your task runs during a production build (if undefined, defaults to `compile`)
- `path` - path to your `*.gradle` build file relative to your repo root directory (usually this is just `build.gradle`)

If you don't specify a task, `fossa` will run the default `dependencies` located in your root `*.gradle` file:

```yaml
analyze:
modules:
- name: :compile # runs the root `dependencies` task with `compile` configuration
path: build.gradle
type: gradle
```

You can customize your configuration by modifying the `name` entry using the template `{task}:{configuration}`.

```yaml
analyze:
modules:
- name: app:runtime # runs `app:dependencies` task with `runtime` configuration
path: build.gradle
type: gradle
```

NOTE: If you don't specify a configuration at all, `fossa` will default to `compile`. For example:

```yaml
analyze:
modules:
- name: your-mvn-project
path: pom.xml
type: mvn
- name: app # runs `app:dependencies` task with `compile` configuration
path: build.gradle
type: gradle
- name: '' # empty - runs root `dependencies` task with `compile` configuration
path: build.gradle
type: gradle
```

## Design
NOTE: While `app` and `compile` are two very common tasks/configurations, your Gradle build may be different. For instance, many Gradle builds may use the configuration `release` or `default` instead of `compile`. See the `Troubleshooting` section below for steps in figuring out the right configuration for you.

## Troubleshooting
Since `fossa` operates directly on Gradle, it requires your build environment to be satisfied and Gradle tasks to reliably succeed before `fossa` can run.

Most issues are often fixed by simply verifying that your Gradle build succeeds.

### Errors during `fossa init`
FOSSA's autoconfiguration depends on `gradle tasks --all`. If `fossa init` is failing to discover the right tasks/configuration, make sure:

1. `gradle tasks --all` succeeds in your environment
2. Your `path` entry in `fossa.yml` is points to the right `*.gradle` file

### Issues with dependency data

If you're having trouble getting correct dependency data, try verifying the following:

1. Your configuration and task name is valid (i.e. `app:compile` vs `customTask:customConfiguration`) in `fossa.yml`
2. You get the desired output from your configured dependencies task `gradle {subtask}:dependencies --configuration={configuration}` (i.e. `gradle app:dependencies --configuration=compile`)

If running the gradle command in your terminal does not succeed that means your gradle build is not properly configured.

If it does succeed but produces unexpected or empty output, it means you have likely selected the wrong task or configuration for `fossa` to analyze. Running without the `--configuration` will give you the full output and help you find the right configuration to select.

### Reporting issues

If you're still having problems, open up an issue on this repo with the full logs/output of `fossa -o --debug` and your `fossa.yml` file.

Optionally, provide your `build.gradle` for more context.

## Roadmap

### Analysis
If you'd like to help us improve our Gradle integration, please feel free to file and assign yourself to an issue on this repo.

Analysis will support any tasks you can run `:dependendencies` on with the "compile" configuration. This is equivalent to `gradle app:dependencies --configuration=compile`.
1. Support multiple independent "root" Gradle builds that may be segregated in a codebase
2. Implement a cheap build validation method to tell the user whether a Gradle build is satisfied without invoking a Gradle runtime
3. Support multiple configurations for a single module definition i.e. `app:compile:runtime`