Un même projet iOS peut s’archiver correctement en local, mais intégrer un Bundle ID erroné, un URL Scheme de débogage ou aucune description d’accès à l’appareil photo lorsqu’il passe par un pipeline Release sur un Mac cloud. Le problème ne vient généralement pas du fichier Info.plist présent dans le dépôt, mais du résultat final écrit dans l’App après la fusion, par Xcode, des réglages de compilation, des configurations générées et des propriétés de la target. Un CI fiable doit auditer le produit compilé, et non se limiter à une vérification textuelle des fichiers sources.
Vérifier d’abord la configuration réellement livrée
Les projets Xcode modernes peuvent générer automatiquement leur Info.plist ou obtenir leurs valeurs depuis différentes targets, configurations et fichiers .xcconfig. Le fichier désigné par INFOPLIST_FILE n’est qu’une des sources utilisées : des réglages comme PRODUCT_BUNDLE_IDENTIFIER, MARKETING_VERSION et CURRENT_PROJECT_VERSION sont également fusionnés pendant la compilation.
Commencez par lancer la compilation en indiquant explicitement le workspace, le scheme, la configuration et le répertoire de sortie :
set -euo pipefail
ROOT="$(pwd)"
DERIVED_DATA="$ROOT/.ci/DerivedData"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED_DATA" \
CODE_SIGNING_ALLOWED=NO \
build
La désactivation de la signature convient uniquement à un audit rapide de la configuration. Elle ne signifie pas que la signature doit aussi être désactivée pour l’archive destinée à la publication. Une fois la compilation terminée, ne tentez pas de deviner le chemin de l’App : récupérez le répertoire et le nom du produit depuis les réglages de compilation.
SETTINGS="$(mktemp)"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED_DATA" \
-showBuildSettings > "$SETTINGS"
BUILD_DIR="$(awk -F ' = ' '/ TARGET_BUILD_DIR = /{print $2; exit}' "$SETTINGS")"
WRAPPER_NAME="$(awk -F ' = ' '/ WRAPPER_NAME = /{print $2; exit}' "$SETTINGS")"
APP_PATH="$BUILD_DIR/$WRAPPER_NAME"
PLIST_PATH="$APP_PATH/Info.plist"
test -d "$APP_PATH"
test -f "$PLIST_PATH"
plutil -lint "$PLIST_PATH"
L’audit doit porter sur l’App qui vient d’être générée par la tâche en cours. Réutiliser un chemin fixe contenant les résultats d’une compilation précédente peut conduire la barrière CI à tirer des conclusions erronées à partir d’un ancien produit.
Versionner les valeurs attendues
Évitez de coder en dur de nombreuses valeurs attendues dans l’interface de la plateforme CI. Une approche plus facile à examiner consiste à conserver un petit fichier JSON pour chaque environnement de publication, par exemple ci/plist-release.json :
{
"bundleIdentifier": "com.example.product",
"urlSchemes": ["example"],
"requiredUsageKeys": [
"NSCameraUsageDescription",
"NSPhotoLibraryUsageDescription"
],
"allowedBackgroundModes": ["remote-notification"]
}
Ce fichier doit uniquement contenir des règles pouvant être publiées dans le dépôt, jamais des jetons, des clés privées ou des mots de passe de signature. La version et le numéro de build sont généralement fournis par les paramètres du pipeline. Le script doit vérifier leur format et leur cohérence avec les entrées de compilation, plutôt que de conserver indéfiniment une valeur numérique fixe.
Chaque target d’App, extension et hôte de test doit disposer de ses propres règles. Les règles de Bundle ID de l’App principale ne doivent pas produire de faux échec pour un Widget. De même, une extension qui n’a pas besoin d’accéder à l’appareil photo ne doit pas réutiliser une liste exigeant cette description d’autorisation.
Écrire un script d’audit capable d’échouer
plutil -extract convient à la lecture des dictionnaires et des tableaux, tandis que PlistBuddy est adapté aux valeurs scalaires simples. Le script suivant présente une structure minimale :
#!/bin/bash
set -euo pipefail
PLIST_PATH="${1:?missing Info.plist path}"
EXPECTED_BUNDLE_ID="${EXPECTED_BUNDLE_ID:?missing bundle id}"
EXPECTED_VERSION="${EXPECTED_VERSION:?missing version}"
EXPECTED_BUILD="${EXPECTED_BUILD:?missing build number}"
read_key() {
/usr/libexec/PlistBuddy -c "Print :$1" "$PLIST_PATH" 2>/dev/null
}
require_key() {
local key="$1"
local value
value="$(read_key "$key" || true)"
if [[ -z "$value" ]]; then
printf 'Missing required key: %s
' "$key" >&2
exit 1
fi
}
[[ "$(read_key CFBundleIdentifier)" == "$EXPECTED_BUNDLE_ID" ]]
[[ "$(read_key CFBundleShortVersionString)" == "$EXPECTED_VERSION" ]]
[[ "$(read_key CFBundleVersion)" == "$EXPECTED_BUILD" ]]
require_key NSCameraUsageDescription
require_key NSPhotoLibraryUsageDescription
SCHEMES_JSON="$(plutil -extract CFBundleURLTypes json -o - "$PLIST_PATH")"
printf '%s' "$SCHEMES_JSON" | grep -q '"example"'
MODES="$(plutil -extract UIBackgroundModes raw -o - "$PLIST_PATH" 2>/dev/null || true)"
if [[ "$MODES" == *"audio"* ]]; then
printf 'Unexpected background mode: audio
' >&2
exit 1
fi
En production, le script doit également afficher le nom de la clé, la valeur attendue et la valeur réelle, sans imprimer l’intégralité du plist. Les descriptions d’autorisation, les URL Schemes interrogés et les configurations tierces peuvent contenir des identifiants internes. Leur copie brute dans les journaux augmente inutilement le risque d’exposition.
Combiner règles positives et règles négatives
Les règles de type « doit être présent » détectent les éléments manquants, mais pas les réglages de débogage qui se retrouvent dans le produit final. Il est donc recommandé de maintenir également une liste d’interdictions, par exemple pour empêcher qu’un produit Release contienne un marqueur de serveur de test, un URL Scheme de débogage, l’activation du partage de fichiers ou un mode d’arrière-plan non approuvé.
Pour les champs de type tableau, une simple recherche de sous-chaîne ne doit pas constituer l’implémentation finale. Une méthode plus robuste consiste à produire du JSON avec plutil -extract ... json, puis à comparer chaque élément avec Ruby, Python ou un script déjà utilisé par le projet. Le script doit traiter explicitement trois cas : champ absent, type incorrect et valeurs dupliquées.
Contrôler l’App, les extensions et l’archive
Une archive peut contenir simultanément l’App principale, un Widget, une extension de service de notification et d’autres fichiers .appex. Vérifier uniquement l’App principale ne permet pas de détecter les erreurs d’identifiant, de version ou d’autorisations présentes dans les extensions. Une fois l’archive créée, vous pouvez parcourir tous les bundles :
find "$ARCHIVE_PATH/Products/Applications" \
\( -name "*.app" -o -name "*.appex" \) -print0 |
while IFS= read -r -d '' bundle; do
plist="$bundle/Info.plist"
plutil -lint "$plist"
printf 'Auditing %s
' "$bundle"
done
Il est recommandé de mettre en place deux barrières : un contrôle rapide après une compilation Release standard afin d’obtenir un retour au plus tôt, puis un second contrôle des produits contenus dans l’archive après l’exécution de archive, utilisé comme condition de publication. Cette seconde étape est indispensable, car l’archivage peut employer une configuration, des paramètres d’exportation ou des variables de pipeline différents.
| Élément contrôlé | Champs prioritaires | Traitement en cas d’échec |
|---|---|---|
| App principale | Identifiant, version, descriptions d’autorisation, URL Scheme | Bloquer l’archivage |
| App Extension | Préfixe d’identifiant, version, type d’extension | Bloquer l’archivage |
| Archive finale | Tous les bundles et les éléments interdits en production | Bloquer la publication |
Éviter les faux positifs et conserver des preuves
Les faux positifs les plus fréquents proviennent des chaînes vides, du type des valeurs booléennes et de l’ordre des tableaux. Pour les descriptions d’autorisation, il ne suffit pas de vérifier la présence de la clé : supprimez les espaces en début et en fin de chaîne, puis confirmez que la valeur n’est pas vide. Les booléens doivent être lus selon leur type plist ; la chaîne "false" ne doit pas être interprétée comme une valeur booléenne. Les URL Schemes et les modes d’arrière-plan sont généralement comparés comme des ensembles, sans dépendre de l’ordre des éléments.
En cas d’échec du script, conservez un rapport d’audit désensibilisé comprenant le scheme, la configuration, le SDK, le chemin de l’App, le nom de la clé en échec et un hachage. N’enregistrez pas l’environnement de compilation complet et n’ajoutez pas toutes les variables d’environnement aux journaux. Si plusieurs tâches s’exécutent sur le même Mac cloud, chacune doit utiliser son propre DerivedData. Avant l’audit, vérifiez également que la date de modification du produit correspond à la tâche en cours.
Enfin, traitez le script d’audit comme du code à part entière : soumettez les changements de règles à une revue avant fusion et prévoyez un cas de test pour chacun des scénarios suivants : clé manquante, type incorrect, valeur interdite et extension omise. Les problèmes liés à Info.plist deviendront ainsi des échecs explicites et reproductibles dans le pipeline, au lieu d’être découverts lors de l’archivage ou de la soumission puis diagnostiqués manuellement.
Questions fréquentes
Pourquoi l’audit du Info.plist source ne suffit-il pas ?
Xcode fusionne les valeurs générées, les réglages de build et les fichiers propres à chaque configuration. Le fichier intégré à l’App compilée est la référence réelle.
Quelles clés faut-il contrôler en premier dans le CI ?
Commencez par CFBundleIdentifier, les versions, le numéro de build, les textes d’autorisation, les URL schemes et UIBackgroundModes. Toute valeur absente ou interdite doit bloquer.
Faut-il lancer l’audit avant ou après archive ?
Lancez-le après un build ordinaire pour un retour rapide, puis répétez-le sur l’App contenue dans l’archive. La décision de publication repose sur ce second résultat.
Besoin d’une machine de build Apple Silicon dédiée ?
Louez un nœud physique dédié à la journée, à la semaine, au mois ou au trimestre pour vos builds Xcode, votre CI iOS, votre développement à distance et vos tâches d’automatisation.