Skip to content

Instantly share code, notes, and snippets.

@eygraber
Last active October 5, 2024 03:24
Show Gist options
  • Save eygraber/482e9942d5812e9efa5ace016aac4197 to your computer and use it in GitHub Desktop.
Save eygraber/482e9942d5812e9efa5ace016aac4197 to your computer and use it in GitHub Desktop.
Initial exploration of a configuration cache compatible way of collecting dependencies in Gradle (Heavily borrowed from https://github.com/google/play-services-plugins/blob/master/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy)
import org.gradle.internal.component.AmbiguousVariantSelectionException
tasks.register<GatherDependenciesTask>("gatherDependencies") {
output.set(
layout.buildDirectory.file(
"generated/gathered-dependencies/dependencies.txt"
)
)
ids.set(
project
.configurations
.filterNot { configuration ->
configuration.shouldSkip()
}
.flatMap { configuration ->
val visitedDependencyNames = mutableSetOf<String>()
configuration
.resolvedConfiguration
.lenientConfiguration
.firstLevelModuleDependencies
.getResolvedArtifacts(visitedDependencyNames)
.toModuleComponentIdentifierWrapper()
}
.toSet()
.sortedBy { "${it.group}:${it.name}:${it.version}" }
)
}
}
fun Set<ResolvedDependency>.getResolvedArtifacts(
visitedDependencyNames: MutableSet<String>
): Set<ResolvedArtifact> {
val resolvedArtifacts = mutableSetOf<ResolvedArtifact>()
for(resolvedDependency in this) {
val name = resolvedDependency.name
if(name !in visitedDependencyNames) {
visitedDependencyNames += name
try {
resolvedArtifacts += when(resolvedDependency.moduleVersion) {
"unspecified" ->
resolvedDependency.children.getResolvedArtifacts(
visitedDependencyNames = visitedDependencyNames
)
else -> resolvedDependency.allModuleArtifacts
}
}
catch(e: AmbiguousVariantSelectionException) {
}
}
}
return resolvedArtifacts
}
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.artifacts.result.UnresolvedArtifactResult
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier
import org.gradle.kotlin.dsl.listProperty
import org.gradle.maven.MavenModule
import org.gradle.maven.MavenPomArtifact
import org.w3c.dom.Element
import javax.inject.Inject
import javax.xml.parsers.DocumentBuilderFactory
/**
* Heavily borrowed from
* https://github.com/google/play-services-plugins/blob/master/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy
*/
@CacheableTask
abstract class GatherDependenciesTask : DefaultTask() {
@get:Inject abstract val dependencyHandler: DependencyHandler
@get:Nested
val ids = project.objects.listProperty<ModuleComponentIdentifierWrapper>()
@Input
fun getDirectDependencies() = project.configurations.collectDependencies(mutableSetOf(project))
@get:OutputFile
abstract val output: RegularFileProperty
@TaskAction
fun run() {
val data =
ids
.get()
.mapNotNull { (group, name, version) ->
val id = DefaultModuleComponentIdentifier(DefaultModuleIdentifier.newId(group, name), version)
val components =
dependencyHandler
.createArtifactResolutionQuery()
.forComponents(id)
.withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java)
.execute()
components
.resolvedComponents
.firstOrNull()
.let { resolvedComponent ->
if(resolvedComponent == null) {
logger.warn("No resolved components for $id")
}
resolvedComponent
}
?.getArtifacts(MavenPomArtifact::class.java)
?.firstOrNull()
.let { pomArtifact ->
when(pomArtifact) {
is ResolvedArtifactResult -> pomArtifact.file
is UnresolvedArtifactResult -> {
logger.warn("Failed to resolve $id", pomArtifact.failure)
null
}
else -> {
logger.warn("Couldn't find a MavenPomArtifact for $id")
null
}
}
}
?.let { pomFile ->
val factory = DocumentBuilderFactory.newInstance()
val doc = factory.newDocumentBuilder().parse(pomFile)
val pom = doc.getElementsByTagName("project").item(0)
// do something
}
}
}
}
private fun ConfigurationContainer.collectDependencies(
visitedProjects: MutableSet<Project>
): List<String> {
val directDependencies = mutableSetOf<String>()
val libraryProjects = mutableSetOf<Project>()
for(configuration in this) {
if(configuration.shouldSkip()) {
continue
}
for(dependency in configuration.allDependencies) {
if(dependency is ProjectDependency) {
libraryProjects.add(dependency.dependencyProject)
}
else if(dependency is ExternalModuleDependency) {
directDependencies.add(dependency.toMavenId())
}
}
}
for(libraryProject in libraryProjects) {
if(libraryProject in visitedProjects) {
continue
}
visitedProjects += libraryProject
directDependencies.addAll(
libraryProject.configurations.collectDependencies(
visitedProjects
)
)
}
return directDependencies.sorted()
}
private fun Dependency.toMavenId() =
"$group:$name:$version"
internal fun Configuration.shouldSkip() =
!isCanBeResolved ||
isTest ||
!isPackagedDependency
private val testCompile = setOf("testCompile", "androidTestCompile")
internal val Configuration.isTest
get() =
name.contains("test", ignoreCase = true) ||
name.contains("androidTest", ignoreCase = true) ||
hierarchy.any { configurationHierarchy ->
testCompile.any { configurationHierarchy.name.contains(it, ignoreCase = true) }
}
private val packagedDependencyPrefixes = setOf("compile", "implementation", "api")
internal val Configuration.isPackagedDependency
get() =
packagedDependencyPrefixes.any { name.contains(it, ignoreCase = true) } ||
hierarchy.any { configurationHierarchy ->
packagedDependencyPrefixes.any { configurationHierarchy.name.contains(it, ignoreCase = true) }
}
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.tasks.Input
data class ModuleComponentIdentifierWrapper(
@get:Input val group: String,
@get:Input val name: String,
@get:Input val version: String
)
internal fun Set<ResolvedArtifact>.toModuleComponentIdentifierWrapper() =
map { artifact ->
val group = artifact.moduleVersion.id.group.trim()
val name = artifact.name.trim()
val version = artifact.moduleVersion.id.version.trim()
ModuleComponentIdentifierWrapper(group, name, version)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment