Rewrite plugin, use filenames in lockfile

This commit is contained in:
Tad Fisher
2024-05-17 14:52:02 -07:00
parent e83e42f9d4
commit 8d2ec45ad4
144 changed files with 8679 additions and 7507 deletions

View File

@@ -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")
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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>
) {
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
)
}

View 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")
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -1,10 +0,0 @@
package org.nixos.gradle2nix.dependencygraph.extractor
open class LegacyDependencyExtractor(
private val rendererClassName: String
) : DependencyExtractor() {
override fun getRendererClassName(): String {
return rendererClassName
}
}

View File

@@ -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
}
}

View File

@@ -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!!)
}
}

View File

@@ -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()
}

View File

@@ -1,6 +0,0 @@
package org.nixos.gradle2nix.dependencygraph.util
internal fun loadOptionalParam(envName: String): String? {
return System.getProperty(envName)
?: System.getenv()[envName]
}

View File

@@ -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() }
}
}

View File

@@ -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)
}
}

View File

@@ -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()) {

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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"
}
})