Valider les bundles de ressources SwiftPM dans une CI sur Mac cloud

Valider les bundles de ressources SwiftPM dans une CI sur Mac cloud

Un Swift Package peut fonctionner correctement dans les aperçus locaux, puis afficher des icônes vides, des libellés par défaut ou des erreurs de lecture JSON une fois intégré à une tâche d’archivage sur un Mac cloud. Le plus problématique n’est généralement pas l’erreur de compilation, mais le fait que le pipeline reste au vert. SwiftPM prend en charge Bundle.module, mais il ne vérifie pas à la place de l’équipe que toutes les ressources sont déclarées, que les noms utilisés par le code correspondent, ni que les fichiers ont bien été intégrés au bundle final de l’application. La solution consiste à valider les ressources comme une partie intégrante des artefacts de build, plutôt que d’attendre qu’un testeur découvre leur absence en ouvrant un écran.

Identifier les trois emplacements où les ressources peuvent disparaître

Avant toute investigation, il faut déterminer à quel niveau se situe le problème. Premier cas : le fichier existe dans le répertoire source, mais la target de Package.swift ne déclare pas la ressource correspondante. Deuxième cas : la ressource a bien été intégrée à un .bundle, mais le code utilise le bundle de l’application principale. Troisième cas : le fichier est présent dans le bundle de l’application, mais la casse de son nom, son sous-répertoire ou son répertoire de localisation ne correspond pas à la requête effectuée à l’exécution.

Il est recommandé de conserver un point d’entrée de reproduction minimal qui charge une image, un fichier JSON et une chaîne localisée. Si les trois types échouent en même temps, vérifiez d’abord la sélection du bundle. Si un seul fichier échoue, contrôlez en priorité les règles de ressources et le chemin.

Une « compilation réussie » prouve uniquement que le compilateur a accepté les entrées actuelles. Elle ne garantit pas que les fichiers requis à l’exécution sont présents dans le livrable.

Dans Package.swift, choisissez explicitement entre .process et .copy. Les images, les répertoires de ressources et les contenus localisés utilisent généralement .process. Réservez .copy aux fichiers dont l’arborescence d’origine ou les octets doivent impérativement être conservés. Ne copiez pas en une seule fois tout un répertoire du dépôt dans le bundle de ressources : vous risqueriez d’y inclure des exemples de test, des sorties temporaires, voire des configurations qui ne doivent pas être livrées.

Figer le contrat des ressources dans un manifeste

La validation des ressources ne doit pas se limiter à comparer le nombre de fichiers. L’ajout d’une ressource, la suppression d’une ancienne ou un renommage peuvent laisser ce nombre totalement inchangé. Une méthode plus fiable consiste à maintenir dans le dépôt un fichier ci/expected-resources.txt, avec sur chaque ligne le chemin relatif à la racine du bundle de ressources :

Assets.car
Defaults/config.json
en.lproj/Localizable.strings
zh-Hans.lproj/Localizable.strings

Ce manifeste ne doit contenir que les fichiers indispensables à l’exécution. Les contenus téléchargés à la demande, les fixtures de test et les ressources d’aperçu utilisées en développement ne doivent pas y être mélangés. Lorsqu’une ressource est renommée, la modification du manifeste doit figurer dans la même revue de code. Les personnes chargées de la revue peuvent ainsi voir précisément comment évolue le contrat de livraison.

Le répertoire de ressources lui-même doit également faire l’objet d’un contrôle inverse. Un fichier présent dans les sources mais non déclaré doit produire un avertissement. À l’inverse, un fichier exigé par le manifeste mais absent du résultat de build doit provoquer un échec immédiat. Le premier contrôle aide à nettoyer le dépôt ; le second protège l’intégrité du livrable.

Inspecter directement le bundle de l’application après le build

Chaque tâche doit utiliser son propre DerivedData afin qu’un ancien bundle laissé par une autre tâche exécutée en parallèle ne produise pas un faux succès. Le script suivant illustre un build pour simulateur. Dans le pipeline réel, il suffit de remplacer le scheme et d’adapter la règle de correspondance du nom du bundle au véritable nom de module du projet.

set -euo pipefail

DERIVED="${RUNNER_TEMP:-/tmp}/resource-check-${BUILD_ID:-local}"
rm -rf "$DERIVED"

xcodebuild \
  -scheme ExampleApp \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$DERIVED" \
  build

APP=$(find "$DERIVED/Build/Products" -type d -name 'ExampleApp.app' -print -quit)
test -n "$APP"

COUNT=$(find "$APP" -type d -name '*FeatureKit*.bundle' | wc -l | tr -d ' ')
test "$COUNT" = "1"

BUNDLE=$(find "$APP" -type d -name '*FeatureKit*.bundle' -print -quit)

while IFS= read -r item; do
  test -z "$item" && continue
  test -e "$BUNDLE/$item" || {
    printf 'missing resource: %s\n' "$item" >&2
    exit 1
  }
done < ci/expected-resources.txt

Le script vérifie volontairement en premier lieu qu’il n’existe qu’un seul bundle. Prendre directement le premier résultat correspondant permettrait à un ancien artefact, à un bundle de test ou à un module portant le même nom de masquer le problème. Le contrôle doit également porter sur le bundle situé dans le .app, et non sur un répertoire intermédiaire de DerivedData, car seul le premier représente le résultat final réellement intégré à l’application.

Transformer les erreurs fréquentes en contrôles bloquants

Chaque type de ressource exige une méthode de validation adaptée. Il ne suffit pas d’appliquer sommairement test -e à tous les fichiers.

Type de ressource Contrôle minimal Échec fréquent
JSON Le fichier existe et peut être analysé Un fichier vide a été copié ou son format est incorrect
Chaînes localisées Le répertoire de la langue cible existe et les clés sont lisibles Le répertoire de langue est mal nommé ou les libellés par défaut sont utilisés
Images et couleurs L’artefact de compilation des ressources existe et peut être chargé par un écran de smoke test La casse du nom ne correspond pas ou le bundle principal est utilisé par erreur
Fichiers de modèle Le hash ou les champs essentiels sont vérifiés L’étape de génération a écrasé la version du dépôt

Les volumes de travail couramment utilisés sous macOS ne sont pas sensibles à la casse. Une différence entre IconDark et icondark peut donc passer inaperçue sur une machine de développement. Ne comptez pas sur la tolérance fortuite du système de fichiers : extrayez les chemins du manifeste et comparez-les caractère par caractère aux chemins réels, en respectant la casse. Exécutez également un parseur sur les fichiers JSON, utilisez plutil -lint pour les listes de propriétés et vérifiez au minimum les langues obligatoires ainsi que les clés essentielles dans les répertoires de chaînes.

Si le Package utilise un plug-in de génération, assurez-vous que l’étape de génération s’exécute avant la validation des ressources et limitez son répertoire de sortie à un chemin temporaire propre à la tâche. Lorsque plusieurs tâches partagent le même répertoire de génération, l’une d’elles peut lire des fichiers provenant d’une autre branche et produire un faux succès particulièrement difficile à reproduire.

Conserver les preuves et garantir la reproductibilité des tâches

En cas d’échec, ne téléversez pas l’intégralité de DerivedData. Ce répertoire est volumineux, contient beaucoup de bruit et peut inclure des informations de chemin qui ne doivent pas être conservées à long terme. Un paquet de preuves plus utile comprend le manifeste des ressources, l’arborescence réelle des fichiers du bundle, la fin du journal de build, les erreurs des parseurs et l’identifiant du commit actuel. L’arborescence peut être générée ainsi :

find "$BUNDLE" -print \
  | sed "s#^$BUNDLE/##" \
  | LC_ALL=C sort \
  > resource-bundle-tree.txt

Avant d’archiver les journaux et l’arborescence, supprimez les jetons d’accès, les répertoires utilisateur et les identifiants temporaires. Une fois la tâche terminée, effacez le DerivedData dédié ainsi que les répertoires temporaires de génération. L’exécution suivante doit impérativement repartir d’un répertoire vide. Les exécuteurs parallèles sur DPLYMAC doivent respecter le même principe : les tâches peuvent réutiliser les caches de téléchargement, mais jamais des sorties de build non identifiées.

Le contrôle final doit répondre à trois questions : qu’exige la déclaration, que contient réellement le bundle final de l’application et les ressources peuvent-elles être lues à l’exécution sous leur véritable nom ? Dès lors que chacune de ces trois couches produit un résultat vérifiable, l’absence de ressources SwiftPM cesse d’être une défaillance d’interface intermittente et devient une erreur de build ordinaire, détectable dès l’étape de commit.

Questions fréquentes

Pourquoi une ressource peut-elle manquer alors que le paquet SwiftPM compile correctement ?

La compilation ne garantit ni la casse du nom utilisé à l’exécution ni l’emplacement final dans l’application. Il faut inspecter directement le fichier .app et son bundle de ressources.

Faut-il vérifier les sources ou uniquement le résultat de la compilation ?

Les deux sont utiles. Les sources révèlent les fichiers non déclarés, tandis que le résultat compilé confirme que les ressources ont bien été traitées et intégrées au livrable.

Mac dans le cloud

Déployez des workflows validés sur des nœuds physiques dédiés

Choisissez M4 ou M4 Pro, le nœud cible et la durée de facturation selon votre tâche, puis vérifiez la configuration et les options supplémentaires avant de commander.

Choisir une configuration et commander