如何使用 SPM 插件从 Pkl 配置文件生成 Swift 接口

前言

Pkl(全称为 Pickle)是苹果推出的一种全新的专用于配置的编程语言。它允许开发人员通过类型和内置验证安全、直观地设计数据模型。

作为苹果语言,Pkl 有一个可用于从 .pkl 配置文件生成 Swift 接口的套件工具,这是它与其他语言的开发者有所不同的地方。

在本文中,你将学习如何安装和使用 pkl-gen-swift 命令行工具,并将其集成到你的 Swift Package Manager(SPM)项目中,方法是使用 SPM 插件。

注意:需要注意的一点是,目前 Pkl 仅适用于 macOS。

示例展示 Pkl 配置

让我们首先创建一个名为 Config 的简单 Pkl 模块,其中包含一组属性,用于定义一个小型 macOS Swift Package 库的配置,Config.pkl 文件配置如下:

swift 复制代码
module Config

baseUrl: String
retryCount: Int(isBetween(0, 3))
timeout: Duration

如上面的片段所示,我们使用类型和范围来约束可以分配给属性的值,并减少错误的可能性。

Pkl CLI 工具将使用这些类型来验证配置文件并帮助生成 Swift 接口。

现在让我们编写一个单独的 .pkl 文件,修改我们之前创建的模块文件,并为本地开发提供配置值,local.pkl 配置如下:

swift 复制代码
amends "Config.pkl"

baseUrl = "https://localhost:8080"
retryCount = 0
timeout = 30.s

就像这样,我们编写了一个小型配置,并指定了一些类型和约束,我们可以强制执行它们。

现在让我们安装pkl命令行工具,并评估定义实际值的模块,终端执行命令如下:

ruby 复制代码
# Install pkl
curl -L -o pkl https://github.com/apple/pkl/releases/download/0.25.2/pkl-macos-aarch64
chmod +x pkl

# Evaluate the local file
./pkl eval Sources/ClientExample/Resources/local.pkl

上述命令的输出将打印正确的值,这意味着配置可以正确验证:

ruby 复制代码
baseUrl = "https://localhost:8080"
retryCount = 0
timeout = 30.s

生成 Swift 绑定

正如我在文章开头提到的,使用Pkl定义配置的最强大功能之一是,你可以为你的应用程序生成 Swift 接口。

要从 .pkl 文件生成 Swift 接口,你需要安装 pklpkl-gen-swift 命令行工具。

手动安装和使用 pkl-gen-swift

首先,让我们安装 pkl-gen-swift 命令行工具:

ruby 复制代码
curl -L https://github.com/apple/pkl-swift/releases/download/0.2.3/pkl-gen-swift-macos.bin -o pkl-gen-swift
chmod +x pkl-gen-swift

现在,让我们通过在终端中运行以下命令来从 .pkl 文件生成 Swift 接口:

ruby 复制代码
PKL_EXEC=./pkl
./pkl-gen-swift Sources/ClientExample/Resources/*.pkl -o Sources/ClientExample/Generated

请注意,pkl-gen-swift 依赖于 pkl 命令行工具,后者需要在你的 PATH 中可用,或者可以使用 PKL_EXEC 环境变量指定。

命令的输出将是一个包含生成接口的单个 Swift 文件:

路径 Sources/ClientExample/Generated/Config.pkl.swift 下文件源代码如下:

swift 复制代码
// Code generated from Pkl module `Config`. DO NOT EDIT.
import PklSwift

public enum Config {}

extension Config {
    public struct Module: PklRegisteredType, Decodable, Hashable {
        public static var registeredIdentifier: String = "Config"

        public var baseUrl: String

        public var retryCount: Int

        public var timeout: Duration

        public init(baseUrl: String, retryCount: Int, timeout: Duration) {
            self.baseUrl = baseUrl
            self.retryCount = retryCount
            self.timeout = timeout
        }
    }

    /// Load the Pkl module at the given source and evaluate it into `Config.Module`.
    ///
    /// - Parameter source: The source of the Pkl module.
    public static func loadFrom(source: ModuleSource) async throws -> Config.Module {
        try await PklSwift.withEvaluator { evaluator in
            try await loadFrom(evaluator: evaluator, source: source)
        }
    }

    /// Load the Pkl module at the given source and evaluate it with the given evaluator into
    /// `Config.Module`.
    ///
    /// - Parameter evaluator: The evaluator to use for evaluation.
    /// - Parameter source: The module to evaluate.
    public static func loadFrom(
        evaluator: PklSwift.Evaluator,
        source: PklSwift.ModuleSource
    ) async throws -> Config.Module {
        try await evaluator.evaluateModule(source: source, as: Module.self)
    }
}

创建 SPM 命令插件

假设你不希望所有积极参与你的 Swift Package 的人在修改配置时手动安装所有必需的工具以生成代码。

相反,你可以创建一个 Swift Package Manager 命令插件,该插件将封装两个命令行工具,并公开一个客户友好的命令,该命令将查找所有配置文件并从中生成 Swift 接口。

让我们考虑以下 Swift Package,代码如下:

swift 复制代码
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "PklSwiftPlugin",
    platforms: [
        // 1
        .macOS(.v13),
    ],
    products: [
        // 2
        .plugin(name: "PklSwiftCommand", targets: ["PklSwiftCommand"])
    ],
    dependencies: [
        // 3
        .package(url: "https://github.com/apple/pkl-swift.git", exact: "0.2.3")
    ],
    targets: [
        // 4
        .plugin(name: "PklSwiftCommand",
                capability: .command(intent: .custom(verb: "swift-pkl", description: ""),
                                     permissions: [.writeToPackageDirectory(reason: "Write pkl to pkg")]),
                dependencies: [.product(name: "pkl-gen-swift", package: "pkl-swift"), "Pkl"]),
        // 5
        .binaryTarget(name: "Pkl",
                      path: "Pkl.artifactbundle"),
        // 6
        .target(name: "ClientExample",
                dependencies: [.product(name: "PklSwift", package: "pkl-swift")])
    ]
)

让我们一步一步来解释上面的内容:

  1. 我们声明该包仅适用于 macOS 13 及更高版本,以满足 pkl-swift 的要求。
  2. 我们声明了一个新产品,类型为插件,将用于公开 swift-pkl 命令。
  3. 我们将 Apple 的 pkl-swift 声明为包的唯一依赖项。pkl-swift 提供了 Pkl 语言的 Swift 绑定和用于生成 Swift 接口的可执行文件。
  4. 我们为 swift-pkl 命令插件声明了一个新目标。我们还声明了插件的依赖项,其中包括 pkl-gen-swift 可执行文件和 Pkl 命令行工具的构件束。幸运的是,我们可以依赖于 pkl-swift 包中的可执行文件产品来将 Swift 生成器作为依赖项,但我们需要手动创建一个 pkl 命令行工具的构件束。
  5. 我们为 pkl 命令行工具的构件束声明了一个新的二进制目标。
  6. 我们为用于测试的库声明了一个新目标。这是包含 .pkl 配置文件的目标。

要创建一个封装 pkl 命令行工具的构件束,你只需要创建一个与包清单中声明的相同名称的目录,后面跟上 .artifactbundle 扩展名。在此目录中,创建以下文件夹结构:

swift 复制代码
Pkl.artifactbundle
├── info.json
├── pkl-0.25.2-macos
│ └── bin
│ └── pkl

info.json 文件应包含以下内容:

json 复制代码
{
  "schemaVersion": "1.0",
  "artifacts": {
    "pkl": {
      "version": "0.2.3",
      "type": "executable",
      "variants": [
        {
          "path": "pkl-0.25.2-macos/bin/pkl",
          "supportedTriples": ["arm64-apple-macosx"]
        }
      ]
    }
  }
}

现在让我们编写命令插件的代码,该代码将从上下文中检索命令行工具,迭代目标以查找所有 .pkl 文件,然后最终运行 pkl-gen-swift 可执行文件以生成 Swift 接口,路径 Sources/PklSwiftCommand/main.swift 下的源代码如下:

swift 复制代码
import PackagePlugin
import Foundation

@main
struct PklSwiftCommandPlugin: CommandPlugin {
    func performCommand(context: PluginContext, arguments: [String]) async throws {
        let pklGenSwift = try context.tool(named: "pkl-gen-swift")
        let pkl = try context.tool(named: "pkl")
        let pklGenSwiftURL = URL(filePath: pklGenSwift.path.string)
        
        for target in context.package.targets {
            let dirEnum = FileManager.default.enumerator(atPath: target.directory.string)
            var pklFiles = [Path]()
            while let file = dirEnum?.nextObject() as? String {
                if file.hasSuffix(".pkl") {
                    pklFiles.append(target.directory.appending(subpath: file))
                }
            }
            
            let process = Process()
            process.executableURL = pklGenSwiftURL
            process.arguments = pklFiles.map { $0.string } + ["-o", target.directory.appending(subpath: "Generated").string]
            process.environment = ["PKL_EXEC": pkl.path.string]
            
            try process.run()
            process.waitUntilExit()
            
            let gracefulExit = process.terminationReason == .exit && process.terminationStatus == 0
            if !gracefulExit {
                throw "🛑 The plugin execution failed with reason: \(process.terminationReason.rawValue) and status: \(process.terminationStatus) "
            }
        }
    }
}

extension String: Error {}

现在可以像这样运行命令插件:

swift 复制代码
swift package --disable-sandbox swift-pkl --allow-writing-to-package-directory

请注意,你需要使用 --disable-sandbox 标志,否则插件将无限期挂起。命令的输出结果与以前相同。

加载 Pkl 配置

现在我们已经生成了 Swift 接口,可以使用以下代码将其加载到我们的应用程序中,路径 Sources/ClientExample/main.swift 下源代码如下:

swift 复制代码
import PklSwift
import Foundation

func load() async throws {
    let pklGenSwift = Bundle.module.bundleURL.deletingLastPathComponent().appending(path: "pkl-gen-swift").path
    let pklFile = Bundle.module.url(forResource: "local", withExtension: "pkl")!
    setenv("PKL_EXEC", pklGenSwift, 1)
    let config = try await SomeConfig.loadFrom(source: .path(pklFile.path))
    print(config.baseUrl)
    print(config.timeout)
    print(config.retryCount)
}

在尝试执行与文档中相同的代码时,我遇到了一个问题,即 PklSwift 无法在路径中找到 pkl。因此,我必须手动设置 PKL_EXEC 环境变量在示例可执行文件中。

总结

本文介绍了 Pkl,这是苹果推出的一种专用于配置的新编程语言。它允许开发人员通过类型和内置验证安全地设计数据模型。Pkl 具有一套工具,可用于从 .pkl 配置文件生成 Swift 接口,这是其与其他语言的区别之一。文章详细介绍了如何安装和使用 pkl-gen-swift 命令行工具,并将其集成到 Swift Package Manager(SPM) 项目中。然后,通过示例展示了如何创建和修改 Pkl 配置文件,以及如何使用 pkl 命令行工具评估配置文件。接着,介绍了如何生成 Swift 接口文件,以及如何创建 SPM 命令插件来自动生成代码。

相关推荐
Jouzzy9 小时前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克9 小时前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨10 小时前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆12 小时前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂1 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T2 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20252 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz2 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频
安和昂2 天前
【iOS】SDWebImage源码学习
学习·ios
ii_best2 天前
按键精灵ios脚本新增元素功能助力辅助工具开发(三)
ios