Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
packages: libnotify-dev libglib2.0-dev libdbusmenu-glib-dev libpipewire-0.3-dev
version: 1.0

- name: Plugin functional tests
run: ./gradlew -p plugin-build :nna:test

- name: Build calculator (compile tests without running)
run: ./gradlew :examples:calculator:jvmTestClasses :examples:calculator:compileKotlinLinuxX64

Expand Down
7 changes: 7 additions & 0 deletions plugin-build/plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ dependencies {
compileOnly(libs.kotlin.gradle.plugin)
testImplementation(libs.junit)
testImplementation(libs.kotlin.compiler.embeddable)
testImplementation(gradleTestKit())
}

kotlin {
jvmToolchain(21)
}

gradlePlugin {
testSourceSets(sourceSets.test.get())
plugins {
create(property("ID").toString()) {
id = property("ID").toString()
Expand All @@ -28,6 +30,11 @@ gradlePlugin {
}
}

tasks.test {
useJUnit()
systemProperty("nna.pluginBuildDir", rootProject.projectDir.absolutePath)
}

gradlePlugin {
website.set(property("WEBSITE").toString())
vcsUrl.set(property("VCS_URL").toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import dev.nucleusframework.nna.plugin.tasks.GenerateNativeBridgesTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.*
Expand All @@ -15,7 +13,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import java.io.File
import kotlin.jvm.optionals.getOrNull

/**
* Main entry point for the kotlin-native-export Gradle plugin.
Expand Down Expand Up @@ -127,9 +124,10 @@ class KotlinNativeExportPlugin : Plugin<Project> {
// Keep old task name as alias
project.tasks.register("generateKneJvmProxies") { dependsOn(generateBridges) }

// read the kotlinx coroutines version from the catalog otherwise fallback to some version
val coroutinesVersion = project.versionCatalog
?.findVersion("kotlinx-coroutines")?.getOrNull()?.toString() ?: "1.11.0"
// Required for generated suspend/Flow bridges. Always added by Maven
// coordinates β€” never look up the consumer's version catalog. Alias
// names in libs.versions.toml are project-local and must not be required.
val coroutinesVersion = DEFAULT_COROUTINES_VERSION

nativeTarget?.let { target ->
kotlin.sourceSets.findByName("${target.name}Main")?.dependencies {
Expand Down Expand Up @@ -294,9 +292,7 @@ class KotlinNativeExportPlugin : Plugin<Project> {
}
}

private val Project.versionCatalog: VersionCatalog?
get() {
val catalogs = project.extensions.getByType<VersionCatalogsExtension>()
return catalogs.find("libs").getOrNull()
}
companion object {
private const val DEFAULT_COROUTINES_VERSION = "1.11.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package dev.nucleusframework.nna.plugin

import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File

/**
* Regression for https://github.com/NucleusFramework/NucleusNativeAccess/issues/26
*
* The plugin must configure a consumer KMP project regardless of how β€” or whether β€”
* that project names dependencies in libs.versions.toml.
*/
class VersionCatalogIndependenceTest {

@get:Rule
val testProjectDir = TemporaryFolder()

@Test
fun `sync succeeds without a version catalog`() {
writeConsumerProject(catalogToml = null)
assertConsumerConfigures()
}

@Test
fun `sync succeeds when catalog has no coroutines aliases`() {
writeConsumerProject(
catalogToml = """
[versions]
kotlin = "2.3.20"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
""".trimIndent(),
)
assertConsumerConfigures()
}

@Test
fun `sync succeeds when coroutines uses a different catalog alias`() {
writeConsumerProject(
catalogToml = """
[versions]
kotlin = "2.3.20"

[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" }

[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
""".trimIndent(),
)
assertConsumerConfigures()
}

private fun writeConsumerProject(catalogToml: String?) {
val root = testProjectDir.root
val pluginBuildDir = File(System.getProperty("nna.pluginBuildDir")).invariantSeparatorsPath
File(root, "settings.gradle.kts").writeText(
"""
pluginManagement {
includeBuild("$pluginBuildDir")
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
rootProject.name = "catalog-consumer"
""".trimIndent(),
)
if (catalogToml != null) {
File(root, "gradle").mkdirs()
File(root, "gradle/libs.versions.toml").writeText(catalogToml)
}
File(root, "build.gradle.kts").writeText(
"""
plugins {
kotlin("multiplatform") version "2.3.20"
id("dev.nucleusframework.nna")
}

kotlin {
jvm()
$hostNativeTarget
}

kotlinNativeExport {
nativeLibName = "repro"
}
""".trimIndent(),
)
val nativeSrc = File(root, "src/nativeMain/kotlin/com/example")
nativeSrc.mkdirs()
File(nativeSrc, "Repro.kt").writeText(
"""
package com.example

class Repro {
fun ping(): String = "ok"
}
""".trimIndent(),
)
}

private fun assertConsumerConfigures() {
val result = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments("help", "generateKneNativeBridges", "--stacktrace")
.forwardOutput()
.build()

assertEquals(TaskOutcome.SUCCESS, result.task(":help")?.outcome)
val generate = result.task(":generateKneNativeBridges")
assertTrue(
"generateKneNativeBridges should run or be up-to-date, was ${generate?.outcome}\n${result.output}",
generate?.outcome == TaskOutcome.SUCCESS || generate?.outcome == TaskOutcome.UP_TO_DATE,
)
assertTrue(
"configuration must not fail with the catalog Optional.get() crash",
"No value present" !in result.output,
)
}

private val hostNativeTarget: String
get() {
val hostOs = System.getProperty("os.name")
return when {
hostOs == "Mac OS X" -> "macosArm64()"
hostOs == "Linux" -> "linuxX64()"
hostOs.startsWith("Windows") -> "mingwX64()"
else -> error("Unsupported host OS: $hostOs")
}
}
}
Loading