Article technique

Valider l’intégration des iOS App Extensions sur un Mac cloud

Valider l’intégration des iOS App Extensions sur un Mac cloud

Un projet iOS comprenant un service de notifications, une extension de partage ou un Widget peut rencontrer le problème suivant : l’application principale se compile et s’archive correctement, mais l’extension n’est déclarée invalide qu’au moment de l’exportation, de l’installation ou des contrôles de publication. La cause ne se trouve généralement pas dans le code source, mais dans une divergence entre le .app et le .appex au niveau des versions, de la cible de déploiement, de l’emplacement d’intégration ou de la signature. En ajoutant ces contrôles après l’étape d’archivage sur le Mac cloud, le CI peut rendre un verdict clair avant que l’artefact ne quitte le nœud de build.

Définir d’abord les critères d’acceptation de l’archive

Une App Extension n’est pas un livrable autonome. Elle doit se trouver dans le répertoire PlugIns de l’application principale et former avec celle-ci une relation d’intégration vérifiable. Le CI doit au minimum contrôler les éléments suivants :

Élément contrôlé Application principale App Extension Règle recommandée
CFBundleVersion Obligatoire Obligatoire Valeurs strictement identiques
CFBundleShortVersionString Obligatoire Obligatoire Valeurs strictement identiques
MinimumOSVersion Obligatoire Obligatoire Valeurs identiques si l’équipe utilise une cible commune
NSExtension Non requis Obligatoire Le dictionnaire doit exister et être lisible
Signature du code Valide Valide La vérification stricte doit réussir
Emplacement d’intégration Applications PlugIns/*.appex Aucune copie isolée n’est acceptée

La version minimale du système ne doit pas nécessairement être identique dans tous les projets. Toutefois, si l’équipe ne maintient pas volontairement des cibles différentes, imposer la même valeur permet de détecter plus facilement les dérives de configuration. Lorsqu’une différence est justifiée, la plage autorisée doit être définie dans le dépôt plutôt qu’ignorée silencieusement par le script.

Une « compilation réussie » signifie seulement que chaque Target peut produire son artefact ; une « archive valide » doit également prouver que ces artefacts sont assemblés selon la bonne relation pour former une application livrable.

Générer un unique xcarchive à valider

Le contrôle doit lire l’archive, et non le répertoire intermédiaire de DerivedData. Celui-ci peut contenir un .appex issu d’une compilation précédente et ne représente pas nécessairement la structure d’intégration finale. Supprimez d’abord le chemin d’archive fixe, puis lancez un archivage Release :

set -euo pipefail

ARCHIVE_PATH="$PWD/build/App.xcarchive"
rm -rf "$ARCHIVE_PATH"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$ARCHIVE_PATH" \
  clean archive

Ne partagez pas le même répertoire d’archive entre plusieurs branches. Les tâches parallèles peuvent créer des chemins distincts à partir du hash du commit ou de l’identifiant de la tâche CI, mais un seul .xcarchive précisément déterminé doit être transmis au script suivant.

Vérifier d’abord le résultat de l’intégration

Une fois l’archivage terminé, l’application principale se trouve généralement dans Products/Applications, tandis que les extensions résident dans le répertoire PlugIns de son bundle. Commencez par utiliser find pour inspecter le résultat réel :

find build/App.xcarchive/Products/Applications \
  \( -name '*.app' -o -name '*.appex' \) \
  -print

Si le projet déclare un Target d’extension mais qu’aucun .appex n’est trouvé, vérifiez en priorité la phase de build Embed App Extensions du Target de l’application principale, ainsi que l’inclusion de l’extension dans le Scheme actif. Ne copiez pas une extension depuis un autre répertoire pour « compléter » l’archive.

Contrôler les métadonnées et les signatures avec un script

Le script Bash ci-dessous utilise uniquement les outils fournis avec macOS. Il localise l’application principale, lit successivement le fichier Info.plist de chaque extension, compare les versions et la version minimale du système, puis vérifie séparément les signatures :

#!/bin/bash
set -euo pipefail

ARCHIVE="${1:?usage: validate-extensions.sh App.xcarchive}"
APP=$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)

if [ -z "$APP" ]; then
  printf '%s
' "main app not found"
  exit 1
fi

read_plist() {
  /usr/libexec/PlistBuddy -c "Print :$2" "$1/Info.plist"
}

host_build=$(read_plist "$APP" CFBundleVersion)
host_version=$(read_plist "$APP" CFBundleShortVersionString)
host_minimum=$(read_plist "$APP" MinimumOSVersion)
count=0
failed=0

while IFS= read -r extension; do
  count=$((count + 1))
  ext_build=$(read_plist "$extension" CFBundleVersion)
  ext_version=$(read_plist "$extension" CFBundleShortVersionString)
  ext_minimum=$(read_plist "$extension" MinimumOSVersion)

  [ "$ext_build" = "$host_build" ] || failed=1
  [ "$ext_version" = "$host_version" ] || failed=1
  [ "$ext_minimum" = "$host_minimum" ] || failed=1

  /usr/libexec/PlistBuddy \
    -c "Print :NSExtension" \
    "$extension/Info.plist" >/dev/null

  codesign --verify --strict --verbose=2 "$extension"
done < <(find "$APP/PlugIns" -maxdepth 1 -name '*.appex' -print)

[ "$count" -gt 0 ] || {
  printf '%s
' "no app extensions found"
  exit 1
}

codesign --verify --deep --strict --verbose=2 "$APP"
[ "$failed" -eq 0 ] || {
  printf '%s
' "extension metadata mismatch"
  exit 1
}

Enregistrez le script sous ci/validate-extensions.sh, exécutez chmod +x, puis appelez-le après l’étape d’archivage. Si le dépôt produit à la fois un Scheme « avec extensions » et un Scheme « sans extension », le nombre d’extensions attendu doit être passé en paramètre ; l’absence d’extension ne doit pas être systématiquement considérée comme un succès.

Centraliser les versions dans les réglages de build

La source de divergence la plus fréquente est la maintenance manuelle de plusieurs fichiers Info.plist. Lorsque le Build Number de l’application principale est modifié mais que l’extension conserve l’ancienne valeur, la compilation du code source n’échoue pas pour autant. Une approche plus fiable consiste à utiliser le même ensemble de variables de build pour tous les Target :

MARKETING_VERSION = 4.8.0
CURRENT_PROJECT_VERSION = 8204
IPHONEOS_DEPLOYMENT_TARGET = 17.0

Les valeurs correspondantes dans les fichiers plist doivent utiliser $(MARKETING_VERSION), $(CURRENT_PROJECT_VERSION) et $(IPHONEOS_DEPLOYMENT_TARGET). Si la configuration est gérée par des fichiers .xcconfig, l’application principale et les extensions doivent inclure un même fichier de base. Seul un nombre limité de fichiers propres à chaque Target doit remplacer les clés qui doivent réellement différer.

Ne pas masquer le problème par une nouvelle signature après l’archivage

codesign --deep convient à la vérification de l’ensemble du bundle, mais ne doit pas servir de commande de correction générique. Une nouvelle signature récursive après l’archivage peut modifier la relation d’intégration d’origine et dissocier l’artefact CI de la configuration du projet. En cas d’échec de signature, corrigez les réglages de signature du Target, les Build Phases et la configuration d’exportation, puis générez à nouveau l’archive complète.

Conserver les preuves dans un ordre précis en cas d’échec

Lorsqu’un contrôle échoue, ne nettoyez pas immédiatement le répertoire de travail. Commencez par conserver les informations suivantes :

  1. les chemins relatifs de tous les .app et .appex présents dans le .xcarchive ;
  2. la version, la version minimale du système et le Bundle Identifier de chaque bundle ;
  3. la sortie complète de codesign --verify --strict --verbose=2 ;
  4. le Scheme et la Configuration utilisés, ainsi que les réglages de build effectivement développés ;
  5. l’identifiant du commit et la version de Xcode utilisés par la tâche d’archivage.

La commande suivante permet d’exporter les réglages de build afin de comparer les variables réellement reçues par l’application principale et les extensions :

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings > build/build-settings.txt

Le contrôle final doit répondre à deux exigences : le message d’échec doit indiquer directement quel .appex et quelle clé présentent une divergence ; la correction doit être apportée à la configuration du projet, et non après la génération de l’artefact. Ainsi, quels que soient les changements de nœuds Mac dans le cloud, les critères d’acceptation restent associés au dépôt et le résultat de l’archivage peut être vérifié de manière reproductible.

Questions fréquentes

Pourquoi une App Extension peut-elle échouer après un archivage réussi ?

L’archivage confirme surtout que les cibles se compilent. L’export et l’installation valident aussi versions, iOS minimal, métadonnées, intégration et signatures.

L’application et ses extensions doivent-elles partager le même numéro de build ?

CFBundleVersion doit être identique. Il est également préférable de générer CFBundleShortVersionString depuis les mêmes réglages centralisés.

Faut-il signer de nouveau une extension après l’archivage ?

Non. Corrigez les réglages de build, la phase d’intégration ou la configuration de signature, puis recréez une archive depuis un espace de travail propre.

Nœud physique dédié

Déployez vos tâches de développement et de build sur un Mac dédié dans le cloud

Choisissez le modèle, la région du nœud et la durée de location selon vos besoins. Chaque instance correspond à une machine physique dédiée, dans un environnement non virtualisé.

Choisir une offre Mac dans le cloud