Add Pkl 0.26 features (#23)

* Add Pkl 0.26 features
* Add multi-version support
* Bump Pkl version in CI
* Update CI
* Split tests across different Pkl versions

Co-authored-by: Daniel Chao <daniel.h.chao@gmail.com>
This commit is contained in:
Philip K.F. Hölzenspies 2024-07-02 12:36:59 +01:00 committed by GitHub
parent b23fd2369f
commit 48bdb4f503
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 406 additions and 166 deletions

View File

@ -15,6 +15,8 @@
// ===----------------------------------------------------------------------===// // ===----------------------------------------------------------------------===//
amends "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.circleci@1.0.1#/PklCI.pkl" amends "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.circleci@1.0.1#/PklCI.pkl"
import "pkl:semver"
local swiftTest = new RunStep { local swiftTest = new RunStep {
name = "swift test" name = "swift test"
command = """ command = """
@ -23,59 +25,78 @@ local swiftTest = new RunStep {
""" """
} }
local pklVersion = "0.25.1" local class PklDistribution {
local pklBinary = "https://repo1.maven.org/maven2/org/pkl-lang/pkl-cli-linux-amd64/\(pklVersion)/pkl-cli-linux-amd64-\(pklVersion).bin" /// The version of this distribution
version: String(semver.isValid(this))
local downloadPkl = new RunStep { /// Normalized version for use in task names
name = "Downloading pkl" fixed normalizedVersion: String = version.replaceAll(".", "-")
/// The URL to download this distribution
fixed downloadUrl: String = "https://github.com/apple/pkl/releases/download/\(version)/pkl-linux-amd64"
fixed downloadRunStep: RunStep = new {
name = "Downloading pkl-\(version)"
command = """ command = """
mkdir /tmp/pkl PKL=$(mktemp /tmp/pkl-\(version)-XXXXXX)
curl -L "\(pklBinary)" > /tmp/pkl/pkl curl -L "\(downloadUrl)" > $PKL
chmod +x /tmp/pkl/pkl chmod +x $PKL
echo 'export PKL_EXEC=/tmp/pkl/pkl' >> $BASH_ENV echo "export PKL_EXEC=$PKL" >> $BASH_ENV
""" """
} }
}
local pklCurrent: PklDistribution = new {
version = "0.26.0"
}
local pklDistributions: Listing<PklDistribution> = new {
new { version = "0.25.3" }
pklCurrent
}
local testJobs = jobs.keys.filter((it) -> it.startsWith("test"))
main { main {
jobs { jobs {
"test" ...testJobs
} }
} }
prb { prb {
jobs { jobs {
"test" ...testJobs
} }
} }
release { release {
jobs { jobs {
"test" ...testJobs
new { new {
["pkl-package"] { ["pkl-package"] {
requires { requires {
"test" ...testJobs
} }
} }
} }
new { new {
["pkl-gen-swift-macos"] { ["pkl-gen-swift-macos"] {
requires { requires {
"test" ...testJobs
} }
} }
} }
new { new {
["pkl-gen-swift-linux-amd64"] { ["pkl-gen-swift-linux-amd64"] {
requires { requires {
"test" ...testJobs
} }
} }
} }
new { new {
["pkl-gen-swift-linux-aarch64"] { ["pkl-gen-swift-linux-aarch64"] {
requires { requires {
"test" ...testJobs
} }
} }
} }
@ -100,7 +121,8 @@ triggerDocsBuild = "release"
triggerPackageDocsBuild = "release" triggerPackageDocsBuild = "release"
jobs { jobs {
["test"] { for (distribution in pklDistributions) {
["test-pkl-\(distribution.normalizedVersion)"] {
docker { docker {
new { new {
image = "swift:5.9-rhel-ubi9" image = "swift:5.9-rhel-ubi9"
@ -109,7 +131,7 @@ jobs {
resource_class = "xlarge" resource_class = "xlarge"
steps { steps {
"checkout" "checkout"
downloadPkl distribution.downloadRunStep
swiftTest swiftTest
new RunStep { command = "make test-snippets" } new RunStep { command = "make test-snippets" }
new RunStep { command = "make test-pkl" } new RunStep { command = "make test-pkl" }
@ -119,6 +141,7 @@ jobs {
} }
} }
} }
}
["pkl-gen-swift-macos"] { ["pkl-gen-swift-macos"] {
macos { macos {
@ -168,7 +191,7 @@ jobs {
} }
steps { steps {
"checkout" "checkout"
downloadPkl pklCurrent.downloadRunStep
new RunStep { new RunStep {
// TODO remove skip-publish-check after initial release // TODO remove skip-publish-check after initial release
command = #"$PKL_EXEC project package --skip-publish-check --output-path out/pkl-package/ codegen/src/"# command = #"$PKL_EXEC project package --skip-publish-check --output-path out/pkl-package/ codegen/src/"#

View File

@ -3,16 +3,42 @@ version: '2.1'
orbs: orbs:
pr-approval: apple/pr-approval@0.1.0 pr-approval: apple/pr-approval@0.1.0
jobs: jobs:
test: test-pkl-0-25-3:
steps: steps:
- checkout - checkout
- run: - run:
command: |- command: |-
mkdir /tmp/pkl PKL=$(mktemp /tmp/pkl-0.25.3-XXXXXX)
curl -L "https://repo1.maven.org/maven2/org/pkl-lang/pkl-cli-linux-amd64/0.25.1/pkl-cli-linux-amd64-0.25.1.bin" > /tmp/pkl/pkl curl -L "https://github.com/apple/pkl/releases/download/0.25.3/pkl-linux-amd64" > $PKL
chmod +x /tmp/pkl/pkl chmod +x $PKL
echo 'export PKL_EXEC=/tmp/pkl/pkl' >> $BASH_ENV echo "export PKL_EXEC=$PKL" >> $BASH_ENV
name: Downloading pkl name: Downloading pkl-0.25.3
- run:
command: |-
mkdir -p .out/test-results/
swift test -vv --parallel --num-workers 1 --xunit-output .out/test-results/xunit.xml -Xswiftc -warnings-as-errors
name: swift test
- run:
command: make test-snippets
- run:
command: make test-pkl
- run:
command: make generate-fixtures
- store_test_results:
path: .out/test-results/
resource_class: xlarge
docker:
- image: swift:5.9-rhel-ubi9
test-pkl-0-26-0:
steps:
- checkout
- run:
command: |-
PKL=$(mktemp /tmp/pkl-0.26.0-XXXXXX)
curl -L "https://github.com/apple/pkl/releases/download/0.26.0/pkl-linux-amd64" > $PKL
chmod +x $PKL
echo "export PKL_EXEC=$PKL" >> $BASH_ENV
name: Downloading pkl-0.26.0
- run: - run:
command: |- command: |-
mkdir -p .out/test-results/ mkdir -p .out/test-results/
@ -82,11 +108,11 @@ jobs:
- checkout - checkout
- run: - run:
command: |- command: |-
mkdir /tmp/pkl PKL=$(mktemp /tmp/pkl-0.26.0-XXXXXX)
curl -L "https://repo1.maven.org/maven2/org/pkl-lang/pkl-cli-linux-amd64/0.25.1/pkl-cli-linux-amd64-0.25.1.bin" > /tmp/pkl/pkl curl -L "https://github.com/apple/pkl/releases/download/0.26.0/pkl-linux-amd64" > $PKL
chmod +x /tmp/pkl/pkl chmod +x $PKL
echo 'export PKL_EXEC=/tmp/pkl/pkl' >> $BASH_ENV echo "export PKL_EXEC=$PKL" >> $BASH_ENV
name: Downloading pkl name: Downloading pkl-0.26.0
- run: - run:
command: $PKL_EXEC project package --skip-publish-check --output-path out/pkl-package/ codegen/src/ command: $PKL_EXEC project package --skip-publish-check --output-path out/pkl-package/ codegen/src/
- persist_to_workspace: - persist_to_workspace:
@ -164,7 +190,11 @@ workflows:
type: approval type: approval
- pr-approval/authenticate: - pr-approval/authenticate:
context: pkl-pr-approval context: pkl-pr-approval
- test: - test-pkl-0-25-3:
requires:
- hold
- pr-approval/authenticate
- test-pkl-0-26-0:
requires: requires:
- hold - hold
- pr-approval/authenticate - pr-approval/authenticate
@ -174,14 +204,21 @@ workflows:
pattern: ^pull/\d+(/head)?$ pattern: ^pull/\d+(/head)?$
main: main:
jobs: jobs:
- test - test-pkl-0-25-3
- test-pkl-0-26-0
when: when:
equal: equal:
- main - main
- << pipeline.git.branch >> - << pipeline.git.branch >>
release: release:
jobs: jobs:
- test: - test-pkl-0-25-3:
filters:
branches:
ignore: /.*/
tags:
only: /^v?\d+\.\d+\.\d+$/
- test-pkl-0-26-0:
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/
@ -189,7 +226,8 @@ workflows:
only: /^v?\d+\.\d+\.\d+$/ only: /^v?\d+\.\d+\.\d+$/
- pkl-package: - pkl-package:
requires: requires:
- test - test-pkl-0-25-3
- test-pkl-0-26-0
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/
@ -197,7 +235,8 @@ workflows:
only: /^v?\d+\.\d+\.\d+$/ only: /^v?\d+\.\d+\.\d+$/
- pkl-gen-swift-macos: - pkl-gen-swift-macos:
requires: requires:
- test - test-pkl-0-25-3
- test-pkl-0-26-0
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/
@ -205,7 +244,8 @@ workflows:
only: /^v?\d+\.\d+\.\d+$/ only: /^v?\d+\.\d+\.\d+$/
- pkl-gen-swift-linux-amd64: - pkl-gen-swift-linux-amd64:
requires: requires:
- test - test-pkl-0-25-3
- test-pkl-0-26-0
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/
@ -213,7 +253,8 @@ workflows:
only: /^v?\d+\.\d+\.\d+$/ only: /^v?\d+\.\d+\.\d+$/
- pkl-gen-swift-linux-aarch64: - pkl-gen-swift-linux-aarch64:
requires: requires:
- test - test-pkl-0-25-3
- test-pkl-0-26-0
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/

View File

@ -1,5 +1,14 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "semanticversion",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftPackageIndex/SemanticVersion",
"state" : {
"revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42",
"version" : "0.4.0"
}
},
{ {
"identity" : "swift-argument-parser", "identity" : "swift-argument-parser",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -5,7 +5,7 @@ let package = Package(
name: "pkl-swift", name: "pkl-swift",
platforms: [ platforms: [
// required because of `Duration` API // required because of `Duration` API
.macOS(.v13) .macOS(.v13),
], ],
products: [ products: [
.library( .library(
@ -24,11 +24,12 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-system", from: "1.2.1"), .package(url: "https://github.com/apple/swift-system", from: "1.2.1"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"),
.package(url: "https://github.com/SwiftPackageIndex/SemanticVersion", from: "0.4.0"),
], ],
targets: [ targets: [
.target( .target(
name: "PklSwift", name: "PklSwift",
dependencies: ["MessagePack", "PklSwiftInternals"] dependencies: ["MessagePack", "PklSwiftInternals", "SemanticVersion"]
), ),
.target( .target(
name: "PklSwiftInternals" name: "PklSwiftInternals"

View File

@ -185,7 +185,6 @@ extension MessagePackValue {
case .ext: case .ext:
// TODO: implement this? // TODO: implement this?
fatalError("Cannot convert \(self) to \(T.self)") fatalError("Cannot convert \(self) to \(T.self)")
case .timestamp: case .timestamp:
// TODO: implement this? // TODO: implement this?
fatalError("Cannot convert \(self) to \(T.self)") fatalError("Cannot convert \(self) to \(T.self)")

View File

@ -36,17 +36,14 @@ public func withEvaluator<T>(_ action: (Evaluator) async throws -> T) async thro
try await withEvaluator(options: .preconfigured, action) try await withEvaluator(options: .preconfigured, action)
} }
/// Like ``withProjectEvaluator(projectDir:options:_:)``, but configured with preconfigured otions. /// Like ``withProjectEvaluator(projectBaseURI:options:_:)``, but configured with preconfigured options.
/// ///
/// - Parameters: /// - Parameters:
/// - projectDir: The directory containing the PklProject file. /// - projectBaseURI: The base path containing the PklProject file.
/// - action: The action to perform. /// - action: The action to perform.
/// - Returns: The result of the action. /// - Returns: The result of the action.
public func withProjectEvaluator<T>( public func withProjectEvaluator<T>(projectBaseURI: URL, _ action: (Evaluator) async throws -> T) async throws -> T {
projectDir: String, try await withProjectEvaluator(projectBaseURI: projectBaseURI, options: .preconfigured, action)
_ action: (Evaluator) async throws -> T
) async throws -> T {
try await withProjectEvaluator(projectDir: projectDir, options: .preconfigured, action)
} }
/// Convenience method for initializing an evaluator from the project. /// Convenience method for initializing an evaluator from the project.
@ -56,17 +53,17 @@ public func withProjectEvaluator<T>(
/// ///
/// After `action` completes, the evaluator is closed. /// After `action` completes, the evaluator is closed.
/// - Parameters: /// - Parameters:
/// - projectDir: The directory containing the PklProject file. /// - projectBaseURI: The base path containing the PklProject file.
/// - options: The base options used to configure the evaluator. /// - options: The base options used to configure the evaluator.
/// - action: The action to perform. /// - action: The action to perform.
/// - Returns: The result of the action. /// - Returns: The result of the action.
public func withProjectEvaluator<T>( public func withProjectEvaluator<T>(
projectDir: String, projectBaseURI: URL,
options: EvaluatorOptions, options: EvaluatorOptions,
_ action: (Evaluator) async throws -> T _ action: (Evaluator) async throws -> T
) async throws -> T { ) async throws -> T {
try await withEvaluatorManager { manager in try await withEvaluatorManager { manager in
let evaluator = try await manager.newProjectEvaluator(projectDir: projectDir, options: options) let evaluator = try await manager.newProjectEvaluator(projectBaseURI: projectBaseURI, options: options)
return try await action(evaluator) return try await action(evaluator)
} }
} }

View File

@ -16,6 +16,7 @@
import Foundation import Foundation
import MessagePack import MessagePack
import SemanticVersion
/// Perfoms `action`, returns its result and then closes the manager. /// Perfoms `action`, returns its result and then closes the manager.
/// ///
@ -37,6 +38,36 @@ public func withEvaluatorManager<T>(_ action: (EvaluatorManager) async throws ->
} }
} }
/// Resolve the (CLI) command to invoke Pkl.
///
/// First, checks the `PKL_EXEC` environment variable. If that is not set, searches the `PATH` for a directory
/// containing `pkl`.
func getPklCommand() throws -> [String] {
if let exec = ProcessInfo.processInfo.environment["PKL_EXEC"] {
return exec.components(separatedBy: " ")
}
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
throw PklError("Unable to read PATH environment variable.")
}
for dir in path.components(separatedBy: ":") {
do {
let contents = try FileManager.default.contentsOfDirectory(atPath: dir)
if let pkl = contents.first(where: { $0 == "pkl" }) {
let file = NSString.path(withComponents: [dir, pkl])
if FileManager.default.isExecutableFile(atPath: file) {
return [file]
}
}
} catch {
if error._domain == NSCocoaErrorDomain {
continue
}
throw error
}
}
throw PklError("Unable to find `pkl` command on PATH.")
}
/// Provides handlers for managing the lifecycles of Pkl evaluators. If binding to Pkl as a child process, an evaluator /// Provides handlers for managing the lifecycles of Pkl evaluators. If binding to Pkl as a child process, an evaluator
/// manager represents a single child process. /// manager represents a single child process.
/// ///
@ -55,6 +86,8 @@ public actor EvaluatorManager {
var isClosed: Bool = false var isClosed: Bool = false
var pklVersion: String?
// note; when our C bindings are released, change `init()` based on compiler flags. // note; when our C bindings are released, change `init()` based on compiler flags.
public init() { public init() {
self.init(transport: ChildProcessMessageTransport()) self.init(transport: ChildProcessMessageTransport())
@ -72,6 +105,31 @@ public actor EvaluatorManager {
} }
} }
/// Get the semantic version as a String of the Pkl interpreter being used.
func getVersion() throws -> String {
if let pklVersion {
return pklVersion
}
let pklCommand = try getPklCommand()
let process = Process()
process.executableURL = URL(fileURLWithPath: pklCommand[0])
process.arguments = Array(pklCommand.dropFirst()) + ["--version"]
let pipe = Pipe()
process.standardOutput = pipe
debug("Spawning command \(pklCommand[0]) with arguments \(process.arguments!)")
try process.run()
guard let outputData = try pipe.fileHandleForReading.readToEnd(),
let output = String(data: outputData, encoding: .utf8)?.split(separator: " "),
output.count > 2,
output[0] == "Pkl" else {
throw PklError("Could not get version from Pkl binary")
}
self.pklVersion = String(output[1])
return self.pklVersion!
}
private func listenForIncomingMessages() async throws { private func listenForIncomingMessages() async throws {
for try await message in try self.transport.getMessages() { for try await message in try self.transport.getMessages() {
debug("EvaluatorManager got message \(message)") debug("EvaluatorManager got message \(message)")
@ -127,24 +185,24 @@ public actor EvaluatorManager {
/// - options: The options used to configure the evaluator. /// - options: The options used to configure the evaluator.
/// - action: The action to run with the evaluator. /// - action: The action to run with the evaluator.
public func withEvaluator<T>(options: EvaluatorOptions, _ action: (Evaluator) async throws -> T) async throws -> T { public func withEvaluator<T>(options: EvaluatorOptions, _ action: (Evaluator) async throws -> T) async throws -> T {
let evalautor = try await newEvaluator(options: options) let evaluator = try await newEvaluator(options: options)
var closed = false var closed = false
do { do {
let result = try await action(evalautor) let result = try await action(evaluator)
try await evalautor.close() try await evaluator.close()
closed = true closed = true
return result return result
} catch { } catch {
if !closed { if !closed {
try await evalautor.close() try await evaluator.close()
} }
throw error throw error
} }
} }
/// Convenience method for constructing a project evaluator with preconfigured base options. /// Convenience method for constructing a project evaluator with preconfigured base options.
public func withProjectEvaluator<T>(projectDir: String, _ action: (Evaluator) async throws -> T) async throws -> T { public func withProjectEvaluator<T>(projectBaseURI: URL, _ action: (Evaluator) async throws -> T) async throws -> T {
try await self.withProjectEvaluator(projectDir: projectDir, options: .preconfigured, action) try await self.withProjectEvaluator(projectBaseURI: projectBaseURI, options: .preconfigured, action)
} }
/// Constructs an evaluator that is configured by the project within the project dir. /// Constructs an evaluator that is configured by the project within the project dir.
@ -155,20 +213,20 @@ public actor EvaluatorManager {
/// After the action completes or throws, the evaluator is closed. /// After the action completes or throws, the evaluator is closed.
/// ///
/// - Parameters: /// - Parameters:
/// - projectDir: The project directory that contains the PklProject file. /// - projectBaseURI: The project base path that contains the PklProject file.
/// - options: The options used to configure the evaluator. /// - options: The options used to configure the evaluator.
/// - action: The action to run with the evaluator. /// - action: The action to run with the evaluator.
public func withProjectEvaluator<T>(projectDir: String, options: EvaluatorOptions, _ action: (Evaluator) async throws -> T) async throws -> T { public func withProjectEvaluator<T>(projectBaseURI: URL, options: EvaluatorOptions, _ action: (Evaluator) async throws -> T) async throws -> T {
let evalautor = try await newProjectEvaluator(projectDir: projectDir, options: options) let evaluator = try await newProjectEvaluator(projectBaseURI: projectBaseURI, options: options)
var closed = false var closed = false
do { do {
let result = try await action(evalautor) let result = try await action(evaluator)
try await evalautor.close() try await evaluator.close()
closed = true closed = true
return result return result
} catch { } catch {
if !closed { if !closed {
try await evalautor.close() try await evaluator.close()
} }
throw error throw error
} }
@ -176,13 +234,18 @@ public actor EvaluatorManager {
/// Creates a new evaluator with the provided options. /// Creates a new evaluator with the provided options.
/// ///
/// To create an evaluator that understands project dependencies, use ``newProjectEvaluator(projectDir:options:)``. /// To create an evaluator that understands project dependencies, use
/// ``newProjectEvaluator(projectBaseURI:options:)``.
/// ///
/// - Parameter options: The options used to configure the evaluator. /// - Parameter options: The options used to configure the evaluator.
public func newEvaluator(options: EvaluatorOptions) async throws -> Evaluator { public func newEvaluator(options: EvaluatorOptions) async throws -> Evaluator {
if self.isClosed { if self.isClosed {
throw PklError("The evaluator manager is closed") throw PklError("The evaluator manager is closed")
} }
let version = try SemanticVersion(getVersion())!
guard options.http == nil || version >= pklVersion0_26 else {
throw PklError("http options are not supported on Pkl versions lower than 0.26")
}
let req = options.toMessage() let req = options.toMessage()
guard let response = try await ask(req) as? CreateEvaluatorResponse else { guard let response = try await ask(req) as? CreateEvaluatorResponse else {
throw PklBugError.invalidMessageCode( throw PklBugError.invalidMessageCode(
@ -209,15 +272,15 @@ public actor EvaluatorManager {
// Any `evaluatorSettings` set within the PklProject file overwrites any fields set on `options`. // Any `evaluatorSettings` set within the PklProject file overwrites any fields set on `options`.
/// ///
/// - Parameters: /// - Parameters:
/// - projectDir: The project directory containing the `PklProject` file. /// - projectBaseURI: The project base path containing the `PklProject` file.
/// - options: The base options used to configure the evaluator. /// - options: The base options used to configure the evaluator.
public func newProjectEvaluator(projectDir: String, options: EvaluatorOptions) async throws -> Evaluator { public func newProjectEvaluator(projectBaseURI: URL, options: EvaluatorOptions) async throws -> Evaluator {
if self.isClosed { if self.isClosed {
throw PklError("The evaluator manager is closed") throw PklError("The evaluator manager is closed")
} }
return try await self.withEvaluator(options: .preconfigured) { projectEvaluator in return try await self.withEvaluator(options: .preconfigured) { projectEvaluator in
let project = try await projectEvaluator.evaluateOutputValue( let project = try await projectEvaluator.evaluateOutputValue(
source: .path("\(projectDir)/PklProject"), source: .path("\(projectBaseURI)/PklProject"),
asType: Project.self asType: Project.self
) )
return try await self.newEvaluator(options: options.withProject(project)) return try await self.newEvaluator(options: options.withProject(project))
@ -287,3 +350,11 @@ enum PklBugError: Error {
case invalidEvaluatorId(String) case invalidEvaluatorId(String)
case unknownMessage(String) case unknownMessage(String)
} }
let pklVersion0_25 = SemanticVersion("0.25.0")!
let pklVersion0_26 = SemanticVersion("0.26.0")!
let supportedPklVersions = [
pklVersion0_25,
pklVersion0_26,
]

View File

@ -30,7 +30,8 @@ public struct EvaluatorOptions {
cacheDir: String? = nil, cacheDir: String? = nil,
outputFormat: String? = nil, outputFormat: String? = nil,
logger: Logger = Loggers.noop, logger: Logger = Loggers.noop,
projectDir: String? = nil, projectBaseURI: URL? = nil,
http: Http? = nil,
declaredProjectDependencies: [String: ProjectDependency]? = nil declaredProjectDependencies: [String: ProjectDependency]? = nil
) { ) {
self.allowedModules = allowedModules self.allowedModules = allowedModules
@ -45,7 +46,8 @@ public struct EvaluatorOptions {
self.cacheDir = cacheDir self.cacheDir = cacheDir
self.outputFormat = outputFormat self.outputFormat = outputFormat
self.logger = logger self.logger = logger
self.projectDir = projectDir self.projectBaseURI = projectBaseURI
self.http = http
self.declaredProjectDependencies = declaredProjectDependencies self.declaredProjectDependencies = declaredProjectDependencies
} }
@ -106,13 +108,19 @@ public struct EvaluatorOptions {
/// It is meant to be set by lower level logic in Swift code that first evaluates the PklProject, /// It is meant to be set by lower level logic in Swift code that first evaluates the PklProject,
/// which then configures ``EvaluatorOptions`` accordingly. /// which then configures ``EvaluatorOptions`` accordingly.
/// ///
/// To emulate the CLI's `--project-dir` flag, create an evaluator with ``withProjectEvaluator(projectDir:_:)``, /// To emulate the CLI's `--project-dir` flag, create an evaluator with ``withProjectEvaluator(projectBaseURI:_:)``,
/// or ``EvaluatorManager/newProjectEvaluator()``. /// or ``EvaluatorManager/newProjectEvaluator()``.
public var projectDir: String? public var projectBaseURI: URL?
/// The set of dependencies available to modules within ``projectDir``. /// Settings that control how Pkl talks to HTTP(S) servers.
/// ///
/// When importing dependencies, a `PklProject.deps.json` file must exist within ``projectDir`` /// Added in Pkl 0.26.
/// These fields are ignored if targeting Pkl 0.25.
public var http: Http?
/// The set of dependencies available to modules within ``projectBaseURI``.
///
/// When importing dependencies, a `PklProject.deps.json` file must exist within ``projectBaseURI``
/// that contains the project's resolved dependencies. /// that contains the project's resolved dependencies.
public var declaredProjectDependencies: [String: ProjectDependency]? public var declaredProjectDependencies: [String: ProjectDependency]?
} }
@ -153,18 +161,19 @@ extension EvaluatorOptions {
rootDir: self.rootDir, rootDir: self.rootDir,
cacheDir: self.cacheDir, cacheDir: self.cacheDir,
outputFormat: self.outputFormat, outputFormat: self.outputFormat,
project: self.project() project: self.project(),
http: self.http
) )
} }
func project() -> ProjectOrDependency? { func project() -> ProjectOrDependency? {
guard let projectDir else { guard let projectBaseURI else {
return nil return nil
} }
return .init( return .init(
packageUri: nil, packageUri: nil,
type: "project", type: "project",
projectFileUri: "file://\(projectDir)/PklProject", projectFileUri: "file://\(projectBaseURI)/PklProject",
checksums: nil, checksums: nil,
dependencies: self.declaredProjectDependenciesToMessage(self.declaredProjectDependencies) dependencies: self.declaredProjectDependenciesToMessage(self.declaredProjectDependencies)
) )
@ -227,28 +236,15 @@ extension EvaluatorOptions {
/// Builds options that configures the evaluator with settings set on the project. /// Builds options that configures the evaluator with settings set on the project.
/// ///
/// Skips any settings that are nil. /// Skips any settings that are nil.
public func withProjectEvaluatorSettings(_ evaluatorSettings: Project.EvaluatorSettings) -> EvaluatorOptions { public func withProjectEvaluatorSettings(_ evaluatorSettings: PklEvaluatorSettings) -> EvaluatorOptions {
var options = self var options = self
if evaluatorSettings.externalProperties != nil { options.properties = evaluatorSettings.externalProperties ?? self.properties
options.properties = evaluatorSettings.externalProperties options.env = evaluatorSettings.env ?? self.env
} options.allowedModules = evaluatorSettings.allowedModules ?? self.allowedModules
if evaluatorSettings.env != nil { options.allowedResources = evaluatorSettings.allowedResources ?? self.allowedResources
options.env = evaluatorSettings.env options.cacheDir = evaluatorSettings.noCache != nil ? nil : (evaluatorSettings.moduleCacheDir ?? self.cacheDir)
} options.rootDir = evaluatorSettings.rootDir ?? self.rootDir
if evaluatorSettings.allowedModules != nil { options.http = evaluatorSettings.http ?? self.http
options.allowedModules = evaluatorSettings.allowedModules
}
if evaluatorSettings.allowedResources != nil {
options.allowedResources = evaluatorSettings.allowedResources
}
if evaluatorSettings.noCache == true {
options.cacheDir = nil
} else if evaluatorSettings.moduleCacheDir != nil {
options.cacheDir = evaluatorSettings.moduleCacheDir
}
if evaluatorSettings.rootDir != nil {
options.rootDir = evaluatorSettings.rootDir
}
return options return options
} }
@ -277,7 +273,8 @@ extension EvaluatorOptions {
/// Builds options with dependencies from the input project. /// Builds options with dependencies from the input project.
public func withProjectDependencies(_ project: Project) -> EvaluatorOptions { public func withProjectDependencies(_ project: Project) -> EvaluatorOptions {
var options = self var options = self
options.projectDir = String(URL(string: project.projectFileUri)!.path.dropLast("/PklProject".count)) options.projectBaseURI = URL(string: project.projectFileUri)!
options.projectBaseURI?.deleteLastPathComponent()
options.declaredProjectDependencies = self.projectDependencies(project) options.declaredProjectDependencies = self.projectDependencies(project)
return options return options
} }

View File

@ -63,7 +63,7 @@ enum MessageType: Int, Codable {
case CREATE_EVALUATOR_RESPONSE = 0x21 case CREATE_EVALUATOR_RESPONSE = 0x21
case CLOSE_EVALUATOR = 0x22 case CLOSE_EVALUATOR = 0x22
case EVALUATOR_REQUEST = 0x23 case EVALUATOR_REQUEST = 0x23
case EVALUATOR_RESPOSNE = 0x24 case EVALUATOR_RESPONSE = 0x24
case LOG_MESSAGE = 0x25 case LOG_MESSAGE = 0x25
case READ_RESOURCE_REQUEST = 0x26 case READ_RESOURCE_REQUEST = 0x26
case READ_RESOURCE_RESPONSE = 0x27 case READ_RESOURCE_RESPONSE = 0x27
@ -87,7 +87,7 @@ extension MessageType {
case is EvaluateRequest: case is EvaluateRequest:
return MessageType.EVALUATOR_REQUEST return MessageType.EVALUATOR_REQUEST
case is EvaluateResponse: case is EvaluateResponse:
return MessageType.EVALUATOR_RESPOSNE return MessageType.EVALUATOR_RESPONSE
case is LogMessage: case is LogMessage:
return MessageType.LOG_MESSAGE return MessageType.LOG_MESSAGE
case is ListResourcesRequest: case is ListResourcesRequest:
@ -122,6 +122,7 @@ struct CreateEvaluatorRequest: ClientRequestMessage {
var cacheDir: String? var cacheDir: String?
var outputFormat: String? var outputFormat: String?
var project: ProjectOrDependency? var project: ProjectOrDependency?
var http: Http?
} }
struct ProjectOrDependency: Codable { struct ProjectOrDependency: Codable {

View File

@ -16,6 +16,7 @@
import Foundation import Foundation
import MessagePack import MessagePack
import SemanticVersion
protocol MessageTransport { protocol MessageTransport {
/// Send a message to the Pkl server. /// Send a message to the Pkl server.
@ -66,32 +67,6 @@ public class ChildProcessMessageTransport: MessageTransport {
self.pklCommand = pklCommand self.pklCommand = pklCommand
} }
private func getPklCommand() throws -> [String] {
if let exec = ProcessInfo.processInfo.environment["PKL_EXEC"] {
return exec.components(separatedBy: " ")
}
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
throw PklError("Unable to find `pkl` command on PATH.")
}
for dir in path.components(separatedBy: ":") {
do {
let contents = try FileManager.default.contentsOfDirectory(atPath: dir)
if let pkl = contents.first(where: { $0 == "pkl" }) {
let file = NSString.path(withComponents: [dir, pkl])
if FileManager.default.isExecutableFile(atPath: file) {
return [file]
}
}
} catch {
if error._domain == NSCocoaErrorDomain {
continue
}
throw error
}
}
throw PklError("Unable to find `pkl` command on PATH.")
}
private func ensureProcessStarted() throws { private func ensureProcessStarted() throws {
if self.process?.isRunning == true { return } if self.process?.isRunning == true { return }
let pklCommand = try getPklCommand() let pklCommand = try getPklCommand()
@ -140,7 +115,7 @@ public class ChildProcessMessageTransport: MessageTransport {
switch messageType { switch messageType {
case MessageType.CREATE_EVALUATOR_RESPONSE: case MessageType.CREATE_EVALUATOR_RESPONSE:
return try self.decoder.decode(as: CreateEvaluatorResponse.self) return try self.decoder.decode(as: CreateEvaluatorResponse.self)
case MessageType.EVALUATOR_RESPOSNE: case MessageType.EVALUATOR_RESPONSE:
return try self.decoder.decode(as: EvaluateResponse.self) return try self.decoder.decode(as: EvaluateResponse.self)
case MessageType.READ_MODULE_REQUEST: case MessageType.READ_MODULE_REQUEST:
return try self.decoder.decode(as: ReadModuleRequest.self) return try self.decoder.decode(as: ReadModuleRequest.self)

View File

@ -0,0 +1,87 @@
// ===----------------------------------------------------------------------===//
// Copyright © 2024 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.
// ===----------------------------------------------------------------------===//
/// The Swift representation of standard library module `pkl.EvaluatorSettings`.
public struct PklEvaluatorSettings: Decodable, Hashable {
let externalProperties: [String: String]?
let env: [String: String]?
let allowedModules: [String]?
let allowedResources: [String]?
let noCache: Bool?
let modulePath: [String]?
let timeout: Duration?
let moduleCacheDir: String?
let rootDir: String?
let http: Http?
}
/// Settings that control how Pkl talks to HTTP(S) servers.
public struct Http: Codable, Hashable {
/// PEM format certificates to trust when making HTTP requests.
///
/// If empty, Pkl will trust its own built-in certificates.
var caCertificates: [UInt8]?
/// Configuration of the HTTP proxy to use.
///
/// If `nil`, uses the operating system's proxy configuration.
/// Configuration of the HTTP proxy to use.
var proxy: Proxy?
}
/// Settings that control how Pkl talks to HTTP proxies.
public struct Proxy: Codable, Hashable {
/// The proxy to use for HTTP(S) connections.
///
/// Only HTTP proxies are supported.
/// The address must start with `"http://"`, and cannot contain anything other than a host and an optional port.
///
/// Example:
/// ```
/// "http://my.proxy.example.com:5080"
/// ```
var address: String?
/// Hosts to which all connections should bypass a proxy.
///
///
/// Values can be either hostnames, or IP addresses.
/// IP addresses can optionally be provided using
/// [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation).
///
/// The value `"*"` is a wildcard that disables proxying for all hosts.
///
/// A hostname matches all subdomains.
/// For example, `example.com` matches `foo.example.com`, but not `fooexample.com`.
/// A hostname that is prefixed with a dot matches the hostname itself,
/// so `.example.com` matches `example.com`.
///
/// Hostnames do not match their resolved IP addresses.
/// For example, the hostname `localhost` will not match `127.0.0.1`.
///
/// Optionally, a port can be specified.
/// If a port is omitted, all ports are matched.
///
/// Example:
///
/// ```
/// [ "127.0.0.1",
/// "169.254.0.0/16",
/// "example.com",
/// "localhost:5050" ]
/// ```
var noProxy: [String]?
}

View File

@ -14,15 +14,13 @@
// limitations under the License. // limitations under the License.
// ===----------------------------------------------------------------------===// // ===----------------------------------------------------------------------===//
import Foundation
/// The Swift representation of `pkl.Project` /// The Swift representation of `pkl.Project`
public struct Project: PklRegisteredType, Hashable, DependencyDeclaredInProjectFile { public struct Project: PklRegisteredType, Hashable, DependencyDeclaredInProjectFile {
public static let registeredIdentifier: String = "pkl.Project" public static let registeredIdentifier: String = "pkl.Project"
let package: Package? let package: Package?
let evaluatorSettings: Project.EvaluatorSettings let evaluatorSettings: PklEvaluatorSettings
let projectFileUri: String let projectFileUri: String
@ -52,7 +50,7 @@ extension Project: Decodable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let dec = try decoder.container(keyedBy: PklCodingKey.self) let dec = try decoder.container(keyedBy: PklCodingKey.self)
let package = try dec.decode(Package?.self, forKey: PklCodingKey(stringValue: "package")!) let package = try dec.decode(Package?.self, forKey: PklCodingKey(stringValue: "package")!)
let evaluatorSettings = try dec.decode(EvaluatorSettings.self, forKey: PklCodingKey(stringValue: "evaluatorSettings")!) let evaluatorSettings = try dec.decode(PklEvaluatorSettings.self, forKey: PklCodingKey(stringValue: "evaluatorSettings")!)
let projectFileUri = try dec.decode(String.self, forKey: PklCodingKey(stringValue: "projectFileUri")!) let projectFileUri = try dec.decode(String.self, forKey: PklCodingKey(stringValue: "projectFileUri")!)
let tests = try dec.decode([String].self, forKey: PklCodingKey(stringValue: "tests")!) let tests = try dec.decode([String].self, forKey: PklCodingKey(stringValue: "tests")!)
let dependencies = try dec.decode([String: PklAny].self, forKey: PklCodingKey(stringValue: "dependencies")!) let dependencies = try dec.decode([String: PklAny].self, forKey: PklCodingKey(stringValue: "dependencies")!)
@ -98,16 +96,11 @@ extension Project {
let uri: String let uri: String
} }
/// The Swift representation of `pkl.Project#EvaluatorSettings @available(
public struct EvaluatorSettings: Decodable, Hashable { *,
let externalProperties: [String: String]? deprecated,
let env: [String: String]? message: "Replaced by PklEvaluatorSettings, independent of Project",
let allowedModules: [String]? renamed: "PklEvaluatorSettings"
let allowedResources: [String]? )
let noCache: Bool? public typealias EvaluatorSettings = PklEvaluatorSettings
let modulePath: [String]?
let timeout: Duration?
let moduleCacheDir: String?
let rootDir: String?
}
} }

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
// ===----------------------------------------------------------------------===// // ===----------------------------------------------------------------------===//
import Foundation import SemanticVersion
import XCTest import XCTest
@testable import PklSwift @testable import PklSwift
@ -55,10 +55,10 @@ class EvaluatorManagerTest: XCTestCase {
let manager1 = EvaluatorManager() let manager1 = EvaluatorManager()
let manager2 = EvaluatorManager() let manager2 = EvaluatorManager()
let manager3 = EvaluatorManager() let manager3 = EvaluatorManager()
async let evalautor1 = try manager1.newEvaluator(options: .preconfigured).evaluateOutputText(source: .text("res = \"evaluator 1\"")) async let evaluator1 = try manager1.newEvaluator(options: .preconfigured).evaluateOutputText(source: .text("res = \"evaluator 1\""))
async let evalautor2 = try manager2.newEvaluator(options: .preconfigured).evaluateOutputText(source: .text("res = \"evaluator 2\"")) async let evaluator2 = try manager2.newEvaluator(options: .preconfigured).evaluateOutputText(source: .text("res = \"evaluator 2\""))
async let evalautor3 = try manager3.newEvaluator(options: .preconfigured).evaluateOutputText(source: .text("res = \"evaluator 3\"")) async let evaluator3 = try manager3.newEvaluator(options: .preconfigured).evaluateOutputText(source: .text("res = \"evaluator 3\""))
let (result1, result2, result3) = try await (evalautor1, evalautor2, evalautor3) let (result1, result2, result3) = try await (evaluator1, evaluator2, evaluator3)
XCTAssertEqual(result1, "res = \"evaluator 1\"\n") XCTAssertEqual(result1, "res = \"evaluator 1\"\n")
XCTAssertEqual(result2, "res = \"evaluator 2\"\n") XCTAssertEqual(result2, "res = \"evaluator 2\"\n")
XCTAssertEqual(result3, "res = \"evaluator 3\"\n") XCTAssertEqual(result3, "res = \"evaluator 3\"\n")

View File

@ -14,9 +14,9 @@
// limitations under the License. // limitations under the License.
// ===----------------------------------------------------------------------===// // ===----------------------------------------------------------------------===//
import Foundation
@testable import MessagePack @testable import MessagePack
@testable import PklSwift @testable import PklSwift
import SemanticVersion
import XCTest import XCTest
class TestLogger: Logger { class TestLogger: Logger {
@ -91,6 +91,33 @@ final class PklSwiftTests: XCTestCase {
XCTAssertEqual(output, "foo = 1\n") XCTAssertEqual(output, "foo = 1\n")
} }
func testVersionCoverage() async throws {
let output = try await SemanticVersion(EvaluatorManager().getVersion())!
XCTAssert(supportedPklVersions.contains { $0.major == output.major && $0.minor == output.minor })
}
func testCustomProxyOptions() async throws {
let version = try await SemanticVersion(EvaluatorManager().getVersion())!
let expected = version < pklVersion0_26
? "http options are not supported on Pkl versions lower than 0.26"
: "ConnectException: Error connecting to host `example.com`"
var options = EvaluatorOptions.preconfigured
options.http = .init(
caCertificates: nil,
proxy: .init(
address: "http://localhost:1",
noProxy: ["myhost.com:1337", "myotherhost.org:42"]
)
)
do {
let evaluator = try await manager.newEvaluator(options: options)
let _ = try await evaluator.evaluateOutputText(source: .uri("https://example.com")!)
XCTFail("Should have thrown an error")
} catch {
XCTAssert("\(error)".contains(expected))
}
}
func testCustomModuleReader() async throws { func testCustomModuleReader() async throws {
let reader = VirtualModuleReader( let reader = VirtualModuleReader(
read: { _ in read: { _ in

View File

@ -14,13 +14,14 @@
// limitations under the License. // limitations under the License.
// ===----------------------------------------------------------------------===// // ===----------------------------------------------------------------------===//
import Foundation import SemanticVersion
import XCTest import XCTest
@testable import PklSwift @testable import PklSwift
class ProjectTest: XCTestCase { class ProjectTest: XCTestCase {
func testLoadProject() async throws { func testLoadProject() async throws {
let version = try await SemanticVersion(EvaluatorManager().getVersion())!
let tempDir = NSTemporaryDirectory() let tempDir = NSTemporaryDirectory()
try FileManager.default.createDirectory(atPath: tempDir + "/subdir", withIntermediateDirectories: true) try FileManager.default.createDirectory(atPath: tempDir + "/subdir", withIntermediateDirectories: true)
let otherProjectFile = URL(fileURLWithPath: tempDir, isDirectory: true) let otherProjectFile = URL(fileURLWithPath: tempDir, isDirectory: true)
@ -39,6 +40,21 @@ class ProjectTest: XCTestCase {
let file = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let file = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent("PklProject") .appendingPathComponent("PklProject")
let httpSetting = version < pklVersion0_26 ? "" : """
http {
proxy {
address = "http://localhost:1"
noProxy {
"example.com"
"foo.bar.org"
}
}
}
"""
let httpExpectation = version < pklVersion0_26 ? nil : Http(
caCertificates: nil,
proxy: .init(address: "http://localhost:1", noProxy: ["example.com", "foo.bar.org"])
)
try #""" try #"""
amends "pkl:Project" amends "pkl:Project"
@ -83,6 +99,7 @@ class ProjectTest: XCTestCase {
timeout = 5.min timeout = 5.min
moduleCacheDir = "/bar/buzz" moduleCacheDir = "/bar/buzz"
rootDir = "/buzzy" rootDir = "/buzzy"
\#(httpSetting)
} }
dependencies { dependencies {
@ -97,7 +114,7 @@ class ProjectTest: XCTestCase {
source: .url(file), source: .url(file),
asType: Project.self asType: Project.self
) )
let expectedSettings = PklSwift.Project.EvaluatorSettings( let expectedSettings = PklSwift.PklEvaluatorSettings(
externalProperties: ["myprop": "1"], externalProperties: ["myprop": "1"],
env: ["myenv": "2"], env: ["myenv": "2"],
allowedModules: ["foo:"], allowedModules: ["foo:"],
@ -106,7 +123,8 @@ class ProjectTest: XCTestCase {
modulePath: ["/bar/baz"], modulePath: ["/bar/baz"],
timeout: .minutes(5), timeout: .minutes(5),
moduleCacheDir: "/bar/buzz", moduleCacheDir: "/bar/buzz",
rootDir: "/buzzy" rootDir: "/buzzy",
http: httpExpectation
) )
let expectedPackage = PklSwift.Project.Package( let expectedPackage = PklSwift.Project.Package(
name: "hawk", name: "hawk",
@ -160,7 +178,8 @@ class ProjectTest: XCTestCase {
modulePath: nil, modulePath: nil,
timeout: nil, timeout: nil,
moduleCacheDir: nil, moduleCacheDir: nil,
rootDir: nil rootDir: nil,
http: nil
), ),
projectFileUri: "\(otherProjectFile)", projectFileUri: "\(otherProjectFile)",
tests: [], tests: [],