Files
pkl/libpkl/libpkl.gradle.kts
Daniel ChaoandGitHub 417c08ae11 Fix various issues in libpkl (#1805)
* 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
2026-07-31 16:23:37 +00:00

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",
)
}
}