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
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}

Expand All @@ -13,7 +13,8 @@ tasks.withType<DependencyUpdatesTask> {

fun String.isNonStable() = "^[0-9,.v-]+(-r)?$".toRegex().matches(this).not()

tasks.register("clean", Delete::class.java) {
tasks.register<Delete>("clean") {
description = "Delete the root project build directory"
delete(rootProject.layout.buildDirectory)
}

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
15 changes: 7 additions & 8 deletions plugin-build/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Delete>("clean") {
description = "Delete the root project build directory"
delete(rootProject.layout.buildDirectory)
}

Expand Down
10 changes: 3 additions & 7 deletions plugin-build/plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}

Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -33,10 +38,8 @@ import java.io.File
class KotlinNativeExportPlugin : Plugin<Project> {

override fun apply(project: Project) {
val extension = project.extensions.create(
"kotlinNativeExport",
KotlinNativeExportExtension::class.java,
)

val extension = project.extensions.create<KotlinNativeExportExtension>("kotlinNativeExport")

extension.nativeLibName.convention("nativelib")
extension.nativePackage.convention("")
Expand All @@ -48,7 +51,7 @@ class KotlinNativeExportPlugin : Plugin<Project> {
}

private fun configureKmp(project: Project, extension: KotlinNativeExportExtension) {
val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
val kotlin = project.extensions.getByType<KotlinMultiplatformExtension>()

val libName = extension.nativeLibName.get()
val pkg = extension.nativePackage.get()
Expand Down Expand Up @@ -100,34 +103,30 @@ class KotlinNativeExportPlugin : Plugin<Project> {
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<GenerateNativeBridgesTask>("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 ->
Expand Down Expand Up @@ -158,28 +157,28 @@ class KotlinNativeExportPlugin : Plugin<Project> {
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<KotlinNativeTarget>()
.forEach { target ->
target.binaries.sharedLib(
namePrefix = libName,
buildTypes = listOf(NativeBuildType.DEBUG, NativeBuildType.RELEASE),
)
}
kotlin.targets.filterIsInstance<KotlinNativeTarget>().forEach { target ->
target.binaries.sharedLib(
namePrefix = libName,
buildTypes = listOf(NativeBuildType.DEBUG, NativeBuildType.RELEASE),
)
}

// ── Bundle native lib into JVM resources (zero-config deployment) ────

Expand All @@ -199,11 +198,11 @@ class KotlinNativeExportPlugin : Plugin<Project> {
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 ->
Expand All @@ -213,7 +212,7 @@ class KotlinNativeExportPlugin : Plugin<Project> {
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/")
}
}
}
Expand All @@ -222,9 +221,9 @@ class KotlinNativeExportPlugin : Plugin<Project> {
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)
}
}
}
Expand Down Expand Up @@ -283,10 +282,10 @@ class KotlinNativeExportPlugin : Plugin<Project> {
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<Test>().configureEach {
dependsOn(project.tasks.matching { it.name == linkTaskName })
useJUnitPlatform()
jvmArgs(
"-Djava.library.path=$libraryPath",
"--enable-native-access=ALL-UNNAMED",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>

@get:Classpath abstract val psiClasspath: ConfigurableFileCollection
@get:Input abstract val libName: Property<String>
@get:Input abstract val jvmPackage: Property<String>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@get:OutputDirectory abstract val jvmOutputDir: DirectoryProperty
@get:OutputDirectory abstract val jvmResourcesDir: DirectoryProperty
@get:Input
abstract val taskJvmPackage: Property<String>

@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)
}
}
}
Loading