Kotlin/Native requires that C libraries are compiled with a specific verison of GCC.
You can use the GCC used by Kotlin/Native itself. These Gradle tasks will help with this.
// build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.konan.target.HostManager
plugins {
kotlin("multiplatform") version "1.9.22"
}
kotlin {
linuxArm64()
linuxX64()
mingwX64()
macosArm64()
macosX64()
}
val zphysicsSrcPrep by tasks.registering {
// download zphysics source to src/main/cpp/ & src/main/cppHeaders/
}
val konanClangCompileJolt by tasks.registering(RunKonanClangTask::class) {
dependsOn(zphysicsSrcPrep)
kotlinTarget = HostManager.host
sourceFiles.from(
layout.projectDirectory
.dir("src/main/cpp/")
.asFileTree
.matching {
include("**/*.cpp")
exclude("**/*_Tests*")
}
)
includeDirs.from(layout.projectDirectory.dir("src/main/cppHeaders/"))
arguments.addAll(
"-std=c++17",
"-fno-sanitize=undefined",
"-D" + "JPH_CROSS_PLATFORM_DETERMINISTIC",
"-D" + "JPH_ENABLE_ASSERTS",
)
runKonan = runKonanFile()
}
val konanLinkJolt by tasks.registering(RunKonanLinkTask::class) {
group = project.name
libName = "jolt"
objectFiles.from(konanClangCompileJolt)
runKonan = runKonanFile()
}
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.PathSensitivity.NAME_ONLY
import org.gradle.api.tasks.PathSensitivity.RELATIVE
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.util.parseSpaceSeparatedArgs
import javax.inject.Inject
import kotlin.io.path.copyTo
/**
* Compile C/C++ source files using the
* [`run_konan`](https://github.com/JetBrains/kotlin/blob/v1.9.0/kotlin-native/HACKING.md#running-clang-the-same-way-kotlinnative-compiler-does)
* utility.
*/
@CacheableTask
abstract class RunKonanClangTask @Inject constructor(
private val exec: ExecOperations,
private val fs: FileSystemOperations,
private val objects: ObjectFactory,
) : DefaultTask() {
/** Destination of compiled `.o` object files */
@get:OutputDirectory
val outputDir: Provider<Directory>
get() = objects.directoryProperty().fileValue(temporaryDir.resolve("output"))
/** C and C++ source files to compile to object files */
@get:InputFiles
@get:PathSensitive(RELATIVE)
abstract val sourceFiles: ConfigurableFileCollection
/** Directories that include `.h` header files */
@get:InputFiles
@get:PathSensitive(RELATIVE)
abstract val includeDirs: ConfigurableFileCollection
/** Path to the (platform specific) `run_konan` utility */
@get:InputFile
@get:PathSensitive(NAME_ONLY)
abstract val runKonan: RegularFileProperty
/** Kotlin target platform, e.g. `mingw_x64` */
@get:Input
abstract val kotlinTarget: Property<KonanTarget>
@get:Input
@get:Optional
abstract val arguments: ListProperty<String>
@get:Internal
val workingDir: DirectoryProperty =
objects.directoryProperty().convention(
// workaround for https://github.com/gradle/gradle/issues/23708
objects.directoryProperty().fileValue(temporaryDir)
)
@TaskAction
fun compile() {
val workingDir = workingDir.asFile.get()
val kotlinTarget = kotlinTarget.get()
// prepare output dir
val compileDir = workingDir.resolve("compile")
fs.delete { delete(compileDir) }
compileDir.mkdirs()
// prepare args file
val includeDirsArgs = includeDirs
.filter { it.isDirectory }
.joinToString("\n") { headersDir ->
/* language=text */ """
|--include-directory ${headersDir.invariantSeparatorsPath}
""".trimMargin()
}
val arguments = arguments.getOrElse(emptyList()).joinToString("\n")
val sourceFilePaths = sourceFiles
.asFileTree
.files
.filter { it.isFile && it.extension in listOf("cpp", "c") }
.joinToString("\n") { it.invariantSeparatorsPath }
compileDir.resolve("args").writeText(/*language=text*/ """
|$includeDirsArgs
|
|$arguments
|
|-c $sourceFilePaths
""".trimMargin()
)
// compile files
val result = exec.execCapture {
executable(runKonan.asFile.get())
args(parseSpaceSeparatedArgs(
"clang clang $kotlinTarget @args"
))
workingDir(workingDir)
}
val outputLog = workingDir.resolve("compileResult.log")
result.logFile.copyTo(outputLog.toPath(), overwrite = true)
logger.lifecycle("compilation output log: file://${outputLog}")
result.assertNormalExitValue()
// move compiled files to output directory
fs.sync {
from(compileDir) {
include("**/*.o")
}
into(outputDir)
}
}
}
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.util.parseSpaceSeparatedArgs
import javax.inject.Inject
import kotlin.io.path.copyTo
/**
* Link compiled C/C++ source files into a static lib using the
* [`run_konan`](https://github.com/JetBrains/kotlin/blob/v1.9.0/kotlin-native/HACKING.md#running-clang-the-same-way-kotlinnative-compiler-does)
* utility.
*/
@CacheableTask
abstract class RunKonanLinkTask @Inject constructor(
private val exec: ExecOperations,
private val fs: FileSystemOperations,
objects: ObjectFactory,
) : DefaultTask() {
/** The linked file */
@get:OutputFile
val compiledLib: Provider<RegularFile>
get() = workingDir.file(libFileName)
/** All `.o` object files that will be linked */
@get:InputFiles
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val objectFiles: ConfigurableFileCollection
@get:Input
@get:Optional
abstract val arguments: ListProperty<String>
/** Path to the (platform specific) `run_konan` utility */
@get:InputFile
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val runKonan: RegularFileProperty
@get:Input
abstract val libName: Property<String>
private val libFileName: Provider<String>
get() = libName.map { "lib${it}.a" }
@get:Internal
val workingDir: DirectoryProperty =
objects.directoryProperty().convention(
// workaround for https://github.com/gradle/gradle/issues/23708
objects.directoryProperty().fileValue(temporaryDir)
)
@TaskAction
fun compile() {
val workingDir = workingDir.asFile.get()
val libFileName = libFileName.get()
// prepare output dir
fs.delete { delete(workingDir) }
workingDir.mkdirs()
// prepare args file
val sourceFilePaths = objectFiles
.asFileTree
.matching { include("**/*.o") }
.joinToString("\n") { it.invariantSeparatorsPath }
val arguments = arguments.getOrElse(emptyList()).joinToString("\n")
workingDir.resolve("args").writeText(/*language=text*/ """
|-rv
|$libFileName
|
|$arguments
|
|$sourceFilePaths
""".trimMargin()
)
// compile files
val result = exec.execCapture {
executable(runKonan.asFile.get())
args(parseSpaceSeparatedArgs(
"llvm llvm-ar @args"
))
workingDir(workingDir)
}
val outputLog = workingDir.resolve("linkResult.log")
result.logFile.copyTo(outputLog.toPath(), overwrite = true)
logger.lifecycle("linking output log: file://${outputLog}")
result.assertNormalExitValue()
}
}
import org.gradle.process.ExecOperations
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.outputStream
fun ExecOperations.execCapture(
configure: ExecSpec.() -> Unit,
): ExecCaptureResult {
val logFile = Files.createTempFile("gradle_exec", "log")
val result = logFile.outputStream().use { os ->
exec {
isIgnoreExitValue = true
standardOutput = os
errorOutput = os
configure()
}
}
return ExecCaptureResult(
logFile = logFile,
result = result
)
}
class ExecCaptureResult(
val logFile: Path,
private val result: ExecResult,
) : ExecResult by result {
val exitCodeSuccess: Boolean get() = result.exitValue == 0
}
/** The `run_konan` or `run_konan.bat` file used to compile/link C/C++ sources. */
fun Project.runKonanFile(): Provider<RegularFile> {
return layout.file(
provider {
val allRunKonanFiles = DependencyDirectories.localKonanDir.walk()
.filter { it.isFile && it.nameWithoutExtension == "run_konan" }
val currentOsName = HostManager.simpleOsName()
val currentArch = HostManager.hostArch()
val currentKotlinVersion = kotlinToolingVersion
val runKonanFile = allRunKonanFiles
.lastOrNull { runKonan ->
val path = runKonan.invariantSeparatorsPath
path.contains(currentOsName, ignoreCase = true)
&& path.contains(currentArch, ignoreCase = true)
&& path.contains(currentKotlinVersion.toString(), ignoreCase = true)
}
if (runKonanFile == null) {
val allOptions = allRunKonanFiles.joinToString("\n") { " - $it" }
error("couldn't find run_konan or run_konan.bat for $currentOsName/$currentArch/$currentKotlinVersion - all options:\n$allOptions}")
}
logger.lifecycle("[project ${project.path}] using ${runKonanFile.invariantSeparatorsPath} for Konan compilation")
runKonanFile
}
)
}