Introduce C library for Pkl (#1238)

This uses native-image to generate a C library for Pkl.
This generated library from native-image is wrapped with our own library,
in `pkl.h`.

This produces a static and a dynamic library for each os/arch variant
that Pkl currently supports.

Co-authored-by: Kushal Pisavadia <kushal.p@apple.com>
Co-authored-by: Jen Basch <jbasch94@gmail.com>
Co-authored-by: Islon Scherer <i_desouzascherer@apple.com>
This commit is contained in:
Daniel Chao
2026-07-25 04:15:26 +00:00
committed by GitHub
co-authored by Kushal Pisavadia Jen Basch Islon Scherer
parent 175e2b6273
commit 67df676359
51 changed files with 4982 additions and 349 deletions
+25 -12
View File
@@ -15,7 +15,10 @@
*/
@file:Suppress("MemberVisibilityCanBePrivate")
import Target.Arch
import Target.OS
import java.io.File
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
@@ -102,29 +105,31 @@ open class BuildInfo(private val project: Project) {
val installDir: File by lazy { File(homeDir, baseName) }
val baseDir: String by lazy {
if (os.isMacOsX) "$installDir/Contents/Home" else installDir.toString()
if (os.isMacOS) "$installDir/Contents/Home" else installDir.toString()
}
}
/** The target machine to build, defaulting to the host system machine. */
val targetMachine: Target by lazy { Target.from(os = os, arch = targetArch, musl = musl) }
/** The target architecture to build, defaulting to the system architecture. */
val targetArch by lazy { System.getProperty("pkl.targetArch") ?: arch }
val targetArch: Arch by lazy { System.getProperty("pkl.targetArch")?.let(Arch::fromName) ?: arch }
/** Tells if this is a cross-arch build (e.g. targeting amd64 when on an aarch64 machine). */
val isCrossArch by lazy { arch != targetArch }
/** Tells if cross-arch builds are supported on this machine. */
val isCrossArchSupported by lazy { os.isMacOsX }
val isCrossArchSupported by lazy { os.isMacOS }
/** Whether to build native executables using the musl toolchain or not. */
val musl: Boolean by lazy { java.lang.Boolean.getBoolean("pkl.musl") }
/** Same logic as [org.gradle.internal.os.OperatingSystem#arch], which is protected. */
val arch: String by lazy {
val arch: Arch by lazy {
when (val arch = System.getProperty("os.arch")) {
"x86" -> "i386"
"x86_64" -> "amd64"
"powerpc" -> "ppc"
else -> arch
"x86_64",
"amd64" -> Arch.AMD64
"aarch64" -> Arch.AARCH64
else -> throw GradleException("Cannot build Pkl on arch: $arch")
}
}
@@ -135,10 +140,10 @@ open class BuildInfo(private val project: Project) {
private fun createGraalVm(arch: String): GraalVm {
val osName =
when {
os.isMacOsX -> "macos"
os.isMacOS -> "macos"
os.isLinux -> "linux"
os.isWindows -> "windows"
else -> throw RuntimeException("Unsupported OS for GraalVM: ${os.canonicalName}")
else -> throw RuntimeException("Unsupported OS for GraalVM: ${os.name}")
}
val version = libs.findVersion("graalVm").get().toString()
val graalJdkVersion = libs.findVersion("graalVmJdkVersion").get().toString()
@@ -378,7 +383,15 @@ open class BuildInfo(private val project: Project) {
File(System.getProperty("user.home"), "staticdeps/bin/x86_64-linux-musl-gcc").exists()
}
val os: OperatingSystem by lazy { OperatingSystem.current() }
val os: OS by lazy {
val currentOs = OperatingSystem.current()
when {
currentOs.isMacOsX -> OS.MacOS
currentOs.isLinux -> OS.Linux
currentOs.isWindows -> OS.Windows
else -> throw GradleException("Cannot build on ${currentOs.name}")
}
}
// could be `commitId: Provider<String> = project.provider { ... }`
val commitId: String by lazy {
+89
View File
@@ -0,0 +1,89 @@
/*
* 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.
*/
import Target.OS
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
/**
* Task for creating static libraries from object files.
*
* Supports both Unix `ar` and Windows `lib.exe`.
*/
abstract class CArchive : DefaultTask() {
/** The object files to archive. */
@get:InputFiles abstract val objectFiles: ConfigurableFileCollection
/** The output static library file. */
@get:OutputFile abstract val outputFile: RegularFileProperty
@get:Inject protected abstract val execOperations: ExecOperations
private val buildInfo: BuildInfo = project.extensions.getByType(BuildInfo::class.java)
init {
group = "build"
}
@TaskAction
fun archive() {
outputFile.get().asFile.delete()
when (buildInfo.os) {
OS.Linux,
OS.MacOS -> archiveWithAr()
OS.Windows -> archiveWithLib()
}
logger.info("Created static library -> ${outputFile.get().asFile.name}")
}
private fun archiveWithAr() {
val output = outputFile.get().asFile
output.parentFile.mkdirs()
val args = buildList {
add("ar")
add("-rcs")
add(output.absolutePath)
objectFiles.files.forEach { file -> add(file.absolutePath) }
}
execOperations.exec {
workingDir = project.projectDir
commandLine(args)
}
}
private fun archiveWithLib() {
val output = outputFile.get().asFile
output.parentFile.mkdirs()
val args = buildList {
add("lib.exe")
add("/OUT:${output.absolutePath}")
objectFiles.files.forEach { file -> add(file.absolutePath) }
}
execOperations.exec {
workingDir = project.projectDir
commandLine(args)
}
}
}
+410
View File
@@ -0,0 +1,410 @@
/*
* 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.
*/
import Target.OS
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
/**
* Task for compiling C source files to object files or executables. Supports both GCC/Clang and
* MSVC compilers.
*/
abstract class CCompile : DefaultTask() {
/** The C source files to compile. */
@get:InputFiles abstract val sourceFiles: ConfigurableFileCollection
/** The include directories for compilation. */
@get:InputFiles abstract val includeDirs: ConfigurableFileCollection
/** Preprocessor definitions. */
@get:Input @get:Optional abstract val defines: MapProperty<String, String>
/** C standard version (e.g., "c11", "c17", "gnu11"). */
@get:Input @get:Optional abstract val cStandard: Property<String>
/** Optimization level (e.g., "0", "1", "2", "3", "s", "fast"). */
@get:Input @get:Optional abstract val optimizationLevel: Property<String>
/** Whether to include debug symbols. */
@get:Input abstract val debugSymbols: Property<Boolean>
/** Whether to generate position-independent code (GCC/Clang only). */
@get:Input abstract val positionIndependentCode: Property<Boolean>
/** Warning flags (GCC/Clang: prefixed with -W, e.g., "all", "extra"). */
@get:Input abstract val warningFlags: ListProperty<String>
/** Warning level for MSVC (0-4). */
@get:Input @get:Optional abstract val warningLevel: Property<Int>
/** Feature flags (GCC/Clang only, prefixed with -f). */
@get:Input abstract val featureFlags: ListProperty<String>
/** Machine-specific flags (GCC/Clang only, prefixed with -m). */
@get:Input abstract val machineFlags: ListProperty<String>
/** Runtime library for MSVC (e.g., "MT", "MD", "MTd", "MDd"). */
@get:Input @get:Optional abstract val runtimeLibrary: Property<String>
/** Additional compiler arguments. */
@get:Input abstract val compilerArgs: ListProperty<String>
/** The architecture to compile for (macOS only). */
@get:Input @get:Optional abstract val arch: Property<Target.Arch>
// ===== Linking properties =====
/** Whether to link into an executable (true) or just compile to object files (false). */
@get:Input abstract val link: Property<Boolean>
/** Library search paths. */
@get:InputFiles @get:Optional abstract val libraryPaths: ConfigurableFileCollection
/** Libraries to link by name. */
@get:Input @get:Optional abstract val libraries: ListProperty<String>
/** Direct library files to link. */
@get:InputFiles @get:Optional abstract val libraryFiles: ConfigurableFileCollection
/** Linker flags. */
@get:Input @get:Optional abstract val linkerFlags: ListProperty<String>
/** macOS frameworks to link (GCC/Clang only). */
@get:Input @get:Optional abstract val frameworks: ListProperty<String>
/** Output file when linking (executable name). */
@get:OutputFile @get:Optional abstract val outputFile: RegularFileProperty
/** Create a shared library (dll, dylib, so). */
@get:Input abstract val sharedLibrary: Property<Boolean>
/** The output directory for object files (when not linking). */
@get:OutputDirectory @get:Optional abstract val outputDir: DirectoryProperty
@get:Inject protected abstract val execOperations: ExecOperations
private val buildInfo: BuildInfo = project.extensions.getByType(BuildInfo::class.java)
init {
group = "build"
// Defaults
link.convention(false)
debugSymbols.convention(false)
positionIndependentCode.convention(false)
sharedLibrary.convention(false)
warningFlags.convention(listOf("all", "extra"))
warningLevel.convention(3)
featureFlags.convention(emptyList())
machineFlags.convention(emptyList())
compilerArgs.convention(emptyList())
defines.convention(emptyMap())
libraries.convention(emptyList())
linkerFlags.convention(emptyList())
frameworks.convention(emptyList())
}
@TaskAction
fun compile() {
if (link.get()) {
compileAndLink()
} else {
compileOnly()
}
}
private fun compileOnly() {
val outputDirectory = outputDir.get().asFile
outputDirectory.mkdirs()
val cFiles = sourceFiles.files.filter { it.extension == "c" }
if (cFiles.isEmpty()) {
logger.info("No C files to compile")
return
}
cFiles.forEach { cFile ->
when (buildInfo.targetMachine.os) {
OS.Linux,
OS.MacOS -> compileFileGCC(cFile, outputDirectory)
OS.Windows -> compileFileMSVC(cFile, outputDirectory)
}
}
cFiles.forEach { cFile ->
val objectFile = outputDirectory.resolve("${cFile.nameWithoutExtension}.o")
logger.info("Compiled ${cFile.name} -> ${objectFile.name}")
}
}
private fun compileFileGCC(cFile: java.io.File, outputDirectory: java.io.File) {
val objectFile = outputDirectory.resolve("${cFile.nameWithoutExtension}.o")
val args = buildList {
add("cc")
add("-c")
addAll(buildCompilerFlagsGCC())
add(cFile.absolutePath)
add("-o")
add(objectFile.absolutePath)
}
execOperations.exec {
workingDir = project.projectDir
commandLine(args)
}
}
private fun compileFileMSVC(cFile: java.io.File, outputDirectory: java.io.File) {
val objectFile = outputDirectory.resolve("${cFile.nameWithoutExtension}.obj")
val args = buildList {
add("cl.exe")
add("/c")
addAll(buildCompilerFlagsMSVC())
add("/Fo${objectFile.absolutePath}")
add(cFile.absolutePath)
}
execOperations.exec {
workingDir = project.projectDir
commandLine(args)
}
}
private fun compileAndLink() {
when (buildInfo.targetMachine.os) {
OS.Linux,
OS.MacOS -> compileAndLinkGCC()
OS.Windows -> compileAndLinkMSVC()
}
logger.info("Compiled and linked -> ${outputFile.get().asFile.name}")
}
private fun compileAndLinkGCC() {
val output = outputFile.get().asFile
val args = buildList {
add("cc")
addAll(buildCompilerFlagsGCC())
// Add source/object files
sourceFiles.files.forEach { file -> add(file.absolutePath) }
// Library search paths
libraryPaths.files.forEach { dir -> add("-L${dir.absolutePath}") }
// Direct library files. These must come before `-l` libraries below: GNU ld resolves
// symbols in a single left-to-right pass, so a static `-lz` etc. must appear after the
// archives (e.g. libpkl.a) whose undefined symbols it resolves, or those symbols are
// never pulled in.
libraryFiles.files.forEach { file -> add(file.absolutePath) }
// Libraries by name
libraries.get().forEach { lib -> add("-l${lib}") }
// Linker flags
if (linkerFlags.get().isNotEmpty()) {
add("-Wl,${linkerFlags.get().joinToString(",")}")
}
// macOS frameworks
if (buildInfo.os.isMacOS) {
frameworks.get().forEach { framework ->
add("-framework")
add(framework)
}
}
if (sharedLibrary.get()) {
add("-shared")
}
// Output file
add("-o")
add(output.absolutePath)
}
output.parentFile.mkdirs()
execOperations.exec {
workingDir = project.projectDir
commandLine(args)
}
}
private fun compileAndLinkMSVC() {
val output = outputFile.get().asFile
output.parentFile.mkdirs()
val args = buildList {
add("cl.exe")
addAll(buildCompilerFlagsMSVC())
// Output executable
add("/Fe${output.absolutePath}")
// Add source/object files
sourceFiles.files.forEach { file -> add(file.absolutePath) }
// Linker section
if (
libraryPaths.files.isNotEmpty() ||
libraries.get().isNotEmpty() ||
libraryFiles.files.isNotEmpty() ||
linkerFlags.get().isNotEmpty()
) {
add("/link")
if (sharedLibrary.get()) {
add("/DLL")
}
// Library search paths
libraryPaths.files.forEach { dir -> add("/LIBPATH:${dir.absolutePath}") }
// Library files by name
libraries.get().forEach { lib -> add("lib${lib}.lib") }
// Direct library files
libraryFiles.files.forEach { file -> add(file.absolutePath) }
// Additional linker flags
addAll(linkerFlags.get())
}
}
execOperations.exec {
workingDir = project.projectDir
commandLine(args)
}
}
private fun buildCompilerFlagsGCC(): List<String> = buildList {
// Warning flags
warningFlags.get().forEach { flag -> add("-W${flag}") }
// Include directories
includeDirs.files.forEach { dir -> add("-I${dir.absolutePath}") }
// Preprocessor definitions
defines.get().forEach { (key, value) ->
if (value.isEmpty()) {
add("-D${key}")
} else {
add("-D${key}=${value}")
}
}
// C standard
if (cStandard.isPresent) {
add("-std=${cStandard.get()}")
}
// Optimization level
if (optimizationLevel.isPresent) {
add("-O${optimizationLevel.get()}")
}
// Debug symbols
if (debugSymbols.get()) {
add("-g")
}
// Position-independent code
if (positionIndependentCode.get()) {
add("-fPIC")
}
// Feature flags
featureFlags.get().forEach { flag -> add("-f${flag}") }
// Machine flags
machineFlags.get().forEach { flag -> add("-m${flag}") }
// Platform-specific flags
if (buildInfo.os.isMacOS && arch.isPresent) {
add("-arch")
add(arch.get().cCompilerName)
}
// Additional compiler arguments
addAll(compilerArgs.get())
}
private fun buildCompilerFlagsMSVC(): List<String> = buildList {
// Warning level
if (warningLevel.isPresent) {
add("/W${warningLevel.get()}")
}
// Include directories
includeDirs.files.forEach { dir -> add("/I${dir.absolutePath}") }
// Preprocessor definitions
defines.get().forEach { (key, value) ->
if (value.isEmpty()) {
add("/D${key}")
} else {
add("/D${key}=${value}")
}
}
// C standard
if (cStandard.isPresent) {
add("/std:${cStandard.get()}")
}
// Optimization level
if (optimizationLevel.isPresent) {
when (optimizationLevel.get().lowercase()) {
"0",
"d" -> add("/Od")
"1" -> add("/O1")
"2" -> add("/O2")
"x",
"fast" -> add("/Ox")
"s" -> add("/Os")
"t" -> add("/Ot")
}
}
// Debug symbols
if (debugSymbols.get()) {
add("/Zi")
}
// Runtime library
if (runtimeLibrary.isPresent) {
add("/${runtimeLibrary.get()}")
}
// Additional compiler arguments
addAll(compilerArgs.get())
}
}
+94 -19
View File
@@ -16,49 +16,64 @@
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.ClasspathNormalizer
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFiles
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.registerIfAbsent
import org.gradle.kotlin.dsl.withNormalizer
import org.gradle.process.ExecOperations
enum class Architecture {
AMD64,
AARCH64,
}
abstract class NativeImageBuildService : BuildService<BuildServiceParameters.None>
abstract class NativeImageBuild : DefaultTask() {
@get:Input abstract val imageName: Property<String>
@get:Input abstract val outputName: Property<String>
@get:Input abstract val extraNativeImageArgs: ListProperty<String>
@get:Input abstract val arch: Property<Architecture>
@get:Input abstract val arch: Property<Target.Arch>
@get:Input abstract val mainClass: Property<String>
/**
* The main class entrypoint for the executable.
*
* This option is not necessary if [sharedLibrary] is true.
*/
@get:Optional @get:Input abstract val mainClass: Property<String>
/** Create a shared library, instead of an executable. */
@get:Input abstract val sharedLibrary: Property<Boolean>
@get:InputFiles abstract val classpath: ConfigurableFileCollection
private val outputDir = project.layout.buildDirectory.dir("executable")
@get:Input abstract val envVars: MapProperty<String, String>
@get:OutputFile val outputFile = outputDir.flatMap { it.file(imageName) }
@get:InputFile @get:Optional abstract val nativeCompilerPath: RegularFileProperty
@get:Internal abstract val outputDir: DirectoryProperty
@get:Inject protected abstract val execOperations: ExecOperations
@get:Inject protected abstract val objectFactory: ObjectFactory
private val graalVm: Provider<BuildInfo.GraalVm> = arch.map { a ->
when (a) {
Architecture.AMD64 -> buildInfo.graalVmAmd64
Architecture.AARCH64 -> buildInfo.graalVmAarch64
Target.Arch.AMD64 -> buildInfo.graalVmAmd64
Target.Arch.AARCH64 -> buildInfo.graalVmAarch64
}
}
@@ -88,6 +103,10 @@ abstract class NativeImageBuild : DefaultTask() {
// CPU resources).
usesService(buildService)
sharedLibrary.convention(false)
outputDir.convention(project.layout.buildDirectory.dir("executable"))
group = "build"
inputs
@@ -98,6 +117,51 @@ abstract class NativeImageBuild : DefaultTask() {
.files(nativeImageExecutable)
.withPropertyName("graalVmNativeImage")
.withPathSensitivity(PathSensitivity.ABSOLUTE)
if (nativeCompilerPath.isPresent) {
inputs.file(nativeCompilerPath)
}
}
@Suppress("unused")
@OutputFiles
fun getEffectiveOutputFiles(): FileCollection {
return objectFactory
.fileCollection()
.from(
sharedLibrary.map { isLibrary ->
val dir = outputDir.get()
val libraryName = outputName.get()
// if building a library, native-image outputs the shared library plus four headers
// (and, on Windows, an import library); otherwise, it outputs just the one file
if (isLibrary) {
val sharedLibraryExtension = buildInfo.os.sharedLibraryExtension
val staticLibraryExtension = buildInfo.os.staticLibraryExtension
dir.files(
buildList {
add("$libraryName.$sharedLibraryExtension")
if (buildInfo.os.isWindows) {
// the DLL's import library, which consumers must link against instead of
// the .dll itself
add("$libraryName.$staticLibraryExtension")
// a genuine, self-contained static archive with no runtime DLL dependency,
// produced by build_windows.bat
add("${libraryName}_s.$staticLibraryExtension")
} else {
// a genuine, self-contained static archive with no runtime dependency,
// produced by build_unix.sh
add("$libraryName.$staticLibraryExtension")
}
add("$libraryName.h")
add("${libraryName}_dynamic.h")
add("graal_isolate.h")
add("graal_isolate_dynamic.h")
}
)
} else {
dir.file(libraryName)
}
}
)
}
@TaskAction
@@ -111,6 +175,7 @@ abstract class NativeImageBuild : DefaultTask() {
workingDir(outputDir)
args = buildList {
add("--color=always")
// must be emitted before any experimental options are used
add("-H:+UnlockExperimentalVMOptions")
// currently gives a deprecation warning, but we've been told
@@ -126,9 +191,14 @@ abstract class NativeImageBuild : DefaultTask() {
add("-H:IncludeResourceBundles=org.pkl.core.errorMessages")
add("-H:IncludeResourceBundles=org.pkl.parser.errorMessages")
add("-H:IncludeResources=org/pkl/commons/cli/PklCARoots.pem")
add("-H:Class=${mainClass.get()}")
if (mainClass.isPresent) {
add("-H:Class=${mainClass.get()}")
}
if (sharedLibrary.get()) {
add("--shared")
}
add("-o")
add(imageName.get())
add(outputName.get())
// the actual limit (currently) used by native-image is this number + 1400 (idea is to
// compensate for Truffle's own nodes)
add("-H:MaxRuntimeCompileMethods=1800")
@@ -138,10 +208,11 @@ abstract class NativeImageBuild : DefaultTask() {
// disable automatic support for JVM CLI options (puts our main class in full control of
// argument parsing)
add("-H:-ParseRuntimeOptions")
// quick build mode: 40% faster compilation, 20% smaller (but presumably also slower)
// executable
if (!buildInfo.isReleaseBuild) {
add("-Ob")
// disable all optimizations
add("-O0")
// generate debugging information
add("-g")
}
if (buildInfo.isNativeArch) {
add("-march=native")
@@ -157,8 +228,12 @@ abstract class NativeImageBuild : DefaultTask() {
// make sure dev machine stays responsive (15% slowdown on my laptop)
val processors =
Runtime.getRuntime().availableProcessors() /
if (buildInfo.os.isMacOsX && !buildInfo.isCiBuild) 4 else 1
if (buildInfo.os.isMacOS && !buildInfo.isCiBuild) 4 else 1
add("-J-XX:ActiveProcessorCount=${processors}")
addAll(envVars.get().entries.map { "-E${it.key}=${it.value}" })
if (nativeCompilerPath.isPresent) {
add("--native-compiler-path=${nativeCompilerPath.get().asFile}")
}
// Pass through all `HOMEBREW_` prefixed environment variables to allow build with shimmed
// tools.
addAll(environment.keys.filter { it.startsWith("HOMEBREW_") }.map { "-E$it" })
+110
View File
@@ -0,0 +1,110 @@
/*
* 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("MemberVisibilityCanBePrivate")
/**
* A build target when building native libraries or executables.
*
* Pkl only builds for the following listed targets.
*/
enum class Target(val os: OS, val arch: Arch, val musl: Boolean) {
MacosAarch64(os = OS.MacOS, arch = Arch.AARCH64, musl = false),
LinuxAarch64(os = OS.Linux, arch = Arch.AARCH64, musl = false),
LinuxAmd64(os = OS.Linux, arch = Arch.AMD64, musl = false),
AlpineLinuxAmd64(os = OS.Linux, arch = Arch.AMD64, musl = true),
WindowsAmd64(os = OS.Windows, arch = Arch.AMD64, musl = false);
companion object {
fun from(os: OS, arch: Arch, musl: Boolean): Target {
for (target in entries) {
if (target.os == os && target.arch == arch && target.musl == musl) {
return target
}
}
throw IllegalArgumentException("Cannot build for $os-$arch with musl: $musl")
}
}
val targetName: String
get() {
return if (musl) {
assert(os == OS.Linux)
"alpine-linux-$arch"
} else "$os-$arch"
}
enum class Arch(
/** What we call this arch */
val simpleName: String,
/** What the C compiler calls this arch */
val cCompilerName: String,
) {
AARCH64("aarch64", "arm64"),
AMD64("amd64", "x86_64");
override fun toString() = simpleName
companion object {
fun fromName(name: String): Arch =
when (name) {
"aarch64" -> AARCH64
"amd64" -> AMD64
else -> throw IllegalArgumentException("Unknown arch: $name")
}
}
}
enum class OS(
val simpleName: String,
val sharedLibraryExtension: String,
val staticLibraryExtension: String,
val objectFileExtension: String,
val displayName: String,
) {
MacOS(
simpleName = "macos",
sharedLibraryExtension = "dylib",
staticLibraryExtension = "a",
objectFileExtension = "o",
displayName = "macOS",
),
Linux(
simpleName = "linux",
sharedLibraryExtension = "so",
staticLibraryExtension = "a",
objectFileExtension = "o",
displayName = "Linux",
),
Windows(
simpleName = "windows",
sharedLibraryExtension = "dll",
staticLibraryExtension = "lib",
objectFileExtension = "obj",
displayName = "Windows",
);
override fun toString(): String = simpleName
val isWindows: Boolean
get() = this == Windows
val isMacOS: Boolean
get() = this == MacOS
val isLinux: Boolean
get() = this == Linux
}
}
@@ -53,33 +53,40 @@ val nativeImageClasspath =
val libs = the<LibrariesForLibs>()
dependencies {
fun executableFile(suffix: String) =
fun executableFile(target: Target) =
files(
layout.buildDirectory.dir("executable").map { dir ->
dir.file(executableSpec.name.map { "$it-$suffix" })
dir.file(
executableSpec.name.map { name ->
if (target.os.isWindows) "$name-${target.targetName}.exe"
else "$name-${target.targetName}"
}
)
}
)
nativeImageClasspath(libs.truffleRuntime)
nativeImageClasspath(libs.graalSdk)
stagedMacAarch64Executable(executableFile("macos-aarch64"))
stagedLinuxAmd64Executable(executableFile("linux-amd64"))
stagedLinuxAarch64Executable(executableFile("linux-aarch64"))
stagedAlpineLinuxAmd64Executable(executableFile("alpine-linux-amd64"))
stagedWindowsAmd64Executable(executableFile("windows-amd64.exe"))
stagedMacAarch64Executable(executableFile(Target.MacosAarch64))
stagedLinuxAarch64Executable(executableFile(Target.LinuxAarch64))
stagedLinuxAmd64Executable(executableFile(Target.LinuxAmd64))
stagedAlpineLinuxAmd64Executable(executableFile(Target.AlpineLinuxAmd64))
stagedWindowsAmd64Executable(executableFile(Target.WindowsAmd64))
}
private fun NativeImageBuild.amd64() {
arch = Architecture.AMD64
dependsOn(":installGraalVmAmd64")
}
private fun NativeImageBuild.configure(target: Target) {
arch = target.arch
private fun NativeImageBuild.aarch64() {
arch = Architecture.AARCH64
dependsOn(":installGraalVmAarch64")
}
outputName = executableSpec.name.map { "$it-${target.targetName}" }
mainClass = executableSpec.mainClass
if (target.arch == Target.Arch.AARCH64) {
dependsOn(":installGraalVmAarch64")
} else {
dependsOn(":installGraalVmAmd64")
}
private fun NativeImageBuild.setClasspath() {
classpath.from(sourceSets.main.map { it.output })
classpath.from(
project(":pkl-commons-cli").extensions.getByType(SourceSetContainer::class)["svm"].output
@@ -88,27 +95,14 @@ private fun NativeImageBuild.setClasspath() {
}
val macExecutableAarch64 =
tasks.register<NativeImageBuild>("macExecutableAarch64") {
imageName = executableSpec.name.map { "$it-macos-aarch64" }
mainClass = executableSpec.mainClass
aarch64()
setClasspath()
}
tasks.register<NativeImageBuild>("macExecutableAarch64") { configure(Target.MacosAarch64) }
val linuxExecutableAmd64 =
tasks.register<NativeImageBuild>("linuxExecutableAmd64") {
imageName = executableSpec.name.map { "$it-linux-amd64" }
mainClass = executableSpec.mainClass
amd64()
setClasspath()
}
tasks.register<NativeImageBuild>("linuxExecutableAmd64") { configure(Target.LinuxAmd64) }
val linuxExecutableAarch64 =
tasks.register<NativeImageBuild>("linuxExecutableAarch64") {
imageName = executableSpec.name.map { "$it-linux-aarch64" }
mainClass = executableSpec.mainClass
aarch64()
setClasspath()
configure(Target.LinuxAarch64)
// Ensure compatibility for kernels with page size set to 4k, 16k and 64k
// (e.g. Raspberry Pi 5, Asahi Linux)
extraNativeImageArgs.add("-H:PageSize=65536")
@@ -116,19 +110,14 @@ val linuxExecutableAarch64 =
val alpineExecutableAmd64 =
tasks.register<NativeImageBuild>("alpineExecutableAmd64") {
imageName = executableSpec.name.map { "$it-alpine-linux-amd64" }
mainClass = executableSpec.mainClass
amd64()
setClasspath()
extraNativeImageArgs.addAll(listOf("--static", "--libc=musl"))
configure(Target.AlpineLinuxAmd64)
extraNativeImageArgs.addAll("--static", "--libc=musl")
}
val windowsExecutableAmd64 =
tasks.register<NativeImageBuild>("windowsExecutableAmd64") {
imageName = executableSpec.name.map { "$it-windows-amd64" }
mainClass = executableSpec.mainClass
amd64()
setClasspath()
configure(Target.WindowsAmd64)
extraNativeImageArgs.add("-Dfile.encoding=UTF-8")
}
val assembleNative = tasks.named("assembleNative")
@@ -216,93 +205,51 @@ val assembleNativeAlpineLinuxAmd64 =
val assembleNativeWindowsAmd64 =
tasks.named("assembleNativeWindowsAmd64") { wraps(windowsExecutableAmd64) }
private fun MavenPublication.configurePublication(target: Target, configuration: Configuration) {
artifactId = "${executableSpec.publicationName.get()}-${target.targetName}"
pom {
name = "${executableSpec.publicationName.get()}-${target.targetName}"
url = executableSpec.website
artifact(configuration.singleFile) {
classifier = null
extension = if (target.os.isWindows) "exe" else "bin"
builtBy(configuration)
}
description =
executableSpec.documentationName.map { name ->
buildString {
append("Native $name executable for ${target.os.displayName}/${target.arch}")
if (target.musl) {
append(" and statically linked to musl")
}
append(".")
}
}
}
}
publishing {
publications {
// need to put in `afterEvaluate` because `artifactId` cannot be set lazily.
project.afterEvaluate {
create<MavenPublication>("macExecutableAarch64") {
artifactId = "${executableSpec.publicationName.get()}-macos-aarch64"
artifact(stagedMacAarch64Executable.singleFile) {
classifier = null
extension = "bin"
builtBy(stagedMacAarch64Executable)
}
pom {
name = "${executableSpec.publicationName.get()}-macos-aarch64"
url = executableSpec.website
description =
executableSpec.documentationName.map { name ->
"Native $name executable for macOS/aarch64."
}
}
configurePublication(Target.MacosAarch64, stagedMacAarch64Executable)
}
create<MavenPublication>("linuxExecutableAmd64") {
artifactId = "${executableSpec.publicationName.get()}-linux-amd64"
artifact(stagedLinuxAmd64Executable.singleFile) {
classifier = null
extension = "bin"
builtBy(stagedLinuxAmd64Executable)
}
pom {
name = "${executableSpec.publicationName.get()}-linux-amd64"
url = executableSpec.website
description =
executableSpec.documentationName.map { name ->
"Native $name executable for linux/amd64."
}
}
configurePublication(Target.LinuxAmd64, stagedLinuxAmd64Executable)
}
create<MavenPublication>("linuxExecutableAarch64") {
artifactId = "${executableSpec.publicationName.get()}-linux-aarch64"
artifact(stagedLinuxAarch64Executable.singleFile) {
classifier = null
extension = "bin"
builtBy(stagedLinuxAarch64Executable)
}
pom {
name = "${executableSpec.publicationName.get()}-linux-aarch64"
url = executableSpec.website
description =
executableSpec.documentationName.map { name ->
"Native $name executable for linux/aarch64."
}
}
configurePublication(Target.LinuxAarch64, stagedLinuxAarch64Executable)
}
create<MavenPublication>("alpineLinuxExecutableAmd64") {
artifactId = "${executableSpec.publicationName.get()}-alpine-linux-amd64"
artifact(stagedAlpineLinuxAmd64Executable.singleFile) {
classifier = null
extension = "bin"
builtBy(stagedAlpineLinuxAmd64Executable)
}
pom {
name = "${executableSpec.publicationName.get()}-alpine-linux-amd64"
url = executableSpec.website
description =
executableSpec.documentationName.map { name ->
"Native $name executable for linux/amd64 and statically linked to musl."
}
}
configurePublication(Target.AlpineLinuxAmd64, stagedAlpineLinuxAmd64Executable)
}
create<MavenPublication>("windowsExecutableAmd64") {
artifactId = "${executableSpec.publicationName.get()}-windows-amd64"
artifact(stagedWindowsAmd64Executable.singleFile) {
classifier = null
extension = "exe"
builtBy(stagedWindowsAmd64Executable)
}
pom {
name = "${executableSpec.publicationName.get()}-windows-amd64"
url = executableSpec.website
description =
executableSpec.documentationName.map { name ->
"Native $name executable for windows/amd64."
}
}
configurePublication(Target.WindowsAmd64, stagedWindowsAmd64Executable)
}
}
}
@@ -46,31 +46,20 @@ val assembleNative =
tasks.register("assembleNative") {
group = "build"
@Suppress("DuplicatedCode")
if (!buildInfo.isCrossArchSupported && buildInfo.isCrossArch) {
throw GradleException("Cross-arch builds are not supported on ${buildInfo.os.name}")
doLast {
throw GradleException("Cross-arch builds are not supported on ${buildInfo.os.name}")
}
}
when {
buildInfo.os.isMacOsX && buildInfo.targetArch == "aarch64" -> {
wraps(assembleNativeMacOsAarch64)
}
buildInfo.os.isLinux && buildInfo.targetArch == "aarch64" -> {
wraps(assembleNativeLinuxAarch64)
}
buildInfo.os.isLinux && buildInfo.targetArch == "amd64" -> {
if (buildInfo.musl) wraps(assembleNativeAlpineLinuxAmd64)
else wraps(assembleNativeLinuxAmd64)
}
buildInfo.os.isWindows && buildInfo.targetArch == "amd64" -> {
wraps(assembleNativeWindowsAmd64)
}
else -> {
doLast {
throw GradleException(
"Cannot build targeting ${buildInfo.os.name}/${buildInfo.targetArch} with musl=${buildInfo.musl}"
)
}
}
@Suppress("DuplicatedCode")
when (buildInfo.targetMachine) {
Target.MacosAarch64 -> wraps(assembleNativeMacOsAarch64)
Target.LinuxAarch64 -> wraps(assembleNativeLinuxAarch64)
Target.LinuxAmd64 -> wraps(assembleNativeLinuxAmd64)
Target.AlpineLinuxAmd64 -> wraps(assembleNativeAlpineLinuxAmd64)
Target.WindowsAmd64 -> wraps(assembleNativeWindowsAmd64)
}
}
@@ -79,31 +68,18 @@ val testNative =
group = "verification"
dependsOn(assembleNative)
@Suppress("DuplicatedCode")
if (!buildInfo.isCrossArchSupported && buildInfo.isCrossArch) {
throw GradleException("Cross-arch builds are not supported on ${buildInfo.os.name}")
}
when {
buildInfo.os.isMacOsX && buildInfo.targetArch == "aarch64" -> {
dependsOn(testNativeMacOsAarch64)
}
buildInfo.os.isLinux && buildInfo.targetArch == "aarch64" -> {
dependsOn(testNativeLinuxAarch64)
}
buildInfo.os.isLinux && buildInfo.targetArch == "amd64" -> {
if (buildInfo.musl) dependsOn(testNativeAlpineLinuxAmd64)
else dependsOn(testNativeLinuxAmd64)
}
buildInfo.os.isWindows && buildInfo.targetArch == "amd64" -> {
dependsOn(testNativeWindowsAmd64)
}
else -> {
doLast {
throw GradleException(
"Cannot build targeting ${buildInfo.os.name}/${buildInfo.targetArch} with musl=${buildInfo.musl}"
)
}
}
@Suppress("DuplicatedCode")
when (buildInfo.targetMachine) {
Target.MacosAarch64 -> wraps(testNativeMacOsAarch64)
Target.LinuxAarch64 -> wraps(testNativeLinuxAarch64)
Target.LinuxAmd64 -> wraps(testNativeLinuxAmd64)
Target.AlpineLinuxAmd64 -> wraps(testNativeAlpineLinuxAmd64)
Target.WindowsAmd64 -> wraps(testNativeWindowsAmd64)
}
}
@@ -0,0 +1,15 @@
::===----------------------------------------------------------------------===//
:: Copyright © $YEAR 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.
::===----------------------------------------------------------------------===//
@@ -0,0 +1,15 @@
#===----------------------------------------------------------------------===//
# Copyright © $YEAR 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.
#===----------------------------------------------------------------------===//