如何使用 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 命令插件来自动生成代码。

相关推荐
Jinkey3 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
程序猿看视界9 小时前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
dr李四维13 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神13 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
小江村儿的文杰1 天前
XCode Build时遇到 .entitlements could not be opened 的问题
ide·macos·ue4·xcode
比格丽巴格丽抱1 天前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪1 天前
iOS应用网络安全之HTTPS
web安全·ios·https
今天啥也没干1 天前
使用 Sparkle 实现 macOS 应用自定义更新弹窗
前端·javascript·swift
1024小神1 天前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
lzhdim1 天前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone