Le mode langage Swift 6 fait du compilateur un auditeur de concurrence : au lieu de repérer les mauvais usages de @MainActor ou le partage inter-Actor en revue, xcodebuild échoue tôt. Il faut un nœud de build à calcul stable et version figée — compilations complètes sans jitter CPU des voisins sur la latence de queue. Cet article présente l'adoption progressive de la concurrence stricte Swift 6 sur les nœuds Mac distants dédiés NUKCLOUD et son verrouillage dans les portes de fusion PR.
00Pourquoi migrer maintenant — et pourquoi des nœuds dédiés
Les contrôles de concurrence stricte Swift 6 (-strict-concurrency=complete) évoluent avec la taille du code : 200 à 800 erreurs de compilation au premier passage est courant pour un projet iOS de taille moyenne. Les correctifs s'étalent sur des semaines ou des mois ; pendant cette période, le CI doit garantir :
- Versions mineures Xcode figées : les diagnostics d'isolation Actor changent de formulation selon les versions Xcode ; figer la version rend les sorties CI comparables.
- Pas de contention CPU des voisins : les compilations complètes en concurrence stricte durent 20 à 40 % de plus qu'une build debug typique ; les nœuds dédiés stabilisent la latence de queue P95.
- Espaces de noms DerivedData isolés : plusieurs PR exécutant des contrôles de concurrence ont besoin de caches incrémentaux séparés, sinon les résultats deviennent peu fiables.
01Trois niveaux de contrôle de concurrence et un chemin de migration
Swift propose trois niveaux de drapeaux compilateur dans OTHER_SWIFT_FLAGS (réglages de build Xcode) ou swiftSettings dans Package.swift :
| Drapeau | Signification | Phase recommandée |
|---|---|---|
-strict-concurrency=minimal | Vérifie uniquement la conformité syntaxique de base async/await, pas les frontières Actor | Échauffement du code existant (semaines 1–2) |
-strict-concurrency=targeted | Vérifie les frontières Actor pour les types explicitement marqués Sendable, @MainActor, etc. | Correction module par module (semaines 3–6) |
-strict-concurrency=complete | Contrôles complets équivalents au mode langage Swift 6 ; tout partage concurrentiel non traité génère une erreur | Intégrer aux portes après 0 erreur |
En pratique, migrez module par module : activez complete d'abord sur les modules feuilles, puis sur le cœur. SwiftPM .enableUpcomingFeature("StrictConcurrency") contrôle précisément le niveau par cible.
02Frontières d'isolation Actor : trois types d'erreurs fréquents
Environ 90 % des erreurs de compilation en migration relèvent de ces trois catégories ; les corriger efface la plupart des builds rouges :
① Accès inter-Actor à des types non Sendable
// ❌ Erreur : accès à l état @MainActor hors du contexte @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) }
}
}
Correction : faire conformer UserModel à Sendable (les types valeur le satisfont souvent automatiquement), ou effectuer la conversion inter-frontière à l'intérieur de la frontière Actor.
② @MainActor manquant
Les sous-classes ViewController et ObservableObject de UIKit/SwiftUI tournaient implicitement sur le thread principal avant migration ; Swift 6 exige une annotation @MainActor explicite. Le bouton Corriger de Xcode peut traiter en lot, mais relisez chaque site pour ne pas tirer de la logique non UI sur le thread principal.
③ Accès concurrent aux variables globales
// ❌ En Swift 6, les globales doivent être Sendable ou nonisolated
var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ nonisolated(unsafe) en transition
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()
// ✅ Mieux : encapsuler dans un Actor avec API async
actor CacheStore {
private let cache = NSCache<NSString, AnyObject>()
func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}
03Configurer les portes CI sur Mac distant
Idée centrale pour les portes PR : exécuter les contrôles de concurrence dans un Job séparé des tests habituels. Les échecs ne polluent pas les rapports de test et vous pouvez appliquer des stratégies différentes pendant les correctifs (warn-only vs. block-merge). Sur NUKCLOUD : tarifs, puis commander — évitez les pics temporaires de pool à la minute pendant la migration.
Les fenêtres de correctif durent souvent des semaines : journalisez erreurs de concurrence, wall time de build complet et taille DerivedData, et liez-les aux changements de spec.
name: Swift 6 Concurrency Gate
on:
pull_request:
branches: [main, release/*]
jobs:
concurrency-check:
runs-on: self-hosted # Runner Mac distant dédié NUKCLOUD
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Sélectionner Xcode (version mineure figée)
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/
-derivedDataPath ~/DerivedData/swift6-check séparé des builds normales pour que les artefacts de concurrence ne contaminent pas les caches de debug. Quand plusieurs PR tournent, ajoutez runner.name ou le numéro de PR pour isoler davantage.04Runbook de migration progressive en six étapes
-
01
Instantané de base : sur un Mac distant dédié, lancez une build complète avec
-strict-concurrency=minimalet enregistrez le nombre d'erreurs et le temps de compilation comme point de départ quantifié. -
02
Modules feuilles en premier : dans le graphe SwiftPM, choisissez les cibles sans dépendants aval et activez
completeune par une. Soumettez une PR distincte par cible pour des diffs vérifiables. -
03
Stratégie de branche CI : sur une branche longue
feature/swift6-migration, exécutez les Jobs de concurrence enwarn-only(continue-on-error: true) sans bloquer les fusions PR quotidiennes. Passez une cible en block-merge uniquement quand elle atteint 0 erreur. -
04
Figez la version Xcode : inscrivez la version
xcode-selectdans le runbook et le README pour que les développeurs Mac locaux et les nœuds CI partagent la même version mineure et évitent le bruit « passe en local, échoue en CI ». -
05
Tableau de bord des tendances d'erreurs : journalisez le nombre d'erreurs Swift 6 par exécution CI dans des logs structurés ou des métriques de build ; suivez la migration par courbes et fixez des objectifs hebdomadaires d'équipe pour réduire X erreurs.
-
06
Acceptation de porte complète : après 0 erreur sur tous les modules, activez Require swift6-concurrency-check to pass sur la protection de branche
mainet définissezSWIFT_VERSION = 6sur toutes les cibles pour basculer en mode langage Swift 6.
05Mac local vs. nœud distant dédié
La migration Swift 6 exige à la fois une itération locale rapide et une validation CI complète fiable ; les besoins de nœud diffèrent :
| Dimension | Mac local du développeur | Mac distant dédié NUKCLOUD (CI) |
|---|---|---|
| Version Xcode | Gérée par le développeur, peut diverger | Version mineure figée par script, identique pour toute l'équipe |
| DerivedData | Partagé avec le dev quotidien ; le cache incrémental peut se corrompre | Chemins séparés, compartimentés par PR ou Runner |
| Disponibilité CPU | Contention avec d'autres apps ; temps de compilation variable | Bare-metal dédié, latence P95 prévisible |
| Fiabilité du résultat | Réussite locale n'implique pas réussite CI | Arbitrage final à l'étape de porte |
| Idéal pour | Essais rapides sur un module et lecture des diagnostics Xcode | Contrôles de porte de concurrence complets et régression quotidienne |
06FAQ
-strict-concurrency=complete est un drapeau compilateur activable en mode langage Swift 5 pour voir toutes les erreurs avant de basculer. Le mode langage Swift 6 formel (SWIFT_VERSION = 6) supprime aussi certaines syntaxes héritées. Corrigez d'abord avec le drapeau complete, puis changez de mode langage..unsafeFlags(["-strict-concurrency=minimal"]) dans Package.swift, ou définissez SWIFT_STRICT_CONCURRENCY = minimal dans un hook post_install du Podfile. La plupart des bibliothèques majeures ont terminé la migration en 2026 — consultez d'abord les notes de version.--concurrent-jobs N à l'enregistrement du Runner et compartimentez DerivedData par PR (-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}) pour que les Jobs compilent indépendamment. Les nœuds bare-metal dédiés excellent ici : pas de contention CPU des voisins et faible variance entre Jobs concurrents.-strict-concurrency=complete sur des semaines, les nœuds Mac bare-metal / cloud multi-régions NUKCLOUD stabilisent Xcode épinglé, compartiments DerivedData et files observables — avec le runbook console.