Rendre les builds SwiftPM reproductibles sur un Mac distant
Lorsqu’un même commit fonctionne sur le Mac de développement, mais résout des dépendances différentes sur un autre Mac distant, le compilateur est rarement en cause. Le problème vient généralement du fait que le fichier de résolution, la version de Xcode et les limites du cache n’ont pas été verrouillés ensemble. Les nœuds loués pour de courtes périodes révèlent particulièrement vite ce défaut : leurs répertoires sont neufs à la livraison, alors que les scripts du pipeline peuvent réutiliser implicitement un ancien cache ou modifier silencieusement les versions des dépendances pendant le build.
Commencer par définir ce qui constitue un même build
Un build reproductible n’exige pas de conserver l’intégralité du répertoire de travail à chaque exécution. Il exige que des entrées identiques produisent le même ensemble de dépendances. Il faut enregistrer au minimum quatre éléments : le commit du code, le contenu de Package.resolved, la version de build de Xcode et l’architecture du processeur. La version majeure de Xcode ne suffit pas, car deux installations d’une même version majeure peuvent utiliser des correctifs différents de la chaîne d’outils.
Commencez par enregistrer un instantané de l’environnement sur le nœud :
set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD
Pour une application, Package.resolved doit généralement être versionné dans le dépôt. Dans un workspace, son emplacement courant est App.xcworkspace/xcshareddata/swiftpm/Package.resolved. Si le dépôt ne contient qu’un fichier de projet, il peut aussi se trouver dans le répertoire de workspace interne au projet. N’entretenez pas deux fichiers de résolution en parallèle : vérifiez d’abord lequel est effectivement lu par le pipeline.
Le critère est simple : si
git status --porcelainsignale une modification du fichier de résolution après l’exécution du script de build, ce build ne peut pas être considéré comme une validation du commit d’origine.
Verrouiller la résolution, pas seulement les plages de versions
Les plages de versions définies dans Package.swift indiquent « ce qui peut être sélectionné », tandis que Package.resolved enregistre « ce qui a réellement été sélectionné pour ce build ». Une résolution explicite peut être effectuée lors de la préparation initiale du nœud, mais le build final ne doit accepter que les versions déjà enregistrées.
set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$CACHE"
git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=macOS' \
-clonedSourcePackagesDirPath "$CACHE" \
-onlyUsePackageVersionsFromResolvedFile \
build
Pour une cible iOS, remplacez destination par la destination générique ou le simulateur déjà défini dans le pipeline. Ne laissez pas le script choisir automatiquement l’appareil actuellement démarré. Un package Swift autonome peut utiliser swift package resolve et swift build. En revanche, les projets et les workspaces doivent systématiquement passer par xcodebuild, afin d’éviter que deux points d’entrée de résolution produisent des états de répertoire différents.
Délimiter le cache à partir d’une empreinte des entrées
Le cache ne sert qu’à réduire le temps consacré aux téléchargements et aux checkouts. Il ne doit jamais devenir la source des versions de dépendances. Une clé de cache fiable doit inclure l’architecture, les informations de build de Xcode et l’empreinte du fichier de résolution :
set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
| Entrée modifiée | Réutilisation du cache de travail | Raison |
|---|---|---|
| Code métier uniquement | Oui | L’ensemble des dépendances ne change pas |
Package.resolved |
Non | Les versions ou révisions extraites ont changé |
| Version de build de Xcode | Non | La chaîne d’outils et le format des artefacts peuvent changer |
Passage de arm64 à une autre architecture |
Non | Les artefacts binaires ne sont pas interchangeables |
Avec plusieurs nœuds, ne laissez pas plusieurs machines écrire simultanément dans le même répertoire. Vous pouvez conserver un cache en lecture seule, archivé par clé, puis le copier dans un répertoire de travail local propre à chaque nœud physique. Lors d’une nouvelle tentative après un échec, nettoyez d’abord la copie de travail courante au lieu de supprimer toutes les archives précédentes.
Faire échouer immédiatement le job en cas de dérive des dépendances
En contrôlant l’état du dépôt avant et après le build, vous pouvez détecter à la fois une réécriture du fichier de résolution et la création accidentelle d’artefacts de script dans le répertoire source. Un job de production doit toujours démarrer depuis un checkout propre :
set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
-clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
-onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"
Après un échec, ne supprimez pas immédiatement les contraintes de versions pour relancer la résolution. Cette pratique transforme une erreur de configuration en faux succès. Conservez plutôt les journaux de résolution, l’instantané de l’environnement et la clé de cache, puis déterminez si le dépôt ne contient pas de résolution verrouillée, si une révision de dépendance n’est plus valide ou si le nœud a sélectionné la mauvaise version de Xcode.
Diagnostiquer les échecs courants dans le bon ordre
Expiration du délai pendant la résolution
Vérifiez d’abord que le nœud peut accéder aux sources des dépendances, puis contrôlez l’espace disque et les droits du répertoire de cache. Une fois le réseau rétabli, réessayez avec le même fichier de résolution sans commencer par mettre les dépendances à niveau. Si l’échec concerne toujours le même package, vérifiez séparément que sa déclaration et sa révision correspondent encore.
Résolution réussie, mais compilation en échec
Commencez par comparer les sorties de xcode-select -p, xcodebuild -version et uname -m sur les deux nœuds. En présence de dépendances binaires, vérifiez également qu’elles contiennent une tranche compatible avec l’architecture courante et qu’elles respectent la version minimale du système définie dans le projet. Supprimez la copie locale correspondant à la clé de cache concernée, puis relancez le build afin de distinguer un cache corrompu d’une incompatibilité du code source.
Liste de contrôle avant le commit
- Le dépôt ne contient qu’un seul fichier
Package.resolvedeffectivement utilisé. - La commande de build active
-onlyUsePackageVersionsFromResolvedFile. - La clé de cache inclut l’architecture, les informations de build de Xcode et l’empreinte du fichier de résolution.
- Chaque nœud utilise son propre répertoire de travail accessible en écriture.
- L’empreinte du fichier de résolution est identique avant et après le build.
- Les journaux conservent le commit, la clé de cache et la version de la chaîne d’outils, mais aucune information d’identification.
Questions fréquentes
Faut-il versionner Package.resolved dans le dépôt ?
Oui dans la plupart des applications et services déployables, puis le build doit se limiter aux versions enregistrées. Une bibliothèque peut suivre une autre règle, mais doit tester sa plage déclarée dans un répertoire propre.
Plusieurs Mac distants peuvent-ils écrire dans le même cache SwiftPM ?
Il faut éviter les écritures concurrentes dans un même dossier. La clé doit inclure l’architecture, la version de build de Xcode et l’empreinte de Package.resolved, avec une copie de travail propre à chaque nœud.
Que vérifier si la résolution réussit mais que la compilation échoue ?
Contrôlez d’abord le chemin et la version de Xcode, recréez la copie de cache concernée, puis vérifiez les architectures des dépendances binaires, la version minimale du système et toute modification de Package.resolved.
Conserver un environnement reproductible sur le même Mac dans le cloud
Choisissez un modèle, une région et une durée de location fixes, puis utilisez l’interface graphique complète de macOS et la ligne de commande pour vos tâches de développement ou d’automatisation.