更新
虽然不完美,但新的Swift Package Manager 插件提供了我的 Builder 实验旨在实现的大部分功能。 因此,此工具将被存档。
此软件包构建的 Builder 可执行文件旨在用于构建其他的 Swift Package Manager 软件包。
请在 Swift 论坛上或在 github 上的 issues 中留下您的评论和建议。
Swift Package Manager 是一个功能强大的软件包管理器,但目前它作为一个构建系统还比较基础。 它在底层使用 llbuild,但目前它没有提供太多超出构建/运行/测试软件包的功能。
特别是,它目前不支持在构建过程中运行脚本或其他工具,也没有办法批量指定配置设置并统一应用它们,除了在命令行中逐个指定。
该项目是 Swift Package Manager 的元构建器的概念验证实现。 它说明了一种可以添加设置配置和自定义构建阶段的方法。
它最初受到 Swift 论坛上的一些讨论的启发(这里和这里),但此后有所发展。
所采用的方法是经过慎重选择的,以便与 SwiftPM 的当前能力一起使用,这样它可以作为一个独立的工具,位于 SwiftPM 的之上并使用它。 这也意味着您可以使用 SwiftPM 构建此工具来引导构建过程。
自从我创建这个项目以来,Swift 团队已经宣布了他们自己的可扩展构建工具解决方案,一旦它存在,可能会使这个工具过时。
但与此同时,这是一个有趣的实验,并且仍然具有 SwiftPM 目前缺少的一些其他功能。
可以在 BuilderExample 中找到一个示例软件包。
查看 Builder 的实际操作
git clone "https://github.com/elegantchaos/BuilderExample"
cd BuilderExample
swift run builder run
它所做的是克隆示例,然后构建和运行 Builder(它是一个依赖项)。
然后运行 Builder 会构建和运行 Example 目标。
是不是很有趣?
Builder 的调用方式如下:builder <action>
。 如果未提供,则 action 默认为 build
(稍后会详细介绍 action)。
Builder 通过查找它试图构建的软件包中的一个名为 Configure
的特殊目标来工作。 您可以在 Package.swift
文件中像任何其他目标一样定义此目标。 它应该包含一个 main.swift
文件,以便 Builder 可以运行它。
假设它找到了 Configure
目标,Builder 使用 swift run
构建并运行它,并捕获其输出。
一切顺利的话,此输出应该是一个有效的 JSON 字典,Builder 会解析该字典以获得配置。
此配置由设置部分和一个或多个 action 定义组成。
Builder 查找与在命令行中提供的名称(如果未提供,则为 build
)匹配的 action 定义。 如果存在,此 action 定义描述了一组要执行以执行 action 的命令。
这些命令可以包括
swift build
构建一个产品,应用来自配置的构建设置swift test
测试一个产品,应用来自配置的构建设置swift run
运行一个产品,应用来自配置的构建设置swift run
构建和运行它,传递配置中指定的参数这种方法背后的想法是
通过将 Builder 添加为项目的配置,整个构建依赖链(包括 Builder 本身和其他工具(例如 protobuf
))都可以由软件包管理器本身在本地获取和构建。
这旨在产生一个稳定的构建环境,该环境不依赖于全局预安装工具。
它还使人们可以轻松共享可重复使用的构建工具,因为它们只是 Swift 软件包 - 希望这将鼓励人们编写跨平台的 Swift 工具(或用 Swift 包装其他工具),并产生一系列现成的软件包来满足大多数需求。
当然,如果您确实有特殊要求,例如需要安装或使用 brew
/apt-get
/npm
/whatever
运行的工具,那也没问题。 您只需构建一个 Swift 工具来执行安装,并执行您需要的任何操作。
在此演示中,示例项目的 Configure
目标如下所示
import BuilderConfiguration
let settings = Settings(specs: [
.base(
values: [
.setting("definition", "example")
],
inherits: [
.spec(name: "mac", filter: ["macOS"]),
.spec(name: "debug", filter: ["debug"])
]
),
.spec(
name: "mac",
values: [
.setting("minimum-target", "macosx10.12"),
]
),
.spec(
name: "debug",
values: [
.setting("optimisation", "none")
]
)
]
)
let configuration = Configuration(
settings: settings,
actions: [
.action(name:"build", phases:[
.toolPhase(name:"Preparing", tool: "BuilderToolExample"),
.buildPhase(name:"Building", target:"BuilderExample"),
.toolPhase(name:"Packaging", tool: "BuilderToolExample", arguments:["--show-environment", "--show-arguments"]),
]),
.action(name:"test", phases:[
.testPhase(name:"Testing", target:"BuilderExample"),
]),
.action(name:"run", phases:[
.actionPhase(name:"Building", action: "build"),
.toolPhase(name:"Running", tool: "run", arguments:["BuilderExample"]),
]),
]
)
configuration.outputToBuilder()
Configure
目标的任务是输出配置的 JSON 描述。
有很多方法可以实现这个目标:只需编写代码来直接打印 JSON,创建一个字典并将其转换为 JSON 然后打印,从文件中将其作为文本加载然后打印,等等。
在此示例中,我们使用在 BuilderConfiguration
模块1 中定义的一些实用程序代码,这些代码使我们能够以类似于 SwiftPM 的 Package 描述的方式编写 Settings
和 Configuration
定义。
在 Settings
部分中,我们提供了一个基本的设置方案,该方案始终添加一个 Swift 设置:"definition" : "example"
。
我们还定义了另外几个设置方案,这些方案可以根据过滤器选择性地混合使用。 如果平台是 macOS
,我们会混合使用目标设置。 如果配置是 debug
,我们会混合使用优化器设置。
在 Configuration
部分中,我们描述了三个构建方案:“build”、“test” 和 “run”。 这些列出了在用 build
、test
或 run
action 调用 Builder 时要执行的构建阶段2。
在 “build” 阶段中,我们看到了一个调用外部工具的示例:BuilderToolExample
。 此工具本身是另一个外部依赖项。
希望这个例子说明了一些事情
在上面的描述中,我们略过了设置的详细信息。
像 "minimum-target" : "macosx10.12"
这样的设置如何传递给实际工具?
答案是存在一个转换层,并且它是每个工具的转换。
对于 swift 编译器,上面的设置将被转换为:-Xswiftc, -target, -Xswiftc, x86_64-apple-macosx10.12
。
但是,还有其他工具可能也想了解最低目标。
生成 xcconfig 文件的工具可以将相同的设置转换为:MACOSX_DEPLOYMENT_TARGET = 10.12
。
应用程序打包工具可以将值 10.12
插入到 Info.plist 文件的 LSMinimumSystemVersion
键中。
等等。
那么这些设置映射在哪里呢? 现在,它们非常少,并且被硬编码到 Builder 二进制文件中。 在 Builder 的未来版本中,它们将是外部的并且是可扩展的,以便可以以灵活的方式教会 Builder 关于新设置和新工具。
我将它拼凑在一起作为一个快速演示,尽管从那以后它已经发展了一点。
它可以在 MacOS 和 Linux 上为我构建 - 您的结果可能会有所不同。
很多事情都被忽略了,包括
软件包格式的一个既定目标是创建一个软件包的声明式模型,SwiftPM 可以使用它来构建它。
Builder 正在做的事情可以说违背了这一点。
我理解为什么完全声明式的模型具有吸引力,但根据定义,它需要一种足够丰富的语言来描述构建任何产品的所有方式。
由于这在范围上实际上是无限的,因此这是一个非常难以解决的问题 - 存在许多奇怪的要求和自定义构建步骤。
软件包格式包含可选的附加代码部分这一事实似乎承认了这一缺陷本身,而且在我看来,这目前是一种必要的恶,而且可能永远都是如此。
因此,我实际上认为 Builder 提供了一种比可选部分更干净的方式来在构建期间执行任意 swift 代码。 至少使用 Builder,自定义代码位于 Package.swift
文件之外,并且实际上被整齐地打包到它自己的软件包中,这些软件包可以安全地重复使用、具有依赖关系并且可以任意复杂。
我们正在考虑为 Swift Package Manager 添加 hooks,以便调用其他构建系统,或者执行 shell 脚本。
添加此功能将进一步改善现有库适配到 Swift 代码的过程。
我们打算在此方面谨慎行事,因为包含 shell 脚本和其他外部构建过程会显著限制 Swift Package Manager 理解和分析构建过程的能力。
实际上,这个工具提供的就是这些 hooks,但它们不是存在于 SwiftPM 内部,而是存在于外部。
我们方法的一个明显缺陷是上述引言的最后一段中提到的问题。
SwiftPM 构建系统本身由 llbuild
提供支持,因此可以高效地仅重建需要重建的内容。如果一个文件发生更改,只需要重建依赖于它的内容。
我们执行的自定义阶段位于 SwiftPM 构建系统之外。它对它们一无所知,因此我们无法利用此行为。
假设我们添加一个自定义构建阶段,该阶段运行 protobuf
并从模型创建一些 Swift 源代码文件。如果运气好,SwiftPM 可能足够智能,只重新编译实际已更改的生成源文件。
然而,Builder 不会那么智能,每次都会运行 protobuf
,无论模型是否已更改。
显然,protobuf
可以实现它自己的依赖项检查,每个自定义工具也可以这样做。但这并不理想。
我们可能会以某种方式扩展 Builder 的模型,允许自定义工具描述它们的依赖关系,以便我们可以为所有自定义工具提供依赖项逻辑,甚至可能由 llbuild
提供支持。 Xcode 的自定义构建阶段试图做到这一点,允许您描述输入和输出文件列表,但它不是很灵活,并且需要单独列出文件。一个不错的系统需要良好的模式匹配和编写通用规则的能力...
如果我们这样做,那么我们不仅仅是增强 SwiftPM 构建系统,而至少是在重新实现它的一半。
我们真正需要的是能够 hook 到 SwiftPM 自身对 llbuild
的使用,并且在它知道需要时,能够在我们需要的级别调用我们的自定义阶段。
我们无法做到这一点并不理想 - 但这也是这个工具存在的原因之一 😁。
更新: Swift 团队的可扩展构建工具提案 旨在解决 SwiftPM 自身存在的这个问题。
可以集成到 Builder 中的工具显然是无限的,但我认为一些比较直接有用的工具是那些可以取代 Xcode 内置功能的工具,例如
这是一个原型,因此为了便于开发,我将其制作成一个独立的工具,而不是尝试修改 SwiftPM 本身。
但理论上,它可以集成到 swift
命令中。
它可以取代现有的 swift-build
工具(将其重命名为其他名称,以便此工具可以使用它)。调用 swift build
将运行此工具(如果 manifest 中不存在 Configuration target,我们可以回退到之前的 swift build
行为)。
显然,这会带来性能影响。
此原型是 SwiftPM 的一个覆盖层,因此不会更改 Package.swift
格式。因此,配置和工具 targets 只是与我们正在构建的包中的 targets 一起列在 manifest 中。
这可能会令人困惑,一个适当集成的实现可能会稍微更改格式,以允许显式列出特殊的 targets,就像这样
let package = Package(
name: "Example",
products: [
.executable(
name: "Example",
targets: ["Example"]),
],
dependencies: [
.package(url: "https://github.com/elegantchaos/BuilderToolExample.git", from: "1.0.3"),
.package(url: "https://github.com/elegantchaos/BuilderConfiguration.git", from: "1.1.0"),
],
configuration:[
.configurationTarget(
name: "Configure",
dependencies: ["BuilderToolExample"]),
.toolTarget(
name: "BuilderToolExample",
dependencies: [])
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "Example",
dependencies: []),
.testTarget(
name: "ExampleTests",
dependencies: ["Example"]),
]
)
我怀疑这将是首选的,但我希望从不需要修改 SwiftPM 本身的东西开始。
配置和工具信息可以位于其自己的文件中,而不是位于主 Package.swift
文件中,该文件与主文件位于同一位置。
它可以有一个标准名称,例如 Configure.swift
(这将需要更改 SwiftPM),或者它可以位于一个子文件夹中,例如 Configure/Package.swift
。
将 Configure
target 构建为可执行文件有点麻烦,因此必须将配置输出为 JSON,然后让 Builder 在另一端将其转换回对象。
理论上,应该可以将 Configure 构建为动态库,然后链接到它并直接从 Builder 中使用它。
类似地,这也可以对独立工具完成 - 尽管可以从命令行手动调用它们可能更有优势。