Un projet iOS qui fonctionne parfaitement dans un simulateur local peut encore révéler des problèmes au moment de l’installation, même après la création réussie d’une Archive sur un Mac cloud HopVM : un Framework précompilé peut contenir une tranche x86_64, une extension peut être liée à la mauvaise plateforme ou une bibliothèque dynamique imbriquée peut relever discrètement la version minimale du système. Plutôt que d’attendre la livraison pour enquêter, mieux vaut analyser directement le paquet .app exporté et transformer les caractéristiques réelles des binaires en contrôle bloquant du pipeline.
Pourquoi contrôler le paquet final exporté
Les réglages du projet Xcode indiquent seulement « comment la compilation est censée se dérouler ». Seul le paquet final montre « ce qui est réellement livré ». L’exécutable principal, les App Extensions, les Frameworks et les fichiers .dylib peuvent tous posséder leur propre en-tête Mach-O. Les paquets binaires téléchargés par un gestionnaire de dépendances n’héritent pas nécessairement non plus des réglages du projet principal.
Le contrôle doit répondre au minimum à trois questions :
- Les binaires destinés à de vrais appareils iOS contiennent-ils uniquement les architectures autorisées ?
- Chaque fichier Mach-O cible-t-il bien iOS, et non le simulateur ou macOS ?
- La version minimale du système requise par un binaire imbriqué dépasse-t-elle celle déclarée par l’app principale ?
Ne considérez pas la réussite de l’Archive comme une validation de la livraison. L’éditeur de liens vérifie seulement que la cible actuelle peut produire un artefact ; il ne détermine pas à la place de l’équipe si tous les fichiers imbriqués respectent les critères de publication.
Il faut contrôler le paquet final produit par xcodebuild -exportArchive, et non se limiter à DerivedData. L’exportation réorganise les Frameworks, les extensions et les éléments signés : son résultat constitue donc l’entrée la plus proche de l’état réellement livré.
Figer les entrées et les critères d’acceptation
Commencez par fixer les répertoires de l’archive, de l’exportation et du rapport afin d’éviter que le script n’analyse des fichiers résiduels d’une tâche précédente. L’isolation du répertoire de travail est particulièrement importante lorsqu’un même Mac cloud est utilisé successivement par plusieurs pipelines.
set -euo pipefail
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_PATH="$PWD/output/export"
REPORT_PATH="$PWD/output/macho-report.tsv"
rm -rf "$EXPORT_PATH"
mkdir -p "$EXPORT_PATH"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$PWD/ci/ExportOptions.plist"
APP_PATH="$(find "$EXPORT_PATH" -maxdepth 2 -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
Les critères de référence du produit doivent être versionnés dans le dépôt plutôt que dispersés dans l’interface de configuration du CI. Par exemple, ci/macho-policy.env peut contenir les architectures autorisées et la version minimale :
EXPECTED_ARCHS="arm64"
EXPECTED_PLATFORM="IOS"
DECLARED_MIN_IOS="17.0"
La valeur de DECLARED_MIN_IOS doit correspondre à la plage réellement prise en charge par le projet. La version indiquée ici sert uniquement d’exemple pour le script ; elle ne doit pas être copiée telle quelle et adoptée comme décision produit.
Énumérer récursivement tous les fichiers Mach-O
Une recherche fondée uniquement sur les extensions ne suffit pas, car le binaire principal d’un Framework n’en possède généralement pas. Une méthode plus fiable consiste à parcourir les fichiers, puis à utiliser file pour déterminer s’ils sont au format Mach-O.
: > "$REPORT_PATH"
failure=0
while IFS= read -r -d '' candidate; do
if ! file -b "$candidate" | grep -q 'Mach-O'; then
continue
fi
archs="$(lipo -archs "$candidate" 2>/dev/null || true)"
build="$(vtool -show-build "$candidate" 2>/dev/null || true)"
platform="$(awk '/platform/{print $2; exit}' <<<"$build")"
minos="$(awk '/minos/{print $2; exit}' <<<"$build")"
printf '%s %s %s %s
' \
"${candidate#"$APP_PATH"/}" "$archs" "$platform" "$minos" \
>> "$REPORT_PATH"
if [[ "$archs" != "$EXPECTED_ARCHS" ]]; then
printf 'architecture mismatch: %s (%s)
' "$candidate" "$archs" >&2
failure=1
fi
if [[ "$platform" != "$EXPECTED_PLATFORM" ]]; then
printf 'platform mismatch: %s (%s)
' "$candidate" "$platform" >&2
failure=1
fi
done < <(find "$APP_PATH" -type f -print0)
exit "$failure"
Le rapport utilise des tabulations comme séparateurs. Il peut ainsi être conservé comme artefact de compilation et facilement converti en tableau par la suite. Chaque message d’échec doit impérativement inclure le chemin du fichier et la valeur réellement détectée. Un simple message tel que « échec du contrôle d’architecture » obligerait la personne chargée du diagnostic à relancer toute la tâche.
Ne pas modifier le paquet pendant le contrôle
Exécuter immédiatement lipo -remove après avoir détecté x86_64 peut sembler pratique, mais cette opération modifie un contenu déjà signé et masque le problème dans le processus de production de la dépendance. Il faut plutôt déterminer si le fichier provient d’une compilation des sources, d’une dépendance binaire ou d’un script de copie, puis corriger la source du problème et recréer l’Archive.
Comparer les versions minimales du système
Commencez par lire la valeur déclarée par l’app principale :
PLIST="$APP_PATH/Info.plist"
APP_MIN_IOS="$(/usr/libexec/PlistBuddy \
-c 'Print :MinimumOSVersion' "$PLIST")"
printf 'declared minimum iOS: %s
' "$APP_MIN_IOS"
Lisez ensuite la valeur minos dans LC_BUILD_VERSION pour chaque fichier Mach-O. La comparaison doit respecter la sémantique des numéros de version et ne pas utiliser une simple comparaison de chaînes, sans quoi 17.10 pourrait être placé à tort avant 17.9.
| Élément contrôlé | Emplacement de la valeur | Condition d’échec |
|---|---|---|
| Déclaration de l’app principale | MinimumOSVersion dans Info.plist |
Diffère de la référence du dépôt |
| Exécutable principal | minos dans LC_BUILD_VERSION |
Supérieure à la valeur déclarée par le produit |
| Frameworks et bibliothèques dynamiques | Leur propre LC_BUILD_VERSION |
Supérieure à la valeur déclarée par le produit |
| App Extension | Info.plist de l’extension et Mach-O | La valeur déclarée ou réelle dépasse la référence |
Le tri de versions fourni par le système permet d’effectuer cette comparaison :
version_gt() {
[[ "$1" != "$2" ]] &&
[[ "$(printf '%s
%s
' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
}
if version_gt "$minos" "$APP_MIN_IOS"; then
printf 'minimum iOS mismatch: %s requires %s
' \
"$candidate" "$minos" >&2
failure=1
fi
Si vtool ne renvoie ni plateforme ni version, le fichier ne doit pas être ignoré silencieusement. Commencez par enregistrer l’ensemble des commandes de chargement avec otool -l, puis vérifiez s’il s’agit d’un binaire dans un ancien format, d’un fichier anormal ou d’une détection erronée du script.
Intégrer le contrôle au CI et traiter les faux positifs courants
Placez le contrôle après la création de l’Archive et l’exportation, mais avant l’envoi du produit. Le fichier macho-report.tsv doit toujours être téléversé. Même en cas d’échec de la tâche, le rapport doit être conservé au moyen du mécanisme d’artefacts post-échec du CI.
Les faux positifs les plus courants se répartissent en trois catégories :
- Le script analyse des fichiers de symboles ou le répertoire d’archive au lieu du paquet
.appfinal. - La casse du nom de plateforme varie selon les outils, mais les valeurs sont comparées directement comme des chaînes strictement identiques.
- Une politique destinée aux utilitaires macOS est appliquée à une app iOS, ce qui bloque à tort des binaires universels pourtant valides.
Le script doit donc maintenir des politiques distinctes selon le type de produit. Un même jeu de règles ne doit pas couvrir simultanément iOS, les paquets de test pour simulateur et les outils macOS. Si le pipeline génère plusieurs types d’artefacts, exécutez le contrôle séparément dans chaque répertoire d’exportation et inscrivez le type d’artefact dans la première colonne du rapport.
Terminez par une nouvelle vérification de la structure de signature :
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
Cette commande ne remplace pas le contrôle Mach-O, mais elle permet de détecter des problèmes ultérieurs, comme la modification accidentelle d’un binaire par un script ou l’invalidation d’une signature imbriquée. L’ordre complet doit être le suivant : exportation, analyse, comparaison des versions, vérification de la signature, puis conservation du rapport. La livraison ne doit être autorisée qu’après ces étapes. Chaque échec peut ainsi être rattaché à un fichier précis, à un champ précis et à une action corrective précise, au lieu d’obliger l’équipe à deviner la cause après un échec d’installation.
Questions fréquentes
Pourquoi ne pas vérifier uniquement le binaire principal de l’app ?
Les Framework, extensions et bibliothèques dynamiques possèdent leurs propres fichiers Mach-O. Une seule dépendance incohérente peut empêcher l’installation ou le lancement.
Que faire si une tranche x86_64 apparaît dans le paquet iOS ?
Identifiez la dépendance qui l’a produite et corrigez sa compilation ou l’étape d’archivage. Évitez de supprimer la tranche avec lipo juste avant la livraison.
Quelle source utiliser pour la version minimale d’iOS ?
Comparez MinimumOSVersion dans Info.plist avec minos dans LC_BUILD_VERSION. Aucun binaire imbriqué ne doit exiger une version supérieure à celle annoncée par l’app.
Reproduisez vos workflows sur un Mac cloud que vous activez et arrêtez selon vos projets
Choisissez la ressource adaptée parmi trois configurations Apple Silicon et louez-la à la journée, à la semaine, au mois ou au trimestre. Chaque appareil est une machine physique dédiée, et non une machine virtuelle ; sa disponibilité réelle est indiquée en temps réel dans la console.