diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 54e07fdd..40fe3143 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,3 +16,8 @@ updates: - gradle-plugin-portal schedule: interval: "weekly" + groups: + gradle-dependencies: + patterns: + - "*" + diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index ecbfc811..23c46f4a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -4,23 +4,24 @@ on: push: paths-ignore: - 'release/**' + pull_request: jobs: quick-check: runs-on: ubuntu-latest - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} steps: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - - name: Set up Gradle - uses: gradle/gradle-build-action@v2 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Execute Gradle Build run: ./gradlew build @@ -30,57 +31,81 @@ jobs: strategy: fail-fast: false matrix: - # Test latest version 5.x, 6.x and 7.x, as well as all patched minor versions of 8.x - # Latest 8.x is tested in 'quick-check' job - gradle-version: [ "5.6.4", "6.9.4", "7.6.3", "8.0.2", "8.1.1", "8.2.1", "8.3"] + # Test earliest and latest supported version of 5.x, 6.x, 7.x, and 8.x + # Latest 8.x is tested in 'quick-check' job using the wrapper + gradle-version: [ "5.2.1", "5.6.4", "6.0.1", "6.9.4", "7.1.1", "7.6.4", "8.0.2", "8.8"] runs-on: ubuntu-latest - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} steps: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: Execute Gradle Build - uses: gradle/gradle-build-action@v2 + run: ./gradlew -S build -DtestGradleVersion=${{ matrix.gradle-version }} + + test-jvm-version: + needs: quick-check + strategy: + fail-fast: false + matrix: + jvm-version: [ "8", "11", "17", "21", "22"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.jvm-version }} + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 with: - arguments: -S build -DtestGradleVersion=${{ matrix.gradle-version }} + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + + - name: Execute Gradle Build + run: ./gradlew -S build -DtestGradleVersion=8.8 self-test: - needs: quick-check - runs-on: ubuntu-latest - env: - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: 11 - distribution: temurin - - - name: Set up Gradle - uses: gradle/gradle-build-action@v2 - - - name: Self Test :plugin - run: ./plugin-self-test ForceDependencyResolutionPlugin_resolveAllDependencies - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_DEPENDENCY_GRAPH_JOB_ID: ${{ github.run_id }} - GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR: "plugin-self-test" - GITHUB_DEPENDENCY_GRAPH_REF: ${{ github.ref }} - GITHUB_DEPENDENCY_GRAPH_SHA: ${{ github.sha }} - GITHUB_DEPENDENCY_GRAPH_WORKSPACE: ${{ github.workspace }} - - - name: Save plugin JSON report - uses: actions/upload-artifact@v3 - with: - name: plugin-json - path: build/reports/dependency-graph-snapshots/plugin-self-test.json - if-no-files-found: error + needs: quick-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 11 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + + - name: Self Test :plugin + run: ./plugin-self-test ForceDependencyResolutionPlugin_resolveAllDependencies + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_DEPENDENCY_GRAPH_JOB_ID: ${{ github.run_id }} + GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR: "plugin-self-test" + GITHUB_DEPENDENCY_GRAPH_REF: ${{ github.ref }} + GITHUB_DEPENDENCY_GRAPH_SHA: ${{ github.sha }} + GITHUB_DEPENDENCY_GRAPH_WORKSPACE: ${{ github.workspace }} + + - name: Save plugin JSON report + uses: actions/upload-artifact@v4 + with: + name: plugin-json + path: build/reports/dependency-graph-snapshots/plugin-self-test.json + if-no-files-found: error diff --git a/README.md b/README.md index fc659e1b..3aea17ef 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ A Gradle plugin for generating a GitHub dependency graph for a Gradle build, which can be uploaded to the [GitHub Dependency Submission API](https://docs.github.com/en/rest/dependency-graph/dependency-submission). ## Usage -This plugin is designed to be used in a GitHub Actions workflow, with support coming in a future release of the [Gradle Build Action](https://github.com/gradle/gradle-build-action). +This plugin is designed to be used in a GitHub Actions workflow, and is tightly integrated into the +[gradle/actions/dependency-submission](https://github.com/gradle/actions/tree/main/dependency-submission) action. For other uses, the [core plugin](https://plugins.gradle.org/plugin/org.gradle.github-dependency-graph-gradle-plugin) (`org.gradle.github.GitHubDependencyGraphPlugin`) should be applied to the `Gradle` instance via a Gradle init script as follows: -``` +```groovy import org.gradle.github.GitHubDependencyGraphPlugin initscript { repositories { @@ -27,15 +28,24 @@ This causes 2 separate plugins to be applied, that can be used independently: - `GitHubDependencyExtractorPlugin` collects all dependencies that are resolved during a build execution and writes these to a file. The output file can be found at `/build/reports/github-depenency-graph-snapshots/.json`. - `ForceDependencyResolutionPlugin` creates a `ForceDependencyResolutionPlugin_resolveAllDependencies` task that will attempt to resolve all dependencies for a Gradle build, by simply invoking `dependencies` on all projects. -### Required environment variables +### Environment variables The following environment variables configure the snapshot generated by the `GitHubDependencyExtractorPlugin`. See the [GitHub Dependency Submission API docs](https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28) for details: + +#### Required environment variables + - `GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR`: Sets the `job.correlator` value for the dependency submission - `GITHUB_DEPENDENCY_GRAPH_JOB_ID`: Sets the `job.id` value for the dependency submission - `GITHUB_DEPENDENCY_GRAPH_REF`: Sets the `ref` value for the commit that generated the dependency graph - `GITHUB_DEPENDENCY_GRAPH_SHA`: Sets the `sha` value for the commit that generated the dependency graph -- `GITHUB_DEPENDENCY_GRAPH_WORKSPACE`: Sets the root directory of the github repository -- `DEPENDENCY_GRAPH_REPORT_DIR` (optional): Specifies where the dependency graph report will be generated +- `GITHUB_DEPENDENCY_GRAPH_WORKSPACE`: Sets the root directory of the github repository. Must be an absolute path. +- `DEPENDENCY_GRAPH_REPORT_DIR` (optional): Specifies where the dependency graph report will be generated. Must be an absolute path. + +#### Optional environment variables + +- `GITHUB_DEPENDENCY_GRAPH_DETECTOR_NAME`: Sets the `detector.name` value for the dependency submission. Defaults to `GitHub Dependency Graph Gradle Plugin` +- `GITHUB_DEPENDENCY_GRAPH_DETECTOR_VERSION`: Sets the `detector.version` value for the dependency submission. Defaults to current version of the plugin. +- `GITHUB_DEPENDENCY_GRAPH_DETECTOR_URL`: Sets the `detector.url` value for the dependency submission. Defaults to `https://github.com/gradle/github-dependency-graph-gradle-plugin` Each of these values can also be provided via a system property. eg: Env var `DEPENDENCY_GRAPH_REPORT_DIR` can be set with `-DDEPENDENCY_GRAPH_REPORT_DIR=...` on the command-line. @@ -45,24 +55,130 @@ eg: Env var `DEPENDENCY_GRAPH_REPORT_DIR` can be set with `-DDEPENDENCY_GRAPH_RE If you do not want to include every dependency configuration in every project in your build, you can limit the dependency extraction to a subset of these. -To restrict which Gradle subprojects contribute to the report, specify which projects to include via a regular expression. -You can provide this value via the `DEPENDENCY_GRAPH_INCLUDE_PROJECTS` environment variable or system property. +The following parameters control the set of projects and configurations that contribute dependencies. +Each of these is a regular expression value, and can set either as an environment variable or as a system property on the command line. + +| Property | Description | Default | +|-----------------------------------------|---------------------------|---------------------------------| +| DEPENDENCY_GRAPH_INCLUDE_PROJECTS | Projects to include | All projects are included | +| DEPENDENCY_GRAPH_EXCLUDE_PROJECTS | Projects to exclude | No projects are excluded | +| DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS | Configurations to include | All configurations are included | +| DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS | Configurations to exclude | No configurations are excluded | + +### Controlling the scope of dependencies in the dependency graph + +The GitHub dependency graph allows a scope to be assigned to each reported dependency. +The only permissible values for scope are 'runtime' and 'development'. + +The following parameters control the set of projects and configurations that provide 'runtime' scoped dependencies. +Any dependency resolution that does not match these parameters will be scoped 'development'. + +Each of these parameters is a regular expression value, and can set either as an environment variable or as a system property on the command line. + +| Property | Description | Default | +|-------------------------------------------------|-----------------------------------------------------------|---------------------------------| +| DEPENDENCY_GRAPH_RUNTIME_INCLUDE_PROJECTS | Projects that can provide 'runtime' dependencies | All projects are included | +| DEPENDENCY_GRAPH_RUNTIME_EXCLUDE_PROJECTS | Projects that do not provide 'runtime' dependencies | No projects are excluded | +| DEPENDENCY_GRAPH_RUNTIME_INCLUDE_CONFIGURATIONS | Configurations that contain 'runtime' dependencies | All configurations are included | +| DEPENDENCY_GRAPH_RUNTIME_EXCLUDE_CONFIGURATIONS | Configurations that do not contain 'runtime' dependencies | No configurations are excluded | -To restrict which Gradle configurations contribute to the report, you can filter configurations by name using a regular expression. -You can provide this value via the `DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS` environment variable or system property. +By default, no scope is assigned to dependencies in the graph. To enable scopes in the generated dependency graph, +at least one of these parameters must be configured. + +For dependencies that are resolved in multiple projects and/or multiple configurations, only a single 'runtime' scoped resolution +is required for that dependency to be scoped 'runtime'. ### Gradle compatibility -The plugin should be compatible with all versions of Gradle >= 5.0, and has been tested against -Gradle versions "5.6.4", "6.9.4", "7.0.2", "7.6.2", "8.0.2" and the current Gradle release. +The plugin should be compatible with most versions of Gradle >= 5.2, and has been tested against +Gradle versions "5.2.1", "5.6.4", "6.0.1", "6.9.4", "7.1.1" and "7.6.3", as well as all patched versions of Gradle 8.x. + +The plugin is compatible with running Gradle with the configuration-cache enabled: this support is +limited to Gradle "8.1.0" and later. Earlier Gradle versions will not work with `--configuration-cache`. +Note that no dependency graph will be generated when configuration state is loaded from the configuration-cache. + +| Gradle version | Compatible | Compatible with configuration-cache | +| -------------- | ------- | ------------------------ | +| 1.x - 4.x | :x: | :x: | +| 5.0 - 5.1.1 | :x: | :x: | +| 5.2 - 5.6.4 | ✅ | :x: | +| 6.0 - 6.9.4 | ✅ | :x: | +| 7.0 - 7.0.2 | :x: | :x: | +| 7.1 - 7.6.3 | ✅ | :x: | +| 8.0 - 8.0.2 | ✅ | :x: | +| 8.1+ | ✅ | ✅ | + +### Dependency verification + +When using this plugin with [dependency signature verification enabled](https://docs.gradle.org/current/userguide/dependency_verification.html#sec:signature-verification), +the you should be able to update your `dependency-verification.xml` file using `--write-verification-metadata pgp,sha256`. + +However, if this doesn't work, you can add the following to your `dependency-verificaton.xml` file: + +```xml + + + +``` + +## Using the plugin to generate dependency reports + +As well as the `GitHubDependencyGraphPlugin`, which is tailored for use by the [gradle/actions/dependency-submission](https://github.com/gradle/actions/tree/main/dependency-submission) GitHub Action, this repository also provides the `SimpleDependencyGraphPlugin`, which generates dependency-graph outputs in simple text format. + +To use the `SimpleDependencyGraphPlugin` you'll need to create an `init.gradle` file to apply the plugin to your project: -The plugin is compatible with running Gradle with the configuration-cache enabled. However, this support is -limited to Gradle "8.1.0" and later: -- With Gradle "8.0", the build should run successfully, but an empty dependency graph will be generated. -- With Gradle <= "7.6.4", the plugin will cause the build to fail with configuration-cache enabled. +```groovy +initscript { + repositories { + gradlePluginPortal() + } + dependencies { + classpath "org.gradle:github-dependency-graph-gradle-plugin:+" + } +} +apply plugin: org.gradle.dependencygraph.simple.SimpleDependencyGraphPlugin +``` + +and then execute the task to resolve all dependencies in your project: + +```shell +./gradlew -I init.gradle --dependency-verification=off --no-configuration-cache --no-configure-on-demand :ForceDependencyResolutionPlugin_resolveAllDependencies +``` + +You'll find the generated files in `build/reports/dependency-graph-snapshots`. + +### Using dependency reports to determine the underlying source of a dependency + +After generating the dependency reports as described, it is possible to determine the dependency source by: + +1. Locate the dependency (including matching version) in the `dependency-resolution.json` file. +2. Inspect each `resolvedBy` entry for the `path` and `configuration` values. The `scope` value is unimportant in this context. +3. Use the built-in [dependencyInsight](https://docs.gradle.org/current/userguide/viewing_debugging_dependencies.html#dependency_insights) task to determine exactly how the dependency was resolved. The `path` indicates the project where the task should be executed, and the `configuration` is an input to the task. + +For example, given the following from the `dependency-resolution.json` report: +```json + "dependency" : "com.google.guava:guava:32.1.3-jre", + "effectiveScope" : "Unknown", + "resolvedBy" : [ { + "path" : ":my-subproject", + "configuration" : "compileClasspath", + "scope" : "Unknown" + }, ... +``` + +You would run the command: +```shell +./gradlew :my-subproject:dependencyInsight --configuration compileClasspath --dependency com.google.guava:guava:32.1.3-jre +``` + +#### Dealing with 'classpath' configuration + +If the configuration value in `dependency-resolution.json` is "classpath", or for some other reason the above instructions do not work, +it is possible to recostruct the full resolution path using the generated `dependency-graph.json` file. + +Search for the exact dependency version in `dependency-graph.json`, and you'll see an "id" entry for that dependency as well as one or more +"dependencies" entries. By tracing back through the dependencies you can determine the underlying source of the dependency. -To use this plugin with versions of Gradle older than "8.1.0", you'll need to invoke Gradle with the -configuration-cache disabled. ## Building/Testing @@ -79,3 +195,4 @@ To self-test this plugin and generate a dependency graph for this repository, ru The generated dependency graph will be submitted to GitHub only if you supply a [GitHub API token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) via the environment variable `GITHUB_TOKEN`. + diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..0b97aaf5 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,30 @@ +# Release process for the GitHub Dependency Graph Gradle Plugin + +## Preparation +- Push any outstanding changes to branch main. +- Check that https://github.com/gradle/github-dependency-graph-gradle-plugin/actions is green for all workflows for the main branch. +- Decide on the version number to use for the release. The plugin releases should follow semantic versioning. + - By default, a patch release is assumed (eg. `1.3.0` → `1.3.1`) + - If new features have been added, bump the minor version (eg `1.3.1` → `1.4.0`) + - If a new major release is required, bump the major version (eg `1.3.1` → `2.0.0`) + - Note: The plugin releases follow the semantic version convention of including a .0 patch number for the first release of a minor version, unlike the Gradle convention which omits the trailing .0. +- Update `release/version.txt` with the to-be-published version +- Ensure that `release/changes.md` contains all changes that should be included in the release notes + +## Run the staging release workflow and verify the published plugin +- Publish to staging by executing the [staging release workflow](https://github.com/gradle/dv-solutions/actions/workflows/github-dependency-graph-gradle-plugin-staging-release.yml) job. + - Run the workflow from the `main` branch and enter "STAGING" when requested. + - Once the workflow is complete, check that it was published successfully at https://plugins.grdev.net/plugin/org.gradle.github-dependency-graph-gradle-plugin +- If necessary, run a build that resolves and tests the newly published plugin + +## Run the production release workflow and verify the published plugin +- Publish to production by executing the [production release workflow](https://github.com/gradle/dv-solutions/actions/workflows/github-dependency-graph-gradle-plugin-production-release.yml) job. + - Run the workflow from the `main` branch and enter "PRODUCTION" when requested. +- After the workflow completes, check the outputs: + - Verify that the plugin is available at https://plugins.gradle.org/plugin/org.gradle.github-dependency-graph-gradle-plugin + - If necessary, run a build that resolves and tests the newly published plugin + - Check that the GitHub release was generated at https://github.com/gradle/github-dependency-graph-gradle-plugin/releases + +## Update `gradle/actions` to use the newly published release +- Update the default value for `dependency-graph-plugin.version` [here](https://github.com/gradle/actions/blob/main/sources/src/resources/init-scripts/gradle-actions.github-dependency-graph-gradle-plugin-apply.groovy#L9), to reference the new release. + - Submit a PR with this change. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a30801f..07021de5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,23 +8,24 @@ jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-datab jackson-parameter-names = { group = "com.fasterxml.jackson.module", name = "jackson-module-parameter-names" } jackson-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin" } -apache-commons-io = { group = "commons-io", name = "commons-io", version = "2.15.0" } +apache-commons-io = { group = "commons-io", name = "commons-io", version = "2.19.0" } -github-packageurl = { group = "com.github.package-url", name = "packageurl-java", version = "1.4.1" } +github-packageurl = { group = "com.github.package-url", name = "packageurl-java", version = "1.5.0" } +okio = { group = "com.squareup.okio", name = "okio", version = "3.11.0" } ### Test dependencies spock-core = { group = "org.spockframework", name = "spock-core", version.ref = "spock" } spock-junit4 = { group = "org.spockframework", name = "spock-junit4", version.ref = "spock" } junit-junit4 = { group = "junit", name = "junit", version = "4.13.2" } -junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version = "5.10.1" } +junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version = "5.12.2" } -groovy-json = { group = "org.codehaus.groovy", name = "groovy-json", version = "3.0.19" } -json-schema-validator = { group = "com.networknt", name = "json-schema-validator", version = "1.0.87" } -jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version = "24.0.1" } -google-gson = { group = "com.google.code.gson", name = "gson", version = "2.10.1" } +groovy-json = { group = "org.codehaus.groovy", name = "groovy-json", version = "3.0.24" } +json-schema-validator = { group = "com.networknt", name = "json-schema-validator", version = "1.5.6" } +jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version = "26.0.2" } +google-gson = { group = "com.google.code.gson", name = "gson", version = "2.13.0" } [plugins] shadow-jar = { id = "com.github.johnrengelman.shadow", version = "8.1.1"} -plugin-publish = { id = "com.gradle.plugin-publish", version = "1.2.1" } -github-release = { id = "com.github.breadmoirai.github-release", version = "2.4.1"} +plugin-publish = { id = "com.gradle.plugin-publish", version = "1.3.1" } +github-release = { id = "com.github.breadmoirai.github-release", version = "2.5.2"} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..1b33c55b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 46671acb..247cf2a9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..23d15a93 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..db3a6ac2 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/plugin-self-test b/plugin-self-test index 8ccf15ec..16dd6190 100755 --- a/plugin-self-test +++ b/plugin-self-test @@ -15,6 +15,6 @@ else -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN"\ -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/gradle/dependency-graph-gradle-plugin/dependency-graph/snapshots \ + https://api.github.com/repos/gradle/github-dependency-graph-gradle-plugin/dependency-graph/snapshots \ -d @build/reports/dependency-graph-snapshots/plugin-self-test.json fi diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/BaseExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/BaseExtractorTest.groovy index d0ee6346..1cb2594b 100644 --- a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/BaseExtractorTest.groovy +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/BaseExtractorTest.groovy @@ -6,7 +6,6 @@ import com.networknt.schema.* import groovy.json.JsonSlurper import groovy.transform.CompileDynamic import groovy.transform.CompileStatic -import groovy.transform.Memoized import org.gradle.github.dependencygraph.fixture.TestConfig import org.gradle.internal.hash.Hashing import org.gradle.test.fixtures.SimpleGradleExecuter @@ -49,11 +48,14 @@ abstract class BaseExtractorTest extends Specification { SimpleGradleExecuter createExecuter() { // Create a new JsonManifestLoader for each invocation of the executer - File manifestFile = reportDir.file("dummy-job-correlator.json") - loader = new JsonRepositorySnapshotLoader(manifestFile) + loader = new JsonRepositorySnapshotLoader(dependencyGraphFile) return createExecuter(testGradleVersion) } + File getDependencyGraphFile() { + reportDir.file("dummy-job-correlator.json") + } + static String getTestGradleVersion() { return System.getProperty("testGradleVersion", GradleVersion.current().version) } @@ -97,6 +99,16 @@ abstract class BaseExtractorTest extends Specification { return result } + protected BuildResult runAndFail() { + return runAndFail("ForceDependencyResolutionPlugin_resolveAllDependencies") + } + + protected BuildResult runAndFail(String... names) { + executer.withTasks(names) + result = getExecuter().runWithFailure() + return result + } + @CompileDynamic protected void applyDependencyGraphPlugin() { File pluginJar = TEST_CONFIG.asFile("extractorPlugin.jar.path") @@ -111,6 +123,10 @@ abstract class BaseExtractorTest extends Specification { } apply plugin: GitHubDependencyGraphPlugin """.stripMargin() + resetArguments() + } + + protected SimpleGradleExecuter resetArguments() { getExecuter().withArguments("--init-script", "init.gradle") } @@ -192,6 +208,12 @@ abstract class BaseExtractorTest extends Specification { return (manifestData.file as Map).source_location } + def assertResolved(List expectedResolved) { + def resolved = manifestData.resolved as Map + assert resolved.keySet() == expectedResolved as Set + return true + } + def assertResolved(Map expectedResolved = [:]) { def resolved = manifestData.resolved as Map @@ -206,6 +228,7 @@ abstract class BaseExtractorTest extends Specification { } assert actual.relationship == (expected.relationship ?: "direct") assert actual.dependencies == (expected.dependencies ?: []) + assert actual.scope == expected.scope } return true @@ -219,20 +242,20 @@ abstract class BaseExtractorTest extends Specification { @CompileStatic protected static class JsonRepositorySnapshotLoader { private static final String SCHEMA = "schema/github-repository-snapshot-schema.json" - private final File manifestFile + private final File dependencyGraphFile - JsonRepositorySnapshotLoader(File manifestFile) { - this.manifestFile = manifestFile + JsonRepositorySnapshotLoader(File dependencyGraphFile) { + this.dependencyGraphFile = dependencyGraphFile } protected Map jsonRepositorySnapshot() { def jsonSlurper = new JsonSlurper() - println(manifestFile.text) + println(dependencyGraphFile.text) JsonSchema schema = createSchemaValidator() ObjectMapper mapper = new ObjectMapper() - JsonNode node = mapper.readTree(manifestFile) + JsonNode node = mapper.readTree(dependencyGraphFile) validateAgainstJsonSchema(schema, node) - return jsonSlurper.parse(manifestFile) as Map + return jsonSlurper.parse(dependencyGraphFile) as Map } private static void validateAgainstJsonSchema(JsonSchema schema, JsonNode json) { diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/ConfigurationFilterDependencyExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/ConfigurationFilterDependencyExtractorTest.groovy new file mode 100644 index 00000000..da06a4ae --- /dev/null +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/ConfigurationFilterDependencyExtractorTest.groovy @@ -0,0 +1,326 @@ +package org.gradle.github.dependencygraph + + +import org.gradle.test.fixtures.maven.MavenModule + +class ConfigurationFilterDependencyExtractorTest extends BaseExtractorTest { + private MavenModule foo + private MavenModule bar + private MavenModule baz + + private File settingsFile + private File buildFile + + def setup() { + applyDependencyGraphPlugin() + establishEnvironmentVariables() + + foo = mavenRepo.module("org.test", "foo", "1.0").publish() + bar = mavenRepo.module("org.test", "bar", "1.0").publish() + baz = mavenRepo.module("org.test", "baz", "1.0").dependsOn(bar).publish() + + settingsFile = file("settings.gradle") << """ + rootProject.name = 'parent' + """ + + buildFile = file("build.gradle") << """ + allprojects { + group "org.test" + version "1.0" + + repositories { + maven { url "${mavenRepo.uri}" } + } + } + """ + } + + def "can filter projects to extract dependencies"() { + given: + settingsFile << "include 'a', 'b', 'c'" + + buildFile << """ + project(':a') { + apply plugin: 'java-library' + dependencies { + api 'org.test:foo:1.0' + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + api 'org.test:bar:1.0' + } + } + project(':c') { + apply plugin: 'java-library' + dependencies { + api 'org.test:baz:1.0' + } + } + """ + + when: + executer.withArgument("-DDEPENDENCY_GRAPH_INCLUDE_PROJECTS=:b") + run() + + then: + gitHubManifest().assertResolved(["org.test:bar:1.0"]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_INCLUDE_PROJECTS=:[ab]") + run() + + then: + gitHubManifest().assertResolved(["org.test:foo:1.0", "org.test:bar:1.0"]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_EXCLUDE_PROJECTS=:[bc]") + run() + + then: + gitHubManifest().assertResolved(["org.test:foo:1.0"]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_INCLUDE_PROJECTS=:[ab]") + executer.withArgument("-DDEPENDENCY_GRAPH_EXCLUDE_PROJECTS=:b") + run() + + then: + gitHubManifest().assertResolved(["org.test:foo:1.0"]) + } + + def "can filter configurations to extract dependencies"() { + given: + settingsFile << "include 'a', 'b'" + + buildFile << """ + project(':a') { + apply plugin: 'java-library' + dependencies { + api 'org.test:foo:1.0' + testImplementation 'org.test:baz:1.0' + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + implementation 'org.test:bar:1.0' + testImplementation 'org.test:baz:1.0' + } + } + """ + + when: + executer.withArgument("-DDEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS=compileClasspath") + run() + + then: + gitHubManifest().assertResolved(["org.test:foo:1.0", "org.test:bar:1.0"]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS=test(Compile|Runtime)Classpath") + run() + + then: + gitHubManifest().assertResolved(["org.test:foo:1.0", "org.test:bar:1.0"]) + } + + def "can filter runtime projects to determine scope"() { + given: + settingsFile << "include 'a', 'b'" + + buildFile << """ + project(':a') { + apply plugin: 'java-library' + dependencies { + api 'org.test:foo:1.0' + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + implementation 'org.test:bar:1.0' + } + } + """ + + when: + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_PROJECTS=:a") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "development"] + ]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_EXCLUDE_PROJECTS=:b") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "development"] + ]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_PROJECTS=:[ab]") + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_EXCLUDE_PROJECTS=:b") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "development"] + ]) + } + + def "can filter runtime configurations to determine scope"() { + given: + settingsFile << "include 'a', 'b'" + + buildFile << """ + project(':a') { + apply plugin: 'java-library' + dependencies { + api 'org.test:foo:1.0' + testImplementation 'org.test:baz:1.0' + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + implementation 'org.test:bar:1.0' + testImplementation 'org.test:baz:1.0' + } + } + """ + + when: + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_CONFIGURATIONS=compileClasspath") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "runtime"], + "org.test:baz:1.0": [scope: "development", dependencies: ["org.test:bar:1.0"]] + ]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_CONFIGURATIONS=.*Classpath") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "runtime"], + "org.test:baz:1.0": [scope: "runtime", dependencies: ["org.test:bar:1.0"]] + ]) + + when: + resetArguments() + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_CONFIGURATIONS=.*Classpath") + executer.withArgument("-DDEPENDENCY_GRAPH_RUNTIME_EXCLUDE_CONFIGURATIONS=test(Compile|Runtime)Classpath") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "runtime"], + "org.test:baz:1.0": [scope: "development", dependencies: ["org.test:bar:1.0"]] + ]) + } + + def "can filter runtime projects and configurations to determine scope"() { + given: + settingsFile << "include 'a', 'b', 'c'" + + buildFile << """ + project(':a') { + apply plugin: 'java-library' + dependencies { + api 'org.test:foo:1.0' + testImplementation 'org.test:baz:1.0' + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + api 'org.test:bar:1.0' + testImplementation 'org.test:baz:1.0' + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + api 'org.test:baz:1.0' + } + } + """ + + when: + executer + .withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_PROJECTS=:[ab]") + .withArgument("-DDEPENDENCY_GRAPH_RUNTIME_INCLUDE_CONFIGURATIONS=.*Classpath") + .withArgument("-DDEPENDENCY_GRAPH_RUNTIME_EXCLUDE_PROJECTS=:b") + .withArgument("-DDEPENDENCY_GRAPH_RUNTIME_EXCLUDE_CONFIGURATIONS=test(Compile|Runtime)Classpath") + run() + + then: + gitHubManifest().assertResolved([ + "org.test:foo:1.0": [scope: "runtime"], + "org.test:bar:1.0": [scope: "development"], + "org.test:baz:1.0": [scope: "development", dependencies: ["org.test:bar:1.0"]] + ]) + } + + def "does not attempt to resolve excluded configurations"() { + given: + settingsFile << "include 'a', 'b'" + + buildFile << """ + project(':a') { + apply plugin: 'java-library' + dependencies { + api 'org.test:foo:1.0' + } + configurations.all { + incoming.beforeResolve { + throw new RuntimeException("Should not resolve project :a") + } + } + } + project(':b') { + apply plugin: 'java-library' + dependencies { + api 'org.test:bar:1.0' + testImplementation 'org.test:baz:1.0' + } + configurations.testCompileClasspath { + incoming.beforeResolve { + throw new RuntimeException("Should not resolve configuration 'testCompileClasspath'") + } + } + } + """ + + when: + executer.withArgument("-DDEPENDENCY_GRAPH_EXCLUDE_PROJECTS=:a") + executer.withArgument("-DDEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS=test(Compile|Runtime)Classpath") + run() + + then: + gitHubManifest().assertResolved(["org.test:bar:1.0"]) + } + +} diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyExtractorConfigTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyExtractorConfigTest.groovy index 196fb72a..713f3893 100644 --- a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyExtractorConfigTest.groovy +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyExtractorConfigTest.groovy @@ -52,6 +52,17 @@ class DependencyExtractorConfigTest extends BaseExtractorTest { assert job.id == environmentVars.jobId } + def "fails gracefully if configuration values not set"() { + when: + def envVars = environmentVars.asEnvironmentMap() + envVars.remove("GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR") + executer.withEnvironmentVars(envVars) + def result = executer.runWithFailure() + + then: + result.output.contains("> The configuration parameter 'GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR' must be set") + } + @IgnoreIf({ // There is an issue where BuildService is closed too early in Gradle 8.0, // resulting in empty dependency graph. @@ -76,16 +87,50 @@ class DependencyExtractorConfigTest extends BaseExtractorTest { manifest.assertResolved([ "org.test:foo:1.0": [package_url: purlFor(foo)] ]) + + // Execute again to test loading from config-cache + when: + dependencyGraphFile.delete() + executer.withArgument("--configuration-cache") + def buildResult = run() + + then: + buildResult.output.contains("Gradle build state was reused from the configuration-cache: Dependency Graph file will not be generated.") + !dependencyGraphFile.exists() } - def "fails gracefully if configuration values not set"() { + def "does not generate dependency-graph on configuration failure"() { + given: + buildFile << """ + throw new RuntimeException("Failure during configuration") + """ + when: - def envVars = environmentVars.asEnvironmentMap() - envVars.remove("GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR") - executer.withEnvironmentVars(envVars) - def result = executer.runWithFailure() + def buildResult = runAndFail() + + then: + buildResult.output.contains("Gradle Build did not complete successfully: Dependency Graph file will not be generated.") + !dependencyGraphFile.exists() + } + + def "does not generate dependency-graph on task failure"() { + given: + buildFile << """ + tasks.register("taskThatSucceeds") { + doLast {} + } + tasks.register("taskThatFails") { + doLast { + throw new RuntimeException("Failure in task") + } + } + """ + + when: + def buildResult = runAndFail("taskThatSucceeds", "taskThatFails") then: - result.output.contains("'GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR' must be set") + buildResult.output.contains("Gradle Build did not complete successfully: Dependency Graph file will not be generated.") + !dependencyGraphFile.exists() } } diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyLockingDependencyExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyLockingDependencyExtractorTest.groovy new file mode 100644 index 00000000..ba04b1c4 --- /dev/null +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/DependencyLockingDependencyExtractorTest.groovy @@ -0,0 +1,99 @@ +package org.gradle.github.dependencygraph + +import org.gradle.test.fixtures.PluginPublisher +import org.gradle.test.fixtures.maven.MavenModule +import org.gradle.util.GradleVersion +import spock.lang.IgnoreIf + +class DependencyLockingDependencyExtractorTest extends BaseExtractorTest { + private MavenModule foo + private MavenModule bar + private MavenModule baz + private File settingsFile + private File buildFile + + def setup() { + establishEnvironmentVariables() + + foo = mavenRepo.module("org.test", "foo", "1.0").publish() + + settingsFile = file("settings.gradle") << """ + rootProject.name = 'a' + """ + + buildFile = file("build.gradle") << """ + apply plugin: 'java' + + repositories { + maven { url "${mavenRepo.uri}" } + } + """ + } + + def "extracts dependencies when dependency locking is enabled"() { + given: + buildFile << """ + dependencies { + implementation "org.test:foo:+" + } + + dependencyLocking { + lockAllConfigurations() + } + """ + + // Write dependency lock file + run("dependencies", "--write-locks") + mavenRepo.module("org.test", "foo", "1.1").publish() + + when: + applyDependencyGraphPlugin() + run() + + then: + def manifest = gitHubManifest() + manifest.sourceFile == "settings.gradle" + + manifest.assertResolved([ + "org.test:foo:1.0": [ + package_url: purlFor(foo) + ] + ]) + } + + @IgnoreIf({ + // `LockMode.STRICT` was introduced in Gradle 6.1 + GradleVersion.version(testGradleVersion) < GradleVersion.version("6.1") + }) + def "extracts dependencies when Strict dependency locking is enabled"() { + given: + buildFile << """ + dependencies { + implementation "org.test:foo:+" + } + + dependencyLocking { + lockAllConfigurations() + lockMode = LockMode.STRICT + } + """ + + // Write dependency lock file + run("dependencies", "--write-locks") + mavenRepo.module("org.test", "foo", "1.1").publish() + + when: + applyDependencyGraphPlugin() + run() + + then: + def manifest = gitHubManifest() + manifest.sourceFile == "settings.gradle" + + manifest.assertResolved([ + "org.test:foo:1.0": [ + package_url: purlFor(foo) + ] + ]) + } +} diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/MultiProjectDependencyExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/MultiProjectDependencyExtractorTest.groovy index 2f6f3b2d..cb6f0e3f 100644 --- a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/MultiProjectDependencyExtractorTest.groovy +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/MultiProjectDependencyExtractorTest.groovy @@ -2,6 +2,8 @@ package org.gradle.github.dependencygraph import org.gradle.test.fixtures.maven.MavenModule +import org.gradle.util.GradleVersion +import spock.lang.IgnoreIf class MultiProjectDependencyExtractorTest extends BaseExtractorTest { private MavenModule foo @@ -251,70 +253,31 @@ class MultiProjectDependencyExtractorTest extends BaseExtractorTest { ]) } - def "can filter projects to extract dependencies"() { + @IgnoreIf({ + // `includeBuild('.')` is not possible with Gradle < 6.1 + GradleVersion.version(testGradleVersion) < GradleVersion.version("6.1") + }) + def "extracts dependencies from build that includes itself"() { given: - settingsFile << "include 'a', 'b'" - + settingsFile << """ + includeBuild('.') +""" buildFile << """ - project(':a') { - apply plugin: 'java-library' - dependencies { - api 'org.test:foo:1.0' - } - } - project(':b') { - apply plugin: 'java-library' - dependencies { - api 'org.test:bar:1.0' - } + apply plugin: 'java' + dependencies { + implementation 'org.test:bar:1.0' } - """ - when: - executer.withArgument("-DDEPENDENCY_GRAPH_INCLUDE_PROJECTS=:b") - run() - - then: - def manifest = gitHubManifest() - manifest.sourceFile == "settings.gradle" - manifest.assertResolved([ - "org.test:bar:1.0": [package_url: purlFor(bar)] - ]) - } - - def "can filter configurations to extract dependencies"() { - given: - settingsFile << "include 'a', 'b'" - - buildFile << """ - project(':a') { - apply plugin: 'java-library' - dependencies { - api 'org.test:foo:1.0' - testImplementation 'org.test:baz:1.0' - } - } - project(':b') { - apply plugin: 'java-library' - dependencies { - implementation 'org.test:bar:1.0' - testImplementation 'org.test:baz:1.0' - } - } - """ +""" when: - executer.withArgument("-DDEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS=compileClasspath") - run() + run(":ForceDependencyResolutionPlugin_resolveAllDependencies") then: def manifest = gitHubManifest() manifest.sourceFile == "settings.gradle" manifest.assertResolved([ - "org.test:foo:1.0": [package_url: purlFor(foo)], "org.test:bar:1.0": [package_url: purlFor(bar)] ]) } - - } diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/PluginDependencyExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/PluginDependencyExtractorTest.groovy index a1136791..45dcd5f0 100644 --- a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/PluginDependencyExtractorTest.groovy +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/PluginDependencyExtractorTest.groovy @@ -124,61 +124,29 @@ class PluginDependencyExtractorTest extends BaseExtractorTest { // Settings plugin Map pluginDependencies = settingsPluginsAreSupported() ? [ - "my.settings.plugin:my.settings.plugin.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: ["com.example:settingPlugin:1.0"] - ], "com.example:settingPlugin:1.0" : [ - relationship: "indirect", + relationship: "direct", dependencies: ["org.test:foo:1.0"] ], "org.test:foo:1.0" : [ relationship: "indirect", dependencies: [] - ], - - // The project plugins are resolved at build level without any transitive deps - "my.project.plugin1:my.project.plugin1.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: [] - ], - "my.project.plugin2:my.project.plugin2.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: [] - ] - ] - : [ - // The project plugins are resolved at build level without any transitive deps - "my.project.plugin1:my.project.plugin1.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: [] - ], - "my.project.plugin2:my.project.plugin2.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: [] ] ] + : [:] // Plugin 1 pluginDependencies.putAll([ - "my.project.plugin1:my.project.plugin1.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: ["com.example:plugin1:1.0"] - ], "com.example:plugin1:1.0" : [ - relationship: "indirect", + relationship: "direct", dependencies: [] ] ]) // Plugin 2 pluginDependencies.putAll([ - "my.project.plugin2:my.project.plugin2.gradle.plugin:1.0": [ - relationship: "direct", - dependencies: ["com.example:plugin2:1.0"] - ], "com.example:plugin2:1.0" : [ - relationship: "indirect", + relationship: "direct", dependencies: ["org.test:bar:1.0"] ], "org.test:bar:1.0" : [ @@ -186,5 +154,9 @@ class PluginDependencyExtractorTest extends BaseExtractorTest { dependencies: [] ] ]) + + def manifest = gitHubManifest() + manifest.sourceFile == "settings.gradle" + manifest.assertResolved(pluginDependencies) } } diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SampleProjectDependencyExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SampleProjectDependencyExtractorTest.groovy index d3474c92..4bd15d5b 100644 --- a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SampleProjectDependencyExtractorTest.groovy +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SampleProjectDependencyExtractorTest.groovy @@ -4,8 +4,8 @@ import org.apache.commons.io.FileUtils import org.gradle.util.GradleVersion import spock.lang.IgnoreIf -// Samples aren't designed to run on Gradle 5.x -@IgnoreIf({ GradleVersion.version(testGradleVersion) < GradleVersion.version("6.0") }) +// Samples aren't designed to run on Gradle < 6.4 +@IgnoreIf({ GradleVersion.version(testGradleVersion) < GradleVersion.version("6.4") }) class SampleProjectDependencyExtractorTest extends BaseExtractorTest { def setup() { applyDependencyGraphPlugin() @@ -26,8 +26,7 @@ class SampleProjectDependencyExtractorTest extends BaseExtractorTest { def manifestDependencies = manifest.resolved [ // plugin dependencies - "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.12.6", - "com.gradle:gradle-enterprise-gradle-plugin:3.12.6", + "com.gradle:develocity-gradle-plugin:3.19", "com.diffplug.spotless:spotless-plugin-gradle:4.5.1", "com.diffplug.durian:durian-core:1.2.0", ].forEach { diff --git a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SingleProjectDependencyExtractorTest.groovy b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SingleProjectDependencyExtractorTest.groovy index e4ff0dd0..bca3c1a2 100644 --- a/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SingleProjectDependencyExtractorTest.groovy +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/SingleProjectDependencyExtractorTest.groovy @@ -365,13 +365,8 @@ class SingleProjectDependencyExtractorTest extends BaseExtractorTest { def manifest = gitHubManifest() manifest.sourceFile == "settings.gradle" manifest.assertResolved([ - "my.project.plugin:my.project.plugin.gradle.plugin:1.0": [ - package_url : purlFor("my.project.plugin", "my.project.plugin.gradle.plugin", "1.0"), - dependencies: ["com.example:plugin:1.0"] - ], - "com.example:plugin:1.0" : [ - package_url : purlFor("com.example", "plugin", "1.0"), - relationship: "indirect" + "com.example:plugin:1.0": [ + package_url : purlFor("com.example", "plugin", "1.0") ] ]) } @@ -401,13 +396,8 @@ class SingleProjectDependencyExtractorTest extends BaseExtractorTest { def manifest = gitHubManifest() manifest.sourceFile == "settings.gradle" manifest.assertResolved([ - "my.settings.plugin:my.settings.plugin.gradle.plugin:1.0": [ - package_url : purlFor("my.settings.plugin", "my.settings.plugin.gradle.plugin", "1.0"), - dependencies: ["com.example:plugin:1.0"] - ], - "com.example:plugin:1.0" : [ - package_url : purlFor("com.example", "plugin", "1.0"), - relationship: "indirect" + "com.example:plugin:1.0": [ + package_url : purlFor("com.example", "plugin", "1.0") ] ]) } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index d7229204..cdb5258a 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -5,6 +5,21 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.util.jar.JarFile +// Upgrade transitive dependencies in plugin classpath +buildscript { + repositories { + gradlePluginPortal() + } + dependencies { + constraints { + // The plugin com.github.breadmoirai.github-release:2.5.2 depends on vulnerable library releases. + // We constrain these to newer, patched versions. + classpath(libs.okio) + classpath(libs.apache.commons.io) + } + } +} + plugins { kotlin("jvm") version(libs.versions.kotlin) alias(libs.plugins.plugin.publish) @@ -51,11 +66,11 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -tasks.withType { +tasks.withType().configureEach { compilerOptions { - apiVersion.set(KotlinVersion.KOTLIN_1_3) - languageVersion.set(KotlinVersion.KOTLIN_1_3) - jvmTarget.set(JvmTarget.JVM_1_8) + apiVersion = KotlinVersion.KOTLIN_1_3 + languageVersion = KotlinVersion.KOTLIN_1_3 + jvmTarget = JvmTarget.JVM_1_8 } } @@ -77,7 +92,7 @@ tasks.withType().configureEach { } val shadowJarTask = tasks.named("shadowJar") { - archiveClassifier.set("") + archiveClassifier = "" configurations = listOf(shadowImplementation) val projectGroup = project.group doFirst { @@ -133,8 +148,8 @@ tasks.named("jar").configure { * Configuration for publishing the plugin, locally and to the Gradle Plugin Portal. */ gradlePlugin { - website.set("https://github.com/gradle/github-dependency-graph-gradle-plugin") - vcsUrl.set("https://github.com/gradle/github-dependency-graph-gradle-plugin") + website = "https://github.com/gradle/github-dependency-graph-gradle-plugin" + vcsUrl = "https://github.com/gradle/github-dependency-graph-gradle-plugin" plugins { create("dependencyGraphPlugin") { @@ -162,8 +177,8 @@ publishing { } tasks.withType(ValidatePlugins::class).configureEach { - failOnWarning.set(true) - enableStricterValidation.set(true) + failOnWarning = true + enableStricterValidation = true } signing { @@ -189,16 +204,16 @@ fun loadReleaseNotes():String { } val createReleaseTag = tasks.register("createReleaseTag") { - tagName.set(releaseTag) + tagName = releaseTag } githubRelease { setToken(System.getenv("GITHUB_DEPENDENCY_GRAPH_GIT_TOKEN") ?: "") - owner.set("gradle") - repo.set("github-dependency-graph-gradle-plugin") - releaseName.set(releaseVersion) - tagName.set(releaseTag) - body.set(releaseNotes) + owner = "gradle" + repo = "github-dependency-graph-gradle-plugin" + releaseName = releaseVersion + tagName = releaseTag + body = releaseNotes } tasks.named("githubRelease").configure { diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/AbstractDependencyExtractorPlugin.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/AbstractDependencyExtractorPlugin.kt index 616b9746..843302f5 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/AbstractDependencyExtractorPlugin.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/AbstractDependencyExtractorPlugin.kt @@ -20,7 +20,7 @@ abstract class AbstractDependencyExtractorPlugin : Plugin { */ abstract fun getRendererClassName(): String - internal lateinit var dependencyExtractorProvider: Provider + private lateinit var dependencyExtractorProvider: Provider override fun apply(gradle: Gradle) { val gradleVersion = GradleVersion.current() @@ -36,7 +36,7 @@ abstract class AbstractDependencyExtractorPlugin : Plugin { gradle.rootProject { project -> dependencyExtractorProvider .get() - .rootProjectBuildDirectory = project.buildDir + .rootProjectBuildDirectory = project.layout.buildDirectory.get().asFile } // Register the service to listen for Build Events @@ -89,9 +89,10 @@ abstract class AbstractDependencyExtractorPlugin : Plugin { extractorServiceProvider: Provider ) { gradle.buildFinished { - extractorServiceProvider.get().close() - gradle.buildOperationListenerManager - .removeListener(extractorServiceProvider.get()) + val extractorService = extractorServiceProvider.get() + extractorService.handleBuildCompletion(it.failure) + extractorService.close() + gradle.buildOperationListenerManager.removeListener(extractorService) } } } diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractor.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractor.kt index 878f19da..da017b38 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractor.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractor.kt @@ -1,6 +1,6 @@ package org.gradle.dependencygraph.extractor -import org.gradle.api.GradleException +import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.artifacts.result.ResolvedDependencyResult @@ -9,7 +9,8 @@ import org.gradle.api.internal.artifacts.configurations.ResolveConfigurationDepe import org.gradle.api.logging.Logging import org.gradle.dependencygraph.DependencyGraphRenderer import org.gradle.dependencygraph.model.* -import org.gradle.dependencygraph.util.* +import org.gradle.dependencygraph.model.DependencyScope.* +import org.gradle.dependencygraph.util.PluginParameters import org.gradle.initialization.EvaluateSettingsBuildOperationType import org.gradle.initialization.LoadProjectsBuildOperationType import org.gradle.internal.exceptions.DefaultMultiCauseException @@ -20,6 +21,13 @@ import java.util.* const val PARAM_INCLUDE_PROJECTS = "DEPENDENCY_GRAPH_INCLUDE_PROJECTS" const val PARAM_INCLUDE_CONFIGURATIONS = "DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS" +const val PARAM_EXCLUDE_PROJECTS = "DEPENDENCY_GRAPH_EXCLUDE_PROJECTS" +const val PARAM_EXCLUDE_CONFIGURATIONS = "DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS" +const val PARAM_RUNTIME_INCLUDE_PROJECTS = "DEPENDENCY_GRAPH_RUNTIME_INCLUDE_PROJECTS" +const val PARAM_RUNTIME_INCLUDE_CONFIGURATIONS = "DEPENDENCY_GRAPH_RUNTIME_INCLUDE_CONFIGURATIONS" +const val PARAM_RUNTIME_EXCLUDE_PROJECTS = "DEPENDENCY_GRAPH_RUNTIME_EXCLUDE_PROJECTS" +const val PARAM_RUNTIME_EXCLUDE_CONFIGURATIONS = "DEPENDENCY_GRAPH_RUNTIME_EXCLUDE_CONFIGURATIONS" + const val PARAM_REPORT_DIR = "DEPENDENCY_GRAPH_REPORT_DIR" @@ -29,6 +37,10 @@ abstract class DependencyExtractor : private val pluginParameters = PluginParameters() + private var settingsEvaluated = false + private var buildCompleted = false + private var buildFailed = false + private val resolvedConfigurations = Collections.synchronizedList(mutableListOf()) private val thrownExceptions = Collections.synchronizedList(mutableListOf()) @@ -40,10 +52,7 @@ abstract class DependencyExtractor : // Properties are lazily initialized so that System Properties are initialized by the time // the values are used. This is required due to a bug in older Gradle versions. (https://github.com/gradle/gradle/issues/6825) private val configurationFilter by lazy { - ResolvedConfigurationFilter( - pluginParameters.loadOptional(PARAM_INCLUDE_PROJECTS), - pluginParameters.loadOptional(PARAM_INCLUDE_CONFIGURATIONS) - ) + ResolvedConfigurationFilter(pluginParameters) } private val dependencyGraphReportDir by lazy { @@ -53,16 +62,17 @@ abstract class DependencyExtractor : abstract fun getRendererClassName(): String override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { - // This method will never be called when registered in a `BuildServiceRegistry` (ie. Gradle 6.1 & higher) + // This method will never be called when registered in a `BuildServiceRegistry` (i.e. Gradle 6.1 & higher) // No-op } override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { - // This method will never be called when registered in a `BuildServiceRegistry` (ie. Gradle 6.1 & higher) + // This method will never be called when registered in a `BuildServiceRegistry` (i.e. Gradle 6.1 & higher) // No-op } override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { + handleBuildOperationType< ResolveConfigurationDependenciesBuildOperationType.Details, ResolveConfigurationDependenciesBuildOperationType.Result @@ -98,20 +108,23 @@ abstract class DependencyExtractor : val details: D? = buildOperation.details.let { if (it is D) it else null } - val result: R? = finishEvent.result.let { - if (it is R) it else null + if (details == null) { + return // Ignore other build operation types } - if (details == null && result == null) { - return - } else if (details == null || result == null) { - throw IllegalStateException("buildOperation.details & finishedEvent.result were unexpected types") + + val failure = finishEvent.failure + if (failure != null) { + throw failure } + + val result: R = finishEvent.result as R handler(details, result) } open fun extractSettings( details: EvaluateSettingsBuildOperationType.Details ) { + settingsEvaluated = true val settingsFile = details.settingsFile if (settingsFile != null) { buildLayout.addSettings(details.buildPath, settingsFile) @@ -151,72 +164,110 @@ abstract class DependencyExtractor : // It is possible to do better. By tracking the current build operation context, we can assign more precisely. // See the Gradle Enterprise Build Scan Plugin: `ConfigurationResolutionCapturer_5_0` val rootPath = projectIdentityPath ?: details.buildPath + val configurationName = details.configurationName - if (!configurationFilter.include(rootPath, details.configurationName)) { - LOGGER.debug("Ignoring resolved configuration: ${rootPath} - ${details.configurationName}") + if (!configurationFilter.include(rootPath, configurationName)) { + LOGGER.info("Excluding resolved configuration from dependency graph: $rootPath - $configurationName") return } + LOGGER.info("Including resolved configuration in dependency graph: $rootPath - $configurationName") + + val scope = dependencyScope(rootPath, configurationName) val rootId = if (projectIdentityPath == null) "build $rootPath" else componentId(rootComponent) - val rootSource = DependencySource(rootId, rootPath) - val resolvedConfiguration = ResolvedConfiguration(rootSource, details.configurationName) + val rootOrigin = DependencyOrigin(rootId, rootPath) + val resolvedConfiguration = ResolvedConfiguration(rootOrigin, configurationName, scope) for (dependencyComponent in getResolvedDependencies(rootComponent)) { val directDep = createComponentNode( componentId(dependencyComponent), - rootSource, + rootOrigin, true, dependencyComponent, repositoryLookup ) resolvedConfiguration.addDependency(directDep) - walkComponentDependencies(dependencyComponent, directDep.source, repositoryLookup, resolvedConfiguration) + walkComponentDependencies(dependencyComponent, directDep.origin, repositoryLookup, resolvedConfiguration) } resolvedConfigurations.add(resolvedConfiguration) } + private fun dependencyScope( + rootPath: String, + configurationName: String + ): DependencyScope { + if (configurationFilter.scopesAreConfigured()) { + return if (configurationFilter.isRuntime(rootPath, configurationName)) Runtime else Development + } + return Unknown + } + private fun walkComponentDependencies( component: ResolvedComponentResult, - parentSource: DependencySource, + parentOrigin: DependencyOrigin, repositoryLookup: RepositoryUrlLookup, resolvedConfiguration: ResolvedConfiguration ) { - val componentSource = getSource(component, parentSource) - val direct = componentSource != parentSource + val componentOrigin = getOrigin(component, parentOrigin) + val direct = componentOrigin != parentOrigin for (dependencyComponent in getResolvedDependencies(component)) { val dependencyId = componentId(dependencyComponent) if (!resolvedConfiguration.hasDependency(dependencyId)) { val dependencyNode = - createComponentNode(dependencyId, componentSource, direct, dependencyComponent, repositoryLookup) + createComponentNode(dependencyId, componentOrigin, direct, dependencyComponent, repositoryLookup) resolvedConfiguration.addDependency(dependencyNode) - walkComponentDependencies(dependencyComponent, componentSource, repositoryLookup, resolvedConfiguration) + walkComponentDependencies(dependencyComponent, componentOrigin, repositoryLookup, resolvedConfiguration) } } } - private fun getSource(component: ResolvedComponentResult, source: DependencySource): DependencySource { + private fun getOrigin(component: ResolvedComponentResult, parentOrigin: DependencyOrigin): DependencyOrigin { val componentId = component.id if (componentId is DefaultProjectComponentIdentifier) { - return DependencySource(componentId(component), componentId.identityPath.path) + return DependencyOrigin(componentId(component), componentId.identityPath.path) } - return source + return parentOrigin } private fun getResolvedDependencies(component: ResolvedComponentResult): List { - return component.dependencies.filterIsInstance().map { it.selected }.filter { it != component } + return component.dependencies + .filterIsInstance() + .map { it.selected } + .mapNotNull { traversePluginMarker(it) } + .filter { it != component } + } + + /** + * If the rawComponent represents a plugin marker artifact, then use the target plugin dependency instead. + * For a plugin marker with no dependencies return null, so it can be filtered from the list. + */ + private fun traversePluginMarker(rawComponent: ResolvedComponentResult): ResolvedComponentResult? { + if (rawComponent.id is ModuleComponentIdentifier) { + val componentId = rawComponent.id as ModuleComponentIdentifier + if (componentId.module == componentId.group + ".gradle.plugin") { + if (rawComponent.dependencies.isEmpty()) { + return null + } + if (rawComponent.dependencies.size == 1) { + val pluginDep = rawComponent.dependencies.iterator().next() as? ResolvedDependencyResult + return pluginDep?.selected + } + } + } + return rawComponent } - private fun createComponentNode(componentId: String, source: DependencySource, isDirectDependency: Boolean, component: ResolvedComponentResult, repositoryLookup: RepositoryUrlLookup): ResolvedDependency { + private fun createComponentNode(componentId: String, origin: DependencyOrigin, isDirectDependency: Boolean, component: ResolvedComponentResult, repositoryLookup: RepositoryUrlLookup): ResolvedDependency { val componentDependencies = component.dependencies.filterIsInstance().map { componentId(it.selected) } val repositoryUrl = repositoryLookup.doLookup(component) val isProjectDependency = component.id is ProjectComponentIdentifier return ResolvedDependency( componentId, - source, + origin, isDirectDependency, isProjectDependency, coordinates(component), @@ -271,7 +322,12 @@ abstract class DependencyExtractor : private fun createRenderer(): DependencyGraphRenderer { LOGGER.lifecycle("Constructing renderer: ${getRendererClassName()}") - return Class.forName(getRendererClassName()).getDeclaredConstructor().newInstance() as DependencyGraphRenderer + val dependencyGraphRenderer = + Class.forName(getRendererClassName()).getDeclaredConstructor().newInstance() as DependencyGraphRenderer + if (LOGGER.isInfoEnabled) { + return LoggingDependencyGraphRenderer(dependencyGraphRenderer) + } + return dependencyGraphRenderer } private fun getOutputDir(): File { @@ -288,6 +344,13 @@ abstract class DependencyExtractor : ) } + fun handleBuildCompletion(failure: Throwable?) { + buildCompleted = true + if (failure != null) { + buildFailed = true + } + } + override fun close() { if (thrownExceptions.isNotEmpty()) { throw DefaultMultiCauseException( @@ -296,18 +359,46 @@ abstract class DependencyExtractor : thrownExceptions ) } + + // We use the absence of Settings Evaluated to determine if the build was loaded from the configuration-cache + if (!settingsEvaluated) { + LOGGER.lifecycle( + "Gradle build state was reused from the configuration-cache: " + + "Dependency Graph file will not be generated." + ) + return + } + // Do not write an incomplete graph when build didn't complete successfully + if (!buildCompleted || buildFailed) { + LOGGER.lifecycle( + "Gradle Build did not complete successfully: Dependency Graph file will not be generated." + ) + return + } try { writeDependencyGraph() } catch (e: RuntimeException) { - throw GradleException( - "The dependency-graph extractor plugin encountered errors while writing the dependency snapshot json file. " + - "Please report this issue at: https://github.com/gradle/github-dependency-graph-gradle-plugin/issues", - e - ) + throw DefaultMultiCauseException("Failed to write dependency-graph to file", e) + } + } + + private class LoggingDependencyGraphRenderer(private val delegate: DependencyGraphRenderer) : DependencyGraphRenderer { + override fun outputDependencyGraph( + pluginParameters: PluginParameters, + buildLayout: BuildLayout, + resolvedConfigurations: List, + outputDirectory: File + ) { + for (configuration in resolvedConfigurations) { + for (dependency in configuration.allDependencies) { + LOGGER.info("Detected dependency '${dependency.id}': project = '${configuration.rootOrigin.path}', configuration = '${configuration.configurationName}'") + } + } + delegate.outputDependencyGraph(pluginParameters, buildLayout, resolvedConfigurations, outputDirectory) } } companion object { private val LOGGER = Logging.getLogger(DependencyExtractor::class.java) } -} \ No newline at end of file +} diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractorBuildService.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractorBuildService.kt index d0448c17..a7e75fa9 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractorBuildService.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractorBuildService.kt @@ -3,6 +3,9 @@ package org.gradle.dependencygraph.extractor import org.gradle.api.provider.Property import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters +import org.gradle.internal.operations.BuildOperationCategory +import org.gradle.internal.operations.BuildOperationDescriptor +import org.gradle.internal.operations.OperationFinishEvent abstract class DependencyExtractorBuildService : DependencyExtractor(), @@ -16,4 +19,13 @@ abstract class DependencyExtractorBuildService : override fun getRendererClassName(): String { return parameters.rendererClassName.get() } + + override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { + super.finished(buildOperation, finishEvent) + + // Track build completion without 'buildFinished' + if (buildOperation.metadata == BuildOperationCategory.RUN_WORK) { + handleBuildCompletion(finishEvent.failure) + } + } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/ResolvedConfigurationFilter.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/ResolvedConfigurationFilter.kt index 4c9d7af2..421bea15 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/ResolvedConfigurationFilter.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/ResolvedConfigurationFilter.kt @@ -1,16 +1,44 @@ package org.gradle.dependencygraph.extractor -class ResolvedConfigurationFilter(projectFilter: String?, configurationFilter: String?) { - val projectRegex = projectFilter?.toRegex() - val configurationRegex = configurationFilter?.toRegex() +import org.gradle.dependencygraph.util.PluginParameters + +class ResolvedConfigurationFilter(pluginParameters: PluginParameters) { + private val includeProjects = pluginParameters.loadOptional(PARAM_INCLUDE_PROJECTS)?.toRegex() + private val includeConfigurations = pluginParameters.loadOptional(PARAM_INCLUDE_CONFIGURATIONS)?.toRegex() + private val excludeProjects = pluginParameters.loadOptional(PARAM_EXCLUDE_PROJECTS)?.toRegex() + private val excludeConfigurations = pluginParameters.loadOptional(PARAM_EXCLUDE_CONFIGURATIONS)?.toRegex() + + private val runtimeIncludeProjects = pluginParameters.loadOptional(PARAM_RUNTIME_INCLUDE_PROJECTS)?.toRegex() + private val runtimeIncludeConfigurations = pluginParameters.loadOptional(PARAM_RUNTIME_INCLUDE_CONFIGURATIONS)?.toRegex() + private val runtimeExcludeProjects = pluginParameters.loadOptional(PARAM_RUNTIME_EXCLUDE_PROJECTS)?.toRegex() + private val runtimeExcludeConfigurations = pluginParameters.loadOptional(PARAM_RUNTIME_EXCLUDE_CONFIGURATIONS)?.toRegex() fun include(projectPath: String, configurationName: String): Boolean { - if (projectRegex != null && !projectRegex.matches(projectPath)) { - return false - } - if (configurationRegex != null && !configurationRegex.matches(configurationName)) { - return false - } - return true + return includes(includeProjects, projectPath) + && notExcludes(excludeProjects, projectPath) + && includes(includeConfigurations, configurationName) + && notExcludes(excludeConfigurations, configurationName) + } + + fun scopesAreConfigured(): Boolean { + return runtimeIncludeProjects != null + || runtimeIncludeConfigurations != null + || runtimeExcludeProjects != null + || runtimeExcludeConfigurations != null + } + + fun isRuntime(projectPath: String, configurationName: String): Boolean { + return includes(runtimeIncludeProjects, projectPath) + && notExcludes(runtimeExcludeProjects, projectPath) + && includes(runtimeIncludeConfigurations, configurationName) + && notExcludes(runtimeExcludeConfigurations, configurationName) + } + + private fun includes(regex: Regex?, value: String): Boolean { + return regex == null || regex.matches(value) + } + + private fun notExcludes(regex: Regex?, value: String): Boolean { + return regex == null || !regex.matches(value) } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencySource.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencyOrigin.kt similarity index 75% rename from plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencySource.kt rename to plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencyOrigin.kt index feda3100..1adc7f09 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencySource.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencyOrigin.kt @@ -1,10 +1,10 @@ package org.gradle.dependencygraph.model /** - * The source of a dependency declaration, representing where the direct dependency is declared, + * The origin of a dependency declaration, representing where the direct dependency is declared, * or where the parent dependency is declared for transitive dependencies. * In most cases, this will be the project component that declares the dependency, * but may also be a Version Catalog or the build as a whole. * We attempt to map this to an actual source file location when building a dependency report. */ -data class DependencySource(val id: String, val path: String) +data class DependencyOrigin(val id: String, val path: String) diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencyScope.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencyScope.kt new file mode 100644 index 00000000..65b8fee7 --- /dev/null +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/DependencyScope.kt @@ -0,0 +1,18 @@ +package org.gradle.dependencygraph.model + +/** + * Represents the scope of a resolved dependency. + * At this point, the scopes are limited to those exposed in the GitHub DependencySubmission API. + * Later development may extend this to a richer set of scopes. + */ +enum class DependencyScope { + Unknown, Development, Runtime; + + companion object { + fun getEffectiveScope(scopes: List): DependencyScope { + if (scopes.contains(Runtime)) return Runtime + if (scopes.contains(Development)) return Development + return Unknown + } + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedConfiguration.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedConfiguration.kt index c0206d7b..22612398 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedConfiguration.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedConfiguration.kt @@ -1,8 +1,9 @@ package org.gradle.dependencygraph.model data class ResolvedConfiguration( - val rootSource: DependencySource, + val rootOrigin: DependencyOrigin, val configurationName: String, + val scope: DependencyScope, val allDependencies: MutableList = mutableListOf() ) { fun addDependency(component: ResolvedDependency) { diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedDependency.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedDependency.kt index 362ff359..28e28329 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedDependency.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedDependency.kt @@ -6,7 +6,7 @@ private const val DEFAULT_MAVEN_REPOSITORY_URL = "https://repo.maven.apache.org/ data class ResolvedDependency( val id: String, - val source: DependencySource, + val origin: DependencyOrigin, val isDirect: Boolean, val isProject: Boolean, val coordinates: DependencyCoordinates, diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphPlugin.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphPlugin.kt index 940d6c00..9aa18531 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphPlugin.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphPlugin.kt @@ -4,7 +4,6 @@ import org.gradle.api.Plugin import org.gradle.api.invocation.Gradle import org.gradle.dependencygraph.AbstractDependencyExtractorPlugin import org.gradle.forceresolve.ForceDependencyResolutionPlugin -import org.gradle.github.GitHubDependencyExtractorPlugin @Suppress("unused") class SimpleDependencyGraphPlugin : Plugin { diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphRenderer.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphRenderer.kt index 858de70d..099e403d 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphRenderer.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/simple/SimpleDependencyGraphRenderer.kt @@ -3,6 +3,7 @@ package org.gradle.dependencygraph.simple import org.gradle.dependencygraph.DependencyGraphRenderer import org.gradle.dependencygraph.model.BuildLayout import org.gradle.dependencygraph.model.ResolvedConfiguration +import org.gradle.dependencygraph.model.DependencyScope import org.gradle.dependencygraph.util.JacksonJsonSerializer import org.gradle.dependencygraph.util.PluginParameters import java.io.File @@ -21,18 +22,68 @@ class SimpleDependencyGraphRenderer : DependencyGraphRenderer { resolvedConfigurations: List, outputDirectory: File ) { - val graphOutputFile = File(outputDirectory, "dependency-graph.json") - val graphJson = JacksonJsonSerializer.serializeToJson(resolvedConfigurations) - graphOutputFile.writeText(graphJson) + outputDependencyGraph(outputDirectory, resolvedConfigurations) + outputDependencyScopes(outputDirectory, resolvedConfigurations) + outputDependencyList(outputDirectory, resolvedConfigurations) + } + + private fun outputDependencyGraph( + outputDirectory: File, + resolvedConfigurations: List + ) { + val outputFile = File(outputDirectory, "dependency-graph.json") + val jsonContent = JacksonJsonSerializer.serializeToJson(resolvedConfigurations) + outputFile.writeText(jsonContent) + } - val listOutputFile = File(outputDirectory, "dependency-list.txt") - val dependencyList = resolvedConfigurations.flatMap { it -> - it.allDependencies.map { + private fun outputDependencyScopes( + outputDirectory: File, + resolvedConfigurations: List + ) { + val outputFile = File(outputDirectory, "dependency-resolution.json") + val dependencyList: MutableMap> = mutableMapOf() + for (config in resolvedConfigurations) { + for (dependency in config.allDependencies) { + if (dependency.isProject) continue + + val dependencyResolutions = dependencyList.getOrPut(dependency.id) { mutableSetOf() } + dependencyResolutions.add( + SimpleDependencyResolution( + config.rootOrigin.path, + config.configurationName, + config.scope + ) + ) + } + } + + val simpleDependencies = dependencyList.map { (id, resolutions) -> + SimpleDependency(id, DependencyScope.getEffectiveScope(resolutions.map {it.scope}), resolutions.toList()) + } + val jsonContent = JacksonJsonSerializer.serializeToJson(simpleDependencies) + outputFile.writeText(jsonContent) + } + + private fun outputDependencyList( + outputDirectory: File, + resolvedConfigurations: List + ) { + val outputFile = File(outputDirectory, "dependency-list.txt") + val dependencyList = resolvedConfigurations.flatMap { config -> + config.allDependencies.map { "${it.coordinates.group}:${it.coordinates.module}:${it.coordinates.version}" } }.distinct().sorted() val listTxt = dependencyList.joinToString(separator = "\n") - listOutputFile.writeText(listTxt) + outputFile.writeText(listTxt) } -} \ No newline at end of file +} + +data class SimpleDependency( + val dependency: String, + val effectiveScope: DependencyScope, + val resolvedBy: List +) + +data class SimpleDependencyResolution(val path: String, val configuration: String, val scope: DependencyScope) diff --git a/plugin/src/main/kotlin/org/gradle/dependencygraph/util/GradleExtensions.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/util/GradleExtensions.kt index 450bd2c8..0ad63147 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/util/GradleExtensions.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/util/GradleExtensions.kt @@ -7,10 +7,6 @@ import org.gradle.api.provider.ProviderFactory import org.gradle.internal.operations.BuildOperationListenerManager internal abstract class GradleExtensions { - - inline val Gradle.objectFactory: ObjectFactory - get() = service() - inline val Gradle.providerFactory: ProviderFactory get() = service() diff --git a/plugin/src/main/kotlin/org/gradle/forceresolve/AbstractResolveProjectDependenciesTask.kt b/plugin/src/main/kotlin/org/gradle/forceresolve/AbstractResolveProjectDependenciesTask.kt new file mode 100644 index 00000000..395f0fcb --- /dev/null +++ b/plugin/src/main/kotlin/org/gradle/forceresolve/AbstractResolveProjectDependenciesTask.kt @@ -0,0 +1,43 @@ +package org.gradle.forceresolve + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.Internal +import org.gradle.dependencygraph.extractor.ResolvedConfigurationFilter +import org.gradle.work.DisableCachingByDefault +import java.lang.reflect.Method + +@DisableCachingByDefault(because = "Not worth caching") +abstract class AbstractResolveProjectDependenciesTask : DefaultTask() { + private val canSafelyBeResolvedMethod: Method? = getCanSafelyBeResolvedMethod() + + @Internal + var configurationFilter: ResolvedConfigurationFilter? = null + + @Internal + protected fun getReportableConfigurations(): List { + return project.configurations.filter { + canSafelyBeResolved(it) && configurationFilter!!.include(project.path, it.name) + } + } + + /** + * If `DeprecatableConfiguration.canSafelyBeResolved()` is available, use it. + * Else fall back to `Configuration.canBeResolved`. + */ + private fun canSafelyBeResolved(configuration: Configuration): Boolean { + if (canSafelyBeResolvedMethod != null) { + return canSafelyBeResolvedMethod.invoke(configuration) as Boolean + } + return configuration.isCanBeResolved + } + + private fun getCanSafelyBeResolvedMethod(): Method? { + return try { + val dc = Class.forName("org.gradle.internal.deprecation.DeprecatableConfiguration") + dc.getMethod("canSafelyBeResolved") + } catch (e: ReflectiveOperationException) { + null + } + } +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/gradle/forceresolve/ForceDependencyResolutionPlugin.kt b/plugin/src/main/kotlin/org/gradle/forceresolve/ForceDependencyResolutionPlugin.kt index 8a4d1749..28cb07c3 100644 --- a/plugin/src/main/kotlin/org/gradle/forceresolve/ForceDependencyResolutionPlugin.kt +++ b/plugin/src/main/kotlin/org/gradle/forceresolve/ForceDependencyResolutionPlugin.kt @@ -5,6 +5,8 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.invocation.Gradle import org.gradle.api.tasks.TaskProvider +import org.gradle.dependencygraph.extractor.ResolvedConfigurationFilter +import org.gradle.dependencygraph.util.PluginParameters import org.gradle.util.GradleVersion private const val RESOLVE_PROJECT_TASK = "ForceDependencyResolutionPlugin_resolveProjectDependencies" @@ -14,6 +16,14 @@ private const val RESOLVE_ALL_TASK = "ForceDependencyResolutionPlugin_resolveAll * Adds a task to resolve all dependencies in a Gradle build tree. */ class ForceDependencyResolutionPlugin : Plugin { + + // Properties are lazily initialized so that System Properties are initialized by the time + // the values are used. This is required due to a bug in older Gradle versions. (https://github.com/gradle/gradle/issues/6825) + private val pluginParameters = PluginParameters() + private val configurationFilter by lazy { + ResolvedConfigurationFilter(pluginParameters) + } + override fun apply(gradle: Gradle) { gradle.projectsEvaluated { val resolveAllDeps = gradle.rootProject.tasks.register(RESOLVE_ALL_TASK) @@ -21,7 +31,7 @@ class ForceDependencyResolutionPlugin : Plugin { // Depend on "dependencies" task in all projects gradle.allprojects { project -> val projectTaskFactory = getResolveProjectDependenciesTaskFactory() - val resolveProjectDeps = projectTaskFactory.create(project) + val resolveProjectDeps = projectTaskFactory.create(project, configurationFilter) resolveAllDeps.configure { it.dependsOn(resolveProjectDeps) } @@ -29,8 +39,10 @@ class ForceDependencyResolutionPlugin : Plugin { // Depend on all 'resolveBuildDependencies' task in each included build gradle.includedBuilds.forEach { includedBuild -> - resolveAllDeps.configure { - it.dependsOn(includedBuild.task(":$RESOLVE_ALL_TASK")) + if (includedBuild.projectDir != gradle.rootProject.projectDir) { + resolveAllDeps.configure { + it.dependsOn(includedBuild.task(":$RESOLVE_ALL_TASK")) + } } } } @@ -47,19 +59,22 @@ class ForceDependencyResolutionPlugin : Plugin { } private interface ResolveProjectDependenciesTaskFactory { - fun create(project: Project): TaskProvider + fun create(project: Project, filter: ResolvedConfigurationFilter): TaskProvider object Current : ResolveProjectDependenciesTaskFactory { - override fun create(project: Project): TaskProvider { - return project.tasks.register(RESOLVE_PROJECT_TASK, ResolveProjectDependenciesTask::class.java) + override fun create(project: Project, filter: ResolvedConfigurationFilter): TaskProvider { + return project.tasks.register(RESOLVE_PROJECT_TASK, ResolveProjectDependenciesTask::class.java) { + it.configurationFilter = filter + } } } object Legacy : ResolveProjectDependenciesTaskFactory { - override fun create(project: Project): TaskProvider { - return project.tasks.register(RESOLVE_PROJECT_TASK, LegacyResolveProjectDependenciesTask::class.java) + override fun create(project: Project, filter: ResolvedConfigurationFilter): TaskProvider { + return project.tasks.register(RESOLVE_PROJECT_TASK, LegacyResolveProjectDependenciesTask::class.java) { + it.configurationFilter = filter + } } } } - } \ No newline at end of file diff --git a/plugin/src/main/kotlin/org/gradle/forceresolve/LegacyResolveProjectDependenciesTask.kt b/plugin/src/main/kotlin/org/gradle/forceresolve/LegacyResolveProjectDependenciesTask.kt index af6c45a8..85e14143 100644 --- a/plugin/src/main/kotlin/org/gradle/forceresolve/LegacyResolveProjectDependenciesTask.kt +++ b/plugin/src/main/kotlin/org/gradle/forceresolve/LegacyResolveProjectDependenciesTask.kt @@ -1,15 +1,10 @@ package org.gradle.forceresolve -import org.gradle.api.DefaultTask -import org.gradle.api.artifacts.Configuration import org.gradle.api.tasks.TaskAction import org.gradle.work.DisableCachingByDefault @DisableCachingByDefault(because = "Not worth caching") -abstract class LegacyResolveProjectDependenciesTask: DefaultTask() { - private fun getReportableConfigurations(): List { - return project.configurations.filter { it.isCanBeResolved } - } +abstract class LegacyResolveProjectDependenciesTask: AbstractResolveProjectDependenciesTask() { @TaskAction fun action() { diff --git a/plugin/src/main/kotlin/org/gradle/forceresolve/ResolveProjectDependenciesTask.kt b/plugin/src/main/kotlin/org/gradle/forceresolve/ResolveProjectDependenciesTask.kt index 4fc06619..6d7ef076 100644 --- a/plugin/src/main/kotlin/org/gradle/forceresolve/ResolveProjectDependenciesTask.kt +++ b/plugin/src/main/kotlin/org/gradle/forceresolve/ResolveProjectDependenciesTask.kt @@ -1,7 +1,5 @@ package org.gradle.forceresolve -import org.gradle.api.DefaultTask -import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskAction @@ -9,7 +7,7 @@ import org.gradle.internal.serialization.Cached import org.gradle.work.DisableCachingByDefault @DisableCachingByDefault(because = "Not worth caching") -abstract class ResolveProjectDependenciesTask: DefaultTask() { +abstract class ResolveProjectDependenciesTask: AbstractResolveProjectDependenciesTask() { private val configurationResolvers = Cached.of { createConfigurationResolvers() } private fun createConfigurationResolvers(): List> { @@ -18,10 +16,6 @@ abstract class ResolveProjectDependenciesTask: DefaultTask() { } } - private fun getReportableConfigurations(): List { - return project.configurations.filter { it.isCanBeResolved } - } - @TaskAction fun action() { for (configuration in configurationResolvers.get()) { diff --git a/plugin/src/main/kotlin/org/gradle/github/GitHubDependencyGraphPlugin.kt b/plugin/src/main/kotlin/org/gradle/github/GitHubDependencyGraphPlugin.kt index ff1962d1..5d48f69e 100644 --- a/plugin/src/main/kotlin/org/gradle/github/GitHubDependencyGraphPlugin.kt +++ b/plugin/src/main/kotlin/org/gradle/github/GitHubDependencyGraphPlugin.kt @@ -1,8 +1,10 @@ package org.gradle.github +import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.invocation.Gradle import org.gradle.forceresolve.ForceDependencyResolutionPlugin +import org.gradle.util.GradleVersion /** * A plugin that collects all resolved dependencies in a Gradle build for submission to the @@ -11,6 +13,12 @@ import org.gradle.forceresolve.ForceDependencyResolutionPlugin @Suppress("unused") class GitHubDependencyGraphPlugin : Plugin { override fun apply(gradle: Gradle) { + val gradleVersion = GradleVersion.current().baseVersion + if (gradleVersion < GradleVersion.version("5.2") || + (gradleVersion >= GradleVersion.version("7.0") && gradleVersion < GradleVersion.version("7.1"))) { + throw GradleException("${this.javaClass.simpleName} is not supported for $gradleVersion.") + } + // Only apply the dependency extractor to the root build if (gradle.parent == null) { gradle.pluginManager.apply(GitHubDependencyExtractorPlugin::class.java) diff --git a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubDependencyGraphRenderer.kt b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubDependencyGraphRenderer.kt index 966642c7..bc237da7 100644 --- a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubDependencyGraphRenderer.kt +++ b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubDependencyGraphRenderer.kt @@ -8,7 +8,7 @@ import org.gradle.dependencygraph.model.ResolvedConfiguration import org.gradle.dependencygraph.util.* import java.io.File -class GitHubDependencyGraphRenderer() : DependencyGraphRenderer { +class GitHubDependencyGraphRenderer : DependencyGraphRenderer { override fun outputDependencyGraph( pluginParameters: PluginParameters, @@ -26,6 +26,12 @@ class GitHubDependencyGraphRenderer() : DependencyGraphRenderer { val outputFile = File(outputDirectory, "${snapshotParams.dependencyGraphJobCorrelator}.json") writeDependencySnapshot(snapshot, outputFile) + + // Write the output file as a GitHub Actions step output + val githubOutput = System.getenv("GITHUB_OUTPUT") + if (githubOutput !== null && File(githubOutput).isFile) { + File(githubOutput).appendText("dependency-graph-file=${outputFile.absolutePath}\n") + } } private fun writeDependencySnapshot(graph: GitHubRepositorySnapshot, manifestFile: File) { diff --git a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt index ceffc06f..ae6e0128 100644 --- a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt +++ b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt @@ -3,13 +3,20 @@ package org.gradle.github.dependencygraph import org.gradle.dependencygraph.model.ResolvedDependency import org.gradle.dependencygraph.model.ResolvedConfiguration import org.gradle.dependencygraph.model.BuildLayout +import org.gradle.dependencygraph.model.DependencyScope import org.gradle.github.dependencygraph.model.* class GitHubRepositorySnapshotBuilder( private val snapshotParams: GitHubSnapshotParams ) { - private val detector by lazy { GitHubDetector() } + private val detector by lazy { + GitHubDetector( + name = snapshotParams.githubDetectorName, + version = snapshotParams.githubDetectorVersion, + url = snapshotParams.githubDetectorUrl + ) + } private val job by lazy { GitHubJob( @@ -21,12 +28,12 @@ class GitHubRepositorySnapshotBuilder( fun buildManifest(manifestName: String, resolvedConfigurations: List, buildLayout: BuildLayout): GitHubManifest { val dependencyCollector = DependencyCollector() - for (resolutionRoot in resolvedConfigurations) { - for (dependency in resolutionRoot.allDependencies) { + for (configuration in resolvedConfigurations) { + for (dependency in configuration.allDependencies) { // Ignore project dependencies (transitive deps of projects will be reported with project) if (dependency.isProject) continue - dependencyCollector.addResolved(dependency) + dependencyCollector.addResolved(dependency, determineGitHubScope(configuration)) } } @@ -37,6 +44,14 @@ class GitHubRepositorySnapshotBuilder( ) } + private fun determineGitHubScope(configuration: ResolvedConfiguration): GitHubDependency.Scope? { + return when(configuration.scope) { + DependencyScope.Development -> GitHubDependency.Scope.development + DependencyScope.Runtime -> GitHubDependency.Scope.runtime + DependencyScope.Unknown -> null + } + } + /** * Manifest file is the root build settings file if it exists, or the root build file if not. */ @@ -67,11 +82,12 @@ class GitHubRepositorySnapshotBuilder( /** * Merge each resolved component with the same ID into a single GitHubDependency. */ - fun addResolved(component: ResolvedDependency) { + fun addResolved(component: ResolvedDependency, scope: GitHubDependency.Scope?) { val dep = dependencyBuilders.getOrPut(component.id) { GitHubDependencyBuilder(component.packageUrl()) } dep.addRelationship(relationship(component)) + dep.addScope(scope) dep.addDependencies(component.dependencies) } @@ -89,6 +105,7 @@ class GitHubRepositorySnapshotBuilder( private class GitHubDependencyBuilder(val package_url: String) { var relationship: GitHubDependency.Relationship = GitHubDependency.Relationship.indirect + var scope: GitHubDependency.Scope? = null val dependencies = mutableListOf() fun addRelationship(newRelationship: GitHubDependency.Relationship) { @@ -98,6 +115,13 @@ class GitHubRepositorySnapshotBuilder( } } + fun addScope(newScope: GitHubDependency.Scope?) { + if (newScope == null) return + if (scope == null || scope == GitHubDependency.Scope.development) { + scope = newScope + } + } + fun addDependencies(newDependencies: List) { // Add any dependencies that are not in the existing set for (newDependency in newDependencies.subtract(dependencies.toSet())) { @@ -106,7 +130,7 @@ class GitHubRepositorySnapshotBuilder( } fun build(): GitHubDependency { - return GitHubDependency(package_url, relationship, dependencies) + return GitHubDependency(package_url, relationship, scope, dependencies) } } } diff --git a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubSnapshotParams.kt b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubSnapshotParams.kt index 83066c41..870815ff 100644 --- a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubSnapshotParams.kt +++ b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubSnapshotParams.kt @@ -8,17 +8,26 @@ const val PARAM_JOB_ID = "GITHUB_DEPENDENCY_GRAPH_JOB_ID" const val PARAM_JOB_CORRELATOR = "GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR" const val PARAM_GITHUB_REF = "GITHUB_DEPENDENCY_GRAPH_REF" const val PARAM_GITHUB_SHA = "GITHUB_DEPENDENCY_GRAPH_SHA" +const val PARAM_GITHUB_DETECTOR_NAME = "GITHUB_DEPENDENCY_GRAPH_DETECTOR_NAME" +const val PARAM_GITHUB_DETECTOR_VERSION = "GITHUB_DEPENDENCY_GRAPH_DETECTOR_VERSION" +const val PARAM_GITHUB_DETECTOR_URL = "GITHUB_DEPENDENCY_GRAPH_DETECTOR_URL" /** * Environment variable should be set to the workspace directory that the Git repository is checked out in. * This is used to determine relative path to build files referenced in the dependency graph. */ const val PARAM_GITHUB_WORKSPACE = "GITHUB_DEPENDENCY_GRAPH_WORKSPACE" -class GitHubSnapshotParams(private val pluginParameters: PluginParameters) { +class GitHubSnapshotParams(pluginParameters: PluginParameters) { val dependencyGraphJobCorrelator: String = pluginParameters.load(PARAM_JOB_CORRELATOR) - val dependencyGraphJobId: String =pluginParameters.load(PARAM_JOB_ID) + val dependencyGraphJobId: String = pluginParameters.load(PARAM_JOB_ID) val gitSha: String = pluginParameters.load(PARAM_GITHUB_SHA) val gitRef: String = pluginParameters.load(PARAM_GITHUB_REF) val gitHubWorkspace: Path = Paths.get(pluginParameters.load(PARAM_GITHUB_WORKSPACE)) + val githubDetectorName: String = pluginParameters.loadOptional(PARAM_GITHUB_DETECTOR_NAME) + ?: javaClass.`package`.implementationTitle + val githubDetectorVersion: String = pluginParameters.loadOptional(PARAM_GITHUB_DETECTOR_VERSION) + ?: javaClass.`package`.implementationVersion + val githubDetectorUrl: String = pluginParameters.loadOptional(PARAM_GITHUB_DETECTOR_URL) + ?: "https://github.com/gradle/github-dependency-graph-gradle-plugin" } diff --git a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDependency.kt b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDependency.kt index 419de316..bf2deb17 100644 --- a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDependency.kt +++ b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDependency.kt @@ -3,9 +3,13 @@ package org.gradle.github.dependencygraph.model data class GitHubDependency( val package_url: String, val relationship: Relationship, + val scope: Scope?, val dependencies: List ) { enum class Relationship { indirect, direct } + enum class Scope { + runtime, development + } } diff --git a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDetector.kt b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDetector.kt index f5e256cf..8961b8f9 100644 --- a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDetector.kt +++ b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/model/GitHubDetector.kt @@ -1,7 +1,7 @@ package org.gradle.github.dependencygraph.model data class GitHubDetector( - val name: String = GitHubDetector::class.java.`package`.implementationTitle, - val version: String = GitHubDetector::class.java.`package`.implementationVersion, - val url: String = "https://github.com/gradle/github-dependency-graph-gradle-plugin" + val name: String, + val version: String, + val url: String ) diff --git a/plugin/src/test/groovy/org/gradle/github/dependencygraph/internal/ResolvedConfigurationTest.groovy b/plugin/src/test/groovy/org/gradle/github/dependencygraph/internal/ResolvedConfigurationTest.groovy deleted file mode 100644 index a2db614e..00000000 --- a/plugin/src/test/groovy/org/gradle/github/dependencygraph/internal/ResolvedConfigurationTest.groovy +++ /dev/null @@ -1,98 +0,0 @@ -package org.gradle.github.dependencygraph.internal - -import org.gradle.dependencygraph.extractor.ResolvedConfigurationFilter -import spock.lang.Specification - -class ResolvedConfigurationTest extends Specification { - def "null filter includes everything"() { - when: - def filter = new ResolvedConfigurationFilter(null, null) - - then: - filter.include("foo", "bar") - filter.include(":foo:bar:baz", "not a real name") - filter.include("", "") - } - - def "filters on exact configuration name"() { - when: - def filter = new ResolvedConfigurationFilter(null, "compileClasspath") - - then: - filter.include("", "compileClasspath") - - !filter.include("", "classpath") - !filter.include("", "runtimeClasspath") - !filter.include("", "testCompileClasspath") - } - - def "filters on exact configuration names"() { - when: - def filter = new ResolvedConfigurationFilter(null, "compileClasspath|runtimeClasspath") - - then: - filter.include("", "compileClasspath") - filter.include("", "runtimeClasspath") - - !filter.include("", "classpath") - !filter.include("", "testCompileClasspath") - !filter.include("", "runtimeElements") - } - - def "filters on configuration name match"() { - when: - def filter = new ResolvedConfigurationFilter(null, ".*[cC]lasspath") - - then: - filter.include("", "compileClasspath") - filter.include("", "runtimeClasspath") - filter.include("", "testCompileClasspath") - filter.include("", "classpath") - - !filter.include("", "runtimeElements") - !filter.include("", "compileClasspathOnly") - } - - def "filters on exact project path"() { - when: - def filter = new ResolvedConfigurationFilter(":parent-proj:proj", null) - - then: - filter.include(":parent-proj:proj", "") - - !filter.include(":parent-proj", "") - !filter.include(":parent-proj:", "") - !filter.include(":proj", "") - !filter.include(":", "") - } - - def "filters on exact project paths"() { - when: - def filter = new ResolvedConfigurationFilter(":proj-a|:proj-b", null) - - then: - filter.include(":proj-a", "") - filter.include(":proj-b", "") - - !filter.include(":parent-proj:proj-a", "") - !filter.include(":proj", "") - !filter.include(":", "") - } - - def "filters on project path match"() { - when: - def filter = new ResolvedConfigurationFilter(/(:[\w-]+)*:proj-a(:[\w-]+)*/, null) - - then: - filter.include(":proj-a", "") - filter.include(":proj-a:proj-b", "") - filter.include(":proj-a:proj-b:proj-c", "") - filter.include(":parent-proj:proj-a", "") - filter.include(":parent-proj:proj-a:proj-b", "") - - !filter.include(":proj-another", "") - !filter.include(":proj-a:", "") - !filter.include(":proj-a:proj-b:", "") - !filter.include("parent-proj:proj-a", "") - } -} diff --git a/release/changes.md b/release/changes.md index fc47cd8b..20acfc65 100644 --- a/release/changes.md +++ b/release/changes.md @@ -1,3 +1 @@ -First stable release of the GitHub Dependency Graph Gradle Plugin. - -Contains only minor refactorings and dependency updates from v0.4.1. \ No newline at end of file +- [NEW] GitHub Detector can be customized with environment variables diff --git a/release/version.txt b/release/version.txt index 3eefcb9d..88c5fb89 100644 --- a/release/version.txt +++ b/release/version.txt @@ -1 +1 @@ -1.0.0 +1.4.0 diff --git a/sample-projects/java-single-project/settings.gradle b/sample-projects/java-single-project/settings.gradle index 1c90fdb0..fd9b1d84 100644 --- a/sample-projects/java-single-project/settings.gradle +++ b/sample-projects/java-single-project/settings.gradle @@ -1,4 +1,4 @@ plugins { - id 'com.gradle.enterprise' version '3.12.6' + id 'com.gradle.develocity' version '3.19' } rootProject.name = 'java-lib' diff --git a/settings.gradle.kts b/settings.gradle.kts index 730314da..f702fa5b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,14 @@ plugins { - id("com.gradle.enterprise").version("3.13") - id("com.gradle.common-custom-user-data-gradle-plugin").version("1.10") + id("com.gradle.develocity").version("3.19") + id("com.gradle.common-custom-user-data-gradle-plugin").version("2.0.2") } val isCI = !System.getenv("CI").isNullOrEmpty() -gradleEnterprise { +develocity { server = "https://ge.gradle.org" buildScan { - publishAlways() - capture { isTaskInputFiles = true } - isUploadInBackground = !isCI + uploadInBackground = !isCI obfuscation { ipAddresses { addresses -> addresses.map { _ -> "0.0.0.0" } }