Use custom dependency resolution

- Use Apache Ivy to resolve artifact URLs
- Update build model with full artifact IDs
- Generate Maven module metadata to support dynamic version constraints
- Resolve snapshot versions and generate snapshot metadata
- Add test fixtures and rewrite Gradle plugin tests
- Update dependencies
This commit is contained in:
Tad Fisher
2020-01-23 10:01:38 -08:00
parent 9a47ead9cb
commit 648be6bd07
72 changed files with 5163 additions and 3060 deletions

View File

@@ -0,0 +1,255 @@
package org.nixos.gradle2nix
import org.apache.ivy.Ivy
import org.apache.ivy.core.settings.IvySettings
import org.apache.ivy.plugins.parser.m2.PomReader
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorParser
import org.apache.ivy.plugins.repository.url.URLResource
import org.apache.ivy.plugins.resolver.ChainResolver
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleIdentifier
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.artifacts.component.ComponentArtifactIdentifier
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.query.ArtifactResolutionQuery
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.internal.artifacts.repositories.ResolutionAwareRepository
import org.gradle.ivy.IvyDescriptorArtifact
import org.gradle.ivy.IvyModule
import org.gradle.kotlin.dsl.getArtifacts
import org.gradle.kotlin.dsl.withArtifacts
import org.gradle.maven.MavenModule
import org.gradle.maven.MavenPomArtifact
import org.gradle.util.GradleVersion
import java.io.File
internal class ConfigurationResolverFactory(repositories: RepositoryHandler) {
private val ivySettings = IvySettings().apply {
defaultInit()
setDefaultRepositoryCacheBasedir(createTempDir("gradle2nix-cache").apply(File::deleteOnExit).absolutePath)
setDictatorResolver(ChainResolver().also { chain ->
chain.settings = this@apply
for (resolver in resolvers) chain.add(resolver)
})
}
private val resolvers = repositories.filterIsInstance<ResolutionAwareRepository>()
.mapNotNull { it.repositoryResolver(ivySettings) }
fun create(dependencies: DependencyHandler): ConfigurationResolver =
ConfigurationResolver(ivySettings, resolvers, dependencies)
}
internal class ConfigurationResolver(
ivySettings: IvySettings,
private val resolvers: List<RepositoryResolver>,
private val dependencies: DependencyHandler
) {
private val ivy = Ivy.newInstance(ivySettings)
fun resolve(configuration: Configuration): List<DefaultArtifact> {
val resolved = configuration.resolvedConfiguration
val topLevelMetadata = resolved.firstLevelModuleDependencies
.flatMap { resolveMetadata(it.moduleGroup, it.moduleName, it.moduleVersion) }
val allArtifacts = resolved.resolvedArtifacts
.filter { it.id.componentIdentifier is ModuleComponentIdentifier }
.flatMap(::resolve)
return (topLevelMetadata + allArtifacts).filter { it.urls.isNotEmpty() }
}
private fun resolve(resolvedArtifact: ResolvedArtifact): List<DefaultArtifact> {
val componentId = resolvedArtifact.id.componentIdentifier as ModuleComponentIdentifier
val artifactId = DefaultArtifactIdentifier(
group = componentId.group,
name = componentId.module,
version = componentId.version,
type = resolvedArtifact.type,
extension = resolvedArtifact.extension,
classifier = resolvedArtifact.classifier
)
val sha256 = resolvedArtifact.file.sha256()
val artifacts = resolvers.mapNotNull { it.resolve(artifactId, sha256) }.merge()
return artifacts + componentId.run { resolveMetadata(group, module, version) }
}
private fun resolveMetadata(
group: String,
name: String,
version: String
): List<DefaultArtifact> {
return resolvePoms(group, name, version) +
resolveDescriptors(group, name, version) +
resolveGradleMetadata(group, name, version)
}
private fun resolvePoms(
group: String,
name: String,
version: String
): List<DefaultArtifact> {
return dependencies.createArtifactResolutionQuery()
.forModuleCompat(group, name, version)
.withArtifacts(MavenModule::class, MavenPomArtifact::class)
.execute()
.resolvedComponents
.flatMap { it.getArtifacts(MavenPomArtifact::class) }
.filterIsInstance<ResolvedArtifactResult>()
.flatMap { it.withParentPoms() }
.flatMap { resolvedPom ->
val componentId = resolvedPom.id.componentIdentifier as ModuleComponentIdentifier
val artifactId = DefaultArtifactIdentifier(
group = componentId.group,
name = componentId.module,
version = componentId.version,
type = "pom"
)
val sha256 = resolvedPom.file.sha256()
resolvers.mapNotNull { it.resolve(artifactId, sha256) }.merge()
}
}
private fun resolveDescriptors(
group: String,
name: String,
version: String
): List<DefaultArtifact> {
return dependencies.createArtifactResolutionQuery()
.forModuleCompat(group, name, version)
.withArtifacts(IvyModule::class, IvyDescriptorArtifact::class)
.execute()
.resolvedComponents
.flatMap { it.getArtifacts(IvyDescriptorArtifact::class) }
.filterIsInstance<ResolvedArtifactResult>()
.flatMap { it.withParentDescriptors() }
.flatMap { resolvedDesc ->
val componentId = resolvedDesc.id.componentIdentifier as ModuleComponentIdentifier
val artifactId = DefaultArtifactIdentifier(
group = componentId.group,
name = componentId.module,
version = componentId.version,
type = "ivy",
extension = "xml"
)
val sha256 = resolvedDesc.file.sha256()
resolvers.mapNotNull { it.resolve(artifactId, sha256) }.merge()
}
}
private fun resolveGradleMetadata(
group: String,
name: String,
version: String
): List<DefaultArtifact> {
val artifactId = DefaultArtifactIdentifier(
group = group,
name = name,
version = version,
type = "module"
)
return resolvers.mapNotNull { it.resolve(artifactId) }.merge()
}
private fun ResolvedArtifactResult.parentPom(): ResolvedArtifactResult? {
val resource = URLResource(file.toURI().toURL())
val reader = PomReader(resource.url, resource)
return if (reader.hasParent()) {
dependencies.createArtifactResolutionQuery()
.forModuleCompat(reader.parentGroupId, reader.parentArtifactId, reader.parentVersion)
.withArtifacts(MavenModule::class, MavenPomArtifact::class)
.execute()
.resolvedComponents
.flatMap { it.getArtifacts(MavenPomArtifact::class) }
.filterIsInstance<ResolvedArtifactResult>()
.firstOrNull()
} else {
null
}
}
private fun ResolvedArtifactResult.withParentPoms(): List<ResolvedArtifactResult> =
generateSequence(this) { it.parentPom() }.toList()
private fun ResolvedArtifactResult.parentDescriptors(seen: Set<ComponentArtifactIdentifier>): List<ResolvedArtifactResult> {
val url = file.toURI().toURL()
val parser = XmlModuleDescriptorParser.getInstance()
val descriptor = parser.parseDescriptor(ivy.settings, url, false)
return descriptor.inheritedDescriptors.mapNotNull { desc ->
dependencies.createArtifactResolutionQuery()
.forModuleCompat(
desc.parentRevisionId.organisation,
desc.parentRevisionId.name,
desc.parentRevisionId.revision
)
.withArtifacts(IvyModule::class, IvyDescriptorArtifact::class)
.execute()
.resolvedComponents
.flatMap { it.getArtifacts(IvyDescriptorArtifact::class) }
.filterIsInstance<ResolvedArtifactResult>()
.firstOrNull()
}.filter { it.id !in seen }
}
private fun ResolvedArtifactResult.withParentDescriptors(): List<ResolvedArtifactResult> {
val seen = mutableSetOf<ComponentArtifactIdentifier>()
return generateSequence(listOf(this)) { descs ->
val parents = descs.flatMap { it.parentDescriptors(seen) }
seen.addAll(parents.map(ResolvedArtifactResult::id))
parents.takeUnless { it.isEmpty() }
}.flatten().distinct().toList()
}
}
private fun ArtifactResolutionQuery.forModuleCompat(
group: String,
name: String,
version: String
): ArtifactResolutionQuery {
return if (GradleVersion.current() >= GradleVersion.version("4.5")) {
forModule(group, name, version)
} else {
forComponents(ModuleComponentId(group, name, version))
}
}
private data class ModuleComponentId(
private val moduleId: ModuleId,
private val version: String
) : ModuleComponentIdentifier {
constructor(
group: String,
name: String,
version: String
) : this(ModuleId(group, name), version)
override fun getGroup(): String = moduleId.group
override fun getModule(): String = moduleId.name
override fun getVersion(): String = version
override fun getModuleIdentifier(): ModuleIdentifier = moduleId
override fun getDisplayName(): String =
arrayOf(group, module, version).joinToString(":")
}
private data class ModuleId(
private val group: String,
private val name: String
) : ModuleIdentifier {
override fun getGroup(): String = group
override fun getName(): String = name
}
private fun List<DefaultArtifact>.merge(): List<DefaultArtifact> {
return groupingBy { it.id }
.reduce { _, dest, next -> dest.copy(urls = dest.urls + next.urls) }
.values.toList()
}

View File

@@ -1,169 +0,0 @@
package org.nixos.gradle2nix
import org.apache.maven.model.Parent
import org.apache.maven.model.Repository
import org.apache.maven.model.building.DefaultModelBuilderFactory
import org.apache.maven.model.building.DefaultModelBuildingRequest
import org.apache.maven.model.building.ModelBuildingRequest
import org.apache.maven.model.building.ModelSource2
import org.apache.maven.model.resolution.ModelResolver
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.maven.MavenModule
import org.gradle.maven.MavenPomArtifact
import java.io.File
import java.io.InputStream
import java.net.URI
import java.security.MessageDigest
internal class DependencyResolver(
private val configurations: ConfigurationContainer,
private val dependencies: DependencyHandler,
private val logger: Logger = Logging.getLogger(DependencyResolver::class.simpleName)
) {
private val mavenPomResolver = MavenPomResolver(configurations, dependencies)
fun resolveDependencies(configuration: Configuration): Set<DefaultArtifact> {
if (!configuration.isCanBeResolved) {
logger.warn("Cannot resolve configuration ${configuration.name}; ignoring.")
return emptySet()
}
return configuration.resolvedConfiguration.resolvedArtifacts
.filterNot { it.id.componentIdentifier is ProjectComponentIdentifier }
.mapTo(sortedSetOf()) {
with (it) {
DefaultArtifact(
groupId = moduleVersion.id.group,
artifactId = moduleVersion.id.name,
version = moduleVersion.id.version,
classifier = classifier ?: "",
extension = extension,
sha256 = sha256(file)
)
}
}
}
fun resolveDependencies(
dependencies: Collection<Dependency>,
includeTransitive: Boolean = false
): Set<DefaultArtifact> {
val configuration = configurations.detachedConfiguration(*(dependencies.toTypedArray()))
configuration.isTransitive = includeTransitive
return resolveDependencies(configuration)
}
fun resolvePoms(configuration: Configuration): Set<DefaultArtifact> {
return dependencies.createArtifactResolutionQuery()
.forComponents(configuration.incoming.resolutionResult.allComponents.map { it.id })
.withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java)
.execute()
.resolvedComponents.asSequence()
.flatMap { component ->
val id = component.id
if (id !is ModuleComponentIdentifier) {
emptySequence()
} else {
component.getArtifacts(MavenPomArtifact::class.java).asSequence()
.filterIsInstance<ResolvedArtifactResult>()
.map { id to it }
}
}
.flatMapTo(sortedSetOf()) { (id, artifact) ->
sequenceOf(DefaultArtifact(
groupId = id.group,
artifactId = id.module,
version = id.version,
classifier = "",
extension = artifact.file.extension,
sha256 = sha256(artifact.file)
)) + mavenPomResolver.resolve(artifact.file).asSequence()
}
}
fun resolvePoms(
dependencies: Collection<Dependency>,
includeTransitive: Boolean = false
): Set<DefaultArtifact> {
val configuration = configurations.detachedConfiguration(*(dependencies.toTypedArray()))
configuration.isTransitive = includeTransitive
return resolvePoms(configuration)
}
}
private class MavenPomResolver(
private val configurations: ConfigurationContainer,
private val dependencies: DependencyHandler
) : ModelResolver {
private val modelBuilder = DefaultModelBuilderFactory().newInstance()
private val resolvedDependencies = mutableSetOf<DefaultArtifact>()
@Synchronized
fun resolve(pom: File): Set<DefaultArtifact> {
resolvedDependencies.clear()
modelBuilder.build(
DefaultModelBuildingRequest()
.setModelResolver(this)
.setPomFile(pom)
.setSystemProperties(System.getProperties())
.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL)
).effectiveModel
return resolvedDependencies.toSet()
}
override fun newCopy() = this
override fun resolveModel(
groupId: String,
artifactId: String,
version: String
): ModelSource2 {
val file = configurations
.detachedConfiguration(dependencies.create("$groupId:$artifactId:$version@pom"))
.singleFile
resolvedDependencies.add(DefaultArtifact(
groupId = groupId,
artifactId = artifactId,
version = version,
classifier = "",
extension = file.extension,
sha256 = sha256(file)
))
return object : ModelSource2 {
override fun getLocation(): String = file.absolutePath
override fun getLocationURI(): URI = file.absoluteFile.toURI()
override fun getRelatedSource(relPath: String?): ModelSource2? = null
override fun getInputStream(): InputStream = file.inputStream()
}
}
override fun resolveModel(parent: Parent): ModelSource2 =
resolveModel(parent.groupId, parent.artifactId, parent.version)
override fun resolveModel(dependency: org.apache.maven.model.Dependency): ModelSource2 =
resolveModel(dependency.groupId, dependency.artifactId, dependency.version)
override fun addRepository(repository: Repository) {}
override fun addRepository(repository: Repository, replace: Boolean) {}
}
private const val HEX = "0123456789abcdef"
private fun sha256(file: File): String = buildString {
MessageDigest.getInstance("SHA-256").digest(file.readBytes())
.asSequence()
.map { it.toInt() }
.forEach {
append(HEX[it shr 4 and 0x0f])
append(HEX[it and 0x0f])
}
}

View File

@@ -1,19 +1,16 @@
package org.nixos.gradle2nix
import com.squareup.moshi.Moshi
import okio.buffer
import okio.sink
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.ArtifactRepositoryContainer
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.api.internal.GradleInternal
import org.gradle.api.invocation.Gradle
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.wrapper.Wrapper
import org.gradle.kotlin.dsl.getByName
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.kotlin.dsl.withType
import org.gradle.plugin.management.PluginRequest
@@ -33,15 +30,17 @@ open class Gradle2NixPlugin : Plugin<Gradle> {
rootProject.serviceOf<ToolingModelBuilderRegistry>()
.register(NixToolingModelBuilder(modelProperties, pluginRequests))
rootProject.tasks.register("nixModel") {
rootProject.tasks.registerCompat("nixModel") {
doLast {
val outFile = project.mkdir(project.buildDir.resolve("nix")).resolve("model.json")
val model = project.buildModel(modelProperties, pluginRequests)
outFile.sink().buffer().use { out ->
Moshi.Builder().build()
.adapter(DefaultBuild::class.java)
.indent(" ")
.toJson(out, model)
outFile.bufferedWriter().use { out ->
out.write(
Moshi.Builder().build()
.adapter(DefaultBuild::class.java)
.indent(" ")
.toJson(model)
)
out.flush()
}
}
@@ -50,7 +49,13 @@ open class Gradle2NixPlugin : Plugin<Gradle> {
}
}
private const val NIX_MODEL_NAME = "org.nixos.gradle2nix.Build"
private fun TaskContainer.registerCompat(name: String, configureAction: Task.() -> Unit) {
if (GradleVersion.current() >= GradleVersion.version("4.9")) {
register(name, configureAction)
} else {
create(name, configureAction)
}
}
private class NixToolingModelBuilder(
private val modelProperties: ModelProperties,
@@ -100,6 +105,7 @@ private fun Project.buildModel(
)
}
@Suppress("UnstableApiUsage")
private fun Project.buildGradle(): DefaultGradle =
with(tasks.getByName<Wrapper>("wrapper")) {
DefaultGradle(
@@ -113,18 +119,16 @@ private fun Project.buildGradle(): DefaultGradle =
}
?: throw IllegalStateException(
"""
Failed to find native-platform jar in ${gradle.gradleHomeDir}.
Ask Tad to fix this.
""".trimIndent()
Failed to find native-platform jar in ${gradle.gradleHomeDir}.
Ask Tad to fix this.
""".trimIndent()
)
)
}
private fun Project.buildPlugins(pluginRequests: List<PluginRequest>): DefaultDependencies =
with(PluginResolver(gradle as GradleInternal, pluginRequests)) {
DefaultDependencies(repositories.repositories(), artifacts())
}
private fun Project.buildPlugins(pluginRequests: List<PluginRequest>): List<DefaultArtifact> {
return objects.newInstance<PluginResolver>().resolve(pluginRequests).distinct().sorted()
}
private fun Project.includedBuilds(): List<DefaultIncludedBuild> =
gradle.includedBuilds.map {
@@ -134,7 +138,7 @@ private fun Project.includedBuilds(): List<DefaultIncludedBuild> =
private fun Project.buildProject(
explicitConfigurations: List<String>,
explicitSubprojects: Collection<Project>,
plugins: DefaultDependencies
pluginArtifacts: List<DefaultArtifact>
): DefaultProject {
logger.lifecycle(" Subproject: $path")
return DefaultProject(
@@ -142,34 +146,33 @@ private fun Project.buildProject(
version = version.toString(),
path = path,
projectDir = projectDir.toRelativeString(rootProject.projectDir),
buildscriptDependencies = buildscriptDependencies(plugins),
buildscriptDependencies = buildscriptDependencies(pluginArtifacts),
projectDependencies = projectDependencies(explicitConfigurations),
children = explicitSubprojects.map { it.buildProject(explicitConfigurations, emptyList(), plugins) }
children = explicitSubprojects.map {
it.buildProject(explicitConfigurations, emptyList(), pluginArtifacts)
}
)
}
private fun Project.buildscriptDependencies(plugins: DefaultDependencies): DefaultDependencies =
with(DependencyResolver(buildscript.configurations, buildscript.dependencies)) {
DefaultDependencies(
repositories = buildscript.repositories.repositories(),
artifacts = buildscript.configurations
.filter { it.isCanBeResolved }
.flatMap { resolveDependencies(it) + resolvePoms(it) }
.minus(plugins.artifacts)
.distinct()
)
}
private fun Project.buildscriptDependencies(pluginArtifacts: List<DefaultArtifact>): List<DefaultArtifact> {
val resolverFactory = ConfigurationResolverFactory(buildscript.repositories)
val resolver = resolverFactory.create(buildscript.dependencies)
val pluginIds = pluginArtifacts.map(DefaultArtifact::id)
return buildscript.configurations
.flatMap(resolver::resolve)
.distinct()
.filter { it.id !in pluginIds }
.sorted()
}
private fun Project.projectDependencies(explicitConfigurations: List<String>): DefaultDependencies =
with(DependencyResolver(configurations, dependencies)) {
val toResolve = collectConfigurations(explicitConfigurations)
DefaultDependencies(
repositories = repositories.repositories(),
artifacts = toResolve.flatMap { resolveDependencies(it) + resolvePoms(it) }
.sorted()
.distinct()
)
}
private fun Project.projectDependencies(explicitConfigurations: List<String>): List<DefaultArtifact> {
val resolverFactory = ConfigurationResolverFactory(repositories)
val resolver = resolverFactory.create(dependencies)
return collectConfigurations(explicitConfigurations)
.flatMap(resolver::resolve)
.distinct()
.sorted()
}
private fun Project.dependentSubprojects(explicitConfigurations: List<String>): Set<Project> {
return collectConfigurations(explicitConfigurations)
@@ -190,19 +193,6 @@ private fun Project.collectConfigurations(
}
}
private val excludedRepoNames = setOf(
"Embedded Kotlin Repository",
ArtifactRepositoryContainer.DEFAULT_MAVEN_LOCAL_REPO_NAME
)
internal fun RepositoryHandler.repositories() = DefaultRepositories(
maven = filterIsInstance<MavenArtifactRepository>()
.filter { it.name !in excludedRepoNames }
.map { repo ->
DefaultMaven(listOf(repo.url.toString()) + repo.artifactUrls.map { it.toString() })
}
)
private fun fetchDistSha256(url: String): String {
return URL("$url.sha256").openConnection().run {
connect()
@@ -217,6 +207,9 @@ private val Wrapper.sha256: String
return if (GradleVersion.current() < GradleVersion.version("4.5")) {
fetchDistSha256(distributionUrl)
} else {
@Suppress("UnstableApiUsage")
distributionSha256Sum ?: fetchDistSha256(distributionUrl)
}
}
private const val NIX_MODEL_NAME = "org.nixos.gradle2nix.Build"

View File

@@ -1,6 +1,5 @@
package org.nixos.gradle2nix
import java.util.Properties
import org.gradle.api.Project
data class ModelProperties(

View File

@@ -1,101 +1,27 @@
package org.nixos.gradle2nix
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.internal.GradleInternal
import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionSelectorScheme
import org.gradle.api.internal.plugins.PluginImplementation
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency
import org.gradle.plugin.management.PluginRequest
import org.gradle.plugin.management.internal.PluginRequestInternal
import org.gradle.plugin.use.PluginId
import org.gradle.plugin.use.internal.PluginDependencyResolutionServices
import org.gradle.plugin.use.resolve.internal.ArtifactRepositoriesPluginResolver
import org.gradle.plugin.use.resolve.internal.PluginResolution
import org.gradle.plugin.use.resolve.internal.PluginResolutionResult
import org.gradle.plugin.use.resolve.internal.PluginResolveContext
import javax.inject.Inject
internal class PluginResolver(
gradle: GradleInternal,
private val pluginRequests: Collection<PluginRequest>
internal open class PluginResolver @Inject constructor(
pluginDependencyResolutionServices: PluginDependencyResolutionServices
) {
private val pluginDependencyResolutionServices = gradle.serviceOf<PluginDependencyResolutionServices>()
private val versionSelectorScheme = gradle.serviceOf<VersionSelectorScheme>()
private val configurations = pluginDependencyResolutionServices.configurationContainer
private val artifactRepositoriesPluginResolver = ArtifactRepositoriesPluginResolver(
pluginDependencyResolutionServices,
versionSelectorScheme
)
private val resolver = ConfigurationResolverFactory(pluginDependencyResolutionServices.resolveRepositoryHandler)
.create(pluginDependencyResolutionServices.dependencyHandler)
val repositories = pluginDependencyResolutionServices.resolveRepositoryHandler
private val resolver by lazy {
DependencyResolver(
pluginDependencyResolutionServices.configurationContainer,
pluginDependencyResolutionServices.dependencyHandler
)
}
private val pluginResult by lazy {
PluginResult().apply {
for (request in pluginRequests.filterIsInstance<PluginRequestInternal>()) {
artifactRepositoriesPluginResolver.resolve(request, this)
}
}
}
private val pluginContext by lazy {
PluginContext().apply {
for (result in pluginResult.found) result.execute(this)
}
}
fun artifacts(): List<DefaultArtifact> {
return (resolver.resolveDependencies(pluginContext.dependencies, true) +
resolver.resolvePoms(pluginContext.dependencies, true))
.sorted()
.distinct()
}
private class PluginResult : PluginResolutionResult {
val found = mutableSetOf<PluginResolution>()
override fun notFound(sourceDescription: String?, notFoundMessage: String?) {}
override fun notFound(
sourceDescription: String?,
notFoundMessage: String?,
notFoundDetail: String?
) {
}
override fun isFound(): Boolean = true
override fun found(sourceDescription: String, pluginResolution: PluginResolution) {
found.add(pluginResolution)
}
}
private class PluginContext : PluginResolveContext {
val dependencies = mutableSetOf<ExternalModuleDependency>()
val repositories = mutableSetOf<String>()
override fun add(plugin: PluginImplementation<*>) {
println("add: $plugin")
}
override fun addFromDifferentLoader(plugin: PluginImplementation<*>) {
println("addFromDifferentLoader: $plugin")
}
override fun addLegacy(pluginId: PluginId, m2RepoUrl: String, dependencyNotation: Any) {
repositories.add(m2RepoUrl)
}
override fun addLegacy(pluginId: PluginId, dependencyNotation: Any) {
if (dependencyNotation is ExternalModuleDependency) {
dependencies.add(dependencyNotation)
fun resolve(pluginRequests: List<PluginRequest>): List<DefaultArtifact> {
val markerDependencies = pluginRequests.map {
it.module?.let { selector ->
DefaultExternalModuleDependency(selector.group, selector.name, selector.version)
} ?: it.id.run {
DefaultExternalModuleDependency(id, "$id.gradle.plugin", it.version)
}
}
return resolver.resolve(configurations.detachedConfiguration(*markerDependencies.toTypedArray()))
}
}

View File

@@ -0,0 +1,212 @@
package org.nixos.gradle2nix
import org.apache.ivy.core.LogOptions
import org.apache.ivy.core.cache.ArtifactOrigin
import org.apache.ivy.core.cache.CacheResourceOptions
import org.apache.ivy.core.cache.DefaultRepositoryCacheManager
import org.apache.ivy.core.cache.RepositoryCacheManager
import org.apache.ivy.core.module.id.ArtifactRevisionId
import org.apache.ivy.core.module.id.ModuleRevisionId
import org.apache.ivy.core.resolve.DownloadOptions
import org.apache.ivy.core.settings.IvySettings
import org.apache.ivy.plugins.repository.url.URLResource
import org.apache.ivy.plugins.resolver.IBiblioResolver
import org.apache.ivy.plugins.resolver.URLResolver
import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader
import org.codehaus.plexus.util.ReaderFactory
import org.codehaus.plexus.util.xml.pull.XmlPullParserException
import org.gradle.api.artifacts.repositories.ArtifactRepository
import org.gradle.api.artifacts.repositories.IvyArtifactRepository
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.api.internal.artifacts.repositories.ResolutionAwareRepository
import org.gradle.api.internal.artifacts.repositories.resolver.IvyResolver
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import java.io.IOException
import org.apache.ivy.core.module.descriptor.Artifact as IvyArtifact
import org.apache.ivy.core.module.descriptor.DefaultArtifact as IvyDefaultArtifact
import org.apache.ivy.plugins.resolver.RepositoryResolver as IvyRepositoryResolver
internal fun ResolutionAwareRepository.repositoryResolver(ivySettings: IvySettings): RepositoryResolver? =
when(this) {
is MavenArtifactRepository -> MavenResolver(ivySettings, this)
is IvyArtifactRepository -> IvyResolver(ivySettings, this)
else -> null
}
internal sealed class RepositoryResolver {
companion object {
@JvmStatic
protected val log: Logger = Logging.getLogger("gradle2nix")
}
abstract val ivyResolver: IvyRepositoryResolver
abstract fun resolve(
artifactId: DefaultArtifactIdentifier,
sha256: String? = null
): DefaultArtifact?
}
internal class MavenResolver(
ivySettings: IvySettings,
repository: MavenArtifactRepository
) : RepositoryResolver() {
override val ivyResolver: IBiblioResolver = IBiblioResolver().apply {
name = repository.name
root = repository.url.toString()
isM2compatible = true
settings = ivySettings
setCache(cacheManager(ivySettings, repository).name)
}
override fun resolve(artifactId: DefaultArtifactIdentifier, sha256: String?): DefaultArtifact? {
val ivyArtifact: IvyArtifact = artifactId.toArtifact()
val origin = ivyResolver.locate(ivyArtifact)?.takeIf(ArtifactOrigin::isExists) ?: return null
val hash = sha256 ?: ivyResolver.download(origin, downloadOptions).localFile?.sha256() ?: return null
val snapshotVersion: SnapshotVersion? = artifactId.version.snapshotVersion()?.let {
findSnapshotVersion(artifactId, it)
}
return DefaultArtifact(
id = artifactId,
name = artifactId.filename(snapshotVersion),
path = artifactId.repoPath(),
timestamp = snapshotVersion?.timestamp,
build = snapshotVersion?.build,
urls = listOf(origin.location),
sha256 = hash
)
}
private fun findSnapshotVersion(
artifactId: ArtifactIdentifier,
snapshotVersion: SnapshotVersion
): SnapshotVersion {
if (snapshotVersion.timestamp != null) return snapshotVersion
val metadataLocation = "${ivyResolver.root}${artifactId.repoPath()}/maven-metadata.xml".toUrl()
val metadataFile = ivyResolver.repositoryCacheManager.downloadRepositoryResource(
URLResource(metadataLocation, ivyResolver.timeoutConstraint),
"maven-metadata",
"maven-metadata",
"xml",
CacheResourceOptions(),
ivyResolver.repository
).localFile
if (metadataFile == null) {
log.warn("maven-metadata.xml not found for snapshot dependency: $artifactId")
return snapshotVersion
}
fun parseError(e: Throwable): Pair<String?, Int?> {
log.error("Failed to parse maven-metadata.xml for artifact: $artifactId")
log.error("Error was: ${e.message}", e)
return null to null
}
val (timestamp: String?, build: Int?) = try {
MetadataXpp3Reader()
.read(ReaderFactory.newXmlReader(metadataFile))
.versioning?.snapshot?.run { timestamp to buildNumber }
?: null to null
} catch (e: IOException) {
parseError(e)
} catch (e: XmlPullParserException) {
parseError(e)
}
return snapshotVersion.copy(timestamp = timestamp, build = build)
}
}
internal class IvyResolver(
ivySettings: IvySettings,
repository: IvyArtifactRepository
) : RepositoryResolver() {
override val ivyResolver: URLResolver = URLResolver().apply {
name = repository.name
val ivyResolver = (repository as ResolutionAwareRepository).createResolver() as IvyResolver
isM2compatible = ivyResolver.isM2compatible
for (p in ivyResolver.ivyPatterns) addIvyPattern(p)
for (p in ivyResolver.artifactPatterns) addArtifactPattern(p)
settings = ivySettings
setCache(cacheManager(ivySettings, repository).name)
}
override fun resolve(artifactId: DefaultArtifactIdentifier, sha256: String?): DefaultArtifact? {
val ivyArtifact: IvyArtifact = artifactId.toArtifact()
val origin = ivyResolver.locate(ivyArtifact)?.takeIf(ArtifactOrigin::isExists) ?: return null
val hash = sha256 ?: ivyResolver.download(origin, downloadOptions).localFile?.sha256() ?: return null
return DefaultArtifact(
id = DefaultArtifactIdentifier(artifactId),
name = artifactId.filename(null),
path = artifactId.repoPath(),
urls = listOf(origin.location),
sha256 = hash
)
}
}
private fun cacheManager(ivySettings: IvySettings, repository: ArtifactRepository): RepositoryCacheManager {
return DefaultRepositoryCacheManager(
"${repository.name}-cache",
ivySettings,
createTempDir("gradle2nix-${repository.name}-cache")
).also {
ivySettings.addRepositoryCacheManager(it)
}
}
private val metadataTypes = setOf("pom", "ivy")
private fun ArtifactIdentifier.toArtifact(): IvyArtifact {
val moduleRevisionId = ModuleRevisionId.newInstance(group, name, version)
val artifactRevisionId = ArtifactRevisionId.newInstance(
moduleRevisionId,
name,
type,
extension,
classifier?.let { mapOf("classifier" to it) }
)
return IvyDefaultArtifact(artifactRevisionId, null, null, type in metadataTypes)
}
private data class SnapshotVersion(
val base: String,
val timestamp: String?,
val build: Int?
) {
override fun toString(): String {
return if (timestamp != null && build != null) {
"$base-$timestamp-$build"
} else {
"$base-SNAPSHOT"
}
}
}
private val SNAPSHOT_REGEX = Regex("^(.*)-SNAPSHOT$")
private val SNAPSHOT_TIMESTAMPED_REGEX = Regex("^(.*)-([0-9]{8}.[0-9]{6})-([0-9]+)$")
private fun String.snapshotVersion(): SnapshotVersion? {
return SNAPSHOT_REGEX.find(this)?.destructured?.let { (base) ->
SnapshotVersion(base, null, null)
} ?: SNAPSHOT_TIMESTAMPED_REGEX.find(this)?.destructured?.let { (base, timestamp, build) ->
SnapshotVersion(base, timestamp, build.toInt())
}
}
private fun ArtifactIdentifier.repoPath(): String =
"${group.replace('.', '/')}/$name/$version"
private fun ArtifactIdentifier.filename(
snapshotVersion: SnapshotVersion?
): String = buildString {
append(name, "-", snapshotVersion ?: version)
if (classifier != null) append("-", classifier)
append(".", extension)
}
private val downloadOptions = DownloadOptions().apply { log = LogOptions.LOG_QUIET }

View File

@@ -0,0 +1,21 @@
package org.nixos.gradle2nix
import java.io.File
import java.net.URL
import java.security.MessageDigest
private const val HEX = "0123456789abcdef"
internal fun File.sha256(): String = readBytes().sha256()
private fun ByteArray.sha256() = buildString {
MessageDigest.getInstance("SHA-256").digest(this@sha256)
.asSequence()
.map(Byte::toInt)
.forEach {
append(HEX[it shr 4 and 0x0f])
append(HEX[it and 0x0f])
}
}
internal fun String.toUrl(): URL = URL(this)