Analyser fiablement les sorties macOS et Xcode dans un CI cloud

DevOps et CI/CD ·~6 min de lecture

Analyser fiablement les sorties macOS et Xcode dans un CI cloud

Un même script CI peut fonctionner correctement dans un terminal interactif, puis sélectionner le mauvais Scheme sur un Mac cloud exécuté sans surveillance. Le problème vient généralement moins de Xcode que du fait que le script traite un texte destiné aux humains comme une interface stable. La langue du système, la version des outils, l’emplacement des avertissements ou même une espace supplémentaire peuvent suffire à casser grep, cut ou une sélection fondée sur des numéros de ligne fixes. Une approche plus fiable consiste à privilégier JSON ou plist, puis à conserver séparément le code de sortie, la sortie brute et le résultat analysé.

Repérer les analyses de texte fragiles

Commencez par rechercher les combinaisons à risque dans le pipeline :

grep -RInE 'xcodebuild|simctl|system_profiler|sw_vers' ci scripts \
  | grep -E 'grep|awk|sed|cut|tail|head'

Tous les pipelines ne sont pas nécessairement incorrects. Le risque apparaît lorsque le script dépend du langage naturel, par exemple en recherchant « Build Settings », en récupérant le deuxième champ après un deux-points ou en supposant que l’appareil cible figure toujours à la troisième ligne. LC_ALL=C peut limiter les différences linguistiques, mais ne garantit ni l’ordre des champs, ni le contenu des avertissements, ni le format des versions futures.

Si la sortie d’une commande est principalement destinée à être lue par des humains, ne partez pas du principe qu’elle constitue également une interface machine stable.

Pendant l’audit, documentez trois points pour chaque lecture : le champ requis, l’existence éventuelle d’un format structuré fourni par la commande et le comportement attendu si le champ est absent — échec ou valeur par défaut explicite. Ne laissez pas une chaîne vide se propager jusqu’aux étapes d’archivage suivantes.

Transformer les réglages de build en contrat JSON

xcodebuild -showBuildSettings -json permet d’obtenir directement les réglages de build de la cible. Vérifiez d’abord le code de sortie de la commande, puis analysez le fichier sans mélanger la sortie d’erreur standard au JSON.

set -euo pipefail

mkdir -p artifacts/environment

if ! xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings \
  -json \
  > artifacts/environment/build-settings.json \
  2> artifacts/environment/build-settings.stderr; then
  cat artifacts/environment/build-settings.stderr >&2
  exit 1
fi

python3 - <<'PY'
import json
from pathlib import Path

path = Path("artifacts/environment/build-settings.json")
if not path.exists() or path.stat().st_size == 0:
    raise SystemExit("build settings output is empty")

items = json.loads(path.read_text(encoding="utf-8"))
if len(items) != 1:
    raise SystemExit(f"expected one target, got {len(items)}")

settings = items[0].get("buildSettings", {})
required = ["PRODUCT_BUNDLE_IDENTIFIER", "SDKROOT", "CONFIGURATION_BUILD_DIR"]
missing = [key for key in required if not settings.get(key)]
if missing:
    raise SystemExit("missing build settings: " + ", ".join(missing))

print(settings["PRODUCT_BUNDLE_IDENTIFIER"])
PY

Si le workspace contient plusieurs cibles, n’imposez pas une longueur de tableau égale à un. Sélectionnez plutôt la cible à l’aide du champ target, puis interrompez l’exécution en cas d’absence de correspondance ou de correspondances multiples. Ainsi, l’ajout d’une cible d’extension ne conduira pas le script à lire silencieusement la mauvaise configuration.

Fiabiliser la sélection des appareils avec le JSON de simctl

La liste des simulateurs se prête particulièrement mal à une analyse ligne par ligne. Les noms d’appareils peuvent contenir des espaces, et des appareils portant le même nom peuvent appartenir à des runtimes différents. Enregistrez d’abord le résultat complet, puis filtrez-le par runtime, nom et état de disponibilité :

xcrun simctl list -j devices available \
  > artifacts/environment/simulators.json

python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(
    Path("artifacts/environment/simulators.json").read_text(encoding="utf-8")
)
matches = []
for runtime, devices in data.get("devices", {}).items():
    if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"):
        continue
    for device in devices:
        if device.get("isAvailable") and device.get("name") == "iPhone 16":
            matches.append((runtime, device["udid"]))

if len(matches) != 1:
    raise SystemExit(f"expected one matching simulator, got {len(matches)}")

print(matches[0][1])
PY

Dans un script de production, la version de runtime attendue doit également être définie dans la configuration du dépôt, plutôt que de sélectionner automatiquement « la plus récente ». Choisir automatiquement la dernière version permettrait à une simple mise à jour des composants système de modifier directement la référence des tests. Lors de l’exécution finale des tests, transmettre l’UDID est moins ambigu que de fournir uniquement le nom de l’appareil.

Ne pas ignorer un ensemble vide d’appareils

L’absence d’appareil correspondant doit provoquer un échec explicite. Avant de créer temporairement un simulateur, vérifiez que le runtime requis existe réellement. À la fin de la tâche, supprimez uniquement les appareils créés par cette tâche, sans nettoyer l’ensemble partagé. Pour les tâches parallèles, il est préférable de définir des valeurs -derivedDataPath distinctes ainsi que des ensembles d’appareils CoreSimulator indépendants.

Lire les caractéristiques de l’hôte depuis un plist

Les informations de version de macOS peuvent être lues directement depuis le plist système, sans analyser les libellés textuels de sw_vers :

SYSTEM_PLIST=/System/Library/CoreServices/SystemVersion.plist

product_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductVersion' "$SYSTEM_PLIST"
)
build_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductBuildVersion' "$SYSTEM_PLIST"
)

printf '%s\n' "$product_version" > artifacts/environment/macos-version.txt
printf '%s\n' "$build_version" > artifacts/environment/macos-build.txt
xcodebuild -version > artifacts/environment/xcode-version.txt

Pour lire le plist d’un projet, utilisez de préférence plutil -extract KEY raw -o - FILE. Si un champ peut être absent, vérifiez d’abord son existence avec plutil -type KEY FILE, puis laissez le script décider si cette absence est acceptable. N’utilisez pas d’expression régulière pour traiter du XML : un plist peut être au format binaire et ses chaînes peuvent aussi contenir des caractères échappés.

Définir une frontière de collecte facile à diagnostiquer

Un format structuré ne garantit pas que la commande réussira. Une étape de collecte fiable doit conserver au moins quatre catégories de preuves : le code de sortie de la commande, la sortie d’erreur standard, le fichier brut non modifié et le résultat analysé sous une forme réduite. Le fichier brut sert à reconstituer le comportement de l’outil, tandis que le résultat réduit est consommé par les tâches suivantes.

Il est recommandé de limiter chaque collecteur à un seul objet et d’écrire sa sortie dans un fichier temporaire. Une fois la syntaxe et tous les champs obligatoires validés, remplacez atomiquement le fichier définitif avec mv. Une commande interrompue ne laissera ainsi aucun fichier JSON incomplet.

Vérifications avant validation

Avant de fusionner des modifications du pipeline, vérifiez au minimum les points suivants :

  • Le script n’extrait aucun champ à partir d’un numéro de ligne fixe ou d’un libellé en langage naturel.
  • La sortie standard et la sortie d’erreur standard sont enregistrées dans des fichiers distincts.
  • La syntaxe des fichiers JSON et plist est vérifiée avant leur lecture.
  • Le filtrage des tableaux traite explicitement l’absence de correspondance comme les correspondances multiples.
  • La version de Xcode, le numéro de build de macOS et l’UDID de l’appareil sélectionné peuvent être archivés avec la tâche.
  • Les journaux consignent uniquement les caractéristiques de l’environnement, sans exposer de jetons, d’identifiants d’accès ni l’ensemble des variables d’environnement.
  • Après une mise à jour des outils, la validation de la collecte est exécutée avant de réactiver les tâches de build de production.

L’objectif final n’est pas de convertir toutes les commandes en JSON, mais d’établir une frontière claire : les machines consomment des données structurées, tandis que les humains lisent les journaux de diagnostic. Lorsqu’aucune interface structurée n’existe, il faut tout de même figer la version de la commande, isoler l’environnement linguistique, vérifier le nombre de colonnes et traiter tout échec d’analyse comme un véritable échec, plutôt que de continuer avec une valeur vide qui semble plausible.

Questions fréquentes

Définir LC_ALL=C suffit-il ?

Non. Cela limite certaines variations linguistiques, mais ne stabilise ni l’ordre des champs, ni les espaces, ni les avertissements ou les changements entre versions. Préférez JSON ou plist.

Que faut-il conserver lorsqu’une commande structurée échoue ?

Conservez le code de sortie, stderr, le fichier JSON ou plist brut, la version de Xcode et le numéro de build de macOS. Vérifiez aussi que le fichier est non vide et valide.

Mac physique dédié

Confiez votre prochaine tâche de développement à un Mac dans le cloud

Choisissez Berry M4 ou Berry M4 Pro, vérifiez les cinq nœuds, la durée de location et les options supplémentaires, puis passez commande.

Choisir une configuration et commander