Separate plugins for different Gradle APIs

This commit is contained in:
Tad Fisher
2024-06-04 13:11:18 -07:00
parent a935331795
commit 85cebdd557
40 changed files with 1258 additions and 3993 deletions

View File

@@ -0,0 +1,9 @@
plugins {
`gradle-kotlin-conventions`
}
dependencies {
compileOnly(libs.gradle.api.get69())
api(project(":model"))
implementation(libs.serialization.json)
}

View File

@@ -0,0 +1,138 @@
package org.nixos.gradle2nix
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.gradle.api.internal.artifacts.ivyservice.modulecache.FileStoreAndIndexProvider
import org.gradle.api.invocation.Gradle
import org.gradle.internal.hash.ChecksumService
import org.gradle.internal.operations.BuildOperationDescriptor
import org.gradle.internal.operations.BuildOperationListener
import org.gradle.internal.operations.OperationFinishEvent
import org.gradle.internal.operations.OperationIdentifier
import org.gradle.internal.operations.OperationProgressEvent
import org.gradle.internal.operations.OperationStartEvent
import org.gradle.internal.resource.ExternalResourceReadBuildOperationType
import org.gradle.internal.resource.ExternalResourceReadMetadataBuildOperationType
import org.nixos.gradle2nix.model.DependencyCoordinates
import org.nixos.gradle2nix.model.DependencySet
import org.nixos.gradle2nix.model.impl.DefaultDependencyCoordinates
import org.nixos.gradle2nix.model.impl.DefaultDependencySet
import org.nixos.gradle2nix.model.impl.DefaultResolvedArtifact
import org.nixos.gradle2nix.model.impl.DefaultResolvedDependency
import java.io.File
import java.util.concurrent.ConcurrentHashMap
interface DependencyExtractorApplier {
fun apply(
gradle: Gradle,
extractor: DependencyExtractor,
)
}
class DependencyExtractor : BuildOperationListener {
private val urls = ConcurrentHashMap<String, Unit>()
override fun started(
buildOperation: BuildOperationDescriptor,
startEvent: OperationStartEvent,
) {}
override fun progress(
operationIdentifier: OperationIdentifier,
progressEvent: OperationProgressEvent,
) {}
override fun finished(
buildOperation: BuildOperationDescriptor,
finishEvent: OperationFinishEvent,
) {
when (val details = buildOperation.details) {
is ExternalResourceReadBuildOperationType.Details -> urls.computeIfAbsent(details.location) { Unit }
is ExternalResourceReadMetadataBuildOperationType.Details -> urls.computeIfAbsent(details.location) { Unit }
else -> null
} ?: return
}
fun buildDependencySet(
cacheAccess: GradleCacheAccess,
checksumService: ChecksumService,
fileStoreAndIndexProvider: FileStoreAndIndexProvider,
): DependencySet {
val files = mutableMapOf<DependencyCoordinates, MutableMap<File, String>>()
val mappings = mutableMapOf<DependencyCoordinates, Map<String, String>>()
cacheAccess.useCache {
for ((url, _) in urls) {
fileStoreAndIndexProvider.externalResourceIndex.lookup(url)?.let { cached ->
cached.cachedFile?.let { file ->
cachedComponentId(file)?.let { componentId ->
files.getOrPut(componentId, ::mutableMapOf)[file] = url
if (file.extension == "module") {
parseFileMappings(file)?.let {
mappings[componentId] = it
}
}
}
}
}
}
}
return DefaultDependencySet(
dependencies =
buildList {
for ((componentId, componentFiles) in files) {
add(
DefaultResolvedDependency(
componentId,
buildList {
val remoteMappings = mappings[componentId]
for ((file, url) in componentFiles) {
add(
DefaultResolvedArtifact(
remoteMappings?.get(file.name) ?: file.name,
checksumService.sha256(file).toString(),
url,
),
)
}
},
),
)
}
},
)
}
}
private fun <T> buildList(block: MutableList<T>.() -> Unit): List<T> = mutableListOf<T>().apply(block).toList()
private fun cachedComponentId(file: File): DependencyCoordinates? {
val parts = file.invariantSeparatorsPath.split('/')
if (parts.size < 6) return null
if (parts[parts.size - 6] != "files-2.1") return null
return parts.dropLast(2).takeLast(3).joinToString(":").let(DefaultDependencyCoordinates::parse)
}
@OptIn(ExperimentalSerializationApi::class)
private fun parseFileMappings(file: File): Map<String, String>? =
try {
Json.decodeFromStream<JsonObject>(file.inputStream())
.jsonObject["variants"]?.jsonArray
?.flatMap { it.jsonObject["files"]?.jsonArray ?: emptyList() }
?.map { it.jsonObject }
?.mapNotNull {
val name = it["name"]?.jsonPrimitive?.content ?: return@mapNotNull null
val url = it["url"]?.jsonPrimitive?.content ?: return@mapNotNull null
if (name != url) name to url else null
}
?.toMap()
?.takeUnless { it.isEmpty() }
} catch (e: Throwable) {
null
}

View File

@@ -0,0 +1,27 @@
package org.nixos.gradle2nix
import org.gradle.api.Project
import org.gradle.api.internal.artifacts.ivyservice.modulecache.FileStoreAndIndexProvider
import org.gradle.internal.hash.ChecksumService
import org.gradle.tooling.provider.model.ToolingModelBuilder
import org.nixos.gradle2nix.model.DependencySet
class DependencySetModelBuilder(
private val dependencyExtractor: DependencyExtractor,
private val cacheAccess: GradleCacheAccess,
private val checksumService: ChecksumService,
private val fileStoreAndIndexProvider: FileStoreAndIndexProvider,
) : ToolingModelBuilder {
override fun canBuild(modelName: String): Boolean = modelName == DependencySet::class.qualifiedName
override fun buildAll(
modelName: String,
project: Project,
): DependencySet {
return dependencyExtractor.buildDependencySet(
cacheAccess,
checksumService,
fileStoreAndIndexProvider,
)
}
}

View File

@@ -0,0 +1,32 @@
package org.nixos.gradle2nix
import org.gradle.api.Plugin
import org.gradle.api.internal.artifacts.ivyservice.modulecache.FileStoreAndIndexProvider
import org.gradle.api.invocation.Gradle
import org.gradle.internal.hash.ChecksumService
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
abstract class AbstractGradle2NixPlugin(
private val cacheAccessFactory: GradleCacheAccessFactory,
private val dependencyExtractorApplier: DependencyExtractorApplier,
private val resolveAllArtifactsApplier: ResolveAllArtifactsApplier,
) : Plugin<Gradle> {
override fun apply(gradle: Gradle) {
val extractor = DependencyExtractor()
gradle.service<ToolingModelBuilderRegistry>().register(
DependencySetModelBuilder(
extractor,
cacheAccessFactory.create(gradle),
gradle.service<ChecksumService>(),
gradle.service<FileStoreAndIndexProvider>(),
),
)
dependencyExtractorApplier.apply(gradle, extractor)
gradle.projectsEvaluated {
resolveAllArtifactsApplier.apply(gradle)
}
}
}

View File

@@ -0,0 +1,11 @@
package org.nixos.gradle2nix
import org.gradle.api.invocation.Gradle
fun interface GradleCacheAccessFactory {
fun create(gradle: Gradle): GradleCacheAccess
}
interface GradleCacheAccess {
fun useCache(block: () -> Unit)
}

View File

@@ -0,0 +1,6 @@
package org.nixos.gradle2nix
import org.gradle.api.internal.GradleInternal
import org.gradle.api.invocation.Gradle
inline fun <reified T> Gradle.service(): T = (this as GradleInternal).services.get(T::class.java)

View File

@@ -0,0 +1,50 @@
package org.nixos.gradle2nix
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.file.FileCollection
import org.gradle.api.invocation.Gradle
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskProvider
import org.gradle.internal.deprecation.DeprecatableConfiguration
import org.nixos.gradle2nix.model.RESOLVE_ALL_TASK
fun interface ResolveAllArtifactsApplier {
fun apply(gradle: Gradle)
}
abstract class AbstractResolveAllArtifactsApplier : ResolveAllArtifactsApplier {
abstract fun Project.registerProjectTask(): TaskProvider<*>
final override fun apply(gradle: Gradle) {
val resolveAll = gradle.rootProject.tasks.register(RESOLVE_ALL_TASK)
// Depend on "dependencies" task in all projects
gradle.allprojects { project ->
val resolveProject = project.registerProjectTask()
resolveAll.configure { it.dependsOn(resolveProject) }
}
// Depend on all 'resolveBuildDependencies' task in each included build
gradle.includedBuilds.forEach { includedBuild ->
resolveAll.configure {
it.dependsOn(includedBuild.task(":$RESOLVE_ALL_TASK"))
}
}
}
}
abstract class ResolveProjectDependenciesTask : DefaultTask() {
@Internal
protected fun getReportableConfigurations(): List<Configuration> {
return project.configurations.filter { (it as? DeprecatableConfiguration)?.canSafelyBeResolved() ?: true }
}
protected fun Configuration.artifactFiles(): FileCollection {
return incoming.artifactView { viewConfiguration ->
viewConfiguration.componentFilter { it is ModuleComponentIdentifier }
}.files
}
}