同一个 Swift 包在开发者电脑上生成代码正常,放到云端 Mac 的干净工作区却报 Operation not permitted,或者构建成功但编译到的仍是旧文件。这通常不是机器性能问题,而是构建工具插件依赖了未声明的文件、错误的写入目录,或被本地残留产物掩盖。排查重点不是反复清缓存,而是把插件的输入、输出和执行边界逐项还原。
先确认失败发生在哪一层
SwiftPM 构建工具插件涉及三层:SwiftPM 负责规划命令,插件可执行文件负责生成内容,Xcode 再消费生成文件。先在仓库根目录保留完整日志:
set -o pipefail
rm -rf .ci
mkdir -p .ci
xcodebuild \
-scheme App \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$PWD/.ci/DerivedData" \
-clonedSourcePackagesDirPath "$PWD/.ci/SourcePackages" \
build 2>&1 | tee .ci/build.log
grep -E 'sandbox|deny|plugin|Operation not permitted|No such file' .ci/build.log
如果日志显示插件工具根本没有找到,检查它是否声明为 package target 的 executable tool;如果工具已启动后才失败,就继续检查读写路径。若构建成功但生成内容未更新,则重点核对输入和输出声明。
不要先把关闭沙箱当成修复。沙箱拒绝往往准确指出了生成器正在读取或改写构建图之外的内容,绕过限制只会把问题推迟到下一次干净构建。
把输入输出写进构建图
一个稳妥的插件只读取显式输入,并把结果写进 pluginWorkDirectory。下面的结构把 schema 和生成的 Swift 文件都交给 SwiftPM 跟踪:
import PackagePlugin
@main
struct CodegenPlugin: BuildToolPlugin {
func createBuildCommands(
context: PluginContext,
target: Target
) async throws -> [Command] {
let tool = try context.tool(named: "schema-gen")
let input = target.directory.appending("Schemas/api.json")
let output = context.pluginWorkDirectory
.appending("Generated/API.swift")
return [
.buildCommand(
displayName: "Generate API.swift",
executable: tool.path,
arguments: [input.string, output.string],
inputFiles: [input],
outputFiles: [output]
)
]
}
}
生成器启动后应自行创建 Generated 父目录,并只写传入的输出路径。不要默认写到仓库的 Sources、桌面、用户主目录或固定的 /Users/...。构建工具插件用于生成参与本次编译的派生文件;如果任务必须修改仓库内容,应改为由开发者明确执行的命令插件,而不是在每次构建时偷偷改源码。
识别隐式依赖
常见隐式输入包括当前工作目录下的配置文件、环境变量指向的模板、主目录中的缓存,以及生成器自动扫描到的整个文件夹。逐项回答下面四个问题:
| 检查项 | 正确做法 | 风险信号 |
|---|---|---|
| 输入文件 | 全部列入 inputFiles |
运行时递归扫描未声明目录 |
| 输出文件 | 全部列入 outputFiles |
文件名随时间或机器变化 |
| 工作路径 | 使用参数传入的绝对路径 | 依赖 pwd 或主目录 |
| 生成顺序 | 排序后稳定输出 | 依赖文件系统枚举顺序 |
让生成器本身保持确定性
即使权限正确,生成器也可能让增量构建持续失效。最常见的原因是在文件头写入当前时间、临时目录或主机名。相同输入每次都产生不同字节,SwiftPM 就无法判断变化是否真实。
在云端 Mac 上连续执行两次干净生成,并比较摘要:
rm -rf .ci/first .ci/second
mkdir -p .ci/first .ci/second
.build/debug/schema-gen \
Schemas/api.json .ci/first/API.swift
.build/debug/schema-gen \
Schemas/api.json .ci/second/API.swift
shasum -a 256 .ci/first/API.swift .ci/second/API.swift
cmp .ci/first/API.swift .ci/second/API.swift
两个摘要必须一致。生成器还应对字段、文件和声明排序,统一换行符,仅在内容变化时替换目标文件。可以先写临时文件,比较后再原子移动,避免构建中断时留下半个 Swift 文件。
用干净工作区复现缓存问题
本地成功不代表声明完整。本地 DerivedData、旧的插件输出或曾经手工生成的源码,都会让漏声明暂时不可见。云端构建应至少执行一次无历史状态的验收,然后再执行第二次增量构建。
两轮验收法
第一轮删除专用构建目录并构建,确认所有生成文件都由插件产生。第二轮不修改输入再次构建,确认插件不会无故重写输出。随后只改一个 schema 字段再执行第三轮,确认对应生成文件确实更新。
可用下面的检查快速发现生成文件跑错位置:
find "$PWD" -type f -name 'API.swift' -print
git status --short
正常情况下,派生文件位于构建目录,git status 不应出现插件自动改写的受版本控制源码。若第二轮仍重复执行,检查输出是否真实存在、时间戳是否被工具强制刷新,以及某个输入目录是否声明得过宽。
固化云端 Mac 的验收边界
在 HireVM 的远程构建环境或其他干净 macOS 节点上,建议把以下检查写入流水线,而不是依赖人工观察:
- 固定 Xcode 选择结果,并在日志开头记录
xcodebuild -version与swift --version。 - 为每个工作区设置独立的 DerivedData 和包检出目录,避免并行任务交叉写入。
- 首次构建从空目录开始,验证插件没有依赖主目录中的历史文件。
- 保存插件失败前后的详细构建日志,但不要把令牌、签名材料或完整环境变量写入日志。
- 对关键生成文件执行摘要比较,确认同一提交可重复生成。
- 修改单个输入后复跑,确认增量构建只更新预期输出。
- 在控制台确认当前可选配置,并依据并发构建数量选择资源;插件正确性检查不应依赖某一台机器的偶然状态。
最终目标不是让一次构建“碰巧通过”,而是让 SwiftPM 能完整理解生成步骤:输入变化时准确重建,输入不变时保持安静,换到新的云端 Mac 工作区仍得到相同产物。做到这一点后,沙箱不再是阻碍,而是验证构建边界是否真实、完整的一道自动检查。
常见问题
SwiftPM 构建工具插件可以直接修改源码目录吗?
不应这样设计。构建工具插件应把生成文件写入插件工作目录,并通过 outputFiles 明确声明;需要修改仓库内容的操作应拆成由开发者主动执行的命令插件。
为什么插件在本地成功,在云端 Mac CI 中却被沙箱拒绝?
常见原因是本地残留文件掩盖了未声明输出,或生成器依赖当前目录、用户主目录和绝对路径。使用干净检出、固定构建目录并检查详细日志,通常可以暴露真实依赖。
能否通过关闭沙箱快速解决插件失败?
不建议把关闭沙箱作为长期方案。正确做法是声明所有输入输出、只向授权目录写入,并让生成器在相同输入下产生字节一致的结果。
把下一项远程 Mac 任务放到独享节点运行
按任务规模选择两档在售配置、五个节点与日、周、月、季周期。下单前可完整核对配置和 USD 金额。