mirror of
https://github.com/tadfisher/gradle2nix.git
synced 2026-01-11 23:40:37 -05:00
Rewrite plugin, use filenames in lockfile
This commit is contained in:
@@ -7,10 +7,11 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(kotlin("stdlib-jdk8"))
|
||||
compileOnly(kotlin("reflect"))
|
||||
shadow(kotlin("stdlib-jdk8"))
|
||||
shadow(kotlin("reflect"))
|
||||
implementation(project(":model"))
|
||||
implementation(libs.serialization.json)
|
||||
testImplementation(libs.kotest.assertions)
|
||||
testImplementation(libs.kotest.runner)
|
||||
}
|
||||
|
||||
java {
|
||||
@@ -47,8 +48,6 @@ tasks {
|
||||
shadowJar {
|
||||
archiveClassifier.set("")
|
||||
relocate("kotlin", "${project.group}.shadow.kotlin")
|
||||
relocate("kotlinx.serialization", "${project.group}.shadow.serialization")
|
||||
relocate("net.swiftzer.semver", "${project.group}.shadow.semver")
|
||||
relocate("org.intellij", "${project.group}.shadow.intellij")
|
||||
relocate("org.jetbrains", "${project.group}.shadow.jetbrains")
|
||||
}
|
||||
@@ -56,4 +55,11 @@ tasks {
|
||||
validatePlugins {
|
||||
enableStricterValidation.set(true)
|
||||
}
|
||||
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("passed", "skipped", "failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,48 @@
|
||||
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import javax.inject.Inject
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import org.nixos.gradle2nix.dependencygraph.AbstractDependencyExtractorPlugin
|
||||
import org.gradle.tooling.provider.model.ToolingModelBuilder
|
||||
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
|
||||
import org.nixos.gradle2nix.dependencygraph.DependencyExtractor
|
||||
import org.nixos.gradle2nix.forceresolve.ForceDependencyResolutionPlugin
|
||||
import org.nixos.gradle2nix.model.DependencySet
|
||||
import org.nixos.gradle2nix.util.buildOperationAncestryTracker
|
||||
import org.nixos.gradle2nix.util.buildOperationListenerManager
|
||||
import org.nixos.gradle2nix.util.service
|
||||
|
||||
@Suppress("unused")
|
||||
class Gradle2NixPlugin : Plugin<Gradle> {
|
||||
abstract class Gradle2NixPlugin @Inject constructor(
|
||||
private val toolingModelBuilderRegistry: ToolingModelBuilderRegistry
|
||||
): Plugin<Gradle> {
|
||||
override fun apply(gradle: Gradle) {
|
||||
// Only apply the dependency extractor to the root build
|
||||
if (gradle.parent == null) {
|
||||
gradle.pluginManager.apply(NixDependencyExtractorPlugin::class.java)
|
||||
val dependencyExtractor = DependencyExtractor(
|
||||
gradle.buildOperationAncestryTracker,
|
||||
)
|
||||
toolingModelBuilderRegistry.register(DependencySetModelBuilder(dependencyExtractor))
|
||||
|
||||
gradle.buildOperationListenerManager.addListener(dependencyExtractor)
|
||||
|
||||
// Configuration caching is not enabled with dependency verification so this is fine for now.
|
||||
// Gradle 9.x might remove this though.
|
||||
@Suppress("DEPRECATION")
|
||||
gradle.buildFinished {
|
||||
gradle.buildOperationListenerManager.removeListener(dependencyExtractor)
|
||||
}
|
||||
|
||||
gradle.pluginManager.apply(ForceDependencyResolutionPlugin::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
class NixDependencyExtractorPlugin : AbstractDependencyExtractorPlugin() {
|
||||
override fun getRendererClassName(): String =
|
||||
NixDependencyGraphRenderer::class.java.name
|
||||
internal class DependencySetModelBuilder(
|
||||
private val dependencyExtractor: DependencyExtractor,
|
||||
) : ToolingModelBuilder {
|
||||
|
||||
override fun canBuild(modelName: String): Boolean = modelName == DependencySet::class.qualifiedName
|
||||
|
||||
override fun buildAll(modelName: String, project: Project): DependencySet {
|
||||
return dependencyExtractor.buildDependencySet()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import java.io.File
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import org.nixos.gradle2nix.dependencygraph.DependencyGraphRenderer
|
||||
import org.nixos.gradle2nix.model.ResolvedConfiguration
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
prettyPrintIndent = " "
|
||||
}
|
||||
|
||||
class NixDependencyGraphRenderer : DependencyGraphRenderer {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
override fun outputDependencyGraph(
|
||||
resolvedConfigurations: List<ResolvedConfiguration>,
|
||||
outputDirectory: File
|
||||
) {
|
||||
val graphOutputFile = File(outputDirectory, "dependency-graph.json")
|
||||
graphOutputFile.outputStream().buffered().use { output ->
|
||||
json.encodeToStream(resolvedConfigurations, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.internal.GradleInternal
|
||||
import org.gradle.api.internal.project.DefaultProjectRegistry
|
||||
import org.gradle.api.internal.project.ProjectInternal
|
||||
import org.gradle.api.internal.project.ProjectRegistry
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import org.gradle.api.logging.Logging
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.services.internal.RegisteredBuildServiceProvider
|
||||
import org.gradle.internal.build.BuildProjectRegistry
|
||||
import org.gradle.internal.build.event.BuildEventListenerRegistryInternal
|
||||
import org.gradle.internal.composite.IncludedBuildInternal
|
||||
import org.gradle.internal.operations.BuildOperationAncestryTracker
|
||||
import org.gradle.internal.operations.BuildOperationListenerManager
|
||||
import org.gradle.util.GradleVersion
|
||||
import org.nixos.gradle2nix.dependencygraph.extractor.DependencyExtractor
|
||||
import org.nixos.gradle2nix.dependencygraph.extractor.DependencyExtractorBuildService
|
||||
import org.nixos.gradle2nix.dependencygraph.extractor.LegacyDependencyExtractor
|
||||
import org.nixos.gradle2nix.dependencygraph.util.buildDirCompat
|
||||
import org.nixos.gradle2nix.dependencygraph.util.service
|
||||
import org.nixos.gradle2nix.model.ConfigurationTarget
|
||||
|
||||
abstract class AbstractDependencyExtractorPlugin : Plugin<Gradle> {
|
||||
// Register extension functions on `Gradle` type
|
||||
private companion object : org.nixos.gradle2nix.dependencygraph.util.GradleExtensions()
|
||||
|
||||
/**
|
||||
* The name of an accessible class that implements `org.gradle.dependencygraph.DependencyGraphRenderer`.
|
||||
*/
|
||||
abstract fun getRendererClassName(): String
|
||||
|
||||
internal lateinit var dependencyExtractorProvider: Provider<out DependencyExtractor>
|
||||
|
||||
override fun apply(gradle: Gradle) {
|
||||
val gradleVersion = GradleVersion.current()
|
||||
// Create the adapter based upon the version of Gradle
|
||||
val applicatorStrategy = when {
|
||||
gradleVersion < GradleVersion.version("8.0") -> PluginApplicatorStrategy.LegacyPluginApplicatorStrategy
|
||||
else -> PluginApplicatorStrategy.DefaultPluginApplicatorStrategy
|
||||
}
|
||||
|
||||
// Create the service
|
||||
dependencyExtractorProvider = applicatorStrategy.createExtractorService(gradle, getRendererClassName())
|
||||
|
||||
gradle.rootProject { project ->
|
||||
dependencyExtractorProvider
|
||||
.get()
|
||||
.rootProjectBuildDirectory = project.buildDirCompat
|
||||
}
|
||||
|
||||
val logger = Logging.getLogger(AbstractDependencyExtractorPlugin::class.java.name)
|
||||
|
||||
gradle.projectsLoaded {
|
||||
(gradle as GradleInternal).let { g ->
|
||||
logger.lifecycle("all projects: ${g.owner.projects.allProjects}")
|
||||
logger.lifecycle("included projects: ${g.includedBuilds().flatMap { it.target.projects.allProjects }.joinToString { it.identityPath.path }}")
|
||||
}
|
||||
}
|
||||
|
||||
// Register the service to listen for Build Events
|
||||
applicatorStrategy.registerExtractorListener(gradle, dependencyExtractorProvider)
|
||||
|
||||
// Register the shutdown hook that should execute at the completion of the Gradle build.
|
||||
applicatorStrategy.registerExtractorServiceShutdown(gradle, dependencyExtractorProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapters for creating the [DependencyExtractor] and installing it into [Gradle] based upon the Gradle version.
|
||||
*/
|
||||
private interface PluginApplicatorStrategy {
|
||||
|
||||
fun createExtractorService(
|
||||
gradle: Gradle,
|
||||
rendererClassName: String
|
||||
): Provider<out DependencyExtractor>
|
||||
|
||||
fun registerExtractorListener(
|
||||
gradle: Gradle,
|
||||
extractorServiceProvider: Provider<out DependencyExtractor>
|
||||
)
|
||||
|
||||
fun registerExtractorServiceShutdown(
|
||||
gradle: Gradle,
|
||||
extractorServiceProvider: Provider<out DependencyExtractor>
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object LegacyPluginApplicatorStrategy : PluginApplicatorStrategy {
|
||||
|
||||
override fun createExtractorService(
|
||||
gradle: Gradle,
|
||||
rendererClassName: String
|
||||
): Provider<out DependencyExtractor> {
|
||||
val dependencyExtractor = LegacyDependencyExtractor(rendererClassName)
|
||||
return gradle.providerFactory.provider { dependencyExtractor }
|
||||
}
|
||||
|
||||
override fun registerExtractorListener(
|
||||
gradle: Gradle,
|
||||
extractorServiceProvider: Provider<out DependencyExtractor>
|
||||
) {
|
||||
gradle.buildOperationListenerManager
|
||||
.addListener(extractorServiceProvider.get())
|
||||
}
|
||||
|
||||
override fun registerExtractorServiceShutdown(
|
||||
gradle: Gradle,
|
||||
extractorServiceProvider: Provider<out DependencyExtractor>
|
||||
) {
|
||||
gradle.buildFinished {
|
||||
extractorServiceProvider.get().close()
|
||||
gradle.buildOperationListenerManager
|
||||
.removeListener(extractorServiceProvider.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DefaultPluginApplicatorStrategy : PluginApplicatorStrategy {
|
||||
private const val SERVICE_NAME = "dependencyExtractorService"
|
||||
|
||||
override fun createExtractorService(
|
||||
gradle: Gradle,
|
||||
rendererClassName: String
|
||||
): Provider<out DependencyExtractor> {
|
||||
return gradle.sharedServices.registerIfAbsent(
|
||||
SERVICE_NAME,
|
||||
DependencyExtractorBuildService::class.java
|
||||
) { it.parameters.rendererClassName.set(rendererClassName) }
|
||||
}
|
||||
|
||||
override fun registerExtractorListener(
|
||||
gradle: Gradle,
|
||||
extractorServiceProvider: Provider<out DependencyExtractor>
|
||||
) {
|
||||
gradle.service<BuildEventListenerRegistryInternal>()
|
||||
.onOperationCompletion(extractorServiceProvider)
|
||||
}
|
||||
|
||||
override fun registerExtractorServiceShutdown(
|
||||
gradle: Gradle,
|
||||
extractorServiceProvider: Provider<out DependencyExtractor>
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package org.nixos.gradle2nix.dependencygraph
|
||||
|
||||
import java.net.URI
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import org.gradle.api.internal.artifacts.DownloadArtifactBuildOperationType
|
||||
import org.gradle.api.internal.artifacts.configurations.ResolveConfigurationDependenciesBuildOperationType
|
||||
import org.gradle.api.logging.Logging
|
||||
import org.gradle.internal.operations.BuildOperationAncestryTracker
|
||||
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.ExternalResourceReadMetadataBuildOperationType
|
||||
import org.nixos.gradle2nix.model.DependencyCoordinates
|
||||
import org.nixos.gradle2nix.model.DependencySet
|
||||
import org.nixos.gradle2nix.model.Repository
|
||||
import org.nixos.gradle2nix.model.impl.DefaultDependencyCoordinates
|
||||
import org.nixos.gradle2nix.model.impl.DefaultDependencySet
|
||||
import org.nixos.gradle2nix.model.impl.DefaultRepository
|
||||
import org.nixos.gradle2nix.model.impl.DefaultResolvedArtifact
|
||||
import org.nixos.gradle2nix.model.impl.DefaultResolvedDependency
|
||||
|
||||
class DependencyExtractor(
|
||||
private val ancestryTracker: BuildOperationAncestryTracker,
|
||||
) : BuildOperationListener {
|
||||
|
||||
// Repositories by ID
|
||||
private val repositories: MutableMap<String, DefaultRepository> = ConcurrentHashMap()
|
||||
|
||||
private val thrownExceptions = Collections.synchronizedList(mutableListOf<Throwable>())
|
||||
|
||||
private val artifacts: MutableMap<
|
||||
OperationIdentifier,
|
||||
DownloadArtifactBuildOperationType.Details
|
||||
> = ConcurrentHashMap()
|
||||
|
||||
private val files: MutableMap<
|
||||
OperationIdentifier,
|
||||
ExternalResourceReadMetadataBuildOperationType.Details
|
||||
> = ConcurrentHashMap()
|
||||
|
||||
private val fileArtifacts: MutableMap<OperationIdentifier, OperationIdentifier> = ConcurrentHashMap()
|
||||
|
||||
fun buildDependencySet(): DependencySet {
|
||||
println("DependencyExtractor: buildDependencySet (wtf)")
|
||||
|
||||
val repoList = repositories.values.toList()
|
||||
|
||||
val dependencies = buildMap<DependencyCoordinates, MutableMap<String, MutableSet<Pair<String, MutableSet<String>>>>> {
|
||||
for ((fileId, file) in files) {
|
||||
val filename = file.location.substringAfterLast("/").substringBefore('#').substringBefore('?')
|
||||
if (filename == "maven-metadata.xml") {
|
||||
// Skip Maven metadata, we don't need it for the local repository
|
||||
continue
|
||||
}
|
||||
|
||||
val artifactOperationId = fileArtifacts[fileId]
|
||||
val artifact = artifactOperationId?.let { artifacts[it] }
|
||||
val artifactIdentifier = artifact?.artifactIdentifier?.let(::parseArtifactIdentifier)
|
||||
var coords = artifactIdentifier?.first
|
||||
var name = artifactIdentifier?.second
|
||||
|
||||
if (coords == null || name == null) {
|
||||
val parsed = parseComponent(repoList, file.location)
|
||||
if (parsed == null) {
|
||||
LOGGER.info("Couldn't parse location for ${artifactIdentifier?.first?.toString() ?: name}: ${file.location}")
|
||||
continue
|
||||
}
|
||||
coords = coords ?: parsed.first
|
||||
name = name ?: parseArtifact(parsed.second, coords, file.location)
|
||||
}
|
||||
|
||||
getOrPut(coords) { mutableMapOf() }
|
||||
.getOrPut(name) { mutableSetOf() }
|
||||
.run {
|
||||
val existing = find { it.first == filename }
|
||||
if (existing != null) {
|
||||
existing.second.add(file.location)
|
||||
} else {
|
||||
add(filename to mutableSetOf(file.location))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultDependencySet(
|
||||
dependencies = dependencies.map { (coords, artifacts) ->
|
||||
DefaultResolvedDependency(
|
||||
coords,
|
||||
artifacts.flatMap { (name, files) ->
|
||||
files.map { (filename, urls) ->
|
||||
DefaultResolvedArtifact(name, filename, urls.toList())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) {
|
||||
val id = buildOperation.id ?: return
|
||||
|
||||
when (val details = buildOperation.details) {
|
||||
is ResolveConfigurationDependenciesBuildOperationType.Details -> {
|
||||
for (repository in details.repositories.orEmpty()) {
|
||||
addRepository(repository)
|
||||
}
|
||||
}
|
||||
|
||||
is DownloadArtifactBuildOperationType.Details -> {
|
||||
artifacts[id] = details
|
||||
}
|
||||
|
||||
is ExternalResourceReadMetadataBuildOperationType.Details -> {
|
||||
files[id] = details
|
||||
|
||||
ancestryTracker.findClosestMatchingAncestor(id) { it in artifacts }.getOrNull()?.let {
|
||||
fileArtifacts[id] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) {}
|
||||
|
||||
override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) {}
|
||||
|
||||
private fun addRepository(
|
||||
repository: ResolveConfigurationDependenciesBuildOperationType.Repository
|
||||
): DefaultRepository {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val candidate = DefaultRepository(
|
||||
id = repository.id,
|
||||
type = enumValueOf(repository.type),
|
||||
metadataSources = (repository.properties["METADATA_SOURCES"] as? List<String>) ?: emptyList(),
|
||||
metadataResources = metadataResources(repository),
|
||||
artifactResources = artifactResources(repository),
|
||||
)
|
||||
|
||||
// Repository IDs are not unique across the entire build, unfortunately.
|
||||
val existing = repositories.values.find {
|
||||
it.type == candidate.type &&
|
||||
it.metadataSources == candidate.metadataSources &&
|
||||
it.metadataResources == candidate.metadataResources &&
|
||||
it.artifactResources == candidate.artifactResources
|
||||
}
|
||||
|
||||
if (existing != null) return existing
|
||||
var inc = 0
|
||||
fun incId() = if (inc > 0) "${candidate.id}[$inc]" else candidate.id
|
||||
while (incId() in repositories) inc++
|
||||
|
||||
val added = if (inc > 0) candidate else candidate.copy(id = incId())
|
||||
repositories[added.id] = added
|
||||
return added
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOGGER = Logging.getLogger(DependencyExtractor::class.java)
|
||||
|
||||
internal const val M2_PATTERN =
|
||||
"[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"
|
||||
|
||||
private const val IVY_ARTIFACT_PATTERN = "[organisation]/[module]/[revision]/[type]s/[artifact](.[ext])";
|
||||
|
||||
private fun resources(urls: List<URI>, patterns: List<String>): List<String> {
|
||||
if (urls.isEmpty()) {
|
||||
return patterns
|
||||
}
|
||||
if (patterns.isEmpty()) {
|
||||
return urls.map { it.toString() }
|
||||
}
|
||||
return mutableListOf<String>().apply {
|
||||
for (pattern in patterns) {
|
||||
for (url in urls) {
|
||||
add(
|
||||
url.toString()
|
||||
.removeSuffix("/")
|
||||
.plus("/")
|
||||
.plus(pattern.removePrefix("/"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun metadataResources(
|
||||
repository: ResolveConfigurationDependenciesBuildOperationType.Repository
|
||||
): List<String> {
|
||||
return when (repository.type) {
|
||||
Repository.Type.MAVEN.name -> {
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI),
|
||||
listOf(M2_PATTERN)
|
||||
)
|
||||
}
|
||||
Repository.Type.IVY.name -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val patterns = repository.properties["IVY_PATTERNS"] as? List<String>
|
||||
?: listOf(IVY_ARTIFACT_PATTERN)
|
||||
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI),
|
||||
patterns
|
||||
)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun artifactResources(
|
||||
repository: ResolveConfigurationDependenciesBuildOperationType.Repository
|
||||
): List<String> {
|
||||
return when (repository.type) {
|
||||
Repository.Type.MAVEN.name -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI)
|
||||
.plus(repository.properties["ARTIFACT_URLS"] as? List<URI> ?: emptyList()),
|
||||
listOf(M2_PATTERN)
|
||||
))
|
||||
}
|
||||
Repository.Type.IVY.name -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val patterns = repository.properties["ARTIFACT_PATTERNS"] as? List<String>
|
||||
?: listOf(IVY_ARTIFACT_PATTERN)
|
||||
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI),
|
||||
patterns
|
||||
)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private val artifactRegex = Regex("(?<name>\\S+) \\((?<coordinates>\\S+)\\)")
|
||||
|
||||
private fun parseArtifactIdentifier(input: String): Pair<DependencyCoordinates, String>? {
|
||||
val groups = artifactRegex.matchEntire(input)?.groups ?: return null.also {
|
||||
LOGGER.warn("artifact regex didn't match $input")
|
||||
}
|
||||
val coords = groups["coordinates"]?.value?.let(DefaultDependencyCoordinates::parse) ?: return null
|
||||
val name = groups["name"]?.value ?: return null
|
||||
return coords to name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph
|
||||
|
||||
import java.io.File
|
||||
import org.nixos.gradle2nix.model.ResolvedConfiguration
|
||||
|
||||
interface DependencyGraphRenderer {
|
||||
fun outputDependencyGraph(
|
||||
resolvedConfigurations: List<ResolvedConfiguration>,
|
||||
outputDirectory: File
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.nixos.gradle2nix.dependencygraph
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.nixos.gradle2nix.model.DependencyCoordinates
|
||||
import org.nixos.gradle2nix.model.Repository
|
||||
import org.nixos.gradle2nix.model.impl.DefaultDependencyCoordinates
|
||||
|
||||
|
||||
private val partRegex = Regex("\\[(?<attr>[^]]+)]|\\((?<optional>([^)]+))\\)")
|
||||
|
||||
private fun StringBuilder.appendPattern(
|
||||
input: String,
|
||||
seen: MutableList<String>,
|
||||
) {
|
||||
var literalStart = 0
|
||||
partRegex.findAll(input).forEach { match ->
|
||||
val literal = input.substring(literalStart, match.range.first)
|
||||
if (literal.isNotEmpty()) {
|
||||
append(Regex.escape(literal))
|
||||
}
|
||||
literalStart = match.range.last + 1
|
||||
|
||||
val optionalValue = match.groups["optional"]?.value
|
||||
val attrValue = match.groups["attr"]?.value
|
||||
if (optionalValue != null) {
|
||||
append("(")
|
||||
appendPattern(optionalValue, seen)
|
||||
append(")?")
|
||||
} else if (attrValue != null) {
|
||||
if (attrValue !in seen) {
|
||||
seen.add(attrValue)
|
||||
append("(?<$attrValue>[^/]+)")
|
||||
} else {
|
||||
append("\\k<$attrValue>")
|
||||
}
|
||||
}
|
||||
}
|
||||
val tail = input.substring(literalStart)
|
||||
if (tail.isNotEmpty()) {
|
||||
append(Regex.escape(input.substring(literalStart)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.replaceAttrs(
|
||||
attrs: Map<String, String>
|
||||
): String {
|
||||
return partRegex.replace(this) { match ->
|
||||
val optionalValue = match.groups["optional"]?.value
|
||||
val attrValue = match.groups["attr"]?.value
|
||||
if (optionalValue != null) {
|
||||
val replaced = optionalValue.replaceAttrs(attrs)
|
||||
if (replaced != optionalValue) replaced else match.value
|
||||
} else if (attrValue != null) {
|
||||
attrs[attrValue] ?: match.value
|
||||
} else {
|
||||
match.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun interface ArtifactMatcher {
|
||||
fun match(url: String): Map<String, String>?
|
||||
}
|
||||
|
||||
private fun regexMatcher(regex: Regex, attrs: List<String>): ArtifactMatcher {
|
||||
return ArtifactMatcher { url ->
|
||||
regex.matchEntire(url)?.groups?.let { groups ->
|
||||
buildMap {
|
||||
for (attr in attrs) {
|
||||
groups[attr]?.let { put(attr, it.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun patternMatcher(pattern: String): ArtifactMatcher {
|
||||
val attrs = mutableListOf<String>()
|
||||
val exp = buildString { appendPattern(pattern, attrs) }.toRegex()
|
||||
return regexMatcher(exp, attrs)
|
||||
}
|
||||
|
||||
private fun mavenMatcher(pattern: String): ArtifactMatcher {
|
||||
val attrs = mutableListOf<String>()
|
||||
val exp = buildString { appendPattern(pattern.replaceAfterLast("/", ""), attrs) }
|
||||
.replace("<organisation>[^/]+", "<organisation>.+")
|
||||
.plus("[^/]+")
|
||||
.toRegex()
|
||||
return regexMatcher(exp, attrs)
|
||||
}
|
||||
|
||||
private val matcherCache: MutableMap<String, ArtifactMatcher> = ConcurrentHashMap()
|
||||
|
||||
private fun matcher(
|
||||
pattern: String,
|
||||
): ArtifactMatcher = matcherCache.getOrPut(pattern) {
|
||||
if (pattern.endsWith(DependencyExtractor.M2_PATTERN)) mavenMatcher(pattern) else patternMatcher(pattern)
|
||||
}
|
||||
|
||||
fun parseComponent(
|
||||
repositories: List<Repository>,
|
||||
url: String,
|
||||
): Pair<DependencyCoordinates, String>? {
|
||||
for (repository in repositories) {
|
||||
for (pattern in (repository.metadataResources + repository.artifactResources).distinct()) {
|
||||
val matcher = matcher(pattern)
|
||||
val attrs = matcher.match(url)
|
||||
if (attrs != null) {
|
||||
val group = attrs["organisation"]?.replace('/', '.') ?: continue
|
||||
val artifact = attrs["module"] ?: continue
|
||||
val revision = attrs["revision"] ?: continue
|
||||
return DefaultDependencyCoordinates(group, artifact, revision) to pattern.replaceAttrs(attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun parseArtifact(
|
||||
resource: String,
|
||||
component: DependencyCoordinates,
|
||||
url: String
|
||||
): String {
|
||||
val attrs = mutableListOf<String>()
|
||||
var pattern = buildString { appendPattern(resource, attrs) }
|
||||
if (component.version.endsWith("-SNAPSHOT")) {
|
||||
val base = component.version.substringBeforeLast("-SNAPSHOT", "")
|
||||
pattern = pattern.replace("\\Q-${component.version}\\E", "\\Q-$base-\\E(?:.+)")
|
||||
}
|
||||
|
||||
val values = regexMatcher(pattern.toRegex(), attrs).match(url)
|
||||
val artifact = values?.get("artifact")
|
||||
val classifier = values?.get("classifier")
|
||||
val ext = values?.get("ext")
|
||||
|
||||
if (artifact == null) return artifactFromFilename(
|
||||
url.substringAfterLast('/').substringBefore('#').substringBefore('?'),
|
||||
component.version,
|
||||
classifier
|
||||
)
|
||||
|
||||
return buildString {
|
||||
append("$artifact-${component.version}")
|
||||
if (classifier != null) append("-$classifier")
|
||||
if (ext != null) append(".$ext")
|
||||
}
|
||||
}
|
||||
|
||||
private fun artifactFromFilename(filename: String, version: String, classifier: String?): String {
|
||||
val name = filename.substringBeforeLast('.')
|
||||
val extension = filename.substringAfterLast('.', "")
|
||||
return buildString {
|
||||
append("$name-$version")
|
||||
if (classifier != null) append("-$classifier")
|
||||
if (extension.isNotEmpty()) append(".$extension")
|
||||
}
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.extractor
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.artifacts.DependencyResolutionListener
|
||||
import org.gradle.api.artifacts.ResolvableDependencies
|
||||
import org.gradle.api.artifacts.component.BuildIdentifier
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
|
||||
import org.gradle.api.artifacts.query.ArtifactResolutionQuery
|
||||
import org.gradle.api.artifacts.result.ResolvedArtifactResult
|
||||
import org.gradle.api.artifacts.result.ResolvedComponentResult
|
||||
import org.gradle.api.artifacts.result.ResolvedDependencyResult
|
||||
import org.gradle.api.component.Artifact
|
||||
import org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier
|
||||
import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier
|
||||
import org.gradle.api.internal.artifacts.configurations.ResolveConfigurationDependenciesBuildOperationType
|
||||
import org.gradle.api.internal.artifacts.repositories.resolver.MavenUniqueSnapshotComponentIdentifier
|
||||
import org.gradle.api.logging.Logging
|
||||
import org.gradle.configuration.ApplyScriptPluginBuildOperationType
|
||||
import org.gradle.configuration.ConfigurationTargetIdentifier
|
||||
import org.gradle.initialization.LoadBuildBuildOperationType
|
||||
import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier
|
||||
import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier
|
||||
import org.gradle.internal.exceptions.DefaultMultiCauseException
|
||||
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.ivy.IvyDescriptorArtifact
|
||||
import org.gradle.jvm.JvmLibrary
|
||||
import org.gradle.language.base.artifact.SourcesArtifact
|
||||
import org.gradle.language.java.artifact.JavadocArtifact
|
||||
import org.gradle.maven.MavenPomArtifact
|
||||
import org.gradle.util.GradleVersion
|
||||
import org.nixos.gradle2nix.dependencygraph.DependencyGraphRenderer
|
||||
import org.nixos.gradle2nix.dependencygraph.util.BuildOperationTracker
|
||||
import org.nixos.gradle2nix.dependencygraph.util.loadOptionalParam
|
||||
import org.nixos.gradle2nix.model.ConfigurationTarget
|
||||
import org.nixos.gradle2nix.model.DependencyCoordinates
|
||||
import org.nixos.gradle2nix.model.DependencySource
|
||||
import org.nixos.gradle2nix.model.PARAM_INCLUDE_CONFIGURATIONS
|
||||
import org.nixos.gradle2nix.model.PARAM_INCLUDE_PROJECTS
|
||||
import org.nixos.gradle2nix.model.PARAM_REPORT_DIR
|
||||
import org.nixos.gradle2nix.model.Repository
|
||||
import org.nixos.gradle2nix.model.ResolvedArtifact
|
||||
import org.nixos.gradle2nix.model.ResolvedConfiguration
|
||||
import org.nixos.gradle2nix.model.ResolvedDependency
|
||||
|
||||
abstract class DependencyExtractor :
|
||||
BuildOperationListener,
|
||||
AutoCloseable {
|
||||
|
||||
private val configurations =
|
||||
ConcurrentHashMap<
|
||||
OperationIdentifier,
|
||||
Pair<ResolveConfigurationDependenciesBuildOperationType.Details,
|
||||
ResolveConfigurationDependenciesBuildOperationType.Result>>()
|
||||
|
||||
private val resolvedConfigurations = Collections.synchronizedList(mutableListOf<ResolvedConfiguration>())
|
||||
|
||||
private val thrownExceptions = Collections.synchronizedList(mutableListOf<Throwable>())
|
||||
|
||||
var rootProjectBuildDirectory: File? = null
|
||||
|
||||
private val operationTracker = BuildOperationTracker()
|
||||
|
||||
// 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(
|
||||
loadOptionalParam(PARAM_INCLUDE_PROJECTS),
|
||||
loadOptionalParam(PARAM_INCLUDE_CONFIGURATIONS)
|
||||
)
|
||||
}
|
||||
|
||||
private val dependencyGraphReportDir by lazy {
|
||||
loadOptionalParam(PARAM_REPORT_DIR)
|
||||
}
|
||||
|
||||
abstract fun getRendererClassName(): String
|
||||
|
||||
override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) {}
|
||||
|
||||
override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) {}
|
||||
|
||||
override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) {
|
||||
operationTracker.finished(buildOperation, finishEvent)
|
||||
|
||||
handleFinishBuildOperationType<
|
||||
ResolveConfigurationDependenciesBuildOperationType.Details,
|
||||
ResolveConfigurationDependenciesBuildOperationType.Result
|
||||
>(buildOperation, finishEvent) { details, result ->
|
||||
buildOperation.id?.let { operationId ->
|
||||
configurations[operationId] = details to result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified D, reified R> handleFinishBuildOperationType(
|
||||
buildOperation: BuildOperationDescriptor,
|
||||
finishEvent: OperationFinishEvent,
|
||||
handler: (details: D, result: R) -> Unit
|
||||
) {
|
||||
try {
|
||||
handleFinishBuildOperationTypeRaw<D, R>(buildOperation, finishEvent, handler)
|
||||
} catch (e: Throwable) {
|
||||
thrownExceptions.add(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified D, reified R> handleFinishBuildOperationTypeRaw(
|
||||
buildOperation: BuildOperationDescriptor,
|
||||
finishEvent: OperationFinishEvent,
|
||||
handler: (details: D, result: R) -> Unit
|
||||
) {
|
||||
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 && result == null) {
|
||||
return
|
||||
} else if (details == null || result == null) {
|
||||
throw IllegalStateException("buildOperation.details & finishedEvent.result were unexpected types")
|
||||
}
|
||||
handler(details, result)
|
||||
}
|
||||
|
||||
// This returns null for the root build, because the build operation won't complete until after close() is called.
|
||||
private fun findBuildDetails(buildOperationId: OperationIdentifier?): LoadBuildBuildOperationType.Details? {
|
||||
return operationTracker.findParent(buildOperationId) {
|
||||
it.details as? LoadBuildBuildOperationType.Details
|
||||
}
|
||||
}
|
||||
|
||||
private fun processConfigurations() {
|
||||
for ((operationId, data) in configurations) {
|
||||
val (details, result) = data
|
||||
extractConfigurationDependencies(operationId, details, result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractConfigurationDependencies(
|
||||
operationId: OperationIdentifier,
|
||||
details: ResolveConfigurationDependenciesBuildOperationType.Details,
|
||||
result: ResolveConfigurationDependenciesBuildOperationType.Result
|
||||
) {
|
||||
val repositories = details.repositories?.mapNotNull {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(Repository(
|
||||
id = it.id,
|
||||
type = enumValueOf(it.type),
|
||||
name = it.name,
|
||||
m2Compatible = it.type == "MAVEN" || (it.properties["M2_COMPATIBLE"] as? Boolean) ?: false,
|
||||
metadataSources = (it.properties["METADATA_SOURCES"] as? List<String>) ?: emptyList(),
|
||||
metadataResources = metadataResources(it),
|
||||
artifactResources = artifactResources(it),
|
||||
))
|
||||
} ?: emptyList()
|
||||
|
||||
if (repositories.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val rootComponent = result.rootComponent
|
||||
|
||||
if (rootComponent.dependencies.isEmpty()) {
|
||||
// No dependencies to extract: can safely ignore
|
||||
return
|
||||
}
|
||||
|
||||
val source: DependencySource = when {
|
||||
details.isScriptConfiguration -> {
|
||||
val parent = operationTracker.findParent(operationId) {
|
||||
it.details as? ApplyScriptPluginBuildOperationType.Details
|
||||
} ?: throw IllegalStateException("Couldn't find parent script operation for ${details.configurationName}")
|
||||
DependencySource(
|
||||
targetType = when (parent.targetType) {
|
||||
ConfigurationTargetIdentifier.Type.GRADLE.label -> ConfigurationTarget.GRADLE
|
||||
ConfigurationTargetIdentifier.Type.SETTINGS.label -> ConfigurationTarget.SETTINGS
|
||||
ConfigurationTargetIdentifier.Type.PROJECT.label -> ConfigurationTarget.BUILDSCRIPT
|
||||
else -> throw IllegalStateException("Unknown configuration target type: ${parent.targetType}")
|
||||
},
|
||||
targetPath = parent.targetPath ?: ":",
|
||||
buildPath = parent.buildPath!!
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
DependencySource(
|
||||
targetType = ConfigurationTarget.PROJECT,
|
||||
targetPath = details.projectPath!!,
|
||||
buildPath = details.buildPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val resolvedConfiguration = ResolvedConfiguration(source, details.configurationName, repositories)
|
||||
|
||||
for (directDependency in getResolvedDependencies(rootComponent)) {
|
||||
val coordinates = (directDependency.id as? ModuleComponentIdentifier)?.let(::coordinates)
|
||||
if (coordinates != null) {
|
||||
val directDep = createComponentNode(
|
||||
coordinates,
|
||||
source,
|
||||
true,
|
||||
directDependency,
|
||||
result.getRepositoryId(directDependency)
|
||||
)
|
||||
resolvedConfiguration.addDependency(directDep)
|
||||
|
||||
walkComponentDependencies(result, directDependency, directDep.source, resolvedConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
resolvedConfigurations.add(resolvedConfiguration)
|
||||
}
|
||||
|
||||
private fun walkComponentDependencies(
|
||||
result: ResolveConfigurationDependenciesBuildOperationType.Result,
|
||||
component: ResolvedComponentResult,
|
||||
parentSource: DependencySource,
|
||||
resolvedConfiguration: ResolvedConfiguration
|
||||
) {
|
||||
val componentSource = getSource(component, parentSource)
|
||||
val direct = componentSource != parentSource
|
||||
|
||||
val dependencyComponents = getResolvedDependencies(component)
|
||||
for (dependencyComponent in dependencyComponents) {
|
||||
val coordinates = (dependencyComponent.id as? ModuleComponentIdentifier)?.let(::coordinates)
|
||||
?: continue
|
||||
if (!resolvedConfiguration.hasDependency(coordinates)) {
|
||||
val dependencyNode = createComponentNode(
|
||||
coordinates,
|
||||
componentSource,
|
||||
direct,
|
||||
dependencyComponent,
|
||||
result.getRepositoryId(component)
|
||||
)
|
||||
resolvedConfiguration.addDependency(dependencyNode)
|
||||
walkComponentDependencies(result, dependencyComponent, componentSource, resolvedConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSource(component: ResolvedComponentResult, source: DependencySource): DependencySource {
|
||||
val componentId = component.id
|
||||
if (componentId is DefaultProjectComponentIdentifier) {
|
||||
return DependencySource(
|
||||
ConfigurationTarget.PROJECT,
|
||||
componentId.projectPath,
|
||||
componentId.build.buildPathCompat
|
||||
)
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
private val BuildIdentifier.buildPathCompat: String
|
||||
@Suppress("DEPRECATION")
|
||||
get() = if (GradleVersion.current() < GradleVersion.version("8.2")) name else buildPath
|
||||
|
||||
private fun getResolvedDependencies(component: ResolvedComponentResult): List<ResolvedComponentResult> {
|
||||
return component.dependencies.filterIsInstance<ResolvedDependencyResult>().map { it.selected }.filter { it != component }
|
||||
}
|
||||
|
||||
private fun createComponentNode(
|
||||
coordinates: DependencyCoordinates,
|
||||
source: DependencySource,
|
||||
direct: Boolean,
|
||||
component: ResolvedComponentResult,
|
||||
repositoryId: String?
|
||||
): ResolvedDependency {
|
||||
val componentDependencies =
|
||||
component.dependencies.filterIsInstance<ResolvedDependencyResult>().map { componentId(it.selected) }
|
||||
return ResolvedDependency(
|
||||
coordinates,
|
||||
source,
|
||||
direct,
|
||||
repositoryId,
|
||||
componentDependencies,
|
||||
)
|
||||
}
|
||||
|
||||
private fun componentId(component: ResolvedComponentResult): String {
|
||||
return component.id.displayName
|
||||
}
|
||||
|
||||
private fun coordinates(componentId: ModuleComponentIdentifier): DependencyCoordinates {
|
||||
return DependencyCoordinates(
|
||||
componentId.group,
|
||||
componentId.module,
|
||||
componentId.version,
|
||||
(componentId as? MavenUniqueSnapshotComponentIdentifier)?.timestamp
|
||||
)
|
||||
}
|
||||
|
||||
private fun writeDependencyGraph() {
|
||||
val outputDirectory = getOutputDir()
|
||||
outputDirectory.mkdirs()
|
||||
createRenderer().outputDependencyGraph(resolvedConfigurations, outputDirectory)
|
||||
LOGGER.info("Wrote dependency graph to ${getOutputDir()}")
|
||||
}
|
||||
|
||||
private fun createRenderer(): DependencyGraphRenderer {
|
||||
LOGGER.info("Constructing renderer: ${getRendererClassName()}")
|
||||
return Class.forName(getRendererClassName()).getDeclaredConstructor().newInstance() as DependencyGraphRenderer
|
||||
}
|
||||
|
||||
private fun getOutputDir(): File {
|
||||
if (dependencyGraphReportDir != null) {
|
||||
return File(dependencyGraphReportDir!!)
|
||||
}
|
||||
|
||||
if (rootProjectBuildDirectory == null) {
|
||||
throw RuntimeException("Cannot determine report file location")
|
||||
}
|
||||
return File(
|
||||
rootProjectBuildDirectory,
|
||||
"reports/nix-dependency-graph"
|
||||
)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
LOGGER.lifecycle("DependencyExtractor: CLOSE")
|
||||
|
||||
if (thrownExceptions.isNotEmpty()) {
|
||||
throw DefaultMultiCauseException(
|
||||
"The Gradle2Nix plugin encountered errors while extracting dependencies. " +
|
||||
"Please report this issue at: https://github.com/tadfisher/gradle2nix/issues",
|
||||
thrownExceptions
|
||||
)
|
||||
}
|
||||
try {
|
||||
processConfigurations()
|
||||
|
||||
LOGGER.lifecycle("Resolved ${resolvedConfigurations.size} configurations.")
|
||||
|
||||
writeDependencyGraph()
|
||||
} catch (e: RuntimeException) {
|
||||
throw GradleException(
|
||||
"The Gradle2Nix plugin encountered errors while writing the dependency snapshot json file. " +
|
||||
"Please report this issue at: https://github.com/tadfisher/gradle2nix/issues",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOGGER = Logging.getLogger(DependencyExtractor::class.java)
|
||||
|
||||
private const val M2_PATTERN =
|
||||
"[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier])(.[ext])"
|
||||
|
||||
private const val IVY_ARTIFACT_PATTERN = "[organisation]/[module]/[revision]/[type]s/[artifact](.[ext])";
|
||||
|
||||
private fun resources(urls: List<URI>, patterns: List<String>): List<String> {
|
||||
if (urls.isEmpty()) {
|
||||
return patterns
|
||||
}
|
||||
if (patterns.isEmpty()) {
|
||||
return urls.map { it.toString() }
|
||||
}
|
||||
return mutableListOf<String>().apply {
|
||||
for (pattern in patterns) {
|
||||
for (url in urls) {
|
||||
add(
|
||||
url.toString()
|
||||
.removeSuffix("/")
|
||||
.plus("/")
|
||||
.plus(pattern.removePrefix("/"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun metadataResources(
|
||||
repository: ResolveConfigurationDependenciesBuildOperationType.Repository
|
||||
): List<String> {
|
||||
return when (repository.type) {
|
||||
Repository.Type.MAVEN.name -> {
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI),
|
||||
listOf(M2_PATTERN)
|
||||
)
|
||||
}
|
||||
Repository.Type.IVY.name -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI),
|
||||
repository.properties["IVY_PATTERNS"] as? List<String> ?: listOf(IVY_ARTIFACT_PATTERN)
|
||||
)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun artifactResources(
|
||||
repository: ResolveConfigurationDependenciesBuildOperationType.Repository
|
||||
): List<String> {
|
||||
return when (repository.type) {
|
||||
Repository.Type.MAVEN.name -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI)
|
||||
.plus(repository.properties["ARTIFACT_URLS"] as? List<URI> ?: emptyList()),
|
||||
listOf(M2_PATTERN)
|
||||
)
|
||||
}
|
||||
Repository.Type.IVY.name -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
resources(
|
||||
listOfNotNull(repository.properties["URL"] as? URI),
|
||||
repository.properties["ARTIFACT_PATTERNS"] as? List<String> ?: listOf(IVY_ARTIFACT_PATTERN)
|
||||
)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.extractor
|
||||
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.services.BuildService
|
||||
import org.gradle.api.services.BuildServiceParameters
|
||||
|
||||
abstract class DependencyExtractorBuildService :
|
||||
DependencyExtractor(),
|
||||
BuildService<DependencyExtractorBuildService.Params>
|
||||
{
|
||||
internal interface Params : BuildServiceParameters {
|
||||
val rendererClassName: Property<String>
|
||||
}
|
||||
|
||||
override fun getRendererClassName(): String {
|
||||
return parameters.rendererClassName.get()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.extractor
|
||||
|
||||
open class LegacyDependencyExtractor(
|
||||
private val rendererClassName: String
|
||||
) : DependencyExtractor() {
|
||||
|
||||
override fun getRendererClassName(): String {
|
||||
return rendererClassName
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.extractor
|
||||
|
||||
class ResolvedConfigurationFilter(projectFilter: String?, configurationFilter: String?) {
|
||||
val projectRegex = projectFilter?.toRegex()
|
||||
val configurationRegex = configurationFilter?.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
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.util
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.gradle.api.logging.Logging
|
||||
import org.gradle.internal.operations.BuildOperation
|
||||
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
|
||||
|
||||
class BuildOperationTracker : BuildOperationListener {
|
||||
private val _parents: MutableMap<OperationIdentifier, OperationIdentifier?> = ConcurrentHashMap()
|
||||
private val _operations: MutableMap<OperationIdentifier, BuildOperationDescriptor> = ConcurrentHashMap()
|
||||
private val _results: MutableMap<OperationIdentifier, Any> = ConcurrentHashMap()
|
||||
|
||||
val parents: Map<OperationIdentifier, OperationIdentifier?> get() = _parents
|
||||
val operations: Map<OperationIdentifier, BuildOperationDescriptor> get() = _operations
|
||||
val results: Map<OperationIdentifier, Any> get() = _results
|
||||
|
||||
override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) {
|
||||
}
|
||||
|
||||
override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) {
|
||||
}
|
||||
|
||||
override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) {
|
||||
val id = buildOperation.id ?: return
|
||||
_parents[id] = buildOperation.parentId
|
||||
_operations[id] = buildOperation
|
||||
}
|
||||
|
||||
tailrec fun <T> findParent(id: OperationIdentifier?, block: (BuildOperationDescriptor) -> T?): T? {
|
||||
if (id == null) return null
|
||||
val operation = _operations[id] ?: return null.also {
|
||||
LOGGER.lifecycle("no operation for $id")
|
||||
}
|
||||
return block(operation) ?: findParent(operation.parentId, block)
|
||||
}
|
||||
|
||||
fun <T> findChild(id: OperationIdentifier?, block: (BuildOperationDescriptor) -> T?): T? {
|
||||
if (id == null) return null
|
||||
val operation = operations[id] ?: return null
|
||||
block(operation)?.let { return it }
|
||||
return children(id).firstNotNullOfOrNull { findChild(it, block) }
|
||||
}
|
||||
|
||||
fun children(id: OperationIdentifier): Set<OperationIdentifier> {
|
||||
return parents.filterValues { it == id }.keys
|
||||
}
|
||||
|
||||
inline fun <reified T> getDetails(id: OperationIdentifier): T? {
|
||||
return operations[id]?.details as? T
|
||||
}
|
||||
|
||||
inline fun <reified T> getResult(id: OperationIdentifier): T? {
|
||||
return results[id] as? T
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOGGER = Logging.getLogger(BuildOperationTracker::class.qualifiedName!!)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.util
|
||||
|
||||
import java.io.File
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.internal.GradleInternal
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import org.gradle.api.model.ObjectFactory
|
||||
import org.gradle.api.provider.ProviderFactory
|
||||
import org.gradle.internal.operations.BuildOperationListenerManager
|
||||
import org.gradle.util.GradleVersion
|
||||
|
||||
internal abstract class GradleExtensions {
|
||||
|
||||
inline val Gradle.providerFactory: ProviderFactory
|
||||
get() = service()
|
||||
|
||||
inline val Gradle.buildOperationListenerManager: BuildOperationListenerManager
|
||||
get() = service()
|
||||
}
|
||||
|
||||
internal inline fun <reified T> Gradle.service(): T =
|
||||
(this as GradleInternal).services.get(T::class.java)
|
||||
|
||||
internal val Project.buildDirCompat: File
|
||||
get() = if (GradleVersion.current() < GradleVersion.version("8.3")) {
|
||||
@Suppress("DEPRECATION")
|
||||
buildDir
|
||||
} else {
|
||||
layout.buildDirectory.asFile.get()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.nixos.gradle2nix.dependencygraph.util
|
||||
|
||||
internal fun loadOptionalParam(envName: String): String? {
|
||||
return System.getProperty(envName)
|
||||
?: System.getenv()[envName]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.nixos.gradle2nix.forceresolve
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.artifacts.Configuration
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.work.DisableCachingByDefault
|
||||
import org.nixos.gradle2nix.util.canSafelyBeResolved
|
||||
|
||||
@DisableCachingByDefault(because = "Not worth caching")
|
||||
abstract class AbstractResolveProjectDependenciesTask : DefaultTask() {
|
||||
@Internal
|
||||
protected fun getReportableConfigurations(): List<Configuration> {
|
||||
return project.configurations.filter { it.canSafelyBeResolved() }
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,7 @@ class ForceDependencyResolutionPlugin : Plugin<Gradle> {
|
||||
gradle.allprojects { project ->
|
||||
val projectTaskFactory = getResolveProjectDependenciesTaskFactory()
|
||||
val resolveProjectDeps = projectTaskFactory.create(project)
|
||||
resolveAllDeps.configure {
|
||||
it.dependsOn(resolveProjectDeps)
|
||||
}
|
||||
resolveAllDeps.configure { it.dependsOn(resolveProjectDeps) }
|
||||
}
|
||||
|
||||
// Depend on all 'resolveBuildDependencies' task in each included build
|
||||
@@ -51,7 +49,7 @@ class ForceDependencyResolutionPlugin : Plugin<Gradle> {
|
||||
fun create(project: Project): TaskProvider<out Task>
|
||||
|
||||
data object Current : ResolveProjectDependenciesTaskFactory {
|
||||
override fun create(project: Project): TaskProvider<out Task> {
|
||||
override fun create(project: Project): TaskProvider<out Task> {
|
||||
return project.tasks.register(RESOLVE_PROJECT_TASK, ResolveProjectDependenciesTask::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.work.DisableCachingByDefault
|
||||
|
||||
@DisableCachingByDefault(because = "Not worth caching")
|
||||
abstract class LegacyResolveProjectDependenciesTask: DefaultTask() {
|
||||
private fun getReportableConfigurations(): List<Configuration> {
|
||||
return project.configurations.filter { it.isCanBeResolved }
|
||||
}
|
||||
|
||||
abstract class LegacyResolveProjectDependenciesTask : AbstractResolveProjectDependenciesTask() {
|
||||
@TaskAction
|
||||
fun action() {
|
||||
for (configuration in getReportableConfigurations()) {
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
package org.nixos.gradle2nix.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 javax.inject.Inject
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
|
||||
import org.gradle.api.file.FileCollection
|
||||
import org.gradle.api.model.ObjectFactory
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.internal.serialization.Cached
|
||||
import org.gradle.work.DisableCachingByDefault
|
||||
|
||||
@DisableCachingByDefault(because = "Not worth caching")
|
||||
abstract class ResolveProjectDependenciesTask: DefaultTask() {
|
||||
private val configurationResolvers = Cached.of { createConfigurationResolvers() }
|
||||
abstract class ResolveProjectDependenciesTask @Inject constructor(
|
||||
private val objects: ObjectFactory
|
||||
): AbstractResolveProjectDependenciesTask() {
|
||||
private val artifactFiles = Cached.of { artifactFiles() }
|
||||
|
||||
private fun createConfigurationResolvers(): List<Provider<ResolvedComponentResult>> {
|
||||
return getReportableConfigurations().map {
|
||||
it.incoming.resolutionResult.rootComponent
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReportableConfigurations(): List<Configuration> {
|
||||
return project.configurations.filter { it.isCanBeResolved }
|
||||
private fun artifactFiles(): FileCollection {
|
||||
return objects.fileCollection().from(
|
||||
getReportableConfigurations().map { configuration ->
|
||||
configuration.incoming.artifactView { viewConfiguration ->
|
||||
viewConfiguration.componentFilter { it is ModuleComponentIdentifier }
|
||||
}.files
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun action() {
|
||||
for (configuration in configurationResolvers.get()) {
|
||||
configuration.get()
|
||||
}
|
||||
artifactFiles.get().count()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.nixos.gradle2nix.util
|
||||
|
||||
import java.lang.reflect.Method
|
||||
import org.gradle.api.artifacts.Configuration
|
||||
import org.gradle.api.internal.GradleInternal
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import org.gradle.internal.operations.BuildOperationAncestryTracker
|
||||
import org.gradle.internal.operations.BuildOperationListenerManager
|
||||
|
||||
internal inline val Gradle.buildOperationAncestryTracker: BuildOperationAncestryTracker
|
||||
get() = service()
|
||||
|
||||
internal inline val Gradle.buildOperationListenerManager: BuildOperationListenerManager
|
||||
get() = service()
|
||||
|
||||
internal inline fun <reified T> Gradle.service(): T =
|
||||
(this as GradleInternal).services.get(T::class.java)
|
||||
|
||||
private val canSafelyBeResolvedMethod: Method? = try {
|
||||
val dc = Class.forName("org.gradle.internal.deprecation.DeprecatableConfiguration")
|
||||
dc.getMethod("canSafelyBeResolved")
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
null
|
||||
}
|
||||
|
||||
internal fun Configuration.canSafelyBeResolved(): Boolean =
|
||||
canSafelyBeResolvedMethod?.invoke(this) as? Boolean ?: isCanBeResolved
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.nixos.gradle2nix.dependencygraph
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.nixos.gradle2nix.model.Repository
|
||||
import org.nixos.gradle2nix.model.impl.DefaultDependencyCoordinates
|
||||
import org.nixos.gradle2nix.model.impl.DefaultRepository
|
||||
|
||||
class DependencyUrlParserTest : FunSpec({
|
||||
val mavenCentral = DefaultRepository(
|
||||
"MavenRepo",
|
||||
Repository.Type.MAVEN,
|
||||
metadataSources = listOf("mavenPom"),
|
||||
metadataResources = listOf("https://repo.maven.apache.org/maven2/${DependencyExtractor.M2_PATTERN}"),
|
||||
artifactResources = listOf("https://repo.maven.apache.org/maven2/${DependencyExtractor.M2_PATTERN}")
|
||||
)
|
||||
|
||||
test("parses maven url") {
|
||||
val url = "https://repo.maven.apache.org/maven2/com/github/ajalt/clikt-metadata/2.8.0/clikt-metadata-2.8.0.jar"
|
||||
val (coords, pattern) = parseComponent(listOf(mavenCentral), url).shouldNotBeNull()
|
||||
coords shouldBe DefaultDependencyCoordinates("com.github.ajalt", "clikt-metadata", "2.8.0")
|
||||
parseArtifact(pattern, coords, url) shouldBe "clikt-metadata-2.8.0.jar"
|
||||
}
|
||||
|
||||
test("parses maven snapshot url") {
|
||||
val url = "https://repo.maven.apache.org/maven2/org/apache/test-SNAPSHOT2/2.0.2-SNAPSHOT/test-SNAPSHOT2-2.0.2-SNAPSHOT.jar"
|
||||
val (coords, pattern) = parseComponent(listOf(mavenCentral), url).shouldNotBeNull()
|
||||
coords shouldBe DefaultDependencyCoordinates("org.apache", "test-SNAPSHOT2", "2.0.2-SNAPSHOT")
|
||||
parseArtifact(pattern, coords, url) shouldBe "test-SNAPSHOT2-2.0.2-SNAPSHOT.jar"
|
||||
}
|
||||
|
||||
test("parses maven timestamped snapshot url") {
|
||||
val url = "https://repo.maven.apache.org/maven2/org/apache/test-SNAPSHOT1/2.0.2-SNAPSHOT/test-SNAPSHOT1-2.0.2-20070310.181613-3.jar"
|
||||
val (coords, pattern) = parseComponent(listOf(mavenCentral), url).shouldNotBeNull()
|
||||
coords shouldBe DefaultDependencyCoordinates("org.apache", "test-SNAPSHOT1", "2.0.2-SNAPSHOT")
|
||||
parseArtifact(pattern, coords, url) shouldBe "test-SNAPSHOT1-2.0.2-SNAPSHOT.jar"
|
||||
}
|
||||
|
||||
test("parses ivy descriptor url") {
|
||||
val url = "https://asset.opendof.org/ivy2/org.opendof.core-java/dof-cipher-sms4/1.0/ivy.xml"
|
||||
val (coords, pattern) = parseComponent(
|
||||
listOf(
|
||||
DefaultRepository(
|
||||
"ivy",
|
||||
Repository.Type.IVY,
|
||||
metadataSources = listOf("ivyDescriptor"),
|
||||
metadataResources = listOf("https://asset.opendof.org/ivy2/[organisation]/[module]/[revision]/ivy(.[platform]).xml"),
|
||||
artifactResources = listOf("https://asset.opendof.org/artifact/[organisation]/[module]/[revision](/[platform])(/[type]s)/[artifact]-[revision](-[classifier]).[ext]")
|
||||
)
|
||||
),
|
||||
url
|
||||
).shouldNotBeNull()
|
||||
|
||||
coords shouldBe DefaultDependencyCoordinates("org.opendof.core-java", "dof-cipher-sms4", "1.0")
|
||||
parseArtifact(pattern, coords, url) shouldBe "ivy-1.0.xml"
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user