diff --git a/build.gradle.kts b/build.gradle.kts index 22ae165..763426e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask plugins { - alias(libs.plugins.kotlin) apply false + alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.versionCheck) } @@ -13,7 +13,8 @@ tasks.withType { fun String.isNonStable() = "^[0-9,.v-]+(-r)?$".toRegex().matches(this).not() -tasks.register("clean", Delete::class.java) { +tasks.register("clean") { + description = "Delete the root project build directory" delete(rootProject.layout.buildDirectory) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa68ac6..cd149a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kotlinx-coroutines = "1.11.0" junit = "4.13.2" [plugins] -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "pluginPublish" } versionCheck = { id = "com.github.ben-manes.versions", version.ref = "versionCheck" } diff --git a/plugin-build/build.gradle.kts b/plugin-build/build.gradle.kts index 3746852..347a490 100644 --- a/plugin-build/build.gradle.kts +++ b/plugin-build/build.gradle.kts @@ -1,22 +1,21 @@ plugins { - alias(libs.plugins.kotlin) apply false alias(libs.plugins.pluginPublish) apply false alias(libs.plugins.versionCheck) } -val resolvedVersion = - providers - .environmentVariable("GITHUB_REF") - .orNull - ?.removePrefix("refs/tags/v") - ?: "0.1.0" +val resolvedVersion = providers + .environmentVariable("GITHUB_REF") + .orNull + ?.removePrefix("refs/tags/v") + ?: "0.1.0" allprojects { group = property("GROUP").toString() version = resolvedVersion } -tasks.register("clean", Delete::class.java) { +tasks.register("clean") { + description = "Delete the root project build directory" delete(rootProject.layout.buildDirectory) } diff --git a/plugin-build/plugin/build.gradle.kts b/plugin-build/plugin/build.gradle.kts index 002aa05..5ff3329 100644 --- a/plugin-build/plugin/build.gradle.kts +++ b/plugin-build/plugin/build.gradle.kts @@ -1,18 +1,12 @@ plugins { - kotlin("jvm") - `java-gradle-plugin` + `kotlin-dsl` alias(libs.plugins.pluginPublish) } dependencies { - implementation(kotlin("stdlib")) - implementation(gradleApi()) - // kotlin-compiler-embeddable for PSI parsing (compileOnly — loaded at runtime via isolated Worker classloader) compileOnly(libs.kotlin.compiler.embeddable) - compileOnly(libs.kotlin.gradle.plugin) - testImplementation(libs.junit) } @@ -49,6 +43,8 @@ publishing { */ tasks.register("setupPluginUploadFromEnvironment") { + description = "Upload plugin to publish" + doLast { val key = System.getenv("GRADLE_PUBLISH_KEY") val secret = System.getenv("GRADLE_PUBLISH_SECRET") 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 3514a3c..b2b494b 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 @@ -1,5 +1,6 @@ package dev.nucleusframework.nna.plugin +import dev.nucleusframework.nna.plugin.catalog.kotlinEmbeddedCompiler import dev.nucleusframework.nna.plugin.catalog.kotlinxCoroutineDependency import dev.nucleusframework.nna.plugin.catalog.kotlinxCoroutineJvmDependency import dev.nucleusframework.nna.plugin.catalog.kotlinxCoroutineTestDependency @@ -9,6 +10,10 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.logging.LogLevel import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType @@ -33,10 +38,8 @@ import java.io.File class KotlinNativeExportPlugin : Plugin { override fun apply(project: Project) { - val extension = project.extensions.create( - "kotlinNativeExport", - KotlinNativeExportExtension::class.java, - ) + + val extension = project.extensions.create("kotlinNativeExport") extension.nativeLibName.convention("nativelib") extension.nativePackage.convention("") @@ -48,7 +51,7 @@ class KotlinNativeExportPlugin : Plugin { } private fun configureKmp(project: Project, extension: KotlinNativeExportExtension) { - val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val kotlin = project.extensions.getByType() val libName = extension.nativeLibName.get() val pkg = extension.nativePackage.get() @@ -100,34 +103,30 @@ class KotlinNativeExportPlugin : Plugin { val commonSources = project.files(if (commonMainDir.exists()) commonMainDir else null) // ── PSI parser classpath (kotlin-compiler-embeddable for isolated Worker classloader) ── - val kotlinVersion = kotlin.coreLibrariesVersion - val psiClasspath = project.configurations.create("knePsiClasspath") { - it.isCanBeConsumed = false - it.isCanBeResolved = true - it.isVisible = false + val knePsiScope = project.configurations.dependencyScope("knePsiScope").get() + val psiClasspath = project.configurations.resolvable("knePsiClasspath") { + extendsFrom(knePsiScope) + description = "Classpath for KNE PSI resolution" } - project.dependencies.add("knePsiClasspath", "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion") + project.dependencies.add(knePsiScope.name, project.kotlinEmbeddedCompiler) // ── Code-generation tasks ──────────────────────────────────────────── // Single task generates both native bridges and JVM proxies (PSI parsing + codegen in isolated worker) - val generateBridges = project.tasks.register( - "generateKneNativeBridges", - GenerateNativeBridgesTask::class.java, - ) { task -> - task.group = "kne" - task.description = "Generate Kotlin/Native bridges and JVM FFM proxies" - task.nativeSources.from(userNativeSources) - task.commonSources.from(commonSources) - task.libName.set(libName) - task.jvmPackage.set(pkg) - task.outputDir.set(nativeBridgesDir) - task.jvmOutputDir.set(jvmProxiesDir) - task.jvmResourcesDir.set(jvmResourcesDir) - task.psiClasspath.from(psiClasspath) + val generateBridges = project.tasks.register("generateKneNativeBridges") { + group = "kne" + description = "Generate Kotlin/Native bridges and JVM FFM proxies" + taskNativeSources.from(userNativeSources) + taskCommonSources.from(commonSources) + taskLibName.set(libName) + taskJvmPackage.set(pkg) + taskOutputDir.set(nativeBridgesDir) + taskJvmOutputDir.set(jvmProxiesDir) + taskJvmResourcesDir.set(jvmResourcesDir) + taskPsiClasspath.from(psiClasspath) } // Keep old task name as alias - project.tasks.register("generateKneJvmProxies") { it.dependsOn(generateBridges) } + project.tasks.register("generateKneJvmProxies") { dependsOn(generateBridges) } // ── Coroutines dependency (required for suspend function support) ── nativeTarget?.let { target -> @@ -158,28 +157,28 @@ class KotlinNativeExportPlugin : Plugin { kotlin.sourceSets.findByName(jvmMainSourceSetName)?.resources?.srcDir(jvmResourcesDir) // Ensure compilation waits for generation - project.tasks.configureEach { task -> + project.tasks.filter { task -> val name = task.name - if (name.startsWith("compileKotlin") && - (name.contains("Native", ignoreCase = true) || name.contains(nativeTargetTaskName, ignoreCase = true)) - ) { - task.dependsOn(generateBridges) - } - if (name == "compileKotlin$jvmTaskName" || name == "compileKotlin$jvmMainTaskName") { - task.dependsOn(generateBridges) - } + val isCompileKotlinOrNative = task.name.startsWith("compileKotlin") && + (task.name.contains("Native", ignoreCase = true) || task.name.contains( + nativeTargetTaskName, + ignoreCase = true + )) + + val isJvmTask = name == "compileKotlin$jvmTaskName" || name == "compileKotlin$jvmMainTaskName" + isJvmTask || isCompileKotlinOrNative + }.forEach { task -> + task.dependsOn(generateBridges) } // ── Native binaries (both debug + release, like swift-java) ────────── - kotlin.targets - .filterIsInstance() - .forEach { target -> - target.binaries.sharedLib( - namePrefix = libName, - buildTypes = listOf(NativeBuildType.DEBUG, NativeBuildType.RELEASE), - ) - } + kotlin.targets.filterIsInstance().forEach { target -> + target.binaries.sharedLib( + namePrefix = libName, + buildTypes = listOf(NativeBuildType.DEBUG, NativeBuildType.RELEASE), + ) + } // ── Bundle native lib into JVM resources (zero-config deployment) ──── @@ -199,11 +198,11 @@ class KotlinNativeExportPlugin : Plugin { val nativeLibResourceDir = project.layout.buildDirectory.dir("generated/kne/nativeLib") val buildDir = project.layout.buildDirectory - val copyNativeLib = project.tasks.register("copyKneNativeLib") { task -> - task.group = "kne" - task.description = "Copy native shared library into JVM resources for JAR bundling" - task.dependsOn(linkTaskName) - task.doLast { + val copyNativeLib = project.tasks.register("copyKneNativeLib") { + group = "kne" + description = "Copy native shared library into JVM resources for JAR bundling" + dependsOn(linkTaskName) + doLast { val releaseDir = buildDir .dir("bin/$targetAliasName/${libName}ReleaseShared").get().asFile val nativeFile = releaseDir.listFiles()?.firstOrNull { f -> @@ -213,7 +212,7 @@ class KotlinNativeExportPlugin : Plugin { val destDir = nativeLibResourceDir.get().asFile.resolve("kne/native/$platform") destDir.mkdirs() nativeFile.copyTo(destDir.resolve(nativeFile.name), overwrite = true) - task.logger.lifecycle("kne: Bundled ${nativeFile.name} → kne/native/$platform/") + logger.lifecycle("kne: Bundled ${nativeFile.name} → kne/native/$platform/") } } } @@ -222,9 +221,9 @@ class KotlinNativeExportPlugin : Plugin { kotlin.sourceSets.findByName(jvmMainSourceSetName)?.resources?.srcDir(nativeLibResourceDir) // Ensure processResources waits for the native lib copy - project.tasks.configureEach { task -> - if (task.name == "${jvmTarget.name}ProcessResources" || task.name == "process${jvmMainTaskName}Resources") { - task.dependsOn(copyNativeLib) + project.tasks.configureEach { + if (name == "${jvmTarget.name}ProcessResources" || name == "process${jvmMainTaskName}Resources") { + dependsOn(copyNativeLib) } } } @@ -283,10 +282,10 @@ class KotlinNativeExportPlugin : Plugin { val buildType = extension.buildType.get().replaceFirstChar { it.uppercaseChar() } val linkTaskName = "link${libCap}${buildType}Shared$targetCap" - project.tasks.withType(Test::class.java).configureEach { testTask -> - testTask.dependsOn(project.tasks.matching { it.name == linkTaskName }) - testTask.useJUnitPlatform() - testTask.jvmArgs( + project.tasks.withType().configureEach { + dependsOn(project.tasks.matching { it.name == linkTaskName }) + useJUnitPlatform() + jvmArgs( "-Djava.library.path=$libraryPath", "--enable-native-access=ALL-UNNAMED", ) diff --git a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/tasks/GenerateNativeBridgesTask.kt b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/tasks/GenerateNativeBridgesTask.kt index af15602..7f1744d 100644 --- a/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/tasks/GenerateNativeBridgesTask.kt +++ b/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/nna/plugin/tasks/GenerateNativeBridgesTask.kt @@ -4,68 +4,80 @@ import dev.nucleusframework.nna.plugin.analysis.PsiParseWorkAction import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property -import org.gradle.api.tasks.Classpath -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SkipWhenEmpty -import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.* import org.gradle.work.DisableCachingByDefault -import org.gradle.api.model.ObjectFactory import org.gradle.workers.WorkerExecutor import javax.inject.Inject @DisableCachingByDefault(because = "Bridge generation depends on source analysis that is not yet cacheable") abstract class GenerateNativeBridgesTask : DefaultTask() { - @get:Inject abstract val workerExecutor: WorkerExecutor - @get:Inject abstract val objectFactory: ObjectFactory + @get:Inject + abstract val taskWorkerExecutor: WorkerExecutor + + @get:Inject + abstract val taskObjectFactory: ObjectFactory + + @get:InputFiles + @get:SkipWhenEmpty + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val taskNativeSources: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val taskCommonSources: ConfigurableFileCollection - @get:InputFiles @get:SkipWhenEmpty @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val nativeSources: ConfigurableFileCollection + @get:Classpath + abstract val taskPsiClasspath: ConfigurableFileCollection - @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val commonSources: ConfigurableFileCollection + @get:Input + abstract val taskLibName: Property - @get:Classpath abstract val psiClasspath: ConfigurableFileCollection - @get:Input abstract val libName: Property - @get:Input abstract val jvmPackage: Property - @get:OutputDirectory abstract val outputDir: DirectoryProperty - @get:OutputDirectory abstract val jvmOutputDir: DirectoryProperty - @get:OutputDirectory abstract val jvmResourcesDir: DirectoryProperty + @get:Input + abstract val taskJvmPackage: Property + + @get:OutputDirectory + abstract val taskOutputDir: DirectoryProperty + + @get:OutputDirectory + abstract val taskJvmOutputDir: DirectoryProperty + + @get:OutputDirectory + abstract val taskJvmResourcesDir: DirectoryProperty @TaskAction fun generate() { - outputDir.get().asFile.apply { deleteRecursively(); mkdirs() } - jvmOutputDir.get().asFile.apply { deleteRecursively(); mkdirs() } + taskOutputDir.get().asFile.apply { deleteRecursively(); mkdirs() } + taskJvmOutputDir.get().asFile.apply { deleteRecursively(); mkdirs() } - val ktFiles = nativeSources.asFileTree.filter { it.extension == "kt" }.files - if (ktFiles.isEmpty()) { logger.lifecycle("kne: No Kotlin sources found, skipping."); return } + val ktFiles = taskNativeSources.asFileTree.filter { it.extension == "kt" }.files + if (ktFiles.isEmpty()) { + logger.lifecycle("kne: No Kotlin sources found, skipping."); return + } - val commonKtFiles = commonSources.asFileTree.filter { it.extension == "kt" }.files + val commonKtFiles = taskCommonSources.asFileTree.filter { it.extension == "kt" }.files logger.lifecycle("kne: Parsing ${ktFiles.size} native + ${commonKtFiles.size} common source file(s) [PSI]...") val pluginJarUrl = PsiParseWorkAction::class.java.protectionDomain?.codeSource?.location - val pluginJar = objectFactory.fileCollection().apply { + val pluginJar = taskObjectFactory.fileCollection().apply { pluginJarUrl?.let { from(java.io.File(it.toURI())) } } - val workQueue = workerExecutor.classLoaderIsolation { spec -> - spec.classpath.from(psiClasspath) - spec.classpath.from(pluginJar) + val workQueue = taskWorkerExecutor.classLoaderIsolation { + classpath.from(taskPsiClasspath) + classpath.from(pluginJar) } - workQueue.submit(PsiParseWorkAction::class.java) { params -> - params.nativeSourceFiles.from(ktFiles) - params.commonSourceFiles.from(commonKtFiles) - params.libName.set(libName) - params.jvmPackage.set(jvmPackage) - params.nativeBridgesDir.set(outputDir) - params.jvmProxiesDir.set(jvmOutputDir) - params.jvmResourcesDir.set(jvmResourcesDir) + workQueue.submit(PsiParseWorkAction::class.java) { + nativeSourceFiles.from(ktFiles) + commonSourceFiles.from(commonKtFiles) + libName.set(taskLibName) + jvmPackage.set(taskJvmPackage) + nativeBridgesDir.set(taskOutputDir) + jvmProxiesDir.set(taskJvmOutputDir) + jvmResourcesDir.set(taskJvmResourcesDir) } } }