diff --git a/README.md b/README.md index 9e485cb4..6a30f696 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,38 @@ 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. -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. +| Property | Description | Default | +|-----------------------------------------|---------------------------|---------------------------------| +| DEPENDENCY_GRAPH_INCLUDE_PROJECTS | Projects to include | All projects are included | +| DEPENDENCY_GRAPH_EXCLUDE_PROJECTS | Projects to exclude | No projects are included | +| DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS | Configurations to include | All configurations are included | +| DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS | Configurations to exclude | No configurations are included | + +### 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 included | +| 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 included | + +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 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 ea1041fb..d3ba3cf1 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 @@ -113,6 +113,10 @@ abstract class BaseExtractorTest extends Specification { } apply plugin: GitHubDependencyGraphPlugin """.stripMargin() + resetArguments() + } + + protected SimpleGradleExecuter resetArguments() { getExecuter().withArguments("--init-script", "init.gradle") } @@ -194,6 +198,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 @@ -208,6 +218,7 @@ abstract class BaseExtractorTest extends Specification { } assert actual.relationship == (expected.relationship ?: "direct") assert actual.dependencies == (expected.dependencies ?: []) + assert actual.scope == expected.scope } return true 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..b4da125e --- /dev/null +++ b/plugin-test/src/test/groovy/org/gradle/github/dependencygraph/ConfigurationFilterDependencyExtractorTest.groovy @@ -0,0 +1,287 @@ +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"]] + ]) + } + +} 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..a9d70f7d 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 @@ -250,71 +250,4 @@ class MultiProjectDependencyExtractorTest extends BaseExtractorTest { "org.test:foo:1.0": [package_url: purlFor(foo)] ]) } - - def "can filter projects to extract dependencies"() { - 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 { - api '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() - - 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/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractor.kt b/plugin/src/main/kotlin/org/gradle/dependencygraph/extractor/DependencyExtractor.kt index 94779807..dcf1eae1 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,5 @@ package org.gradle.dependencygraph.extractor -import org.gradle.api.GradleException import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.artifacts.result.ResolvedDependencyResult @@ -9,10 +8,10 @@ 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.model.DependencyScope.* import org.gradle.dependencygraph.util.* import org.gradle.initialization.EvaluateSettingsBuildOperationType import org.gradle.initialization.LoadProjectsBuildOperationType -import org.gradle.internal.exceptions.Contextual import org.gradle.internal.exceptions.DefaultMultiCauseException import org.gradle.internal.operations.* import java.io.File @@ -21,6 +20,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" @@ -43,10 +49,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 { @@ -56,12 +59,12 @@ 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 } @@ -157,15 +160,18 @@ 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.debug("Ignoring resolved configuration: $rootPath - $configurationName") return } + val scope = dependencyScope(rootPath, configurationName) + val rootId = if (projectIdentityPath == null) "build $rootPath" else componentId(rootComponent) val rootOrigin = DependencyOrigin(rootId, rootPath) - val resolvedConfiguration = ResolvedConfiguration(rootOrigin, details.configurationName) + val resolvedConfiguration = ResolvedConfiguration(rootOrigin, configurationName, scope) for (dependencyComponent in getResolvedDependencies(rootComponent)) { val directDep = createComponentNode( @@ -183,6 +189,16 @@ abstract class DependencyExtractor : 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, parentOrigin: DependencyOrigin, 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 3a9f0606..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?) { - private val projectRegex = projectFilter?.toRegex() - private 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/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 9bfcc7f2..22612398 100644 --- a/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedConfiguration.kt +++ b/plugin/src/main/kotlin/org/gradle/dependencygraph/model/ResolvedConfiguration.kt @@ -3,6 +3,7 @@ package org.gradle.dependencygraph.model data class ResolvedConfiguration( 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/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/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt index ceffc06f..3fc3a444 100644 --- a/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt +++ b/plugin/src/main/kotlin/org/gradle/github/dependencygraph/GitHubRepositorySnapshotBuilder.kt @@ -3,6 +3,7 @@ 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( @@ -21,12 +22,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 +38,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 +76,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 +99,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 +109,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 +124,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/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/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", "") - } -}