mirror of
https://github.com/apple/pkl.git
synced 2026-08-03 10:48:41 +02:00
* Fix possible SIGSEGV from host process. - Require that the same `pkl_exec_t` be used in the same OS thread; returning meaningful error if this fails * Remove `System.exitProcess(1)` logic in NativeTransport; this would kill the host process too and isn't an appropriate action for a received ProtocolException * Allow multiple calls to pkl_init without pkl_close; this limitation doesn't really make any sense * Prefer calling methods defined in graal_isolate.h, instead of creating the same method via CEntryPoint * Add a CMakeLists.txt so that CLion can follow the code and provide proper diagnostics * Improve doc comments; describe params as either in or out
535 lines
18 KiB
Kotlin
535 lines
18 KiB
Kotlin
/*
|
|
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
@file:Suppress("unused")
|
|
|
|
import org.apache.tools.ant.filters.ReplaceTokens
|
|
import org.gradle.kotlin.dsl.filter
|
|
|
|
plugins {
|
|
id("pklAllProjects")
|
|
id("pklGraalVm")
|
|
id("pklJavaLibrary")
|
|
id("pklNativeLifecycle")
|
|
}
|
|
|
|
val stagedMacAarch64NativeLibrary: Configuration =
|
|
configurations.create("stagedMacAarch64NativeLibrary")
|
|
val stagedLinuxAmd64NativeLibrary: Configuration =
|
|
configurations.create("stagedLinuxAmd64NativeLibrary")
|
|
val stagedLinuxAarch64NativeLibrary: Configuration =
|
|
configurations.create("stagedLinuxAarch64NativeLibrary")
|
|
val stagedAlpineLinuxAmd64NativeLibrary: Configuration =
|
|
configurations.create("stagedAlpineLinuxAmd64NativeLibrary")
|
|
val stagedWindowsAmd64NativeLibrary: Configuration =
|
|
configurations.create("stagedWindowsAmd64NativeLibrary")
|
|
|
|
val nativeTestSourceSet: SourceSet = sourceSets.create("nativeTest")
|
|
|
|
val nativeTestImplementation: Configuration =
|
|
configurations.getByName("nativeTestImplementation") {
|
|
extendsFrom(configurations.testImplementation.get())
|
|
}
|
|
|
|
val nativeTestRuntimeOnly: Configuration =
|
|
configurations.getByName("nativeTestRuntimeOnly") {
|
|
extendsFrom(configurations.testRuntimeOnly.get())
|
|
}
|
|
|
|
dependencies {
|
|
compileOnly(libs.graalSdk)
|
|
|
|
implementation(projects.pklCore)
|
|
implementation(projects.pklServer)
|
|
|
|
implementation(libs.msgpack)
|
|
implementation(libs.truffleApi)
|
|
implementation(libs.truffleRuntime)
|
|
|
|
nativeTestImplementation(projects.pklCommonsTest)
|
|
nativeTestImplementation(libs.jna)
|
|
nativeTestImplementation(libs.jnaPlatform)
|
|
nativeTestRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
|
}
|
|
|
|
tasks.withType(CCompile::class) {
|
|
if (buildInfo.targetMachine.os.isMacOS) {
|
|
arch = buildInfo.targetMachine.arch
|
|
}
|
|
}
|
|
|
|
private fun NativeImageBuild.configure(target: Target) {
|
|
arch = target.arch
|
|
|
|
if (target.arch == Target.Arch.AARCH64) {
|
|
dependsOn(":installGraalVmAarch64")
|
|
} else {
|
|
dependsOn(":installGraalVmAmd64")
|
|
}
|
|
|
|
outputDir = target.tempOutputDir
|
|
outputName = "libpkl_internal"
|
|
|
|
classpath.from(sourceSets.main.map { it.output })
|
|
classpath.from(
|
|
project(":pkl-commons-cli").extensions.getByType(SourceSetContainer::class)["svm"].output
|
|
)
|
|
classpath.from(configurations.runtimeClasspath)
|
|
|
|
sharedLibrary = true
|
|
val scriptName = if (buildInfo.os.isWindows) "build_windows.bat" else "build_unix.sh"
|
|
nativeCompilerPath = projectDir.resolve("scripts/${scriptName}")
|
|
|
|
if (!buildInfo.os.isWindows) {
|
|
// build_unix.sh extracts the object files of every archive it links against into a loose
|
|
// `objects/` dir so that `buildStaticLibrary` can merge them into a single, self-contained
|
|
// static library.
|
|
//
|
|
// Don't need to do this for Windows because it produces a merged archive.
|
|
additionalOutputDirectoryNames.add("objects")
|
|
}
|
|
}
|
|
|
|
val macNativeImageAarch64 =
|
|
tasks.register<NativeImageBuild>("macNativeImageAarch64") {
|
|
configure(Target.MacosAarch64)
|
|
// macOS only supports 16K page size
|
|
extraNativeImageArgs = buildList {
|
|
add("-H:PageSize=16384")
|
|
if (buildInfo.isCiBuild) {
|
|
add("-J-Xmx12g")
|
|
add("-H:+DeadlockWatchdogExitOnTimeout")
|
|
add("-H:DeadlockWatchdogInterval=10")
|
|
}
|
|
}
|
|
}
|
|
|
|
val linuxNativeImageAmd64 =
|
|
tasks.register<NativeImageBuild>("linuxNativeImageAmd64") { configure(Target.LinuxAmd64) }
|
|
|
|
val linuxNativeImageAarch64 =
|
|
tasks.register<NativeImageBuild>("linuxNativeImageAarch64") {
|
|
configure(Target.LinuxAarch64)
|
|
extraNativeImageArgs =
|
|
listOf(
|
|
// Ensure compatibility for kernels with page size set to 4k, 16k and 64k
|
|
// (e.g. Raspberry Pi 5, Asahi Linux)
|
|
"-H:PageSize=65536"
|
|
)
|
|
}
|
|
|
|
val alpineLinuxNativeImageAmd64 =
|
|
tasks.register<NativeImageBuild>("alpineLinuxNativeImageAmd64") {
|
|
configure(Target.AlpineLinuxAmd64)
|
|
extraNativeImageArgs.addAll("--libc=musl")
|
|
}
|
|
|
|
val windowsNativeImageAmd64 =
|
|
tasks.register<NativeImageBuild>("windowsNativeImageAmd64") {
|
|
configure(Target.WindowsAmd64)
|
|
extraNativeImageArgs.addAll("-Dfile.encoding=UTF-8", "-H:-CheckToolchain")
|
|
}
|
|
|
|
val buildNativeImageLibrary =
|
|
tasks.register("buildNativeImageLibrary") {
|
|
group = "build"
|
|
val underlyingTask =
|
|
when (buildInfo.targetMachine) {
|
|
Target.MacosAarch64 -> macNativeImageAarch64
|
|
Target.LinuxAarch64 -> linuxNativeImageAarch64
|
|
Target.LinuxAmd64 -> linuxNativeImageAmd64
|
|
Target.WindowsAmd64 -> windowsNativeImageAmd64
|
|
Target.AlpineLinuxAmd64 -> alpineLinuxNativeImageAmd64
|
|
}
|
|
dependsOn(underlyingTask)
|
|
outputs.files(underlyingTask)
|
|
}
|
|
|
|
val buildPklObjectFile =
|
|
tasks.register<CCompile>("buildPklObjectFile") {
|
|
dependsOn(buildNativeImageLibrary)
|
|
includeDirs.from(buildInfo.targetMachine.tempOutputDir)
|
|
includeDirs.from(file("src/main/c/include/"))
|
|
sourceFiles.from(file("src/main/c/pkl.c"))
|
|
positionIndependentCode = true
|
|
if (buildInfo.os.isWindows) {
|
|
// Match the dynamic CRT (ucrt/msvcrt) that native-image's bundled JDK objects expect -
|
|
// otherwise linking pkl.obj together with the merged libpkl_internal_s.lib archive hits a
|
|
// LIBCMT/MSVCRT default-lib conflict and unresolved __imp_* CRT import symbols.
|
|
runtimeLibrary = "MD"
|
|
}
|
|
|
|
outputDir =
|
|
layout.buildDirectory.dir("tmp/compileC/${name}/${buildInfo.targetMachine.targetName}")
|
|
|
|
val versionMacro =
|
|
if (buildInfo.targetMachine.os.isWindows) "\\\"${buildInfo.pklVersion}\\\""
|
|
else "\"${buildInfo.pklVersion}\""
|
|
defines.put("PKL_VERSION", versionMacro)
|
|
}
|
|
|
|
val buildStaticLibrary =
|
|
tasks.register<CArchive>("buildStaticLibrary") {
|
|
dependsOn(buildNativeImageLibrary, buildPklObjectFile)
|
|
|
|
val targetMachine = buildInfo.targetMachine
|
|
val objectExtension = targetMachine.os.objectFileExtension
|
|
val pklObjectFile = buildPklObjectFile.flatMap {
|
|
it.outputDir.map { dir -> dir.file("pkl.${objectExtension}") }
|
|
}
|
|
|
|
if (targetMachine.os.isWindows) {
|
|
// build_windows.bat produces a single self-contained merged archive
|
|
// (libpkl_internal_s.lib) directly, rather than extracting loose .obj members into an
|
|
// "objects" directory the way build_unix.sh does - pull that in instead.
|
|
objectFiles.from(
|
|
buildNativeImageLibrary.map { task ->
|
|
task.outputs.files.find {
|
|
it.name == "libpkl_internal_s.${targetMachine.os.staticLibraryExtension}"
|
|
}
|
|
}
|
|
)
|
|
} else {
|
|
objectFiles.from(
|
|
fileTree(targetMachine.tempOutputDir.get().dir("objects")).matching {
|
|
include("**/*.${objectExtension}")
|
|
}
|
|
)
|
|
}
|
|
objectFiles.from(pklObjectFile)
|
|
|
|
outputFile =
|
|
targetMachine.outputDir.map { dir ->
|
|
dir.file("lib/libpkl.${targetMachine.os.staticLibraryExtension}")
|
|
}
|
|
}
|
|
|
|
val buildSharedLibrary =
|
|
tasks.register<CCompile>("buildSharedLibrary") {
|
|
dependsOn(buildNativeImageLibrary, buildPklObjectFile)
|
|
|
|
link = true
|
|
val targetMachine = buildInfo.targetMachine
|
|
|
|
val libraryFile = buildPklObjectFile.flatMap { task ->
|
|
task.outputDir.map { dir -> dir.file("pkl.${targetMachine.os.objectFileExtension}") }
|
|
}
|
|
|
|
val extension = targetMachine.os.sharedLibraryExtension
|
|
val staticLib = buildNativeImageLibrary.map { task ->
|
|
// link against the self-contained static archive (produced by build_unix.sh /
|
|
// build_windows.bat) rather than the shared library, so libpkl doesn't carry a runtime
|
|
// dependency on a separate libpkl_internal shared library.
|
|
val fileName =
|
|
if (targetMachine.os.isWindows)
|
|
"libpkl_internal_s.${targetMachine.os.staticLibraryExtension}"
|
|
else "libpkl_internal.${targetMachine.os.staticLibraryExtension}"
|
|
task.outputs.files.find { it.name == fileName }
|
|
}
|
|
|
|
sourceFiles.from(libraryFile)
|
|
includeDirs.from(file("src/main/c/"))
|
|
libraryFiles.from(staticLib)
|
|
positionIndependentCode = true
|
|
sharedLibrary = true
|
|
|
|
outputFile = targetMachine.outputDir.map { it.file("lib/libpkl.${extension}") }
|
|
|
|
if (buildInfo.os.isMacOS) {
|
|
frameworks.addAll("Foundation", "CoreServices")
|
|
linkerFlags.addAll("-current_version", project.version.toString())
|
|
linkerFlags.addAll("-compatibility_version", "0.1.0")
|
|
|
|
// libpkl_internal_s bundles Native Image's own JNI-named implementations of JDK native
|
|
// methods (e.g. Java_sun_nio_ch_UnixFileDispatcherImpl_write0), which by default get
|
|
// exported with global visibility. Loading libpkl.dylib into a JVM process (e.g. via JNA)
|
|
// then lets the host JVM's own native-method resolution bind to these internal
|
|
// implementations instead of the JDK's real ones, silently corrupting unrelated file I/O.
|
|
// Restrict the dylib's exported symbols to just the public pkl_* API to prevent this.
|
|
val exportedSymbolsFile = file("src/main/c/pkl.exported_symbols")
|
|
inputs.file(exportedSymbolsFile)
|
|
linkerFlags.addAll("-exported_symbols_list", exportedSymbolsFile.absolutePath)
|
|
}
|
|
|
|
if (targetMachine.os.isWindows) {
|
|
// Without an explicit /IMPLIB, MSVC would name the DLL's import library "libpkl.lib" —
|
|
// the same path buildStaticLibrary writes its static archive to.
|
|
linkerFlags.add(
|
|
targetMachine.libraryDir.map { dir -> "/IMPLIB:${dir.file("libpkl_dll.lib").asFile}" }
|
|
)
|
|
} else {
|
|
libraries.add("z")
|
|
libraries.add("pthread")
|
|
}
|
|
|
|
if (targetMachine.os.isLinux) {
|
|
libraries.add("dl")
|
|
|
|
// Same symbol-collision hazard as the macOS case above (see comment there), but scoped via
|
|
// a GNU ld version script instead of an exported-symbols list.
|
|
val versionScriptFile = file("src/main/c/pkl.map")
|
|
inputs.file(versionScriptFile)
|
|
linkerFlags.add("--version-script=${versionScriptFile.absolutePath}")
|
|
}
|
|
}
|
|
|
|
val Target.outputDir
|
|
get() = layout.buildDirectory.dir("native-libs/$targetName")
|
|
|
|
val Target.libraryDir
|
|
get() = layout.buildDirectory.dir("native-libs/$targetName/lib")
|
|
|
|
val Target.tempOutputDir
|
|
get() = layout.buildDirectory.dir("tmp/native-libs/$targetName")
|
|
|
|
val distTar =
|
|
tasks.register<Tar>("distTar") {
|
|
val isUnix = buildInfo.os.isMacOS || buildInfo.os.isLinux
|
|
onlyIf { isUnix }
|
|
dependsOn(buildSharedLibrary, buildStaticLibrary, processFiles)
|
|
compression = Compression.GZIP
|
|
val baseName = "libpkl-${buildInfo.pklVersionNonUnique}-${buildInfo.targetMachine.targetName}"
|
|
archiveFileName = "${baseName}.tar.gz"
|
|
from(buildInfo.targetMachine.outputDir)
|
|
into(baseName)
|
|
}
|
|
|
|
val distZip =
|
|
tasks.register<Zip>("distZip") {
|
|
val isWindows = buildInfo.os.isWindows
|
|
onlyIf { isWindows }
|
|
dependsOn(buildSharedLibrary, buildStaticLibrary, processFiles)
|
|
val baseName = "libpkl-${buildInfo.pklVersionNonUnique}-${buildInfo.targetMachine.targetName}"
|
|
archiveFileName = "${baseName}.zip"
|
|
from(buildInfo.targetMachine.outputDir)
|
|
into(baseName)
|
|
}
|
|
|
|
tasks.assembleNative { dependsOn(distZip, distTar) }
|
|
|
|
val processFiles =
|
|
tasks.register<Copy>("processFiles") {
|
|
inputs.property("version", buildInfo.pklVersion)
|
|
inputs.dir("src/main/c")
|
|
inputs.dir("src/main/files")
|
|
from(fileTree("src/main/files"))
|
|
from(fileTree("src/main/c") { exclude("*.{c,map,exported_symbols}") })
|
|
includeEmptyDirs = true
|
|
into(buildInfo.targetMachine.outputDir)
|
|
|
|
filesMatching("libpkl.pc") {
|
|
filter<ReplaceTokens>("tokens" to mapOf("version" to buildInfo.pklVersion))
|
|
}
|
|
}
|
|
|
|
val testNativeJava =
|
|
tasks.register<Test>("testNativeJava") {
|
|
dependsOn(tasks.assembleNative)
|
|
|
|
description = "Test native libraries from Java"
|
|
group = "verification"
|
|
|
|
testClassesDirs = nativeTestSourceSet.output.classesDirs
|
|
classpath = nativeTestSourceSet.runtimeClasspath
|
|
|
|
// It's not good enough to just provide `jna.library.path` on Linux; need to also provide
|
|
// `LD_LIBRARY_PATH` or `java.library.path` so that transitive libraries can be loaded.
|
|
jvmArgumentProviders.add(
|
|
CommandLineArgumentProvider {
|
|
listOf(
|
|
"-Djna.library.path=" + buildInfo.targetMachine.libraryDir.get().asFile.absolutePath,
|
|
"-Djava.library.path=" + buildInfo.targetMachine.libraryDir.get().asFile.absolutePath,
|
|
"--enable-native-access=ALL-UNNAMED",
|
|
)
|
|
}
|
|
)
|
|
|
|
environment("LD_LIBRARY_PATH", buildInfo.targetMachine.libraryDir.get().asFile.absolutePath)
|
|
|
|
useJUnitPlatform()
|
|
}
|
|
|
|
// link to static library
|
|
val compileNativeTestStatic =
|
|
tasks.register<CCompile>("compileNativeTestStatic") {
|
|
description = "Build C test with static library"
|
|
group = "verification"
|
|
|
|
dependsOn(tasks.assembleNative)
|
|
|
|
val libDir = buildInfo.targetMachine.libraryDir.get().asFile
|
|
val testSrc = file("src/nativeTest/c/test_pkl.c")
|
|
|
|
link = true
|
|
sourceFiles.from(testSrc)
|
|
includeDirs.from(buildInfo.targetMachine.outputDir.map { it.file("include") })
|
|
// Match the dynamic CRT (ucrt/msvcrt) that libpkl.lib's bundled JDK objects expect -
|
|
// otherwise this defaults to the static CRT (LIBCMT), causing a defaultlib conflict and
|
|
// unresolved __imp_* CRT import symbols, same as buildPklObjectFile.
|
|
if (buildInfo.os.isWindows) {
|
|
runtimeLibrary = "MD"
|
|
}
|
|
outputFile =
|
|
layout.buildDirectory.file(
|
|
"native-test/${buildInfo.targetMachine.targetName}/test_pkl_static"
|
|
)
|
|
|
|
libraryFiles.from("${libDir.absolutePath}/libpkl.${buildInfo.os.staticLibraryExtension}")
|
|
|
|
if (!buildInfo.os.isWindows) {
|
|
libraries.add("pthread")
|
|
libraries.add("z")
|
|
}
|
|
|
|
if (buildInfo.os.isLinux) {
|
|
libraries.add("dl")
|
|
}
|
|
|
|
if (buildInfo.os.isMacOS) {
|
|
frameworks.addAll("Foundation", "CoreServices")
|
|
}
|
|
}
|
|
|
|
// link to dynamic library
|
|
val compileNativeTestDynamic =
|
|
tasks.register<CCompile>("compileNativeTestDynamic") {
|
|
description = "Build C test with dynamic library"
|
|
group = "verification"
|
|
|
|
dependsOn(tasks.assembleNative)
|
|
|
|
val testSrc = file("src/nativeTest/c/test_pkl.c")
|
|
|
|
link = true
|
|
sourceFiles.from(testSrc)
|
|
includeDirs.from(buildInfo.targetMachine.outputDir.map { it.file("include") })
|
|
outputFile =
|
|
layout.buildDirectory.file(
|
|
"native-test/${buildInfo.targetMachine.targetName}/test_pkl_dynamic"
|
|
)
|
|
|
|
libraryPaths.from(buildInfo.targetMachine.libraryDir)
|
|
|
|
if (buildInfo.os.isWindows) {
|
|
// "pkl" would resolve to libpkl.lib, which is buildStaticLibrary's static archive, not
|
|
// the DLL's import library — link against the import library directly instead.
|
|
libraryFiles.from(buildInfo.targetMachine.libraryDir.map { it.file("libpkl_dll.lib") })
|
|
} else {
|
|
libraries.add("pkl")
|
|
libraries.add("z")
|
|
libraries.add("pthread")
|
|
// Bake the library's location into the test executable so the dynamic loader can find
|
|
// libpkl.so/libpkl.dylib without needing LD_LIBRARY_PATH/DYLD_LIBRARY_PATH set at run time.
|
|
linkerFlags.add("-rpath")
|
|
linkerFlags.add(buildInfo.targetMachine.libraryDir.get().asFile.absolutePath)
|
|
}
|
|
|
|
if (buildInfo.os.isLinux) {
|
|
libraries.add("dl")
|
|
}
|
|
}
|
|
|
|
val buildNativeTestC =
|
|
tasks.register("buildNativeTestC") {
|
|
description = "Build both static and dynamic C tests"
|
|
group = "verification"
|
|
|
|
dependsOn(compileNativeTestStatic, compileNativeTestDynamic)
|
|
}
|
|
|
|
fun Task.configureDummyOutput() {
|
|
group = "verification"
|
|
|
|
// dummy output to satisfy up-to-date check
|
|
val outputFile = layout.buildDirectory.file(name)
|
|
outputs.file(outputFile)
|
|
|
|
doFirst { outputFile.get().asFile.delete() }
|
|
doLast { outputFile.get().asFile.writeText("OK") }
|
|
}
|
|
|
|
val testNativeCStatic =
|
|
tasks.register<Exec>("testNativeCStatic") {
|
|
configureDummyOutput()
|
|
group = "verification"
|
|
dependsOn(compileNativeTestStatic)
|
|
|
|
val testDir = layout.buildDirectory.dir("native-test").get().asFile
|
|
|
|
workingDir = projectDir
|
|
commandLine(compileNativeTestStatic.get().outputFile.get().asFile.absolutePath)
|
|
}
|
|
|
|
val testNativeCDynamic =
|
|
tasks.register<Exec>("testNativeCDynamic") {
|
|
configureDummyOutput()
|
|
group = "verification"
|
|
|
|
dependsOn(compileNativeTestDynamic)
|
|
|
|
val testDir = layout.buildDirectory.dir("native-test").get().asFile
|
|
|
|
workingDir = projectDir
|
|
commandLine(compileNativeTestDynamic.get().outputFile.get().asFile.absolutePath)
|
|
|
|
if (buildInfo.os.isWindows) {
|
|
// Windows has no rpath equivalent — the DLL loader only searches the exe's own
|
|
// directory and PATH, so libpkl.dll must be made findable via PATH.
|
|
doFirst {
|
|
environment(
|
|
"PATH",
|
|
"${buildInfo.targetMachine.libraryDir.get().asFile.absolutePath};${System.getenv("PATH")}",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.testNative {
|
|
dependsOn(testNativeCStatic, testNativeCDynamic)
|
|
|
|
// can't run libraries from other arch in Java
|
|
if (!buildInfo.isCrossArch) {
|
|
dependsOn(testNativeJava)
|
|
}
|
|
}
|
|
|
|
spotless {
|
|
cpp {
|
|
licenseHeaderFile(
|
|
rootProject.file("build-logic/src/main/resources/license-header.star-block.txt"),
|
|
"// ",
|
|
)
|
|
target("src/*/c/**/*.c", "src/*/c/**/*.h")
|
|
eclipseCdt(libs.versions.eclipseCdtFormat.get())
|
|
}
|
|
shell {
|
|
licenseHeaderFile(
|
|
rootProject.file("build-logic/src/main/resources/license-header.hash-comment.txt"),
|
|
"## build_",
|
|
)
|
|
// skip shebang
|
|
.skipLinesMatching("^#!.+?$")
|
|
|
|
target("scripts/*.sh")
|
|
}
|
|
format("bat") {
|
|
target("scripts/*.bat")
|
|
licenseHeaderFile(
|
|
rootProject.file("build-logic/src/main/resources/license-header.batch-script.txt"),
|
|
"@echo",
|
|
)
|
|
}
|
|
}
|