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

@@ -1,97 +1,51 @@
package org.nixos.gradle2nix
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
import kotlin.test.assertEquals
import dev.minutest.Tests
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import org.gradle.api.internal.artifacts.dsl.DefaultRepositoryHandler.BINTRAY_JCENTER_URL
import org.gradle.api.internal.artifacts.dsl.DefaultRepositoryHandler.MAVEN_CENTRAL_URL
import strikt.api.expectThat
import strikt.assertions.all
import strikt.assertions.containsExactly
import strikt.assertions.get
import strikt.assertions.hasSize
import strikt.assertions.isEqualTo
import strikt.assertions.map
import strikt.assertions.startsWith
class BasicTest : JUnit5Minutests {
@Tests
fun tests() = rootContext<Fixture>("basic tests") {
withFixture("basic/basic-java-project") {
test("builds basic java project") {
expectThat(build()) {
get("gradle version") { gradle.version }.isEqualTo(System.getProperty("compat.gradle.version"))
class BasicTest {
@TempDir lateinit var projectDir: File
get("root project dependencies") { rootProject.projectDependencies }.and {
ids.containsExactly(
"com.squareup.moshi:moshi:1.8.0@jar",
"com.squareup.moshi:moshi:1.8.0@pom",
"com.squareup.moshi:moshi-parent:1.8.0@pom",
"com.squareup.okio:okio:2.2.2@jar",
"com.squareup.okio:okio:2.2.2@pom",
"org.jetbrains:annotations:13.0@jar",
"org.jetbrains:annotations:13.0@pom",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@pom",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@pom",
"org.sonatype.oss:oss-parent:7@pom"
)
@Test
fun `builds basic project with kotlin dsl`() {
val model = projectDir.buildKotlin("""
plugins {
java
map { it.urls }.all {
hasSize(2)
get(0).startsWith(BINTRAY_JCENTER_URL)
get(1).startsWith(MAVEN_CENTRAL_URL)
}
}
}
}
repositories {
jcenter()
}
dependencies {
implementation("com.squareup.okio:okio:2.2.2")
implementation("com.squareup.moshi:moshi:1.8.0")
}
""".trimIndent())
assertEquals(model.gradle.version, System.getProperty("compat.gradle.version"))
with(model.rootProject.projectDependencies) {
with(repositories) {
assertEquals(1, maven.size)
assertEquals(maven[0].urls[0], "https://jcenter.bintray.com/")
}
assertArtifacts(
pom("com.squareup.moshi:moshi-parent:1.8.0"),
jar("com.squareup.moshi:moshi:1.8.0"),
pom("com.squareup.moshi:moshi:1.8.0"),
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
pom("org.sonatype.oss:oss-parent:7"),
actual = artifacts
)
}
}
@Test
fun `builds basic project with groovy dsl`() {
val model = projectDir.buildGroovy("""
plugins {
id("java")
}
repositories {
jcenter()
}
dependencies {
implementation 'com.squareup.okio:okio:2.2.2'
implementation 'com.squareup.moshi:moshi:1.8.0'
}
""".trimIndent())
assertEquals(model.gradle.version, System.getProperty("compat.gradle.version"))
with(model.rootProject.projectDependencies) {
with(repositories) {
assertEquals(1, maven.size)
assertEquals(maven[0].urls[0], "https://jcenter.bintray.com/")
}
assertArtifacts(
pom("com.squareup.moshi:moshi-parent:1.8.0"),
jar("com.squareup.moshi:moshi:1.8.0"),
pom("com.squareup.moshi:moshi:1.8.0"),
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
pom("org.sonatype.oss:oss-parent:7"),
actual = artifacts
)
}
}
}

View File

@@ -0,0 +1,69 @@
package org.nixos.gradle2nix
import dev.minutest.Tests
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import strikt.api.expectThat
import strikt.assertions.all
import strikt.assertions.contains
import strikt.assertions.containsExactly
import strikt.assertions.filter
import strikt.assertions.isEqualTo
import strikt.assertions.isNotEqualTo
import strikt.assertions.isNotNull
import strikt.assertions.single
import strikt.assertions.startsWith
class DependencyTest : JUnit5Minutests {
@Tests
fun tests() = rootContext<Fixture>("dependency tests") {
withFixture("dependency/classifier") {
test("resolves dependency with classifier") {
expectThat(build()) {
get("root project dependencies") { rootProject.projectDependencies }.ids.containsExactly(
"com.badlogicgames.gdx:gdx-parent:1.9.9@pom",
"com.badlogicgames.gdx:gdx-platform:1.9.9:natives-desktop@jar",
"com.badlogicgames.gdx:gdx-platform:1.9.9@pom",
"org.sonatype.oss:oss-parent:5@pom"
)
}
}
}
withFixture("dependency/dynamic-snapshot") {
test("resolves snapshot dependency with dynamic version") {
expectThat(build()) {
get("root project dependencies") { rootProject.projectDependencies }
.filter { it.id.name == "packr" }
.all {
get("id.version") { id.version }.isEqualTo("-SNAPSHOT")
get("timestamp") { timestamp }.isNotNull()
get("build") { build }.isNotNull()
}
}
}
}
withFixture("dependency/snapshot") {
test("resolves snapshot dependency") {
expectThat(build()) {
get("root project dependencies") { rootProject.projectDependencies }
.filter { it.id.name == "okio" }
.and {
ids.containsExactly(
"com.squareup.okio:okio:2.5.0-SNAPSHOT@jar",
"com.squareup.okio:okio:2.5.0-SNAPSHOT@module",
"com.squareup.okio:okio:2.5.0-SNAPSHOT@pom"
)
all {
get("timestamp") { timestamp }.isNotNull()
get("build") { build }.isNotNull()
get("urls") { urls }.single().startsWith(SONATYPE_OSS_URL)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
package org.nixos.gradle2nix
import dev.minutest.Tests
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import strikt.api.expectThat
import strikt.assertions.all
import strikt.assertions.containsExactly
import strikt.assertions.map
import strikt.assertions.single
import strikt.assertions.startsWith
class IvyTest : JUnit5Minutests {
@Tests
fun tests() = rootContext<Fixture>("ivy tests") {
withFixture("ivy/basic") {
test("resolves ivy dependencies") {
expectThat(build()) {
get("root project dependencies") { rootProject.projectDependencies }.and {
ids.containsExactly(
"org.opendof.core-java:dof-cipher-sms4:1.0@jar",
"org.opendof.core-java:dof-oal:7.0.2@jar"
)
map { it.urls }.all {
single().startsWith("https://asset.opendof.org/artifact")
}
}
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
package org.nixos.gradle2nix
import dev.minutest.Tests
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import strikt.api.expectThat
import strikt.assertions.contains
class PluginTest : JUnit5Minutests {
@Tests
fun tests() = rootContext<Fixture>("plugin tests") {
withFixture("plugin/resolves-from-default-repo") {
test("resolves plugin from default repo") {
expectThat(build()) {
get("plugin dependencies") { pluginDependencies }.ids
.contains("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:1.3.50@pom")
}
}
}
}
}

View File

@@ -1,461 +1,151 @@
package org.nixos.gradle2nix
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import dev.minutest.Tests
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import org.gradle.api.internal.artifacts.dsl.DefaultRepositoryHandler.BINTRAY_JCENTER_URL
import strikt.api.expectThat
import strikt.assertions.all
import strikt.assertions.containsExactly
import strikt.assertions.containsExactlyInAnyOrder
import strikt.assertions.get
import strikt.assertions.hasSize
import strikt.assertions.isEqualTo
import strikt.assertions.map
import java.io.File
import kotlin.test.assertEquals
import strikt.assertions.single
import strikt.assertions.startsWith
class SubprojectsTest {
@TempDir
lateinit var root: File
class SubprojectsTest : JUnit5Minutests {
@Tests
fun tests() = rootContext<Fixture>("subproject tests") {
withFixture("subprojects/multi-module") {
test("builds multi-module project") {
expectThat(build().rootProject) {
get("root project dependencies") { projectDependencies }.and {
ids.containsExactly(
"junit:junit:4.12@jar",
"junit:junit:4.12@pom",
"org.hamcrest:hamcrest-core:1.3@jar",
"org.hamcrest:hamcrest-core:1.3@pom",
"org.hamcrest:hamcrest-parent:1.3@pom"
)
all {
get("urls") { urls }.single().startsWith(BINTRAY_JCENTER_URL)
}
}
@Test
fun `builds multi-module project with kotlin dsl`() {
root.resolve("child-a").also { it.mkdirs() }
.resolve("build.gradle.kts").writeText("""
plugins {
java
}
get("children") { children }.and {
hasSize(2)
dependencies {
implementation("com.squareup.okio:okio:2.2.2")
}
""".trimIndent())
get(0).and {
get("name") { name }.isEqualTo("child-a")
get("projectDir") { projectDir }.isEqualTo("child-a")
root.resolve("child-b").also { it.mkdirs() }
.resolve("build.gradle.kts").writeText("""
plugins {
java
}
get("child-a project dependencies") { projectDependencies }.and {
ids.containsExactly(
"com.squareup.okio:okio:2.2.2@jar",
"com.squareup.okio:okio:2.2.2@pom",
"org.jetbrains:annotations:13.0@jar",
"org.jetbrains:annotations:13.0@pom",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@pom",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@pom"
)
dependencies {
implementation(project(":child-a"))
implementation("com.squareup.moshi:moshi:1.8.0")
}
""".trimIndent())
all {
get("urls") { urls }.single().startsWith(BINTRAY_JCENTER_URL)
}
}
}
root.resolve("settings.gradle.kts").writeText("""
include(":child-a", ":child-b")
""".trimIndent())
get(1).and {
get("name") { name }.isEqualTo("child-b")
get("projectDir") { projectDir }.isEqualTo("child-b")
val model = root.buildKotlin("""
plugins {
java
}
get("child-b project dependencies") { projectDependencies }.and {
ids.containsExactly(
"com.squareup.moshi:moshi:1.8.0@jar",
"com.squareup.moshi:moshi:1.8.0@pom",
"com.squareup.moshi:moshi-parent:1.8.0@pom",
"com.squareup.okio:okio:1.16.0@jar", // compileClasspath
"com.squareup.okio:okio:1.16.0@pom", // compileClasspath
"com.squareup.okio:okio:2.2.2@jar", // runtimeClasspath
"com.squareup.okio:okio:2.2.2@pom", // runtimeClasspath
"com.squareup.okio:okio-parent:1.16.0@pom", // compileClasspath
"org.jetbrains:annotations:13.0@jar",
"org.jetbrains:annotations:13.0@pom",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@pom",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@pom",
"org.sonatype.oss:oss-parent:7@pom"
)
allprojects {
repositories {
jcenter()
all {
get("urls") { urls }.single().startsWith(BINTRAY_JCENTER_URL)
}
}
}
}
}
}
dependencies {
testImplementation("junit:junit:4.12")
}
""".trimIndent())
test("builds single subproject") {
expectThat(build(subprojects = listOf(":child-a")).rootProject) {
get("root project dependencies") { projectDependencies }.and {
ids.containsExactly(
"junit:junit:4.12@jar",
"junit:junit:4.12@pom",
"org.hamcrest:hamcrest-core:1.3@jar",
"org.hamcrest:hamcrest-core:1.3@pom",
"org.hamcrest:hamcrest-parent:1.3@pom"
)
with(model.rootProject) {
with(projectDependencies) {
assertEquals(listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven)
all {
get("urls") { urls }.single().startsWith(BINTRAY_JCENTER_URL)
}
}
assertArtifacts(
jar("junit:junit:4.12"),
pom("junit:junit:4.12"),
jar("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-parent:1.3"),
actual = artifacts)
}
get("children") { children }.single().and {
get("name") { name }.isEqualTo("child-a")
get("projectDir") { projectDir }.isEqualTo("child-a")
assertEquals(2, children.size)
get("child-a project dependencies") { projectDependencies }.and {
ids.containsExactly(
"com.squareup.okio:okio:2.2.2@jar",
"com.squareup.okio:okio:2.2.2@pom",
"org.jetbrains:annotations:13.0@jar",
"org.jetbrains:annotations:13.0@pom",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib:1.2.60@pom",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@jar",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60@pom"
)
with(children[0]) {
assertEquals("child-a", name)
assertEquals(root.resolve("child-a").toRelativeString(root), projectDir)
with(projectDependencies) {
assertEquals(
listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven
)
assertArtifacts(
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
actual = artifacts
)
}
}
with(children[1]) {
assertEquals("child-b", name)
assertEquals(root.resolve("child-b").toRelativeString(root), projectDir)
with(projectDependencies) {
assertEquals(
listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven
)
assertArtifacts(
pom("com.squareup.moshi:moshi-parent:1.8.0"),
jar("com.squareup.moshi:moshi:1.8.0"),
pom("com.squareup.moshi:moshi:1.8.0"),
pom("com.squareup.okio:okio-parent:1.16.0"),
jar("com.squareup.okio:okio:1.16.0"),
pom("com.squareup.okio:okio:1.16.0"),
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
pom("org.sonatype.oss:oss-parent:7"),
actual = artifacts
)
all {
get("urls") { urls }.single().startsWith(BINTRAY_JCENTER_URL)
}
}
}
}
}
}
}
@Test
fun `builds multi-module project with groovy dsl`() {
root.resolve("child-a").also { it.mkdirs() }
.resolve("build.gradle").writeText("""
plugins {
id 'java'
withFixture("subprojects/dependent-subprojects") {
test("includes dependent subprojects") {
expectThat(build(subprojects = listOf(":child-a"))) {
get("children") { rootProject.children }
.map { it.path }
.containsExactlyInAnyOrder(":child-a", ":child-b", ":child-c")
}
dependencies {
implementation 'com.squareup.okio:okio:2.2.2'
}
""".trimIndent())
root.resolve("child-b").also { it.mkdirs() }
.resolve("build.gradle").writeText("""
plugins {
id 'java'
}
dependencies {
implementation project(':child-a')
implementation 'com.squareup.moshi:moshi:1.8.0'
}
""".trimIndent())
root.resolve("settings.gradle").writeText("""
include ':child-a', ':child-b'
""".trimIndent())
val model = root.buildGroovy("""
plugins {
id 'java'
}
allprojects {
repositories {
jcenter()
expectThat(build(subprojects = listOf(":child-b"))) {
get("children") { rootProject.children }
.map { it.path }
.containsExactlyInAnyOrder(":child-b", ":child-c")
}
}
dependencies {
testImplementation 'junit:junit:4.12'
}
""".trimIndent())
with(model.rootProject) {
with(projectDependencies) {
assertEquals(listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven)
assertArtifacts(
jar("junit:junit:4.12"),
pom("junit:junit:4.12"),
jar("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-parent:1.3"),
actual = artifacts)
}
assertEquals(2, children.size)
with(children[0]) {
assertEquals("child-a", name)
assertEquals(root.resolve("child-a").toRelativeString(root), projectDir)
with(projectDependencies) {
assertEquals(
listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven
)
assertArtifacts(
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
actual = artifacts
)
}
}
with(children[1]) {
assertEquals("child-b", name)
assertEquals(root.resolve("child-b").toRelativeString(root), projectDir)
with(projectDependencies) {
assertEquals(listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven)
assertArtifacts(
pom("com.squareup.moshi:moshi-parent:1.8.0"),
jar("com.squareup.moshi:moshi:1.8.0"),
pom("com.squareup.moshi:moshi:1.8.0"),
pom("com.squareup.okio:okio-parent:1.16.0"),
jar("com.squareup.okio:okio:1.16.0"),
pom("com.squareup.okio:okio:1.16.0"),
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
pom("org.sonatype.oss:oss-parent:7"),
actual = artifacts)
}
}
}
}
@Test
fun `builds single subproject in multi-module project with kotlin dsl`() {
root.resolve("child-a").also { it.mkdirs() }
.resolve("build.gradle.kts").writeText("""
plugins {
java
}
dependencies {
implementation("com.squareup.okio:okio:2.2.2")
}
""".trimIndent())
root.resolve("child-b").also { it.mkdirs() }
.resolve("build.gradle.kts").writeText("""
plugins {
java
}
dependencies {
implementation("com.squareup.moshi:moshi:1.8.0")
}
""".trimIndent())
root.resolve("settings.gradle.kts").writeText("""
include(":child-a", ":child-b")
""".trimIndent())
val model = root.buildKotlin("""
plugins {
java
}
allprojects {
repositories {
jcenter()
}
}
dependencies {
testImplementation("junit:junit:4.12")
}
""".trimIndent(),
subprojects = listOf(":child-a"))
with(model.rootProject) {
with(projectDependencies) {
assertEquals(listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven)
assertArtifacts(
jar("junit:junit:4.12"),
pom("junit:junit:4.12"),
jar("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-parent:1.3"),
actual = artifacts)
}
assertEquals(1, children.size)
with(children[0]) {
assertEquals("child-a", name)
assertEquals(root.resolve("child-a").toRelativeString(root), projectDir)
with(projectDependencies) {
assertEquals(
listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven
)
assertArtifacts(
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
actual = artifacts
)
}
}
}
}
@Test
fun `builds single subproject in multi-module project with groovy dsl`() {
root.resolve("child-a").also { it.mkdirs() }
.resolve("build.gradle").writeText("""
plugins {
id 'java'
}
dependencies {
implementation 'com.squareup.okio:okio:2.2.2'
}
""".trimIndent())
root.resolve("child-b").also { it.mkdirs() }
.resolve("build.gradle").writeText("""
plugins {
id 'java'
}
dependencies {
implementation 'com.squareup.moshi:moshi:1.8.0'
}
""".trimIndent())
root.resolve("settings.gradle").writeText("""
include ':child-a', ':child-b'
""".trimIndent())
val model = root.buildGroovy("""
plugins {
id 'java'
}
allprojects {
repositories {
jcenter()
}
}
dependencies {
testImplementation 'junit:junit:4.12'
}
""".trimIndent(),
subprojects = listOf(":child-a"))
with(model.rootProject) {
with(projectDependencies) {
assertEquals(listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven)
assertArtifacts(
jar("junit:junit:4.12"),
pom("junit:junit:4.12"),
jar("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-core:1.3"),
pom("org.hamcrest:hamcrest-parent:1.3"),
actual = artifacts)
}
assertEquals(1, children.size)
with(children[0]) {
assertEquals("child-a", name)
assertEquals(root.resolve("child-a").toRelativeString(root), projectDir)
with(projectDependencies) {
assertEquals(
listOf(DefaultMaven(urls = listOf("https://jcenter.bintray.com/"))),
repositories.maven
)
assertArtifacts(
jar("com.squareup.okio:okio:2.2.2"),
pom("com.squareup.okio:okio:2.2.2"),
jar("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib-common:1.2.60"),
jar("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
pom("org.jetbrains.kotlin:kotlin-stdlib:1.2.60"),
jar("org.jetbrains:annotations:13.0"),
pom("org.jetbrains:annotations:13.0"),
actual = artifacts
)
}
}
}
}
@Test
fun `includes subproject dependencies`() {
root.resolve("child-a").also { it.mkdirs() }.resolve("build.gradle.kts").writeText("")
root.resolve("child-b").also { it.mkdirs() }.resolve("build.gradle.kts").writeText("")
root.resolve("child-c").also { it.mkdirs() }.resolve("build.gradle.kts").writeText("")
root.resolve("child-d").also { it.mkdirs() }.resolve("build.gradle.kts").writeText("")
root.resolve("settings.gradle.kts").writeText("""
include(":child-a", ":child-b", ":child-c", ":child-d")
""".trimIndent())
val buildscript = """
subprojects {
apply(plugin = "java")
}
project(":child-a") {
dependencies {
"implementation"(project(":child-b"))
}
}
project(":child-b") {
dependencies {
"implementation"(project(":child-c"))
}
}
""".trimIndent()
with(root.buildKotlin(buildscript, subprojects = listOf(":child-a"))) {
expectThat(rootProject.children).map { it.path }
.containsExactlyInAnyOrder(":child-a", ":child-b", ":child-c")
}
with(root.buildKotlin(buildscript, subprojects = listOf(":child-b"))) {
expectThat(rootProject.children).map { it.path }.containsExactlyInAnyOrder(":child-b", ":child-c")
}
}
}

View File

@@ -1,20 +1,33 @@
package org.nixos.gradle2nix
import com.squareup.moshi.Moshi
import dev.minutest.ContextBuilder
import dev.minutest.MinutestFixture
import dev.minutest.TestContextBuilder
import okio.buffer
import okio.source
import org.gradle.api.internal.artifacts.dsl.ParsedModuleStringNotation
import org.gradle.internal.classpath.DefaultClassPath
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.internal.DefaultGradleRunner
import org.gradle.testkit.runner.internal.PluginUnderTestMetadataReading
import org.gradle.tooling.GradleConnector
import org.gradle.tooling.events.ProgressListener
import org.gradle.util.GradleVersion
import org.junit.jupiter.api.Assumptions.assumeTrue
import strikt.api.Assertion
import strikt.assertions.map
import java.io.File
import kotlin.test.assertTrue
import java.io.StringWriter
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.streams.toList
const val SONATYPE_OSS_URL = "https://oss.sonatype.org/content/repositories/snapshots/"
private val moshi = Moshi.Builder().build()
private val gradleVersion = GradleVersion.version(System.getProperty("compat.gradle.version"))
val GRADLE_4_5 = GradleVersion.version("4.5")
private fun File.initscript() = resolve("init.gradle").also {
it.writer().use { out ->
val classpath = DefaultClassPath.of(PluginUnderTestMetadataReading.readImplementationClasspath())
@@ -31,20 +44,12 @@ private fun File.initscript() = resolve("init.gradle").also {
}
}
fun File.buildGroovy(
script: String,
configurations: List<String> = emptyList(),
subprojects: List<String> = emptyList()
): DefaultBuild {
resolve("build.gradle").writeText(script)
return build(configurations, subprojects)
}
fun File.buildKotlin(
script: String,
configurations: List<String> = emptyList(),
subprojects: List<String> = emptyList()
): DefaultBuild {
assumeTrue(gradleVersion >= GRADLE_4_5)
resolve("build.gradle.kts").writeText(script)
return build(configurations, subprojects)
}
@@ -53,10 +58,13 @@ private fun File.build(
configurations: List<String>,
subprojects: List<String>
): DefaultBuild {
GradleRunner.create()
.withGradleVersion(System.getProperty("compat.gradle.version"))
val log = StringWriter()
val result = GradleRunner.create()
.withGradleVersion(gradleVersion.version)
.withProjectDir(this)
.forwardOutput()
.forwardStdOutput(log)
.forwardStdError(log)
.withArguments(
"nixModel",
"--init-script=${initscript()}",
@@ -64,8 +72,12 @@ private fun File.build(
"-Porg.nixos.gradle2nix.configurations=${configurations.joinToString(",")}",
"-Porg.nixos.gradle2nix.subprojects=${subprojects.joinToString(",")}"
)
.build()
.runCatching { build() }
result.onFailure { error ->
System.err.print(log)
throw error
}
return resolve("build/nix/model.json").run {
println(readText())
source().buffer().use { src ->
@@ -73,75 +85,48 @@ private fun File.build(
}
}
}
//
// return GradleConnector.newConnector()
// .useGradleVersion(System.getProperty("compat.gradle.version"))
// .forProjectDirectory(this)
// .connect()
// .model(Build::class.java).apply {
// addArguments("--init-script=${initscript()}", "--stacktrace")
// addJvmArguments(
// "-Dorg.gradle.debug=true",
// "-Dorg.nixos.gradle2nix.configurations=${configurations.joinToString(",")}",
// "-Dorg.nixos.gradle2nix.subprojects=${subprojects.joinToString(",")}"
// )
// setStandardOutput(System.out)
// setStandardError(System.out)
// }
// .get()
// .let { DefaultBuild(it) }
fun jar(notation: String, sha256: String = ""): DefaultArtifact =
artifact(notation, sha256, "jar")
val <T : Iterable<Artifact>> Assertion.Builder<T>.ids: Assertion.Builder<Iterable<String>>
get() = map { it.id.toString() }
fun pom(notation: String, sha256: String = ""): DefaultArtifact =
artifact(notation, sha256, "pom")
@MinutestFixture
class Fixture(val testRoots: List<Path>)
private fun artifact(notation: String, sha256: String, type: String): DefaultArtifact {
val parsed = ParsedModuleStringNotation(notation, type)
return DefaultArtifact(
groupId = parsed.group ?: "",
artifactId = parsed.name ?: "",
version = parsed.version ?: "",
classifier = parsed.classifier ?: "",
extension = type,
sha256 = sha256
)
@MinutestFixture
class ProjectFixture(val testRoot: Path) {
fun build(
configurations: List<String> = emptyList(),
subprojects: List<String> = emptyList()
) = testRoot.toFile().build(configurations, subprojects)
}
private fun artifactEquals(expected: DefaultArtifact, actual: DefaultArtifact?): Boolean {
return actual != null && with (expected) {
groupId == actual.groupId &&
artifactId == actual.artifactId &&
version == actual.version &&
classifier == actual.classifier &&
extension == actual.extension &&
(sha256.takeIf { it.isNotEmpty() }?.equals(actual.sha256) ?: true)
fun ContextBuilder<Fixture>.withFixture(
name: String,
block: TestContextBuilder<Fixture, ProjectFixture>.() -> Unit
) = context(name) {
val url = checkNotNull(Thread.currentThread().contextClassLoader.getResource(name)?.toURI()) {
"$name: No test fixture found"
}
}
val fixtureRoot = Paths.get(url)
val dest = createTempDir("gradle2nix").toPath()
val src = checkNotNull(fixtureRoot.takeIf(Files::exists)) {
"$name: Test fixture not found: $fixtureRoot}"
}
src.toFile().copyRecursively(dest.toFile())
val testRoots = Files.list(dest).filter { Files.isDirectory(it) }.toList()
fun assertArtifacts(vararg expected: DefaultArtifact, actual: List<DefaultArtifact>) {
val mismatches = mutableListOf<Mismatch>()
val remaining = mutableListOf<DefaultArtifact>().also { it.addAll(actual) }
expected.forEachIndexed { i: Int, exp: DefaultArtifact ->
val act = actual.elementAtOrNull(i)
if (!artifactEquals(exp, act)) {
mismatches += Mismatch(i, exp, act)
} else if (act != null) {
remaining -= act
fixture {
Fixture(testRoots)
}
afterAll {
dest.toFile().deleteRecursively()
}
testRoots.forEach { testRoot ->
derivedContext<ProjectFixture>(testRoot.fileName.toString()) {
deriveFixture { ProjectFixture(testRoot) }
block()
}
}
assertTrue(mismatches.isEmpty() && remaining.isEmpty(), """
Artifact mismatches:
${mismatches.joinToString("\n ", prefix = " ")}
Missing artifacts:
${remaining.joinToString("\n ", prefix = " ")}
""")
}
data class Mismatch(
val index: Int,
val expected: DefaultArtifact,
val actual: DefaultArtifact?
)

View File

@@ -1,22 +1,25 @@
package org.nixos.gradle2nix
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import dev.minutest.Tests
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import strikt.api.expectThat
import strikt.assertions.isEqualTo
import java.io.File
import kotlin.test.assertEquals
class WrapperTest {
@TempDir
lateinit var root: File
class WrapperTest : JUnit5Minutests {
@Tests
fun tests() = rootContext<File>("wrapper tests") {
fixture { createTempDir("gradle2nix") }
@Test
fun `resolves gradle version from wrapper configuration`() {
val model = root.buildKotlin("""
tasks.withType<org.gradle.api.tasks.wrapper.Wrapper> {
gradleVersion = "5.5.1"
test("resolves gradle wrapper version") {
expectThat(buildKotlin("""
tasks.withType<org.gradle.api.tasks.wrapper.Wrapper> {
gradleVersion = "5.5.1"
}
""".trimIndent())) {
get("gradle version") { gradle.version }.isEqualTo("5.5.1")
}
""".trimIndent())
assertEquals(model.gradle.version, "5.5.1")
}
}
}

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)