diff --git a/docs/src/reference/module.md b/docs/src/reference/module.md index 446566cc8..236dd8175 100644 --- a/docs/src/reference/module.md +++ b/docs/src/reference/module.md @@ -668,6 +668,40 @@ settings: entryPoint: com.example.MainKt.main ``` +##### 'settings.native.cinterop' + +`settings:native:cinterop` configures C/Objective-C interop for native targets. + +Each cinterop module is defined by a name and supports the following attributes: + +| Attribute | Description | +|----------------|-------------------------------------------| +| `defFile` | Path to the Kotlin/Native definition (`.def`) file. | +| `packageName` | The Kotlin package name for the generated bindings. | +| `compilerOpts` | A list of flags to be passed to the C compiler (e.g., `-I/path/to/includes`). | +| `linkerOpts` | A list of C/C++ source files (`.c`, `.cpp`) to be compiled and/or flags to be passed to the linker (e.g., `-lm`). | + +Example: + +```yaml +# Configure cinterop for a native module +settings: + native: + cinterop: + hello: + defFile: cinterop/hello.def + packageName: com.example.hello + linkerOpts: + - cinterop/hello.c + libfoo: + defFile: external/libfoo/api.def + packageName: com.example.libfoo + compilerOpts: + - -I/usr/local/include + linkerOpts: + - -lfoo +``` + ### `settings.springBoot` `settings.springBoot` configures the Spring Boot framework (JVM platform only). diff --git a/docs/src/user-guide/advanced/native-interop.md b/docs/src/user-guide/advanced/native-interop.md new file mode 100644 index 000000000..214263b9f --- /dev/null +++ b/docs/src/user-guide/advanced/native-interop.md @@ -0,0 +1,51 @@ +# Native Interoperability with C + +Amper provides a powerful and flexible way to interoperate with C and Objective-C code in your native projects using the Kotlin/Native `cinterop` tool. This guide explains how to configure and use this feature. + +## How it Works + +The `cinterop` process in Amper allows you to create Kotlin bindings for C libraries by defining modules in your `module.yaml` file. +Each module references a `.def` file that describes the C API and can include additional compiler and linker options. + +## Configuration + +The `cinterop` settings are configured under `settings.native.cinterop` in your `module.yaml`. + +```yaml +settings: + native: + cinterop: + : + defFile: + packageName: + compilerOpts: + - + linkerOpts: + - + - +``` + +### API Details + +| Option | Description | +| :--- | :--- | +| `defFile` | Path to the Kotlin/Native definition (`.def`) file. **Optional** if the module is discovered by convention. | +| `packageName` | The Kotlin package name for the generated bindings. | +| `compilerOpts`| A list of flags to be passed to the C compiler (e.g., `-I/path/to/includes`). | +| `linkerOpts` | A list of C/C++ source files (`.c`, `.cpp`) to be compiled and/or flags to be passed to the linker (e.g., `-lm`). | + +### Example + +To create a cinterop module, define it in your module.yaml with the path to your .def file: + +**`module.yaml`:** +```yaml +settings: + native: + cinterop: + # Define a completely new 'bar' module + bar: + defFile: external/libbar/api.def + packageName: com.example.bar + linkerOpts: + - external/libbar/bar.c \ No newline at end of file diff --git a/examples/ktor-native-sample/module.yaml b/examples/ktor-native-sample/module.yaml new file mode 100644 index 000000000..92c701876 --- /dev/null +++ b/examples/ktor-native-sample/module.yaml @@ -0,0 +1,14 @@ +product: + type: linux/app + platforms: + - linuxX64 + +settings: + kotlin: + serialization: json + native: + entryPoint: org.jetbrains.amper.ktor.main + +dependencies: + - io.ktor:ktor-server-core:3.2.3 + - io.ktor:ktor-server-cio:3.2.3 diff --git a/examples/ktor-native-sample/src/Application.kt b/examples/ktor-native-sample/src/Application.kt new file mode 100644 index 000000000..7c9bf18c4 --- /dev/null +++ b/examples/ktor-native-sample/src/Application.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.amper.ktor + +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.cio.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun main() { + embeddedServer(CIO, port = 8080, host = "0.0.0.0", module = Application::module) + .start(wait = true) +} + +fun Application.module() { + configureRouting() +} diff --git a/examples/ktor-native-sample/src/Routing.kt b/examples/ktor-native-sample/src/Routing.kt new file mode 100644 index 000000000..af6d52948 --- /dev/null +++ b/examples/ktor-native-sample/src/Routing.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.amper.ktor + +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureRouting() { + routing { + get("/") { + call.respondText("Hello World!") + } + } +} diff --git a/examples/native-cinterop/module.yaml b/examples/native-cinterop/module.yaml new file mode 100644 index 000000000..ccca892f5 --- /dev/null +++ b/examples/native-cinterop/module.yaml @@ -0,0 +1,11 @@ +product: + type: linux/app + platforms: [ linuxX64 ] + +settings: + native: + entryPoint: 'org.jetbrains.amper.samples.cinterop.main' + cinterop: + hello: + defFile: 'resources/cinterop/hello.def' + packageName: 'org.jetbrains.amper.samples.cinterop.hello' \ No newline at end of file diff --git a/examples/native-cinterop/resources/cinterop/hello.def b/examples/native-cinterop/resources/cinterop/hello.def new file mode 100644 index 000000000..38e696dcc --- /dev/null +++ b/examples/native-cinterop/resources/cinterop/hello.def @@ -0,0 +1 @@ +headers = src/c/hello.c \ No newline at end of file diff --git a/examples/native-cinterop/src/c/hello.c b/examples/native-cinterop/src/c/hello.c new file mode 100644 index 000000000..277413f7c --- /dev/null +++ b/examples/native-cinterop/src/c/hello.c @@ -0,0 +1,5 @@ +#include + +void sayHello(const char* name) { + printf("Hello. %s from C!\n", name); +} diff --git a/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt new file mode 100644 index 000000000..93c88f368 --- /dev/null +++ b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt @@ -0,0 +1,9 @@ +package org.jetbrains.amper.samples.cinterop + +import kotlinx.cinterop.* +import org.jetbrains.amper.samples.cinterop.hello.* // Import the generated bindings + +fun main() { + println("Hello, John Doe from Kotlin!") + sayHello("John Doe") +} \ No newline at end of file diff --git a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt index 7f39aa4f5..8a246a516 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt @@ -17,6 +17,7 @@ import org.jetbrains.amper.jdk.provisioning.JdkProvider import org.jetbrains.amper.jvm.getDefaultJdk import org.jetbrains.amper.processes.ArgsMode import org.jetbrains.amper.processes.LoggingProcessOutputListener +import org.jetbrains.amper.processes.ProcessResult import org.jetbrains.amper.processes.runJava import org.jetbrains.amper.telemetry.setListAttribute import org.jetbrains.amper.telemetry.spanBuilder @@ -72,31 +73,7 @@ class KotlinNativeCompiler( logger.debug("konanc ${ShellQuoting.quoteArgumentsPosixShellWay(args)}") withKotlinCompilerArgFile(args, tempRoot) { argFile -> - val konanLib = kotlinNativeHome / "konan" / "lib" - - // We call konanc via java because the konanc command line doesn't support spaces in paths: - // https://youtrack.jetbrains.com/issue/KT-66952 - // TODO in the future we'll switch to kotlin tooling api and remove this raw java exec anyway - val result = processRunner.runJava( - jdk = jdk, - workingDir = kotlinNativeHome, - mainClass = "org.jetbrains.kotlin.cli.utilities.MainKt", - classpath = listOf( - konanLib / "kotlin-native-compiler-embeddable.jar", - konanLib / "trove4j.jar", - ), - programArgs = listOf("konanc", "@${argFile}"), - // JVM args partially copied from /bin/run_konan - argsMode = ArgsMode.ArgFile(tempRoot = tempRoot), - jvmArgs = listOf( - "-ea", - "-XX:TieredStopAtLevel=1", - "-Dfile.encoding=UTF-8", - "-Dkonan.home=$kotlinNativeHome", - ), - outputListener = LoggingProcessOutputListener(logger), - ) - + val result = processRunner.runNativeCommand("konanc", listOf("@$argFile"), ArgsMode.ArgFile(tempRoot)) // TODO this is redundant with the java span of the external process run. Ideally, we // should extract higher-level information from the raw output and use that in this span. span.setProcessResultAttributes(result) @@ -112,4 +89,61 @@ class KotlinNativeCompiler( } } } + + suspend fun cinterop( + processRunner: ProcessRunner, + args: List, + module: AmperModule, + ) { + spanBuilder("cinterop") + .setAmperModule(module) + .setListAttribute("args", args) + .setAttribute("version", kotlinVersion) + .use { span -> + logger.debug("cinterop ${ShellQuoting.quoteArgumentsPosixShellWay(args)}") + + val result = processRunner.runNativeCommand("cinterop", args, ArgsMode.CommandLine) + span.setProcessResultAttributes(result) + + if (result.exitCode != 0) { + val errors = result.stderr + .lines() + .filter { it.startsWith("error: ") || it.startsWith("exception: ") } + .joinToString("\n") + val errorsPart = if (errors.isNotEmpty()) ":\n\n$errors" else "" + userReadableError("Kotlin native 'cinterop' failed$errorsPart") + } + } + } + + private suspend fun ProcessRunner.runNativeCommand( + commandName: String, + commandArgs: List, + argsMode: ArgsMode + ): ProcessResult { + val konanLib = kotlinNativeHome / "konan" / "lib" + + // We call konanc via java because the konanc command line doesn't support spaces in paths: + // https://youtrack.jetbrains.com/issue/KT-66952 + // TODO in the future we'll switch to kotlin tooling api and remove this raw java exec anyway + return runJava( + jdk = jdk, + workingDir = kotlinNativeHome, + mainClass = "org.jetbrains.kotlin.cli.utilities.MainKt", + classpath = listOf( + konanLib / "kotlin-native-compiler-embeddable.jar", + konanLib / "trove4j.jar", + ), + programArgs = listOf(commandName) + commandArgs, + argsMode = argsMode, + // JVM args partially copied from /bin/run_konan + jvmArgs = listOf( + "-ea", + "-XX:TieredStopAtLevel=1", + "-Dfile.encoding=UTF-8", + "-Dkonan.home=$kotlinNativeHome", + ), + outputListener = LoggingProcessOutputListener(logger), + ) + } } diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt new file mode 100644 index 000000000..a7eaed912 --- /dev/null +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.amper.tasks.native + +import org.jetbrains.amper.ProcessRunner +import org.jetbrains.amper.compilation.downloadNativeCompiler +import org.jetbrains.amper.compilation.serializableKotlinSettings +import org.jetbrains.amper.core.AmperUserCacheRoot +import org.jetbrains.amper.core.extract.cleanDirectory +import org.jetbrains.amper.engine.BuildTask +import org.jetbrains.amper.engine.TaskGraphExecutionContext +import org.jetbrains.amper.frontend.AmperModule +import org.jetbrains.amper.frontend.Platform +import org.jetbrains.amper.frontend.TaskName +import org.jetbrains.amper.frontend.isDescendantOf +import org.jetbrains.amper.incrementalcache.IncrementalCache +import org.jetbrains.amper.jdk.provisioning.JdkProvider +import org.jetbrains.amper.tasks.TaskOutputRoot +import org.jetbrains.amper.tasks.TaskResult +import org.jetbrains.amper.tasks.artifacts.ArtifactTaskBase +import org.jetbrains.amper.util.BuildType +import org.slf4j.LoggerFactory +import java.nio.file.Path + +/** + * A task that runs the Kotlin/Native cinterop tool. + */ +internal class CinteropTask( + override val module: AmperModule, + override val platform: Platform, + private val userCacheRoot: AmperUserCacheRoot, + private val taskOutputRoot: TaskOutputRoot, + private val incrementalCache: IncrementalCache, + override val taskName: TaskName, + override val isTest: Boolean, + override val buildType: BuildType, + private val defFile: Path, + private val packageName: String?, + private val compilerOpts: List, + private val linkerOpts: List, + private val jdkProvider: JdkProvider, + private val processRunner: ProcessRunner, +): ArtifactTaskBase(), BuildTask { + init { + require(platform.isLeaf) + require(platform.isDescendantOf(Platform.NATIVE)) + } + + override suspend fun run(dependenciesResult: List, executionContext: TaskGraphExecutionContext): TaskResult { + // For now, we assume a single fragment. This might need to be adjusted. + val fragment = module.fragments.first { it.platforms.contains(platform) && it.isTest == isTest } + val kotlinUserSettings = fragment.serializableKotlinSettings() + + val configuration = mapOf( + "kotlin.version" to kotlinUserSettings.compilerVersion, + "def.file" to defFile.toString(), + "package.name" to packageName, + "compiler.opts" to compilerOpts.joinToString(" "), + "linker.opts" to linkerOpts.joinToString(" ") + ).mapNotNull { (k, v) -> v?.let { k to it } }.toMap() + val inputs = listOf(defFile) + linkerOpts.map { module.source.moduleDir.resolve(it) } + + val artifact = incrementalCache.execute(taskName.name, configuration, inputs) { + cleanDirectory(taskOutputRoot.path) + + val outputKLib = taskOutputRoot.path.resolve(defFile.toFile().nameWithoutExtension + ".klib") + + val nativeCompiler = downloadNativeCompiler(kotlinUserSettings.compilerVersion, userCacheRoot, jdkProvider) + val args = buildList { + add("-def") + add(defFile.toString()) + add("-o") + add(outputKLib.toString()) + add("-target") + add(platform.nameForCompiler) + if (packageName != null) { + add("-pkg") + add(packageName) + } + add("-compiler-option") + add("-I${module.source.moduleDir}") + compilerOpts.forEach { + add(it) + } + linkerOpts.forEach { + add("-linker-option") + add(it) + } + } + + logger.info("Running cinterop for '${defFile.fileName}'...") + nativeCompiler.cinterop(processRunner, args, module) + + return@execute IncrementalCache.ExecutionResult(listOf(outputKLib)) + }.outputFiles.singleOrNull() + + return Result( + compiledKlib = artifact, + taskName = taskName, + ) + } + + class Result( + val compiledKlib: Path?, + val taskName: TaskName, + ) : TaskResult + + private val logger = LoggerFactory.getLogger(javaClass) +} \ No newline at end of file diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt index 571421298..7d3402718 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt @@ -4,6 +4,7 @@ package org.jetbrains.amper.tasks.native +import com.intellij.util.applyIf import kotlinx.serialization.json.Json import org.jetbrains.amper.ProcessRunner import org.jetbrains.amper.cli.AmperProjectTempRoot @@ -98,11 +99,19 @@ internal class NativeCompileKlibTask( // todo native resources are what exactly? - val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings() + val cinteropKlibs = dependenciesResult + .filterIsInstance() + .mapNotNull { it.compiledKlib } + + val kotlinUserSettings = fragments.singleLeafFragment() + .serializableKotlinSettings() + .applyIf(cinteropKlibs.isNotEmpty()) { + copy(optIns = optIns + "kotlinx.cinterop.ExperimentalForeignApi") + } logger.debug("native compile klib '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}") - val libraryPaths = compiledModuleDependencies + externalDependencies + val libraryPaths = compiledModuleDependencies + externalDependencies + cinteropKlibs val additionalSources = additionalKotlinJavaSourceDirs.map { artifact -> SourceRoot( diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt index ac9aa2781..fa98a3a6b 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt @@ -107,9 +107,14 @@ internal class NativeLinkTask( val compiledKLibs = compileKLibDependencies.mapNotNull { it.compiledKlib } val exportedKLibs = exportedKLibDependencies.mapNotNull { it.compiledKlib } + val cinteropKLibs = dependenciesResult + .filterIsInstance() + .mapNotNull { it.compiledKlib } + val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings() - logger.debug("native link '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}") + logger.debug("Linking native binary for ${fragments.identificationPhrase()} with compiled klibs:\n" + + compiledKLibs.sorted().joinToString("\n").prependIndent(" ")) val entryPoints = if (module.type.isApplication()) { fragments.mapNotNull { it.settings.native?.entryPoint }.distinct() @@ -169,7 +174,7 @@ internal class NativeLinkTask( kotlinUserSettings = kotlinUserSettings, compilerPlugins = compilerPlugins, entryPoint = entryPoint, - libraryPaths = compiledKLibs + externalKLibs, + libraryPaths = compiledKLibs + externalKLibs + cinteropKLibs, exportedLibraryPaths = exportedKLibs, // no need to pass fragments nor sources, we only build from klibs fragments = emptyList(), diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt index 7e03564fb..5af0072d6 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt @@ -5,9 +5,11 @@ package org.jetbrains.amper.tasks.native import org.jetbrains.amper.compilation.KotlinCompilationType +import org.jetbrains.amper.compilation.singleLeafFragment import org.jetbrains.amper.dependency.resolution.ResolutionScope import org.jetbrains.amper.frontend.AmperModule import org.jetbrains.amper.frontend.Platform +import org.jetbrains.amper.frontend.TaskName import org.jetbrains.amper.frontend.isDescendantOf import org.jetbrains.amper.tasks.CommonTaskType import org.jetbrains.amper.tasks.PlatformTaskType @@ -17,6 +19,7 @@ import org.jetbrains.amper.tasks.getModuleDependencies import org.jetbrains.amper.tasks.ios.IosTaskType import org.jetbrains.amper.tasks.ios.ManageXCodeProjectTask import org.jetbrains.amper.util.BuildType +import kotlin.io.path.Path private fun isIosApp(platform: Platform, module: AmperModule) = platform.isDescendantOf(Platform.IOS) && module.type.isApplication() @@ -38,6 +41,32 @@ fun ProjectTasksBuilder.setupNativeTasks() { .alsoBuildTypes() .alsoTests() .withEach { + val fragment = module.fragments + .filter { it.platforms.contains(platform) && it.isTest == isTest } + .singleLeafFragment() + + val cinteropTasks = fragment.settings.native?.cinterop?.map { (moduleName, cinteropModule) -> + val defFile = cinteropModule.defFile + val cinteropTaskName = NativeTaskType.Cinterop.getTaskName(module, platform, isTest, buildType) + .let { TaskName(it.name + "-" + moduleName) } + CinteropTask( + module = module, + platform = platform, + userCacheRoot = context.userCacheRoot, + taskOutputRoot = context.getTaskOutputPath(cinteropTaskName), + incrementalCache = context.incrementalCache, + taskName = cinteropTaskName, + isTest = isTest, + buildType = buildType, + jdkProvider = context.jdkProvider, + defFile = module.source.moduleDir.resolve(defFile), + packageName = cinteropModule.packageName, + compilerOpts = cinteropModule.compilerOpts, + linkerOpts = cinteropModule.linkerOpts, + processRunner = context.processRunner + ).also { tasks.registerTask(it) } + } + val compileKLibTaskName = NativeTaskType.CompileKLib.getTaskName(module, platform, isTest, buildType) tasks.registerTask( task = NativeCompileKlibTask( @@ -55,6 +84,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { ), dependsOn = buildList { add(CommonTaskType.Dependencies.getTaskName(module, platform, isTest)) + cinteropTasks?.forEach { add(it.taskName) } if (isTest) { // todo (AB) : Check if this is required for test KLib compilation add(NativeTaskType.CompileKLib.getTaskName(module, platform, isTest = false, buildType)) @@ -95,6 +125,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { ), dependsOn = buildList { add(compileKLibTaskName) + cinteropTasks?.forEach { add(it.taskName) } add(CommonTaskType.Dependencies.getTaskName(module, platform, isTest)) if (isTest) { add(NativeTaskType.CompileKLib.getTaskName(module, platform, isTest = false, buildType)) @@ -199,4 +230,5 @@ private fun getNativeLinkTaskDetails( enum class NativeTaskType(override val prefix: String) : PlatformTaskType { CompileKLib("compile"), Link("link"), + Cinterop("cinterop"), } diff --git a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt index fbce98ba2..6d76e2fbf 100644 --- a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt +++ b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt @@ -256,6 +256,23 @@ class NativeSettings : SchemaNode() { // TODO other options from NativeApplicationPart @SchemaDoc("The fully-qualified name of the application's entry point function") val entryPoint by nullableValue() + + @SchemaDoc("C/Objective-C interop settings for native targets") + val cinterop: Map by value(default = emptyMap()) +} + +class CinteropModule : SchemaNode() { + @SchemaDoc("Path to the .def file for cinterop generation.") + val defFile by value() + + @SchemaDoc("The package name for the generated bindings.") + val packageName by nullableValue() + + @SchemaDoc("Options to be passed to the C compiler.") + val compilerOpts: List by value(default = emptyList()) + + @SchemaDoc("Options to be passed to the linker, and C/C++ source files to be compiled.") + val linkerOpts: List by value(default = emptyList()) } class KtorSettings: SchemaNode() { @@ -285,7 +302,7 @@ class SpringBootSettings: SchemaNode() { } class LombokSettings: SchemaNode() { - + @Shorthand @SchemaDoc("Enables Lombok") val enabled by value(default = false) diff --git a/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt b/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt new file mode 100644 index 000000000..9d04097df --- /dev/null +++ b/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt @@ -0,0 +1,127 @@ +package org.jetbrains.amper.frontend.aomBuilder + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.jetbrains.amper.frontend.FrontendPathResolver +import org.jetbrains.amper.frontend.schema.NativeSettings +import org.jetbrains.amper.frontend.schema.RootSchema +import org.jetbrains.amper.frontend.tree.appendDefaultValues +import org.jetbrains.amper.frontend.tree.discoverCinteropDefs +import org.jetbrains.amper.frontend.tree.reading.readTree +import org.jetbrains.amper.frontend.tree.refineTree +import org.jetbrains.amper.frontend.util.assertIs +import org.junit.Test + +class CinteropDiscoveryTest : BasePlatformTestCase() { + + private fun runTest( + moduleYamlContent: String, + setup: (FrontendPathResolver) -> Unit = {}, + assertions: (NativeSettings) -> Unit + ) { + val pathResolver = FrontendPathResolver(project) + setup(pathResolver) + val moduleFile = myFixture.tempDirFixture.createFile("my-module/module.yaml", moduleYamlContent) + + val buildCtx = BuildCtx(pathResolver, project) + val tree = with(buildCtx) { + readTree(moduleFile, schema.types.getType()) + .appendDefaultValues() + .discoverCinteropDefs() + } + val refinedTree = tree.refineTree(setOf("linuxX64"), buildCtx.schema.contexts) + val root = buildCtx.createSchemaNode(refinedTree) + + assertNotNull(root) + val nativeSettings = root!!.product.fragments.single().settings.assertIs() + assertions(nativeSettings) + } + + @Test + fun `cinterop module is discovered automatically`() { + runTest( + moduleYamlContent = """ + product: + type: lib + platforms: [linuxX64] + settings@linuxX64: + native: {} + """.trimIndent(), + setup = { + myFixture.tempDirFixture.findOrCreateDir("my-module/resources/cinterop") + myFixture.tempDirFixture.createFile("my-module/resources/cinterop/mydef.def") + }, + assertions = { nativeSettings -> + val cinterop = nativeSettings.cinterop + assertNotNull(cinterop) + val module = cinterop.modules["mydef"] + assertNotNull(module) + assertEquals("resources/cinterop/mydef.def", module!!.defFile) + } + ) + } + + @Test + fun `discovered and explicit modules are merged`() { + runTest( + moduleYamlContent = """ + product: + type: lib + platforms: [linuxX64] + settings@linuxX64: + native: + cinterop: + discovered: + linkerOpts: [ "src/c/discovered.c" ] + explicit: + defFile: "explicit.def" + """.trimIndent(), + setup = { + myFixture.tempDirFixture.findOrCreateDir("my-module/resources/cinterop") + myFixture.tempDirFixture.createFile("my-module/resources/cinterop/discovered.def") + }, + assertions = { nativeSettings -> + val cinterop = nativeSettings.cinterop + assertNotNull(cinterop) + assertEquals(2, cinterop.modules.size) + + val discoveredModule = cinterop.modules["discovered"] + assertNotNull(discoveredModule) + assertEquals("resources/cinterop/discovered.def", discoveredModule!!.defFile) + assertEquals(listOf("src/c/discovered.c"), discoveredModule.linkerOpts) + + val explicitModule = cinterop.modules["explicit"] + assertNotNull(explicitModule) + assertEquals("explicit.def", explicitModule!!.defFile) + } + ) + } + + @Test + fun `non-conventional module is configured correctly`() { + runTest( + moduleYamlContent = """ + product: + type: lib + platforms: [linuxX64] + settings@linuxX64: + native: + cinterop: + custom: + defFile: "custom/path/custom.def" + packageName: "com.example.custom" + compilerOpts: [ "-I/custom/include" ] + linkerOpts: [ "custom/path/custom.c" ] + """.trimIndent(), + assertions = { nativeSettings -> + val cinterop = nativeSettings.cinterop + assertNotNull(cinterop) + val module = cinterop.modules["custom"] + assertNotNull(module) + assertEquals("custom/path/custom.def", module!!.defFile) + assertEquals("com.example.custom", module.packageName) + assertEquals(listOf("-I/custom/include"), module.compilerOpts) + assertEquals(listOf("custom/path/custom.c"), module.linkerOpts) + } + ) + } +} \ No newline at end of file