diff --git a/buildSrc/src/main/kotlin/NativeImageBuild.kt b/buildSrc/src/main/kotlin/NativeImageBuild.kt index a16ea723b..727640ffa 100644 --- a/buildSrc/src/main/kotlin/NativeImageBuild.kt +++ b/buildSrc/src/main/kotlin/NativeImageBuild.kt @@ -16,6 +16,7 @@ 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.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider @@ -24,6 +25,7 @@ import org.gradle.api.services.BuildServiceParameters import org.gradle.api.tasks.ClasspathNormalizer import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction @@ -49,7 +51,7 @@ abstract class NativeImageBuild : DefaultTask() { @get:InputFiles abstract val classpath: ConfigurableFileCollection - private val outputDir = project.layout.buildDirectory.dir("executable") + @get:OutputDirectory abstract val outputDir: DirectoryProperty @get:OutputFile val outputFile = outputDir.flatMap { it.file(imageName) } @@ -89,6 +91,8 @@ abstract class NativeImageBuild : DefaultTask() { // CPU resources). usesService(buildService) + outputDir.convention(project.layout.buildDirectory.dir("executable")) + group = "build" inputs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79dc64bc1..750ba6e8a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ antlr = "4.+" assertj = "3.+" checksumPlugin = "1.4.0" +clangfmt = "10.0.1" clikt = "5.+" commonMark = "0.+" downloadTaskPlugin = "5.6.0" diff --git a/libpkl/gradle.lockfile b/libpkl/gradle.lockfile new file mode 100644 index 000000000..90ef20152 --- /dev/null +++ b/libpkl/gradle.lockfile @@ -0,0 +1,81 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.github.ajalt.clikt:clikt-core-jvm:5.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt-core:5.0.3=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.github.ajalt.clikt:clikt-jvm:5.0.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt-markdown-jvm:5.0.3=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt-markdown:5.0.3=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.clikt:clikt:5.0.3=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.github.ajalt.colormath:colormath-jvm:3.6.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.colormath:colormath:3.6.0=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.github.ajalt.mordant:mordant-core-jvm:3.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-core:3.0.1=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm-ffm-jvm:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm-ffm:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm-graal-ffi-jvm:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm-graal-ffi:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm-jna-jvm:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm-jna:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-jvm:3.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-markdown-jvm:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant-markdown:3.0.1=runtimeClasspath,testRuntimeClasspath +com.github.ajalt.mordant:mordant:3.0.1=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.15.11=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.17.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.java.dev.jna:jna:5.14.0=runtimeClasspath +net.java.dev.jna:jna:5.17.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata +org.assertj:assertj-core:3.27.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.polyglot:polyglot:24.1.2=compileClasspath,compileOnlyDependenciesMetadata,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:collections:24.1.2=compileClasspath,compileOnlyDependenciesMetadata,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:graal-sdk:24.1.2=compileClasspath,compileOnlyDependenciesMetadata,runtimeClasspath,testRuntimeClasspath +org.graalvm.sdk:jniutils:24.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:nativeimage:24.1.2=compileClasspath,compileOnlyDependenciesMetadata,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.sdk:word:24.1.2=compileClasspath,compileOnlyDependenciesMetadata,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.truffle:truffle-api:24.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.truffle:truffle-compiler:24.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.graalvm.truffle:truffle-runtime:24.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-build-common:2.0.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.0.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.0.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.0.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.0.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:2.0.21=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-native-prebuilt:2.0.21=kotlinNativeBundleConfiguration +org.jetbrains.kotlin:kotlin-reflect:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17 +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17 +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17 +org.jetbrains.kotlin:kotlin-scripting-jvm:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.0.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains:annotations:13.0=compileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinCompilerPluginClasspathTestJdk17,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains:markdown-jvm:0.7.3=runtimeClasspath,testRuntimeClasspath +org.jetbrains:markdown:0.7.3=runtimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.11.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.8.2=testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata,testJdk17RuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.11.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.8.2=testJdk17RuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.11.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.8.2=testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata,testJdk17RuntimeClasspath +org.junit.jupiter:junit-jupiter:5.8.2=testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata,testJdk17RuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.8.2=testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata,testJdk17RuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.8.2=testJdk17RuntimeClasspath +org.junit.platform:junit-platform-launcher:1.8.2=testJdk17RuntimeClasspath +org.junit:junit-bom:5.11.4=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit:junit-bom:5.8.2=testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata,testJdk17RuntimeClasspath +org.msgpack:msgpack-core:0.9.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.opentest4j:opentest4j:1.2.0=testJdk17CompileClasspath,testJdk17ImplementationDependenciesMetadata,testJdk17RuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.organicdesign:Paguro:3.10.3=runtimeClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.9=runtimeClasspath,testRuntimeClasspath +empty=annotationProcessor,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,shadow,signatures,sourcesJar,stagedAlpineLinuxAmd64NativeLibrary,stagedLinuxAarch64NativeLibrary,stagedLinuxAmd64NativeLibrary,stagedMacAarch64NativeLibrary,stagedMacAmd64NativeLibrary,stagedWindowsAmd64NativeLibrary,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testJdk17AnnotationProcessor,testJdk17ApiDependenciesMetadata,testJdk17CompileOnlyDependenciesMetadata,testJdk17IntransitiveDependenciesMetadata,testJdk17KotlinScriptDefExtensions,testKotlinScriptDefExtensions diff --git a/libpkl/libpkl.gradle.kts b/libpkl/libpkl.gradle.kts new file mode 100644 index 000000000..df1a8eebb --- /dev/null +++ b/libpkl/libpkl.gradle.kts @@ -0,0 +1,316 @@ +/* +* Copyright © 2024-2025 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") + +plugins { + pklAllProjects + pklGraalVm + pklJavaLibrary + pklNativeLifecycle +} + +val stagedMacAmd64NativeLibrary: Configuration by configurations.creating +val stagedMacAarch64NativeLibrary: Configuration by configurations.creating +val stagedLinuxAmd64NativeLibrary: Configuration by configurations.creating +val stagedLinuxAarch64NativeLibrary: Configuration by configurations.creating +val stagedAlpineLinuxAmd64NativeLibrary: Configuration by configurations.creating +val stagedWindowsAmd64NativeLibrary: Configuration by configurations.creating + +dependencies { + compileOnly(libs.graalSdk) + + implementation(projects.pklCore) + implementation(projects.pklServer) + + api(projects.pklCommonsCli) + + implementation(libs.msgpack) + implementation(libs.truffleApi) + implementation(libs.truffleRuntime) + + testImplementation(projects.pklCommonsTest) + testImplementation("net.java.dev.jna:jna:5.17.0") + testImplementation("net.java.dev.jna:jna-platform:5.17.0") + + fun sharedLibrary(osAndArch: String) = files(nativeLibraryOutputFiles(osAndArch)) + + stagedMacAarch64NativeLibrary(sharedLibrary("macos-aarch64")) + stagedMacAmd64NativeLibrary(sharedLibrary("macos-amd64")) + stagedLinuxAmd64NativeLibrary(sharedLibrary("linux-amd64")) + stagedLinuxAarch64NativeLibrary(sharedLibrary("linux-aarch64")) + stagedAlpineLinuxAmd64NativeLibrary(sharedLibrary("alpine-linux-amd64")) + stagedWindowsAmd64NativeLibrary(sharedLibrary("windows-amd64.exe")) +} + +private fun extension(osAndArch: String) = + when (osAndArch.split("-").dropWhile { it == "alpine" }.first()) { + "linux" -> "so" + "macos" -> "dylib" + "unix" -> "so" + "windows" -> "dll" + else -> { + throw StopExecutionException( + "Don't know how to construct library extension for OS: ${osAndArch.split("-").first()}" + ) + } + } + +private fun nativeLibraryOutputFiles(osAndArch: String) = + project.layout.buildDirectory.dir("libs/$osAndArch").map { outputDir -> + // TODO(kushal): dashes/underscores for library files? C convention assumes underscores. + val libraryName = "libpkl_internal" + val libraryOutputFiles = + listOf( + "lib${libraryName}.${extension(osAndArch)}", + "${libraryName}_dynamic.h", + "${libraryName}.h", + + // GraalVM shared headers. + "graal_isolate.h", + "graal_isolate_dynamic.h", + ) + + libraryOutputFiles.map { filename -> outputDir.file(filename) } + } + +private fun NativeImageBuild.setOutputFiles(osAndArch: String) { + outputs.files(nativeLibraryOutputFiles(osAndArch)) +} + +private fun NativeImageBuild.amd64() { + arch = Architecture.AMD64 + dependsOn(":installGraalVmAmd64") +} + +private fun NativeImageBuild.aarch64() { + arch = Architecture.AARCH64 + dependsOn(":installGraalVmAarch64") +} + +private fun NativeImageBuild.setClasspath() { + classpath.from(sourceSets.main.map { it.output }) + classpath.from( + project(":pkl-commons-cli").extensions.getByType(SourceSetContainer::class)["svm"].output + ) + classpath.from(configurations.runtimeClasspath) +} + +val macNativeLibraryAmd64 by + tasks.registering(NativeImageBuild::class) { + outputDir = project.layout.buildDirectory.dir("libs/macos-amd64") + imageName = "libpkl_internal" + mainClass = "org.pkl.libpkl.LibPkl" + amd64() + setClasspath() + extraNativeImageArgs = listOf("--shared") + + setOutputFiles("macos-amd64") + } + +val macNativeLibraryAarch64 by + tasks.registering(NativeImageBuild::class) { + outputDir = project.layout.buildDirectory.dir("libs/macos-aarch64") + imageName = "libpkl_internal" + mainClass = "org.pkl.libpkl.LibPkl" + aarch64() + setClasspath() + extraNativeImageArgs = listOf("--shared") + + setOutputFiles("macos-aarch64") + } + +val linuxNativeLibraryAmd64 by + tasks.registering(NativeImageBuild::class) { + outputDir = project.layout.buildDirectory.dir("libs/linux-amd64") + imageName = "libpkl_internal" + mainClass = "org.pkl.libpkl.LibPkl" + amd64() + setClasspath() + extraNativeImageArgs = listOf("--shared") + + setOutputFiles("linux-amd64") + } + +val linuxNativeLibraryAarch64 by + tasks.registering(NativeImageBuild::class) { + outputDir = project.layout.buildDirectory.dir("libs/linux-aarch64") + imageName = "libpkl_internal" + mainClass = "org.pkl.libpkl.LibPkl" + aarch64() + setClasspath() + + extraNativeImageArgs = + listOf( + "--shared", + // Ensure compatibility for kernels with page size set to 4k, 16k and 64k + // (e.g. Raspberry Pi 5, Asahi Linux) + "-H:PageSize=65536", + ) + + setOutputFiles("linux-aarch64") + } + +val alpineNativeLibraryAmd64 by + tasks.registering(NativeImageBuild::class) { + outputDir = project.layout.buildDirectory.dir("libs/alpine-linux-amd64") + imageName = "libpkl_internal" + mainClass = "org.pkl.libpkl.LibPkl" + amd64() + setClasspath() + + extraNativeImageArgs = + listOf( + "--shared", + // TODO(kushal): https://github.com/oracle/graal/issues/3053 + "--libc=musl", + ) + + setOutputFiles("alpine-linux-amd64") + } + +val windowsNativeLibraryAmd64 by + tasks.registering(NativeImageBuild::class) { + outputDir = project.layout.buildDirectory.dir("libs/windows-amd64") + imageName = "libpkl_internal" + mainClass = "org.pkl.libpkl.LibPkl" + amd64() + setClasspath() + extraNativeImageArgs = listOf("--shared", "-Dfile.encoding=UTF-8") + + setOutputFiles("windows-amd64") + } + +val assembleNative by + tasks.existing { + // TODO(kushal): Remove this later. Only exists to debug output files are in the graph. + finalizedBy(validateNativeLibraryFilestasks) + } + +// TODO(kushal): Remove this later. Only exists to debug output files are in the graph. +val validateNativeLibraryFilestasks by + tasks.registering { + val assembleTasks = mutableSetOf>() + + when { + buildInfo.os.isMacOsX -> { + assembleTasks.add(macNativeLibraryAmd64) + if (buildInfo.arch == "aarch64") { + assembleTasks.add(macNativeLibraryAarch64) + } + } + + buildInfo.os.isWindows -> { + assembleTasks.add(windowsNativeLibraryAmd64) + } + + buildInfo.os.isLinux && buildInfo.arch == "aarch64" -> { + assembleTasks.add(linuxNativeLibraryAarch64) + } + + buildInfo.os.isLinux && buildInfo.arch == "amd64" -> { + assembleTasks.add(linuxNativeLibraryAmd64) + if (buildInfo.hasMuslToolchain) { + assembleTasks.add(alpineNativeLibraryAmd64) + } + } + } + + dependsOn(assembleTasks) + + doLast { + for (taskProvider in assembleTasks) { + val task = taskProvider.get() + val outputFiles = task.outputs.files.files + + println("==== Validating Native Library Files Exist ====") + println("${task.name} outputs:") + outputFiles.forEach { file -> println("- ${file.absolutePath} (exists: ${file.exists()})") } + } + } + } + +// Expose underlying task's outputs +private fun Task.wraps(other: TaskProvider) { + dependsOn(other) + outputs.files(other) +} + +val assembleNativeMacOsAarch64 by tasks.existing { wraps(macNativeLibraryAarch64) } + +val assembleNativeMacOsAmd64 by tasks.existing { wraps(macNativeLibraryAmd64) } + +val assembleNativeLinuxAarch64 by tasks.existing { wraps(linuxNativeLibraryAarch64) } + +val assembleNativeLinuxAmd64 by tasks.existing { wraps(linuxNativeLibraryAmd64) } + +val assembleNativeAlpineLinuxAmd64 by tasks.existing { wraps(alpineNativeLibraryAmd64) } + +val assembleNativeWindowsAmd64 by tasks.existing { wraps(windowsNativeLibraryAmd64) } + +val macNativeFullLibraryAarch64 by + tasks.registering(Exec::class) { + dependsOn(macNativeLibraryAarch64) + + val libraryOutputDir = project.layout.buildDirectory.dir("libs/macos-aarch64").get() + val projectDir = project.layout.projectDirectory.asFile.path + + workingDir = libraryOutputDir.asFile + + // TODO: Make this portable. + commandLine( + "/usr/bin/cc", + "-shared", + "-o", + "libpkl.dylib", + "$projectDir/src/main/c/pkl.c", + "-I$projectDir/src/main/c", + "-I$libraryOutputDir", + "-L$libraryOutputDir", + "-lpkl_internal", + ) + } + +val macNativeFullLibraryAarch64Copy by + tasks.registering(Exec::class) { + dependsOn(macNativeFullLibraryAarch64) + + val libraryOutputDir = project.layout.buildDirectory.dir("libs/macos-aarch64").get() + val projectDir = project.layout.projectDirectory.asFile.path + + workingDir = libraryOutputDir.asFile + + commandLine("cp", "$projectDir/src/main/c/pkl.h", libraryOutputDir) + } + +tasks.withType { + dependsOn(macNativeFullLibraryAarch64Copy) + + val nativeLibsDir = project.layout.buildDirectory.dir("libs/macos-aarch64").get().asFile + jvmArgs("-Djna.library.path=${nativeLibsDir.absolutePath}") + + useJUnitPlatform() +} + +private val licenseHeaderFile by lazy { + rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt") +} + +spotless { + cpp { + licenseHeaderFile(licenseHeaderFile, "// ") + target("src/main/c/*.c", "src/main/c/*.h") + } +} diff --git a/libpkl/src/main/c/pkl.c b/libpkl/src/main/c/pkl.c new file mode 100644 index 000000000..b27622d7c --- /dev/null +++ b/libpkl/src/main/c/pkl.c @@ -0,0 +1,86 @@ +/* + * Copyright © 2025 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. + */ +// pkl.c + +#include +#include +#include + +#include +#include + +#include + +#ifndef NULL + #define NULL 0 +#endif + +pthread_mutex_t graal_mutex; +graal_isolatethread_t *isolatethread = NULL; + +int pkl_init(PklMessageResponseHandler handler, void *payload) { + if (isolatethread != NULL) { + perror("pkl_init: isolatethread is already initialised"); + return -1; + } + + if (pthread_mutex_init(&graal_mutex, NULL) != 0) { + perror("pkl_init: couldn't initialise pthread_mutex"); + return -1; + } + + if (pthread_mutex_lock(&graal_mutex) != 0) { + return -1; + } + + isolatethread = pkl_internal_init(); + pkl_internal_register_response_handler(isolatethread, handler, payload); + pkl_internal_server_start(isolatethread); + pthread_mutex_unlock(&graal_mutex); + + return 0; +}; + +int pkl_send_message(int length, char *message) { + if (pthread_mutex_lock(&graal_mutex) != 0) { + return -1; + } + + pkl_internal_send_message(isolatethread, length, message); + pthread_mutex_unlock(&graal_mutex); + + return 0; +}; + +int pkl_close() { + if (pthread_mutex_lock(&graal_mutex) != 0) { + return -1; + } + + pkl_internal_server_stop(isolatethread); + pkl_internal_close(isolatethread); + isolatethread = NULL; + + if (pthread_mutex_unlock(&graal_mutex) != 0) { + return -1; + } + + if (pthread_mutex_destroy(&graal_mutex) != 0) { + return -1; + } + + return 0; +}; diff --git a/libpkl/src/main/c/pkl.h b/libpkl/src/main/c/pkl.h new file mode 100644 index 000000000..4c2df64c6 --- /dev/null +++ b/libpkl/src/main/c/pkl.h @@ -0,0 +1,52 @@ +/* + * Copyright © 2025 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. + */ +// pkl.h + +/** + * The callback that gets called when a message is received from Pkl. + * + * @param length The length the message bytes + * @param message The message itself + * @param payload User-defined data passed to pkl_init. + */ +typedef void (*PklMessageResponseHandler)(int length, char *message, void *payload); + +/** + * Initialises and allocates a Pkl executor. + * + * @param handler The callback that gets called when a message is received from Pkl. + * @param payload User-defined data that gets passed to handler. + * + * @return -1 on failure, 0 on success. + */ +int pkl_init(PklMessageResponseHandler handler, void *payload); + +/** + * Send a message to Pkl, providing the length and a pointer to the first byte. + * + * @param length The length of the message, in bytes. + * @param message The message to send to Pkl. + * + * @return -1 on failure, 0 on success. + */ +int pkl_send_message(int length, char *message); + +/** + * Cleans up any resources that were created as part of the `pkl_init` process. + * + * @return -1 on failure, 0 on success. + */ +int pkl_close(); diff --git a/libpkl/src/main/java/org/pkl/libpkl/LibPkl.java b/libpkl/src/main/java/org/pkl/libpkl/LibPkl.java new file mode 100644 index 000000000..409afce75 --- /dev/null +++ b/libpkl/src/main/java/org/pkl/libpkl/LibPkl.java @@ -0,0 +1,96 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl; + +import org.graalvm.nativeimage.IsolateThread; +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.function.CEntryPoint; +import org.graalvm.nativeimage.c.function.CFunction.Transition; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.c.function.InvokeCFunctionPointer; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.pkl.core.messaging.MessageTransports.Logger; +import org.pkl.server.Server; + +@SuppressWarnings("unused") +public class LibPkl { + public interface MessageCallbackFunctionPointer extends CFunctionPointer { + @InvokeCFunctionPointer(transition = Transition.TO_NATIVE) + void invoke(int length, CCharPointer msg, VoidPointer userData); + } + + private static final Logger logger = new LibPklLogger(); + private static final NativeTransport transport = + new NativeTransport(logger, LibPkl::handleSendMessageToNative); + private static Server server; + private static MessageCallbackFunctionPointer cb; + private static VoidPointer userData; + + private LibPkl() {} + + @CEntryPoint(name = "pkl_internal_init", builtin = CEntryPoint.Builtin.CREATE_ISOLATE) + static native IsolateThread pklInternalInit(); + + @CEntryPoint(name = "pkl_internal_send_message") + public static void pklInternalSendMessage(IsolateThread thread, int length, CCharPointer ptr) { + logger.log("Got message from native"); + transport.sendMessage(length, ptr); + } + + @CEntryPoint(name = "pkl_internal_register_response_handler") + public static void pklInternalRegisterResponseHandler( + IsolateThread thread, LibPkl.MessageCallbackFunctionPointer cb, VoidPointer userData) { + logger.log("Got handler to call from Pkl"); + LibPkl.cb = cb; + LibPkl.userData = userData; + } + + @CEntryPoint(name = "pkl_internal_close", builtin = CEntryPoint.Builtin.TEAR_DOWN_ISOLATE) + public static native void pklInternalClose(IsolateThread thread); + + @CEntryPoint(name = "pkl_internal_server_start") + public static void pklInternalServerStart(IsolateThread thread) { + server = new Server(transport); + server.start(); + } + + @CEntryPoint(name = "pkl_internal_server_stop") + public static void pklInternalServerStop(IsolateThread thread) { + server.close(); + } + + public static void handleSendMessageToNative(byte[] bytes) { + try (var pin = PinnedObject.create(bytes)) { + // TODO: Provide a meaningful error the user if they haven't run `pkl_init`. + cb.invoke(bytes.length, pin.addressOfArrayElement(0), LibPkl.userData); + } + } + + /** + * Needed otherwise we see the following error: + * + *

Error: Method 'org.pkl.libpkl.LibPkl.main' is declared as the main entry point but it can + * not be found. Make sure that class 'org.pkl.libpkl.LibPkl' is on the classpath and that method + * 'main(String[])' exists in that class. + * + *

TODO: Clean this up once merged onto a feature-branch + * + *

This is because we are passing a main class to native-image using the -H:Class= arg. That + * argument is optional and not required when building a shared library. + */ + public static void main(String[] argv) {} +} diff --git a/libpkl/src/main/java/org/pkl/libpkl/LibPklLogger.java b/libpkl/src/main/java/org/pkl/libpkl/LibPklLogger.java new file mode 100644 index 000000000..f6e887d80 --- /dev/null +++ b/libpkl/src/main/java/org/pkl/libpkl/LibPklLogger.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl; + +import java.util.Objects; +import org.pkl.core.messaging.MessageTransports.Logger; + +public class LibPklLogger implements Logger { + + @Override + public void log(String msg) { + if (Objects.equals(System.getenv("PKL_DEBUG"), "1")) { + System.err.println("[libpkl] " + msg); + } + } +} diff --git a/libpkl/src/main/java/org/pkl/libpkl/NativeInputStream.java b/libpkl/src/main/java/org/pkl/libpkl/NativeInputStream.java new file mode 100644 index 000000000..5f58ee592 --- /dev/null +++ b/libpkl/src/main/java/org/pkl/libpkl/NativeInputStream.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl; + +import java.io.InputStream; +import org.graalvm.nativeimage.c.type.CCharPointer; + +public class NativeInputStream extends InputStream { + private int offset; + private final int length; + private final CCharPointer ptr; + + public NativeInputStream(int length, CCharPointer ptr) { + super(); + this.length = length; + this.ptr = ptr; + } + + @Override + public int read() { + if (available() <= 0) { + return -1; + } + var result = ptr.read(offset); + offset++; + return result & 0xFF; + } + + @Override + public int available() { + return length - offset; + } +} diff --git a/libpkl/src/main/java/org/pkl/libpkl/NativeTransport.java b/libpkl/src/main/java/org/pkl/libpkl/NativeTransport.java new file mode 100644 index 000000000..4de65bac8 --- /dev/null +++ b/libpkl/src/main/java/org/pkl/libpkl/NativeTransport.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.function.Consumer; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.msgpack.core.MessagePack; +import org.pkl.core.messaging.Message; +import org.pkl.core.messaging.MessageTransports.AbstractMessageTransport; +import org.pkl.core.messaging.MessageTransports.Logger; +import org.pkl.core.messaging.ProtocolException; +import org.pkl.server.ServerMessagePackDecoder; +import org.pkl.server.ServerMessagePackEncoder; + +public class NativeTransport extends AbstractMessageTransport { + private final Consumer sendMessageToNative; + private final Logger logger; + + protected NativeTransport(Logger logger, Consumer sendMessageToNative) { + super(logger); + this.logger = logger; + this.sendMessageToNative = sendMessageToNative; + } + + @Override + protected void doStart() {} + + @Override + protected void doClose() {} + + @Override + protected void doSend(Message message) { + try (var os = new ByteArrayOutputStream(); + var packer = MessagePack.newDefaultPacker(os)) { + var encoder = new ServerMessagePackEncoder(packer); + encoder.encode(message); + sendMessageToNative.accept(os.toByteArray()); + } catch (IOException | ProtocolException e) { + // TODO: Test that this error message is visible. + logger.log(e.getMessage()); + } + } + + public void sendMessage(int length, CCharPointer ptr) { + try (var is = new NativeInputStream(length, ptr); + var unpacker = MessagePack.newDefaultUnpacker(is)) { + var message = new ServerMessagePackDecoder(unpacker).decode(); + assert message != null; + accept(message); + } catch (IOException | ProtocolException e) { + // TODO: Test that this error message is visible. + logger.log(e.getMessage()); + } + } +} diff --git a/libpkl/src/main/java/org/pkl/libpkl/package-info.java b/libpkl/src/main/java/org/pkl/libpkl/package-info.java new file mode 100644 index 000000000..fb5f09e39 --- /dev/null +++ b/libpkl/src/main/java/org/pkl/libpkl/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.libpkl; + +import org.pkl.core.util.NonnullByDefault; diff --git a/libpkl/src/test/kotlin/org/pkl/libpkl/JNATestClient.kt b/libpkl/src/test/kotlin/org/pkl/libpkl/JNATestClient.kt new file mode 100644 index 000000000..4167e9bfe --- /dev/null +++ b/libpkl/src/test/kotlin/org/pkl/libpkl/JNATestClient.kt @@ -0,0 +1,112 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl + +import com.sun.jna.Pointer +import java.io.ByteArrayOutputStream +import java.lang.AutoCloseable +import java.nio.file.Path +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.BlockingQueue +import org.assertj.core.api.Assertions.assertThat +import org.msgpack.core.MessagePack +import org.pkl.core.messaging.Message +import org.pkl.core.messaging.Messages.ModuleReaderSpec +import org.pkl.core.messaging.Messages.ResourceReaderSpec +import org.pkl.server.CreateEvaluatorRequest +import org.pkl.server.CreateEvaluatorResponse +import org.pkl.server.Http +import org.pkl.server.Project +import org.pkl.server.ServerMessagePackDecoder +import org.pkl.server.ServerMessagePackEncoder + +class JNATestClient : LibPklLibrary.PklMessageResponseHandler, Iterable, AutoCloseable { + val incoming: BlockingQueue = ArrayBlockingQueue(10) + + override fun invoke(length: Int, message: Pointer, userData: Pointer?) { + val receivedBytes: ByteArray = message.getByteArray(0, length) + val message = decode(receivedBytes) + assertThat(message).isInstanceOf(Message::class.java) + incoming.add(message!!) + } + + override fun close() = incoming.clear() + + override fun iterator(): Iterator = incoming.iterator() + + fun send(message: Message): Int = + // TODO: Propagate `handlerContext` through, and validate it. + encode(message).let { LibPklLibrary.INSTANCE.pkl_send_message(it.size, it) } + + inline fun receive(): T { + val message = incoming.take() + assertThat(message).isInstanceOf(T::class.java) + return message as T + } + + fun sendCreateEvaluatorRequest( + requestId: Long = 123, + resourceReaders: List = listOf(), + moduleReaders: List = listOf(), + modulePaths: List = listOf(), + project: Project? = null, + cacheDir: Path? = null, + http: Http? = null, + ): Long { + val message = + CreateEvaluatorRequest( + 123, + listOf(".*"), + listOf(".*"), + moduleReaders, + resourceReaders, + modulePaths, + mapOf(), + mapOf(), + null, + null, + cacheDir, + null, + project, + http, + null, + null, + ) + + send(message) + + val response = receive() + assertThat(response.requestId()).isEqualTo(requestId) + assertThat(response.evaluatorId).isNotNull + assertThat(response.error).isNull() + + return response.evaluatorId!! + } + + private fun encode(message: Message): ByteArray { + ByteArrayOutputStream().use { os -> + val packer = MessagePack.newDefaultPacker(os) + val encoder = ServerMessagePackEncoder(packer) + encoder.encode(message) + return os.toByteArray() + } + } + + private fun decode(receivedBytes: ByteArray): Message? { + val unpacker = MessagePack.newDefaultUnpacker(receivedBytes) + return ServerMessagePackDecoder(unpacker).decode() + } +} diff --git a/libpkl/src/test/kotlin/org/pkl/libpkl/LibPklLibrary.kt b/libpkl/src/test/kotlin/org/pkl/libpkl/LibPklLibrary.kt new file mode 100644 index 000000000..2c54ac666 --- /dev/null +++ b/libpkl/src/test/kotlin/org/pkl/libpkl/LibPklLibrary.kt @@ -0,0 +1,37 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl + +import com.sun.jna.Callback +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer + +interface LibPklLibrary : Library { + companion object { + val INSTANCE: LibPklLibrary = Native.load("pkl", LibPklLibrary::class.java) + } + + interface PklMessageResponseHandler : Callback { + fun invoke(length: Int, message: Pointer, userData: Pointer?) + } + + fun pkl_init(handler: PklMessageResponseHandler, userData: Pointer?): Int + + fun pkl_send_message(length: Int, message: ByteArray): Int + + fun pkl_close(): Int +} diff --git a/libpkl/src/test/kotlin/org/pkl/libpkl/MessagePackDebugRenderer.kt b/libpkl/src/test/kotlin/org/pkl/libpkl/MessagePackDebugRenderer.kt new file mode 100644 index 000000000..8d4d01d4e --- /dev/null +++ b/libpkl/src/test/kotlin/org/pkl/libpkl/MessagePackDebugRenderer.kt @@ -0,0 +1,107 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl + +import java.lang.IllegalStateException +import org.msgpack.core.MessagePack +import org.msgpack.core.MessageUnpacker +import org.msgpack.value.ValueType +import org.pkl.core.util.yaml.YamlEmitter + +/** Renders MessagePack structures in YAML. */ +class MessagePackDebugRenderer(bytes: ByteArray) { + private val unpacker: MessageUnpacker = MessagePack.newDefaultUnpacker(bytes) + private val currIndent = StringBuilder("") + private val sb = StringBuilder() + private val indent = " " + private val yamlEmitter = YamlEmitter.create(sb, "1.2", indent) + + private fun incIndent() { + currIndent.append(indent) + } + + private fun decIndent() { + currIndent.setLength(currIndent.length - indent.length) + } + + private fun newline() { + sb.append("\n") + sb.append(currIndent) + } + + private fun renderKey() { + val mf = unpacker.nextFormat + when (mf.valueType!!) { + ValueType.STRING -> yamlEmitter.emit(unpacker.unpackString(), currIndent, true) + ValueType.MAP, + ValueType.ARRAY -> { + sb.append("? ") + incIndent() + renderValue() + decIndent() + newline() + } + else -> renderValue() + } + sb.append(": ") + } + + private fun renderValue() { + val mf = unpacker.nextFormat + when (mf.valueType!!) { + ValueType.INTEGER, + ValueType.FLOAT, + ValueType.BOOLEAN, + ValueType.NIL -> sb.append(unpacker.unpackValue().toJson()) + ValueType.STRING -> yamlEmitter.emit(unpacker.unpackString(), currIndent, false) + ValueType.ARRAY -> { + val size = unpacker.unpackArrayHeader() + if (size == 0) { + sb.append("[]") + return + } + for (i in 0 until size) { + newline() + sb.append("- ") + incIndent() + renderValue() + decIndent() + } + } + ValueType.MAP -> { + val size = unpacker.unpackMapHeader() + if (size == 0) { + sb.append("{}") + return + } + for (i in 0 until size) { + newline() + renderKey() + incIndent() + renderValue() + decIndent() + } + } + ValueType.BINARY, + ValueType.EXTENSION -> throw IllegalStateException("Unexpected value type ${mf.valueType}") + } + } + + val output: String by lazy { + renderValue() + sb.toString().removePrefix("\n") + } +} diff --git a/libpkl/src/test/kotlin/org/pkl/libpkl/NativeTest.kt b/libpkl/src/test/kotlin/org/pkl/libpkl/NativeTest.kt new file mode 100644 index 000000000..557d38764 --- /dev/null +++ b/libpkl/src/test/kotlin/org/pkl/libpkl/NativeTest.kt @@ -0,0 +1,873 @@ +/* + * Copyright © 2025 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. + */ +package org.pkl.libpkl + +import com.sun.jna.Pointer +import java.net.URI +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.msgpack.core.MessagePack +import org.pkl.commons.test.PackageServer +import org.pkl.core.messaging.Messages.ListModulesRequest +import org.pkl.core.messaging.Messages.ListModulesResponse +import org.pkl.core.messaging.Messages.ListResourcesRequest +import org.pkl.core.messaging.Messages.ListResourcesResponse +import org.pkl.core.messaging.Messages.ModuleReaderSpec +import org.pkl.core.messaging.Messages.ReadModuleRequest +import org.pkl.core.messaging.Messages.ReadModuleResponse +import org.pkl.core.messaging.Messages.ReadResourceRequest +import org.pkl.core.messaging.Messages.ReadResourceResponse +import org.pkl.core.messaging.Messages.ResourceReaderSpec +import org.pkl.core.module.PathElement +import org.pkl.server.* + +// To run this test in IntelliJ, add +// `-Djna.library.path=$ProjectFileDir$/libpkl/build/libs/-` to the +// run configuration. +// +// You can modify the IntelliJ JUnit configuration template so that this flag gets added +// automatically. +// See https://www.jetbrains.com/help/idea/run-debug-configuration.html#templates for more details. +/** + * Binds to the native library using JNA. + * + * @see JNATestClient + * @see LibPklLibrary + */ +class NativeTest { + companion object { + lateinit var client: JNATestClient + } + + @BeforeEach + fun beforeEach() { + client = JNATestClient() + assertThat(LibPklLibrary.INSTANCE.pkl_init(client, Pointer.NULL)).isEqualTo(0) + } + + @AfterEach + fun afterEach() { + client.close() + assertThat(LibPklLibrary.INSTANCE.pkl_close()).isEqualTo(0) + } + + @Test + fun `create evaluator, receive message, and close evaluator`() { + val evaluatorId = client.sendCreateEvaluatorRequest() + + client.send(EvaluateRequest(1, evaluatorId, URI("repl:text"), """foo = 1""", null)) + val response = client.receive() + assertThat(response.evaluatorId).isEqualTo(evaluatorId) + + assertThat(client.send(CloseEvaluator(evaluatorId))).isEqualTo(0) + assertThat(client).hasSize(0) + } + + @Test + fun `evaluate module`() { + val evaluatorId = client.sendCreateEvaluatorRequest() + val requestId = 234L + + client.send( + EvaluateRequest( + requestId, + evaluatorId, + URI("repl:text"), + """ + foo { + bar = "bar" + } + """ + .trimIndent(), + null, + ) + ) + + val response = client.receive() + assertThat(response.error).isNull() + assertThat(response.result).isNotNull + assertThat(response.requestId()).isEqualTo(requestId) + + val unpacker = MessagePack.newDefaultUnpacker(response.result) + val value = unpacker.unpackValue() + assertThat(value.isArrayValue) + } + + @Test + fun `trace logs`() { + val evaluatorId = client.sendCreateEvaluatorRequest() + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """ + foo = trace(1 + 2 + 3) + """ + .trimIndent(), + null, + ) + ) + + val response = client.receive() + assertThat(response.level).isEqualTo(0) + assertThat(response.message).isEqualTo("1 + 2 + 3 = 6") + + // client.receive() + } + + @Test + fun `warn logs`() { + val evaluatorId = client.sendCreateEvaluatorRequest() + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """ + @Deprecated { message = "use bar instead" } + function foo() = 5 + + result = foo() + """ + .trimIndent(), + null, + ) + ) + + val response = client.receive() + assertThat(response.level).isEqualTo(1) + assertThat(response.message).contains("use bar instead") + + client.receive() + } + + @Test + fun `read resource`() { + val reader = ResourceReaderSpec("bahumbug", true, false) + val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = read("bahumbug:/foo.pkl").text""", + "res", + ) + ) + + val readResourceMsg = client.receive() + assertThat(readResourceMsg.uri.toString()).isEqualTo("bahumbug:/foo.pkl") + assertThat(readResourceMsg.evaluatorId).isEqualTo(evaluatorId) + + client.send( + ReadResourceResponse( + readResourceMsg.requestId, + evaluatorId, + "my bahumbug".toByteArray(), + null, + ) + ) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error).isNull() + + val unpacker = MessagePack.newDefaultUnpacker(evaluateResponse.result) + val value = unpacker.unpackValue() + assertThat(value.asStringValue().asString()).isEqualTo("my bahumbug") + } + + @Test + fun `read resource error`() { + val reader = ResourceReaderSpec("bahumbug", true, false) + val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = read("bahumbug:/foo.txt").text""", + "res", + ) + ) + + val readResourceMsg = client.receive() + + client.send( + ReadResourceResponse( + readResourceMsg.requestId, + evaluatorId, + byteArrayOf(), + "cannot read my bahumbug", + ) + ) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error).contains("bahumbug:/foo.txt") + assertThat(evaluateResponse.error).doesNotContain("org.pkl.core.PklBugException") + } + + @Test + fun `glob resource`() { + val reader = ResourceReaderSpec("bird", true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader)) + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """ + res = read*("bird:/**.txt").keys + """ + .trimIndent(), + "res", + ) + ) + val listResourcesRequest = client.receive() + assertThat(listResourcesRequest.uri.toString()).isEqualTo("bird:/") + client.send( + ListResourcesResponse( + listResourcesRequest.requestId, + listResourcesRequest.evaluatorId, + listOf(PathElement("foo.txt", false), PathElement("subdir", true)), + null, + ) + ) + val listResourcesRequest2 = client.receive() + assertThat(listResourcesRequest2.uri.toString()).isEqualTo("bird:/subdir/") + client.send( + ListResourcesResponse( + listResourcesRequest2.requestId, + listResourcesRequest2.evaluatorId, + listOf(PathElement("bar.txt", false)), + null, + ) + ) + val evaluateResponse = client.receive() + assertThat(evaluateResponse.result?.debugYaml) + .isEqualTo( + """ + - 6 + - + - bird:/foo.txt + - bird:/subdir/bar.txt + """ + .trimIndent() + ) + } + + @Test + fun `glob resources -- null pathElements and null error`() { + val reader = ResourceReaderSpec("bird", true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader)) + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """ + res = read*("bird:/**.txt").keys + """ + .trimIndent(), + "res", + ) + ) + val listResourcesRequest = client.receive() + client.send( + ListResourcesResponse( + listResourcesRequest.requestId, + listResourcesRequest.evaluatorId, + null, + null, + ) + ) + val evaluateResponse = client.receive() + assertThat(evaluateResponse.result?.debugYaml) + .isEqualTo( + """ + - 6 + - [] + """ + .trimIndent() + ) + } + + @Test + fun `glob resource error`() { + val reader = ResourceReaderSpec("bird", true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(resourceReaders = listOf(reader)) + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """ + res = read*("bird:/**.txt").keys + """ + .trimIndent(), + "res", + ) + ) + val listResourcesRequest = client.receive() + assertThat(listResourcesRequest.uri.toString()).isEqualTo("bird:/") + client.send( + ListResourcesResponse( + listResourcesRequest.requestId, + listResourcesRequest.evaluatorId, + null, + "didnt work", + ) + ) + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error) + .isEqualTo( + """ + –– Pkl Error –– + I/O error resolving glob pattern `bird:/**.txt`. + IOException: didnt work + + 1 | res = read*("bird:/**.txt").keys + ^^^^^^^^^^^^^^^^^^^^^ + at text#res (repl:text) + + 1 | res + ^^^ + at (repl:text) + + """ + .trimIndent() + ) + } + + @Test + fun `read module`() { + val reader = ModuleReaderSpec("bird", true, true, false) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = import("bird:/pigeon.pkl").value""", + "res", + ) + ) + + val readModuleMsg = client.receive() + assertThat(readModuleMsg.uri.toString()).isEqualTo("bird:/pigeon.pkl") + assertThat(readModuleMsg.evaluatorId).isEqualTo(evaluatorId) + + client.send(ReadModuleResponse(readModuleMsg.requestId, evaluatorId, "value = 5", null)) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error).isNull() + val unpacker = MessagePack.newDefaultUnpacker(evaluateResponse.result) + val value = unpacker.unpackValue() + assertThat(value.asIntegerValue().asInt()).isEqualTo(5) + } + + @Test + fun `read module -- null contents and null error`() { + val reader = ModuleReaderSpec("bird", true, true, false) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + requestId = 1, + evaluatorId = evaluatorId, + moduleUri = URI("repl:text"), + moduleText = """res = import("bird:/pigeon.pkl")""", + expr = "res", + ) + ) + + val readModuleMsg = client.receive() + assertThat(readModuleMsg.uri.toString()).isEqualTo("bird:/pigeon.pkl") + assertThat(readModuleMsg.evaluatorId).isEqualTo(evaluatorId) + + client.send(ReadModuleResponse(readModuleMsg.requestId, evaluatorId, null, null)) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error).isNull() + val unpacker = MessagePack.newDefaultUnpacker(evaluateResponse.result) + val value = unpacker.unpackValue().asArrayValue().list() + assertThat(value[0].asIntegerValue().asLong()).isEqualTo(0x1) + assertThat(value[1].asStringValue().asString()).isEqualTo("pigeon") + assertThat(value[3].asArrayValue().list()).isEmpty() + } + + @Test + fun `read module error`() { + val reader = ModuleReaderSpec("bird", true, true, false) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = import("bird:/pigeon.pkl").value""", + "res", + ) + ) + + val readModuleMsg = client.receive() + assertThat(readModuleMsg.uri.toString()).isEqualTo("bird:/pigeon.pkl") + assertThat(readModuleMsg.evaluatorId).isEqualTo(evaluatorId) + + client.send( + ReadModuleResponse(readModuleMsg.requestId, evaluatorId, null, "Don't know where Pigeon is") + ) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error).contains("Don't know where Pigeon is") + } + + @Test + fun `glob module`() { + val reader = ModuleReaderSpec("bird", true, true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = import*("bird:/**.pkl").keys""", + "res", + ) + ) + + val listModulesMsg = client.receive() + assertThat(listModulesMsg.uri.scheme).isEqualTo("bird") + assertThat(listModulesMsg.uri.path).isEqualTo("/") + client.send( + ListModulesResponse( + listModulesMsg.requestId, + evaluatorId, + listOf( + PathElement("birds", true), + PathElement("majesticBirds", true), + PathElement("Person.pkl", false), + ), + null, + ) + ) + val listModulesMsg2 = client.receive() + assertThat(listModulesMsg2.uri.scheme).isEqualTo("bird") + assertThat(listModulesMsg2.uri.path).isEqualTo("/birds/") + client.send( + ListModulesResponse( + listModulesMsg2.requestId, + listModulesMsg2.evaluatorId, + listOf(PathElement("pigeon.pkl", false), PathElement("parrot.pkl", false)), + null, + ) + ) + val listModulesMsg3 = client.receive() + assertThat(listModulesMsg3.uri.scheme).isEqualTo("bird") + assertThat(listModulesMsg3.uri.path).isEqualTo("/majesticBirds/") + client.send( + ListModulesResponse( + listModulesMsg3.requestId, + listModulesMsg3.evaluatorId, + listOf(PathElement("barnOwl.pkl", false), PathElement("elfOwl.pkl", false)), + null, + ) + ) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.result?.debugRendering) + .isEqualTo( + """ + - 6 + - + - bird:/Person.pkl + - bird:/birds/parrot.pkl + - bird:/birds/pigeon.pkl + - bird:/majesticBirds/barnOwl.pkl + - bird:/majesticBirds/elfOwl.pkl + """ + .trimIndent() + ) + } + + @Test + fun `glob module -- null pathElements and null error`() { + val reader = ModuleReaderSpec("bird", true, true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = import*("bird:/**.pkl").keys""", + "res", + ) + ) + val listModulesMsg = client.receive() + client.send(ListModulesResponse(listModulesMsg.requestId, evaluatorId, null, null)) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.result?.debugRendering) + .isEqualTo( + """ + - 6 + - [] + """ + .trimIndent() + ) + } + + @Test + fun `glob module error`() { + val reader = ModuleReaderSpec("bird", true, true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("repl:text"), + """res = import*("bird:/**.pkl").keys""", + "res", + ) + ) + + val listModulesMsg = client.receive() + assertThat(listModulesMsg.uri.scheme).isEqualTo("bird") + assertThat(listModulesMsg.uri.path).isEqualTo("/") + client.send(ListModulesResponse(listModulesMsg.requestId, evaluatorId, null, "nope")) + val evaluateResponse = client.receive() + assertThat(evaluateResponse.error) + .isEqualTo( + """ + –– Pkl Error –– + I/O error resolving glob pattern `bird:/**.pkl`. + IOException: nope + + 1 | res = import*("bird:/**.pkl").keys + ^^^^^^^^^^^^^^^^^^^^^^^ + at text#res (repl:text) + + 1 | res + ^^^ + at (repl:text) + + """ + .trimIndent() + ) + } + + @Test + fun `import triple-dot path`() { + val reader = ModuleReaderSpec("bird", true, true, true) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send( + EvaluateRequest( + 1, + evaluatorId, + URI("bird:/foo/bar/baz.pkl"), + """ + import ".../buz.pkl" + + res = buz.res + """ + .trimIndent(), + "res", + ) + ) + val readModuleRequest = client.receive() + assertThat(readModuleRequest.uri).isEqualTo(URI("bird:/foo/buz.pkl")) + client.send( + ReadModuleResponse( + readModuleRequest.requestId, + readModuleRequest.evaluatorId, + null, + "not here", + ) + ) + + val readModuleRequest2 = client.receive() + assertThat(readModuleRequest2.uri).isEqualTo(URI("bird:/buz.pkl")) + client.send( + ReadModuleResponse( + readModuleRequest2.requestId, + readModuleRequest2.evaluatorId, + "res = 1", + null, + ) + ) + + val evaluatorResponse = client.receive() + assertThat(evaluatorResponse.result?.debugYaml).isEqualTo("1") + } + + @Test + fun `evaluate error`() { + val evaluatorId = client.sendCreateEvaluatorRequest() + + client.send(EvaluateRequest(1, evaluatorId, URI("repl:text"), """foo = 1""", "foo as String")) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.requestId()).isEqualTo(1) + assertThat(evaluateResponse.error).contains("Expected value of type") + } + + @Test + fun `evaluate client-provided module reader`() { + val reader = ModuleReaderSpec("bird", true, false, false) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + + client.send(EvaluateRequest(1, evaluatorId, URI("bird:/pigeon.pkl"), null, "output.text")) + + val readModuleRequest = client.receive() + assertThat(readModuleRequest.uri.toString()).isEqualTo("bird:/pigeon.pkl") + + client.send( + ReadModuleResponse( + readModuleRequest.requestId, + evaluatorId, + """ + firstName = "Pigeon" + lastName = "Bird" + fullName = firstName + " " + lastName + """ + .trimIndent(), + null, + ) + ) + + val evaluateResponse = client.receive() + assertThat(evaluateResponse.result).isNotNull + assertThat(evaluateResponse.result?.debugYaml) + .isEqualTo( + """ + | + firstName = "Pigeon" + lastName = "Bird" + fullName = "Pigeon Bird" + """ + .trimIndent() + ) + } + + @Test + fun `concurrent evaluations`() { + val reader = ModuleReaderSpec("bird", true, false, false) + val evaluatorId = client.sendCreateEvaluatorRequest(moduleReaders = listOf(reader)) + client.send(EvaluateRequest(1, evaluatorId, URI("bird:/pigeon.pkl"), null, "output.text")) + + client.send(EvaluateRequest(2, evaluatorId, URI("bird:/parrot.pkl"), null, "output.text")) + + // evaluation is single-threaded; `parrot.pkl` gets evaluated after `pigeon.pkl` completes. + val response11 = client.receive() + assertThat(response11.uri.toString()).isEqualTo("bird:/pigeon.pkl") + + client.send( + ReadModuleResponse( + response11.requestId, + evaluatorId, + """ + firstName = "Pigeon" + lastName = "Bird" + fullName = firstName + " " + lastName + """ + .trimIndent(), + null, + ) + ) + + val response12 = client.receive() + assertThat(response12.result).isNotNull + assertThat(response12.result?.debugYaml) + .isEqualTo( + """ + | + firstName = "Pigeon" + lastName = "Bird" + fullName = "Pigeon Bird" + """ + .trimIndent() + ) + + val response21 = client.receive() + assertThat(response21.uri.toString()).isEqualTo("bird:/parrot.pkl") + + client.send( + ReadModuleResponse( + response21.requestId, + evaluatorId, + """ + firstName = "Parrot" + lastName = "Bird" + fullName = firstName + " " + lastName + """ + .trimIndent(), + null, + ) + ) + + val response22 = client.receive() + assertThat(response22.result).isNotNull + assertThat(response22.result?.debugYaml) + .isEqualTo( + """ + | + firstName = "Parrot" + lastName = "Bird" + fullName = "Parrot Bird" + """ + .trimIndent() + ) + } + + @Test + fun `evaluate with project dependencies`(@TempDir tempDir: Path) { + val cacheDir = tempDir.resolve("cache").createDirectories() + PackageServer.populateCacheDir(cacheDir) + val libDir = tempDir.resolve("lib/").createDirectories() + libDir + .resolve("lib.pkl") + .writeText( + """ + text = "This is from lib" + """ + .trimIndent() + ) + libDir + .resolve("PklProject") + .writeText( + """ + amends "pkl:Project" + + package { + name = "lib" + baseUri = "package://localhost:0/lib" + version = "5.0.0" + packageZipUrl = "https://localhost:0/lib.zip" + } + """ + .trimIndent() + ) + val projectDir = tempDir.resolve("proj/").createDirectories() + val module = projectDir.resolve("mod.pkl") + module.writeText( + """ + import "@birds/Bird.pkl" + import "@lib/lib.pkl" + + res: Bird = new { + name = "Birdie" + favoriteFruit { name = "dragonfruit" } + } + + libContents = lib + """ + .trimIndent() + ) + val dollar = '$' + projectDir + .resolve("PklProject.deps.json") + .writeText( + """ + { + "schemaVersion": 1, + "resolvedDependencies": { + "package://localhost:0/birds@0": { + "type": "remote", + "uri": "projectpackage://localhost:0/birds@0.5.0", + "checksums": { + "sha256": "${dollar}skipChecksumVerification" + } + }, + "package://localhost:0/fruit@1": { + "type": "remote", + "uri": "projectpackage://localhost:0/fruit@1.0.5", + "checksums": { + "sha256": "${dollar}skipChecksumVerification" + } + }, + "package://localhost:0/lib@5": { + "type": "local", + "uri": "projectpackage://localhost:0/lib@5.0.0", + "path": "../lib" + } + } + } + + """ + .trimIndent() + ) + val evaluatorId = + client.sendCreateEvaluatorRequest( + cacheDir = cacheDir, + project = + Project( + projectDir.resolve("PklProject").toUri(), + null, + mapOf( + "birds" to RemoteDependency(URI("package://localhost:0/birds@0.5.0"), null), + "lib" to + Project( + libDir.toUri().resolve("PklProject"), + URI("package://localhost:0/lib@5.0.0"), + emptyMap(), + ), + ), + ), + ) + client.send(EvaluateRequest(1, evaluatorId, module.toUri(), null, "output.text")) + val resp2 = client.receive() + assertThat(resp2.error).isNull() + assertThat(resp2.result).isNotNull() + assertThat(resp2.result?.debugRendering?.trim()) + .isEqualTo( + """ + | + res { + name = "Birdie" + favoriteFruit { + name = "dragonfruit" + } + } + libContents { + text = "This is from lib" + } + """ + .trimIndent() + ) + } + + private val ByteArray.debugYaml + get() = MessagePackDebugRenderer(this).output.trimIndent() + + private val ByteArray.debugRendering: String + get() = MessagePackDebugRenderer(this).output +} diff --git a/pkl-core/src/main/java/org/pkl/core/messaging/MessageTransports.java b/pkl-core/src/main/java/org/pkl/core/messaging/MessageTransports.java index 7062638a8..9a8e65b35 100644 --- a/pkl-core/src/main/java/org/pkl/core/messaging/MessageTransports.java +++ b/pkl-core/src/main/java/org/pkl/core/messaging/MessageTransports.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -120,7 +120,7 @@ public void setOther(DirectMessageTransport other) { } } - protected abstract static class AbstractMessageTransport implements MessageTransport { + public abstract static class AbstractMessageTransport implements MessageTransport { private final Logger logger; private MessageTransport.OneWayHandler oneWayHandler; diff --git a/settings.gradle.kts b/settings.gradle.kts index ccac20b85..717957855 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,8 @@ include("bench") include("docs") +include("libpkl") + include("pkl-cli") include("pkl-codegen-java")