Pratiques d’ingénierie

Bloquer les ruptures d’API Swift avant la fusion

Bloquer les ruptures d’API Swift avant la fusion

Un framework Swift utilisé par plusieurs apps peut empêcher les projets en aval de compiler après une mise à niveau dès lors qu’une méthode publique est supprimée, qu’un type de paramètre est restreint ou qu’une exigence obligatoire est ajoutée à un protocole public. Les tests unitaires couvrent généralement le comportement de l’implémentation, mais ils ne signalent pas explicitement qu’une modification rompt l’interface publique. Sur les nœuds de build Mac dans le cloud de OnceMac, Swift API Digester peut être intégré aux contrôles avant fusion : enregistrez d’abord un instantané de l’API publiée, générez ensuite celui de la version candidate avec exactement la même chaîne d’outils, puis comparez les deux.

Figer d’abord les conditions de comparaison

Un instantané d’API ne dépend pas uniquement du code source. La version de Xcode, le compilateur Swift, le SDK, l’architecture cible, la configuration de build et les indicateurs de compilation conditionnelle influencent tous le résultat. Avant de lancer la comparaison, commencez par consigner ces paramètres dans les journaux et par les rendre reproductibles.

set -euo pipefail

xcodebuild -version
xcrun swift --version
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
echo "SDK_PATH=$SDK_PATH"

La CI doit sélectionner explicitement Xcode et utiliser la même image ou le même modèle de nœud pour la référence et la branche candidate. Le triplet cible doit également rester identique, par exemple arm64-apple-ios17.0-simulator dans les deux cas. Ne générez pas l’interface pour un appareil physique d’un côté et pour le simulateur de l’autre, au risque d’interpréter les différences de conditions entre plateformes comme des modifications du code.

La référence d’API constitue un contrat de publication, pas un cache de compilation. Elle doit être placée sous contrôle de version, faire l’objet d’une revue et ne jamais être remplacée automatiquement par une tâche de build ordinaire.

Construire un framework analysable

L’exemple suivant suppose que le scheme et le module s’appellent tous deux MyKit. Commencez par nettoyer un répertoire Derived Data dédié, puis lancez un build avec la configuration Release. BUILD_LIBRARY_FOR_DISTRIBUTION=YES génère les fichiers d’interface nécessaires à la stabilité des modules et rapproche les conditions de contrôle de celles d’une distribution binaire.

DERIVED_DATA="$PWD/.build/api-dd"
PRODUCTS="$DERIVED_DATA/Build/Products/Release-iphonesimulator"

rm -rf "$DERIVED_DATA"

xcodebuild build \
  -scheme MyKit \
  -configuration Release \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_DATA" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO \
  CODE_SIGNING_ALLOWED=NO

Après le build, vérifiez que le module existe bien afin d’éviter que Digester ne produise une erreur « module not found » difficile à interpréter à cause d’un chemin de recherche incorrect.

test -d "$PRODUCTS/MyKit.framework"
find "$PRODUCTS/MyKit.framework/Modules" -maxdepth 3 -type f -print

Si le projet gère ses dépendances au moyen d’un workspace, ajoutez -workspace. Si le scheme n’est pas partagé, la CI ne pourra pas le détecter : partagez-le d’abord dans les réglages du projet au lieu de demander au script de deviner son chemin.

Générer et comparer les instantanés d’API

Effectuez un build séparé pour la version de référence actuellement publiée et pour le code candidat, puis exportez les résultats au format JSON. Il est recommandé de placer le fichier de référence dans le répertoire api-baselines/ du dépôt et d’indiquer la plateforme dans son nom, sans y inclure de chemin propre à une machine ni de numéro de build.

mkdir -p api-baselines .build/api-report

xcrun swift-api-digester \
  -dump-sdk \
  -module MyKit \
  -sdk "$SDK_PATH" \
  -target arm64-apple-ios17.0-simulator \
  -F "$PRODUCTS" \
  -o ".build/api-report/current-ios-simulator.json"

xcrun swift-api-digester \
  -diagnose-sdk \
  -input-paths "api-baselines/MyKit-ios-simulator.json" \
  -input-paths ".build/api-report/current-ios-simulator.json" \
  > ".build/api-report/diagnostics.txt"

Lors de la première intégration, copiez le fichier current-ios-simulator.json validé afin d’en faire la référence, puis validez-le dans le dépôt. Par la suite, la CI doit uniquement lire ce fichier. Si la commande renvoie un code différent de zéro, conservez le rapport de diagnostic comme artefact du build au lieu d’afficher uniquement un message du type « échec du contrôle de compatibilité ».

Changements à examiner en priorité

Changement Évaluation par défaut Traitement
Suppression d’un type ou d’une méthode publique Rupture de compatibilité Restaurer l’interface ou planifier explicitement une nouvelle version majeure
Modification d’un paramètre, d’une valeur de retour ou d’une contrainte générique Rupture de compatibilité Fournir une surcharge compatible et prévoir une période de dépréciation
Ajout d’une exigence obligatoire à un protocole public Risque élevé Envisager de fournir une implémentation par défaut
Ajout d’une méthode ou d’un type public Généralement compatible Vérifier le nommage, la visibilité et les attributs de plateforme
Modification limitée à l’implémentation interne Ne devrait pas apparaître Vérifier que le niveau d’accès n’a pas été élargi par inadvertance

Le mot-clé public ne correspond pas toujours à un engagement intentionnel. Si une déclaration ne doit pas être appelée depuis l’extérieur, réduisez en priorité son niveau d’accès au lieu de maintenir durablement des règles d’exclusion.

Transformer les diagnostics en contrôle CI maintenable

Il est recommandé de diviser le processus en quatre étapes : « build », « génération de l’instantané », « comparaison » et « envoi du rapport ». Activez set -euo pipefail dans le script et placez Derived Data dans un répertoire propre à chaque tâche afin que les jobs parallèles ne lisent pas leurs artefacts respectifs. Pour un dépôt comprenant plusieurs modules, maintenez une liste de modules à traiter successivement au lieu de réutiliser le chemin PRODUCTS du module précédent.

En cas d’échec du contrôle, les personnes chargées de la revue doivent disposer de trois éléments : les versions de Xcode et de Swift utilisées, les instantanés de référence et candidat, ainsi que le rapport de diagnostic complet. La référence ne doit être mise à jour dans la même demande de fusion qu’après confirmation de la conformité du changement avec la stratégie de versionnement. Si une tâche en échec est autorisée à réécrire elle-même la référence, le contrôle réussira toujours et perdra toute utilité.

Ordre de traitement des faux positifs

Vérifiez d’abord la chaîne d’outils, puis le SDK et le triplet cible. Comparez ensuite les paramètres de build et les indicateurs de compilation conditionnelle. Ce n’est qu’en dernier recours qu’il faut conclure à du bruit dans la sortie de Digester. La liste suivante permet d’accélérer le diagnostic :

  1. la sortie de xcodebuild -version est-elle strictement identique ?
  2. le scheme, la configuration et la destination sont-ils identiques ?
  3. BUILD_LIBRARY_FOR_DISTRIBUTION est-il activé des deux côtés ?
  4. le chemin de recherche du module pointe-t-il vers les artefacts de la tâche en cours ?
  5. la référence provient-elle de la dernière interface publiée, et non d’un commit historique quelconque ?
  6. les fichiers générés contiennent-ils des chemins absolus variables, tels que le répertoire de travail ?

Finaliser avec une stratégie de versionnement

Le contrôle de compatibilité détecte les changements, mais ne peut pas choisir le numéro de version à la place de l’équipe. La suppression d’une interface, la modification d’un type public ou l’ajout d’une exigence à un protocole doivent généralement être intégrés à une version incompatible planifiée. Les nouvelles interfaces doivent néanmoins faire l’objet d’une revue de leur nommage et de leur disponibilité. Quant aux interfaces dépréciées, elles doivent être conservées pendant la période convenue avant d’être supprimées dans une version ultérieure.

Le processus le plus fiable consiste à conserver la référence sur la branche de publication, à ne générer que l’instantané candidat dans les demandes de fusion, à laisser la CI produire les diagnostics automatisés, puis à confier la décision aux responsables de maintenance en fonction de la stratégie de versionnement. Cette approche évite à la fois d’interdire indistinctement toute évolution de l’API et de propager une modification public involontaire à tous les projets en aval.

Questions fréquentes

Swift API Digester remplace-t-il les tests unitaires ?

Non. Il détecte les changements structurels de l’API Swift publique, mais ne valide ni le comportement à l’exécution, ni la logique métier, ni les interfaces Objective-C.

Quand faut-il mettre à jour la référence API ?

Uniquement après validation du changement selon la politique de versionnage. Le fichier de référence doit être relu avec le code et ne doit jamais être écrasé automatiquement par la CI courante.

Pourquoi le même code peut-il produire des résultats différents ?

Une version différente de Xcode, du compilateur Swift, du SDK, de l’architecture cible ou de la configuration suffit à modifier le résultat. Les deux instantanés exigent la même chaîne d’outils.

Mac mini physique dédié

Lancez votre prochain build sur un nœud physique dédié.

Choisissez la puce, la mémoire, le stockage, le nœud et la durée de location pour profiter d’un Mac dans le cloud avec des ressources qui ne sont pas partagées avec d’autres clients.

Choisir une configuration et louer