在独占远程 Mac 上迁移 Swift 6:严格并发 CI 门禁与 Actor 隔离 Runbook

Swift 6 把并发正确性从「运行时崩溃」提前到编译期错误:开启 -strict-concurrency=complete 后,数据竞争与 Actor 边界违反都会让构建失败。本文说明如何在 NUKCLOUD 独占远程 Mac 节点上把这个标志纳入 CI 门禁,并给出六步渐进迁移清单,让 PR 合并前先过并发合规关。

Swift 6 语言模式让编译器成为「并发审计员」:不再靠人工 code review 发现 @MainActor 误用或跨 Actor 数据共享,而是在 xcodebuild 阶段直接报错。这意味着你需要一台算力稳定、环境可钉版本的构建节点——既能跑完整编译,也不会因为邻居争 CPU 导致尾延迟抖动。本文结合 NUKCLOUD 独占远程 Mac 节点,展示如何分阶段接入 Swift 6 严格并发检查,并将其锁进 PR 合并门禁。

00为什么现在迁移——以及为什么需要独占节点

Swift 6 严格并发检查(-strict-concurrency=complete)触发的错误数量与代码库规模正相关:中等规模 iOS 项目首次启用时,出现 200–800 个编译错误并不罕见。这些错误需要逐模块修复,修复期往往横跨数周甚至数月。在此期间,CI 环境必须保证:

  • Xcode 小版本固定:Swift 6 的 Actor isolation 诊断在不同 Xcode 版本间措辞会变化,钉住版本才能让 CI 输出可对比。
  • 无邻居 CPU 争抢:严格并发全量编译比普通 debug build 耗时多 20–40%,独占节点避免 P95 尾延迟爆表。
  • 磁盘 DerivedData 命名空间隔离:多个 PR 同时跑并发检查,需要各自独立的增量编译缓存,否则缓存污染导致结果不可信。
  • 错误计数可追踪:把每次构建的 Swift 6 错误数、修复 commit、剩余模块列表写入结构化日志,便于向管理层证明迁移进度,而不是只报「还在改」。

独占节点的价值在于:同一 Xcode 小版本下,错误措辞与行号在数周内保持稳定,方便对比「上周 412 个、本周 87 个」这类可审计曲线;分钟池里频繁换镜像则会把问题混成环境噪声。拨备与验收字段可与节点 Runbook 六步一并对齐。

提示:如果你的 CI 当前用的是「共享分钟池」托管 macOS,建议在引入 Swift 6 严格并发门禁前先评估队列 P95——全量编译卡在排队比卡在编译更难排查。节点拨备、租户边界与区域主链路的工程语义,见 NUKCLOUD 独占 Apple Silicon 节点 Runbook

01三种并发检查级别与迁移路径

Swift 提供三档编译器标志,可在 OTHER_SWIFT_FLAGS(Xcode Build Settings)或 Package.swiftswiftSettings 里设置:

标志含义推荐阶段
-strict-concurrency=minimal仅检查 async/await 基础语法合规,不检查 Actor 边界存量代码热身期(第 1–2 周)
-strict-concurrency=targeted检查显式标注了 Sendable@MainActor 等注解的类型的 Actor 边界逐模块修复期(第 3–6 周)
-strict-concurrency=complete全量检查,等价于 Swift 6 语言模式;未显式处理的并发共享均报错达到 0 错误后纳入门禁

实践中推荐按模块渐进:先在叶子模块(无依赖或依赖少)开启 complete,再向核心模块推进。SwiftPM 的 .enableUpcomingFeature("StrictConcurrency") 可精确控制单个 target 的检查级别。

02Actor 隔离边界:三类高频错误

迁移期 90% 的编译错误集中在以下三类,先处理完这三类可清掉绝大部分红字:

① 跨 Actor 访问非 Sendable 类型

Swift — 典型错误示例
// ❌ 错误:从非 @MainActor 上下文访问 @MainActor 隔离的属性
actor DataFetcher {
    func load() async {
        let result = await networkClient.fetch()
        // ❌ passing argument of non-sendable type 'UserModel' across actor boundary
        await MainActor.run { viewModel.update(result) }
    }
}

修复方式:让 UserModel 实现 Sendable(值类型通常自动满足),或将跨边界的转换提前到 Actor 边界内完成。

② @MainActor 漏标

UIKit/SwiftUI 的 ViewControllerObservableObject 子类在迁移前隐式跑在主线程;Swift 6 要求显式标注 @MainActor。批量修复可用 Xcode 的「Fix」按钮,但建议人工 review 每一处,避免把不应在主线程的逻辑也拉了进来。

③ 全局变量并发访问

Swift — 全局变量修复示例
// ❌ Swift 6 中不可变全局常量需要是 Sendable 或 nonisolated
var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ 用 nonisolated(unsafe) 明确声明(迁移期过渡方案)
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ 更好:包裹进 Actor 并通过 async 接口访问
actor CacheStore {
    private let cache = NSCache<NSString, AnyObject>()
    func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}

03在远程 Mac 上配置 CI 门禁

把严格并发检查纳入 PR 门禁的核心思路:单独开一个 Job 跑并发检查,不与常规测试 Job 合并。这样并发检查失败不会污染测试报告,也便于在修复期设置不同的失败策略(warn-only vs. block-merge)。在 NUKCLOUD 上拨备节点时,建议先在 定价页 选定磁盘与区域,再经 下单页 交付 SSH 与 Runner 基线,避免迁移期临时借用分钟池导致 P95 不可比。

修复期通常持续数周:请把「并发错误计数」「全量编译 wall time」「DerivedData 体积」三项写入结构化日志,并与节点规格变更(加盘、换区)关联,否则很难判断是代码回退还是环境漂移。

GitHub Actions — .github/workflows/swift6-concurrency-check.yml
name: Swift 6 Concurrency Gate

on:
  pull_request:
    branches: [main, release/*]

jobs:
  concurrency-check:
    runs-on: self-hosted          # NUKCLOUD 独占远程 Mac Runner
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode (钉住小版本)
        run: sudo xcode-select -s /Applications/Xcode_16.3.app

      - name: Restore DerivedData cache
        uses: actions/cache@v4
        with:
          path: ~/DerivedData/swift6-check
          key: swift6-${{ runner.name }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.resolved') }}
          restore-keys: swift6-${{ runner.name }}-

      - name: Build with strict concurrency (complete)
        run: |
          xcodebuild build \
            -project MyApp.xcodeproj \
            -scheme MyApp \
            -destination 'generic/platform=iOS Simulator' \
            -derivedDataPath ~/DerivedData/swift6-check \
            OTHER_SWIFT_FLAGS="-strict-concurrency=complete" \
            | xcbeautify --renderer github-actions

      - name: Upload build log on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: swift6-build-log-${{ github.sha }}
          path: ~/DerivedData/swift6-check/Logs/Build/
DerivedData 分桶规则:-derivedDataPath ~/DerivedData/swift6-check 与普通构建的 DerivedData 目录分开,避免并发检查的编译产物污染调试构建缓存。多个 PR 同时跑时,加上 runner.name 或 PR 编号进一步隔离。

04六步渐进迁移 Runbook

  1. 01
    基线快照:在独占远程 Mac 上用 -strict-concurrency=minimal 跑一次全量编译,记录错误数与编译耗时,作为迁移起点的可量化基线。
  2. 02
    叶子模块先行:从 SwiftPM 依赖图中找出无下游依赖的 target,逐一开启 complete。每个 target 修复完毕后提单独 PR,保持 diff 可审查。
  3. 03
    CI 分支策略:在 Git 的 feature/swift6-migration 长期分支上以 warn-onlycontinue-on-error: true)运行并发检查 Job,不阻断日常 PR 合并。只有当某个模块达到 0 错误时,才将该模块的检查级别升级为门禁(block-merge)。
  4. 04
    钉住 Xcode 版本:xcode-select 版本号写入 Runbook 与 README,确保本地 Mac 开发者与 CI 节点使用相同小版本,避免「本地过、CI 挂」的错误扰动。
  5. 05
    错误趋势看板:把每次 CI 运行的 Swift 6 错误数写入结构化日志或 Build Metrics,用折线图跟踪迁移进度;给团队设置「本周减少 X 个错误」的周目标。
  6. 06
    全量门禁验收:全部模块达到 0 错误后,在 main 分支的 Branch Protection Rule 里启用「Require swift6-concurrency-check to pass」,同时把 SWIFT_VERSION = 6 写入所有 target 的 Build Settings,正式切换 Swift 6 语言模式。

05本地 Mac 与独占远程节点对照

Swift 6 迁移期同时需要开发者本地快速试错CI 全量可信验证,两者对节点的需求不同:

维度开发者本地 MacNUKCLOUD 独占远程 Mac(CI)
Xcode 版本开发者自行管理,可能存在差异脚本钉住小版本,全团队一致
DerivedData与日常开发共享,增量缓存可能被污染独立路径,按 PR 或 Runner 分桶
CPU 可用性受其他应用争抢,编译耗时有方差裸金属独占,P95 延迟可预期
结果可信度
「本地过」不代表 CI 过门禁阶段的最终裁决环境
适合场景单模块快速试错与 Xcode 诊断阅读全量并发检查门禁与每日回归

06常见问题

Swift 6 严格并发检查和「Swift 6 语言模式」是同一件事吗?
不完全一样。-strict-concurrency=complete 是编译器标志,可以在 Swift 5 语言模式下启用,让你先看到所有错误再决定何时切换。正式的 Swift 6 语言模式(SWIFT_VERSION = 6)在此基础上还会锁定若干已移除的语法糖。推荐先用 complete 标志修完错误,再切换语言模式。
第三方依赖(CocoaPods / SPM)还没支持 Swift 6 怎么办?
可以在 Package.swift 里用 .unsafeFlags(["-strict-concurrency=minimal"]) 为特定依赖降级检查,或在 Podfile 里用 post_install hook 设置 SWIFT_STRICT_CONCURRENCY = minimal。大多数主流库在 2026 年已陆续完成迁移,可先检查对应库的 Release Notes。
NUKCLOUD 节点能同时跑多个 PR 的并发检查吗?
可以。在 Runner 注册时配置 --concurrent-jobs N,配合 DerivedData 按 PR 编号分桶(-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}),多个 Job 独立编译互不影响。独占裸金属节点的核心优势正在于此:无邻居争 CPU,并发 Job 的完成时间方差小。
定价与节点规格在哪里查看?
请查看 定价页下单页帮助中心;本文只描述工程实践,不构成报价承诺。
分钟池、自购 Mac 与独占远程节点,迁移期该怎么选?
分钟池适合短期试跑,但全量 Swift 6 扫描会放大排队与邻居 CPU 争抢;自购 Mac 环境一致性好,却难随项目周期弹性扩缩;桌面机 不适合做法务可审计的门禁裁决环境。当团队要把 -strict-concurrency=complete 锁进合并门禁并维持数周修复节奏时,NUKCLOUD 多区域裸金属 Mac / 云端 Mac 节点在钉版本、分桶 DerivedData 与可观测队列上更稳,也更易与 控制台拨备 Runbook 里的六步验收对齐。