From acdcdf8af0dd2591be033873ceb77d6e2e716327 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 18 Aug 2026 23:56:50 +0300 Subject: [PATCH] fix(plugin): stop requiring consumer version catalog aliases --- .github/workflows/check.yaml | 3 + plugin-build/plugin/build.gradle.kts | 7 + .../nna/plugin/KotlinNativeExportPlugin.kt | 18 +-- .../plugin/VersionCatalogIndependenceTest.kt | 148 ++++++++++++++++++ 4 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/VersionCatalogIndependenceTest.kt diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 267a033c..572f491b 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -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 diff --git a/plugin-build/plugin/build.gradle.kts b/plugin-build/plugin/build.gradle.kts index 5ff33291..22fb5cb6 100644 --- a/plugin-build/plugin/build.gradle.kts +++ b/plugin-build/plugin/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { compileOnly(libs.kotlin.compiler.embeddable) compileOnly(libs.kotlin.gradle.plugin) testImplementation(libs.junit) + testImplementation(gradleTestKit()) } kotlin { @@ -15,6 +16,7 @@ kotlin { } gradlePlugin { + testSourceSets(sourceSets.test.get()) plugins { create(property("ID").toString()) { id = property("ID").toString() @@ -27,6 +29,11 @@ gradlePlugin { } } +tasks.test { + useJUnit() + systemProperty("nna.pluginBuildDir", rootProject.projectDir.absolutePath) +} + gradlePlugin { website.set(property("WEBSITE").toString()) vcsUrl.set(property("VCS_URL").toString()) diff --git a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/KotlinNativeExportPlugin.kt b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/KotlinNativeExportPlugin.kt index ea34584a..d64c902c 100644 --- a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/KotlinNativeExportPlugin.kt +++ b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/KotlinNativeExportPlugin.kt @@ -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.* @@ -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. @@ -127,9 +124,10 @@ class KotlinNativeExportPlugin : Plugin { // 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 { @@ -294,9 +292,7 @@ class KotlinNativeExportPlugin : Plugin { } } - private val Project.versionCatalog: VersionCatalog? - get() { - val catalogs = project.extensions.getByType() - return catalogs.find("libs").getOrNull() - } + companion object { + private const val DEFAULT_COROUTINES_VERSION = "1.11.0" + } } diff --git a/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/VersionCatalogIndependenceTest.kt b/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/VersionCatalogIndependenceTest.kt new file mode 100644 index 00000000..9d60fe8e --- /dev/null +++ b/plugin-build/plugin/src/test/kotlin/dev/nucleusframework/nna/plugin/VersionCatalogIndependenceTest.kt @@ -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") + } + } +}