Une archive réunissant une app iOS et une app watchOS peut rester impossible à distribuer même si la compilation de l’application principale a réussi : l’app de la montre peut ne pas avoir été incorporée, son numéro de build peut avoir une version de retard, l’extension peut encore pointer vers un ancien Bundle ID ou un composant imbriqué peut être mal signé. Ces problèmes ne devraient pas attendre l’étape d’exportation ou de soumission pour être détectés. Une approche plus fiable consiste à demander au Mac dans le cloud d’inspecter la structure du bundle final après chaque génération d’un xcarchive, puis à utiliser le résultat comme condition de passage du pipeline.
Pourquoi contrôler l’archive finale
Les fichiers du projet décrivent le résultat attendu ; le xcarchive constitue le livrable réel. Les fichiers de configuration, scripts de build et variables d’environnement peuvent tous remplacer un numéro de version ou un identifiant de produit. La seule lecture des réglages du projet ne prouve ni que l’app watchOS se trouve bien dans le répertoire Watch de l’application principale, ni que l’extension est correctement associée à son application conteneur.
Il est recommandé de toujours contrôler l’archive Release plutôt qu’un répertoire intermédiaire de DerivedData. Commencez par produire le livrable avec le même espace de travail, le même Scheme et la même configuration que dans le pipeline de production :
set -euo pipefail
ROOT="$PWD"
OUT="$ROOT/out"
ARCHIVE="$OUT/Client.xcarchive"
rm -rf "$ARCHIVE"
mkdir -p "$OUT"
xcodebuild \
-workspace Client.xcworkspace \
-scheme Client \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
clean archive
clean archive convient à un contrôle de référence. Le pipeline quotidien peut l’adapter à sa stratégie de cache, mais le script de validation doit toujours lire l’archive créée par la tâche en cours, sans jamais se rabattre sur un répertoire présenté comme le « plus récent ».
Le contrôle doit répondre à la question « que contient le bundle sur le point d’être distribué ? », et non « que devrait théoriquement produire le projet ? ».
Établir l’inventaire des composants à partir du bundle
Le chemin habituel est Products/Applications/*.app. Son répertoire Watch contient l’app watchOS, dont le répertoire PlugIns contient à son tour l’extension. N’inscrivez pas le nom du produit en dur dans le script : un chemin fixe peut facilement devenir invalide après le renommage d’une Target.
IOS_APP=$(find "$ARCHIVE/Products/Applications" \
-maxdepth 1 -type d -name '*.app' -print -quit)
test -n "${IOS_APP:-}" || {
printf '%s
' "iOS app not found"
exit 1
}
WATCH_APP=$(find "$IOS_APP/Watch" \
-maxdepth 1 -type d -name '*.app' -print -quit)
test -n "${WATCH_APP:-}" || {
printf '%s
' "watchOS app not found"
exit 1
}
WATCH_EXTENSION=$(find "$WATCH_APP/PlugIns" \
-maxdepth 1 -type d -name '*.appex' -print -quit)
printf 'ios=%s
watch=%s
extension=%s
' \
"$IOS_APP" "$WATCH_APP" "${WATCH_EXTENSION:-embedded}"
Certains projets récents ne comportent pas de couche .appex distincte. L’absence d’une extension ne doit donc pas provoquer automatiquement un échec. Le contrôle doit s’appuyer sur la structure de produit déclarée dans le dépôt : l’ancien modèle exige trois niveaux de composants, tandis que la structure plus récente à Target unique ne requiert que l’app iOS et l’app watchOS. Enregistrez dans le dépôt un bref inventaire de la structure attendue au lieu de déduire le type de projet à partir des répertoires.
Valider les versions et les relations entre identifiants
Commencez par exécuter plutil -lint sur chaque fichier Info.plist, puis lisez CFBundleIdentifier, CFBundleShortVersionString et CFBundleVersion. Pour une même livraison, iOS et watchOS doivent généralement utiliser exactement la même version commerciale et le même numéro de build. Si l’équipe emploie des numéros de build indépendants, les règles autorisées doivent être définies explicitement dans le script.
| Contrôle | Règle recommandée | Risque en cas d’échec |
|---|---|---|
| Version commerciale | Identique pour iOS et watchOS | Relation incohérente entre les versions sur l’App Store |
| Numéro de build | Identique au sein d’une même tâche de publication | L’app de la montre est considérée comme un ancien build |
| Identifiant de l’app associée | Pointe vers l’ID de l’application iOS principale | Impossible d’établir l’association après l’installation |
| Rattachement de l’extension | Pointe vers l’ID de l’app watchOS actuelle | L’extension ne correspond pas à son application conteneur |
read_plist() {
/usr/libexec/PlistBuddy -c "Print :$2" "$1"
}
IOS_PLIST="$IOS_APP/Info.plist"
WATCH_PLIST="$WATCH_APP/Info.plist"
plutil -lint "$IOS_PLIST" "$WATCH_PLIST"
IOS_ID=$(read_plist "$IOS_PLIST" CFBundleIdentifier)
WATCH_ID=$(read_plist "$WATCH_PLIST" CFBundleIdentifier)
IOS_VERSION=$(read_plist "$IOS_PLIST" CFBundleShortVersionString)
WATCH_VERSION=$(read_plist "$WATCH_PLIST" CFBundleShortVersionString)
IOS_BUILD=$(read_plist "$IOS_PLIST" CFBundleVersion)
WATCH_BUILD=$(read_plist "$WATCH_PLIST" CFBundleVersion)
COMPANION_ID=$(read_plist "$WATCH_PLIST" WKCompanionAppBundleIdentifier)
test "$IOS_VERSION" = "$WATCH_VERSION"
test "$IOS_BUILD" = "$WATCH_BUILD"
test "$COMPANION_ID" = "$IOS_ID"
Ne remplacez pas une comparaison exacte par la règle « le Bundle ID watchOS doit commencer par le Bundle ID iOS ». L’équipe peut employer des identifiants explicites, et des préfixes similaires ne prouvent pas que l’association est correcte. Lisez la valeur attendue depuis la configuration de publication et vérifiez le contenu complet de WKCompanionAppBundleIdentifier.
Prendre en charge les structures comportant une extension
Si l’archive contient un fichier .appex, lisez également son WKAppBundleIdentifier et exigez que sa valeur soit identique au Bundle ID de l’app watchOS actuelle. Consignez aussi la version et le numéro de build propres à l’extension afin de détecter une Target qui n’aurait pas reçu le script d’uniformisation des versions.
Vérifier la signature et les architectures exécutables
La réussite de la signature de l’application conteneur ne garantit pas que chaque composant imbriqué pourra être validé. Plutôt que d’appliquer uniquement un contrôle récursif à l’application principale, énumérez les composants réels afin que les journaux indiquent précisément lequel a échoué :
verify_component() {
local item="$1"
codesign --verify --strict --verbose=2 "$item"
codesign -d --entitlements :- "$item" >/dev/null
}
verify_component "$IOS_APP"
verify_component "$WATCH_APP"
if test -n "${WATCH_EXTENSION:-}"; then
verify_component "$WATCH_EXTENSION"
fi
Localisez ensuite chaque binaire à partir de son CFBundleExecutable, puis consignez ses architectures avec file ou lipo -archs. Il n’est pas nécessaire de figer définitivement une liste d’architectures dans le contrôle, car la chaîne d’outils et les cibles de déploiement évoluent. Une règle plus fiable consiste à refuser les exécutables vides et les artefacts de simulateur inattendus, tout en enregistrant la liste actuelle des architectures pour vérification.
Recherchez également dans les composants imbriqués les liens symboliques qui sortent du bundle, les Bundle ID dupliqués et les extensions supplémentaires non déclarées. Ces problèmes proviennent souvent de scripts de copie utilisant des caractères génériques trop larges.
Transformer la validation en contrôle CI fiable
Le code de sortie du script sert uniquement à bloquer le pipeline ; le rapport doit expliquer la cause. Chaque tâche doit enregistrer le chemin relatif des composants, leur Bundle ID, leur version, leur numéro de build, leurs architectures, l’état de validation de leur signature et la somme de contrôle de l’archive. Le rapport ne doit recueillir ni clé privée de certificat, ni liste complète des variables d’environnement, ni chemin intégral du répertoire utilisateur.
Il est recommandé de diviser le processus en quatre étapes :
- Créer un répertoire d’archive unique et interdire la réutilisation du livrable précédent.
- Détecter les composants et les comparer à la structure attendue enregistrée dans le dépôt.
- Valider les métadonnées, les champs d’association, les signatures et les architectures exécutables.
- Produire un rapport texte ou JSON, puis lancer la tâche d’exportation.
Lors de la première activation du contrôle, vous pouvez commencer par quelques exécutions qui consignent les erreurs sans bloquer le pipeline, afin de confirmer que les anciennes et les nouvelles structures de projet sont toutes prises en charge. Une fois les règles stabilisées, traitez l’absence d’un composant, les divergences de version et les échecs de signature comme des erreurs bloquantes. Si plusieurs archivages s’exécutent en parallèle sur le même Mac dans le cloud, chaque tâche doit disposer de son propre archivePath et de son propre répertoire de rapports afin qu’elle ne lise pas les résultats d’une autre tâche.
L’objectif final n’est pas d’ajouter un contrôle purement formel, mais de permettre au pipeline de prouver que l’application principale, l’app de la montre et son extension proviennent bien du même contexte de publication, que leurs relations sont clairement établies et que l’archive finale peut passer à l’étape d’exportation.
Questions fréquentes
La vérification des réglages du projet suffit-elle ?
Non. Seule l’archive finale reflète les substitutions de configuration, les scripts de build, l’intégration réelle et les signatures produites.
Les versions iOS et watchOS doivent-elles être identiques ?
Pour une même livraison, alignez normalement la version commerciale et le numéro de build. Toute exception doit être décrite puis vérifiée explicitement en CI.
Exécutez vos workflows Mac mini en continu dans le cloud
Choisissez RunAMac M4, la durée de location et le nœud de déploiement pour exécuter vos tâches de développement, de compilation ou d’expérimentation sur une machine physique dédiée.