DevOps / CI·CD 24 апреля 2026

2026-04-24: Swift 6 — строгая concurrency, Sendable и @MainActor в xcodebuild CI на арендованном облачном Mac

MacXCode Engineering Team 24 апреля 2026 ~19 мин чтения

Владельцы сборок iOS и macOS, уже перешедшие на хосты Apple Silicon в HK / JP / KR / SG / US, сталкиваются со «вторым подъёмом»: модель языка Swift 6 с строгой проверкой concurrency превращает *«у меня работает»* в измеримую дельту между тёплым ноутбуком и холодным распараллеленным заданием xcodebuild на арендованном SSH-Mac. Рунбук 2026-04-24 — не учебник по языку: в нём сопоставлены настройки сборки (SWIFT_STRICT_CONCURRENCY, версия Swift), изоляция DerivedData и index store и поэтапная миграция шума Sendable / nonisolated / MainActor, который взрывается в топологиях параллельного xcodebuild и self-hosted runner. Сверяйтесь с временным DerivedData и изоляцией xcresult и с удалённым archive, чтобы один и тот же узел в Сингапуре не крутил общий ModuleCache, который шард в US считал приватным.

Почему строгая Swift 6 concurrency ведёт себя иначе в headless CI

Строгие проверки вскрывают гонки по данным, которых симулятор не увидит, потому что UI-главный поток маскирует время жизни захваченного var. В CI параллельные джобы swift-frontend и whole-module-оптимизации переставляют диагностику; тот же пакет, что ночью собрался на 64 GB dev-ноутбуке, может уронить lane с -warnings-as-errors, когда инкрементальная сборка отключена. Относитесь к strict concurrency так же, как к выводу биткода из архивов или к паритету CLT и полного Xcode: отдельный stage пайплайна с зафиксированным DEVELOPER_DIR и письменной политикой, какие цели могут оставаться на SWIFT_STRICT_CONCURRENCY=targeted, пока не дойдут рефакторинги.

Заложите бюджет и на переиндексацию: первая «холодная» Swift 6 сборка на пустом DerivedData при App Intents или тяжёлых WidgetKit-расширениях может сильно удлинить ExtractAppIntentsMetadata и соседние шаги. Скачок по wall clock не сбой concurrency, а холодный кэш—предупредите продукт-менеджмент заранее, как о первом прогреве реестра SwiftPM. Фиксируйте p95 до/после, чтобы «неделя Swift 6» не выглядела деградацией ёмкости пула в облаке.

Настройки сборки: версия языка и режимы strict concurrency

На уровне проекта или xcconfig согласуйте три ручки: (1) SWIFT_VERSION с Swift 6 тулчейном, который вы поставляете; (2) SWIFT_STRICT_CONCURRENCY как complete в блокирующем merge lane, но, возможно, targeted для унаследованных вендорских статических либ, пока не разметите; (3) изоляцию по умолчанию в Swift 6, где модули UIKit и SwiftUI часто требуют явного покрытия @MainActor. В CI YAML передавайте оверрайды только когда схема ещё не тянет настройки, напр. OTHER_SWIFT_FLAGS=-warn-concurrency в advisory-lane, не мешая тихо с релизными archive, которым нужна бит-в-бит воспроизводимость. Зафиксируйте разницу между пакетным плагином и app-target: в логе плагина ошибки могут прятаться, пока не включите -strict-concurrency=complete для бинарника, который реально поставляете.

Вызов xcodebuild: archive, build и analyze

Зафиксируйте единый DEVELOPER_DIR=/Applications/Xcode.app на хосте, затем используйте явные -configuration и -destination, чтобы схема не «плыла». Для строгого compile-only гейта до UI-тестов предпочтительно xcodebuild -scheme App build -destination 'platform=iOS Simulator,name=iPhone 16' с CODE_SIGNING_ALLOWED=NO, если нужны только сигналы компилятора — затем второй job для симулятора с подписью обратно, по тому же разделению, что в гайде XCTest + xcresult. Минимальный strict-гейт:

DEVELOPER_DIR=/Applications/Xcode.app xcodebuild build -workspace App.xcworkspace -scheme App -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 16' -derivedDataPath "$CI_DERIVED/Swift6Gate" OTHER_SWIFT_FLAGS='$(inherited) -warn-concurrency'

Ограничители в CI: держите retry только для инфраструктурных флаков, не для ошибок concurrency — второй идентичный отказ в Sendable это реальный дефект кода, а не «призрак» Xcode.

Параллельные lane, DerivedData и index store

Когда веером запускаете джобы (см. fan-out на M4), каждому дочернему заданию дайте приватный -derivedDataPath и рассмотрите отключение инкрементальной сборки на strict-стадии, чтобы диагностика была детерминированной на билдерах HK / JP / KR / SG / US. Общий DerivedData на смонтированном «как NFS» томе — быстрый путь к фантомным ошибкам вроде «cannot assign through subscript: base is not a concurrent value», которые исчезают на retry — ровно тот паттерн флакинесса, о котором уже предупреждает статья про изоляцию для xcresult. Если обязаны шарить том из соображений стоимости, хотя бы кладите Index/DataStore в префикс на $(CI_JOB_ID).

Матрица миграции: предупреждения, ошибки и владельцы

Сигнал Действие Владелец
захват в Sendable-замыкании Рефактор на value-типы, Sendable-метки или вынесение в actor фича-команда
дрейф изоляции MainActor Протягивать UI-обновления через узкий фасад-тип лид клиентской части
бинарь третьей стороны без Sendable Обернуть на границе модуля, завести тикет вендора или форк платформа сборки

Когда это не ошибка concurrency

Часть сбоев маскируется под Swift 6: провижининг и связка в момент CodeSign может перемежаться в логах со строками swift-frontend; дыры dSYM после archive ломают символикацию, а не компилятор. Держите гигиену dSYM в той же еженедельной ревью, что и strict-lane, иначе дежурный гоняется за неверным диффом.

Подпись, entitlements и «strict» в одной сборке

Включать больше компиляторных проверок в том же релизном поезде, что и ротацию провижининга, рискованно. Порядок: (1) убедиться, что подпись зелёная на throwaway canary, (2) включить strict-стадию, (3) лишь потом вливать feature-флаг, делающий SWIFT_STRICT_CONCURRENCY=complete в схеме по умолчанию. Тот же пул SSH Mac mini M4 может обслуживать оба сценария, но джобам нельзя чередоваться в одной папке воркспейса — параллелизм силён только когда вы уже следуете гайдам archive и сим.

Начните с SwiftPM и кеша реестра и детерминизма Ruby + CocoaPods, когда сбой concurrency на деле — несоответствие графа зависимостей. Если команды смешивают Xcode Cloud и выделенные хосты, выровняйте флаги языка на обоих, иначе dedicated strict-lane зарубит merge, который Xcode Cloud не видел.

FAQ: Swift 6 на общих билдерах

Вопрос Практичный ответ
Сначала включать для SPM-пакетов? Часто да — наметьте границы, затем app-таргеты; при разделении тест-only кода используйте traits пакета.
Нужен ли VNC? Обычно нет — VNC это запасной путь для визуального дебага, не для strict-логов сборки.
Как с диском под index? Берите хосты 1–2 ТБ, если гасите тяжёлые SwiftUI Preview, но храните большие деревья Index; при всплеске артефактов strict-lane смотрите тарифы.

Почему Mac mini M4 на bare metal подходит для тяжёлых по concurrency компиляций

Строгие проверки тянут CPU и I/O: компилятор переписывает графы, index раздувается, index store выигрывает от того же NVMe, что вы уже заложили под архивы на арендованном хосте MacXCode. Незагруженный соседями сетевой стек снижает хвостовую задержку, когда lane ходит в удалённый кеш или реестр на этапе резолва пакетов. Когда strict-конвейер стабилен, масштабируйтесь вторым Mac mini M4 в том же регионе, прежде чем снова сваливать lane в общий DerivedData — ваш 2026 mainline заслуживает детерминированного сигнала, а не лотерею.

Чистые сборки Swift 6 на M4

NVMe · изолированные lane · глобальные регионы