Journal d’ingénierie HireVM

Diagnostiquer la sandbox des plugins SwiftPM sur un Mac dans le cloud

Diagnostiquer la sandbox des plugins SwiftPM sur un Mac dans le cloud

Un même package Swift peut générer correctement son code sur le poste du développeur, puis échouer avec Operation not permitted dans un espace de travail propre sur un Mac dans le cloud. Il arrive aussi que la compilation réussisse tout en utilisant encore d’anciens fichiers. Le problème vient généralement non pas des performances de la machine, mais de fichiers non déclarés dont dépend le plugin d’outil de build, d’un répertoire de sortie incorrect ou d’anciens artefacts locaux qui masquent l’erreur. Plutôt que de vider les caches à répétition, il faut reconstituer méthodiquement les entrées, les sorties et les limites d’exécution du plugin.

Identifier d’abord la couche où survient l’échec

Un plugin d’outil de build SwiftPM fait intervenir trois couches : SwiftPM planifie les commandes, l’exécutable du plugin génère le contenu, puis Xcode consomme les fichiers produits. Commencez par conserver le journal complet depuis la racine du dépôt :

set -o pipefail
rm -rf .ci
mkdir -p .ci
xcodebuild \
  -scheme App \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.ci/DerivedData" \
  -clonedSourcePackagesDirPath "$PWD/.ci/SourcePackages" \
  build 2>&1 | tee .ci/build.log

grep -E 'sandbox|deny|plugin|Operation not permitted|No such file' .ci/build.log

Si le journal indique que l’outil du plugin est introuvable, vérifiez qu’il est déclaré comme outil exécutable d’une target du package. Si l’échec survient après le démarrage de l’outil, examinez ensuite ses chemins de lecture et d’écriture. Si la compilation réussit mais que le contenu généré n’est pas actualisé, contrôlez en priorité les déclarations d’entrées et de sorties.

Ne considérez pas la désactivation de la sandbox comme une première solution. Un refus de la sandbox révèle souvent avec précision que le générateur lit ou modifie des éléments extérieurs au graphe de build. Contourner cette restriction ne ferait que repousser le problème jusqu’au prochain build propre.

Déclarer les entrées et les sorties dans le graphe de build

Un plugin robuste ne lit que des entrées explicites et écrit ses résultats dans pluginWorkDirectory. La structure suivante permet à SwiftPM de suivre à la fois le schéma et le fichier Swift généré :

import PackagePlugin

@main
struct CodegenPlugin: BuildToolPlugin {
    func createBuildCommands(
        context: PluginContext,
        target: Target
    ) async throws -> [Command] {
        let tool = try context.tool(named: "schema-gen")
        let input = target.directory.appending("Schemas/api.json")
        let output = context.pluginWorkDirectory
            .appending("Generated/API.swift")

        return [
            .buildCommand(
                displayName: "Generate API.swift",
                executable: tool.path,
                arguments: [input.string, output.string],
                inputFiles: [input],
                outputFiles: [output]
            )
        ]
    }
}

Après son lancement, le générateur doit créer lui-même le répertoire parent Generated et écrire uniquement dans le chemin de sortie reçu. Il ne doit pas écrire par défaut dans le dossier Sources du dépôt, sur le bureau, dans le répertoire personnel ou dans un chemin fixe tel que /Users/.... Un plugin d’outil de build sert à produire des fichiers dérivés utilisés pendant la compilation en cours. Si une tâche doit modifier le contenu du dépôt, elle doit plutôt prendre la forme d’un plugin de commande lancé explicitement par le développeur, et non modifier discrètement le code source à chaque build.

Repérer les dépendances implicites

Les entrées implicites les plus courantes comprennent les fichiers de configuration du répertoire de travail courant, les modèles désignés par des variables d’environnement, les caches du répertoire personnel et les dossiers entiers détectés automatiquement par le générateur. Répondez point par point aux quatre questions suivantes :

Élément à vérifier Pratique correcte Signal de risque
Fichiers d’entrée Tous les ajouter à inputFiles Analyse récursive à l’exécution d’un répertoire non déclaré
Fichiers de sortie Tous les ajouter à outputFiles Noms de fichiers variables selon l’heure ou la machine
Chemin de travail Utiliser les chemins absolus transmis en arguments Dépendance envers pwd ou le répertoire personnel
Ordre de génération Trier les éléments pour obtenir une sortie stable Dépendance envers l’ordre d’énumération du système de fichiers

Rendre le générateur lui-même déterministe

Même avec des autorisations correctes, le générateur peut invalider continuellement les builds incrémentaux. La cause la plus fréquente est l’ajout, dans l’en-tête du fichier, de l’heure actuelle, d’un répertoire temporaire ou du nom de la machine. Si des entrées identiques produisent des octets différents à chaque exécution, SwiftPM ne peut pas déterminer si le changement est réel.

Sur le Mac dans le cloud, effectuez deux générations propres successives, puis comparez leurs empreintes :

rm -rf .ci/first .ci/second
mkdir -p .ci/first .ci/second

.build/debug/schema-gen \
  Schemas/api.json .ci/first/API.swift

.build/debug/schema-gen \
  Schemas/api.json .ci/second/API.swift

shasum -a 256 .ci/first/API.swift .ci/second/API.swift
cmp .ci/first/API.swift .ci/second/API.swift

Les deux empreintes doivent être identiques. Le générateur doit également trier les champs, les fichiers et les déclarations, uniformiser les fins de ligne et ne remplacer le fichier cible que lorsque son contenu change. Il peut d’abord écrire un fichier temporaire, le comparer, puis effectuer un déplacement atomique afin d’éviter qu’une interruption du build ne laisse un fichier Swift incomplet.

Reproduire les problèmes de cache dans un espace de travail propre

Un succès en local ne prouve pas que les déclarations sont complètes. Le DerivedData local, d’anciennes sorties du plugin ou du code source généré manuellement peuvent temporairement masquer une déclaration manquante. Le build dans le cloud doit faire l’objet d’au moins une validation sans état antérieur, suivie d’un second build incrémental.

Validation en deux passes

Pour la première passe, supprimez le répertoire de build dédié et lancez le build afin de vérifier que tous les fichiers générés proviennent bien du plugin. Pour la deuxième, relancez le build sans modifier les entrées et vérifiez que le plugin ne réécrit pas inutilement les sorties. Modifiez ensuite un seul champ du schéma et effectuez une troisième passe pour confirmer que le fichier généré correspondant est effectivement actualisé.

Les commandes suivantes permettent de détecter rapidement un fichier généré dans le mauvais emplacement :

find "$PWD" -type f -name 'API.swift' -print
git status --short

En situation normale, les fichiers dérivés se trouvent dans le répertoire de build et git status ne doit signaler aucun fichier source versionné réécrit automatiquement par le plugin. Si le plugin s’exécute encore systématiquement lors de la deuxième passe, vérifiez que les sorties existent réellement, que l’outil ne force pas la mise à jour de leur horodatage et qu’aucun répertoire d’entrée n’est déclaré de manière trop large.

Formaliser les limites de validation du Mac dans le cloud

Dans l’environnement de build distant de HireVM ou sur d’autres nœuds macOS propres, il est préférable d’intégrer les vérifications suivantes au pipeline plutôt que de s’en remettre à une observation manuelle :

  1. Figez la sélection de Xcode et consignez xcodebuild -version ainsi que swift --version au début du journal.
  2. Attribuez à chaque espace de travail ses propres répertoires DerivedData et de checkout des packages afin d’éviter les écritures croisées entre tâches parallèles.
  3. Lancez le premier build depuis un répertoire vide pour vérifier que le plugin ne dépend d’aucun ancien fichier du répertoire personnel.
  4. Conservez les journaux de build détaillés avant et après l’échec du plugin, sans y inscrire de jetons, d’éléments de signature ni l’intégralité des variables d’environnement.
  5. Comparez les empreintes des fichiers générés essentiels afin de confirmer qu’un même commit produit des résultats reproductibles.
  6. Modifiez une seule entrée, puis relancez le processus pour vérifier que le build incrémental n’actualise que les sorties attendues.
  7. Vérifiez dans la console les configurations actuellement disponibles et choisissez les ressources en fonction du nombre de builds simultanés. La validation du plugin ne doit pas dépendre de l’état fortuit d’une machine particulière.

L’objectif final n’est pas qu’un build réussisse « par hasard », mais que SwiftPM comprenne entièrement l’étape de génération : reconstruire exactement ce qui est nécessaire lorsque les entrées changent, ne rien faire lorsqu’elles restent identiques et produire les mêmes artefacts dans un nouvel espace de travail sur un Mac dans le cloud. Une fois cet objectif atteint, la sandbox n’est plus un obstacle, mais un contrôle automatique qui vérifie que les limites du build sont réelles et complètes.

Questions fréquentes

Un plugin de build SwiftPM peut-il modifier directement les sources ?

Ce modèle est déconseillé. Le plugin doit écrire dans son répertoire de travail et déclarer chaque fichier dans outputFiles. Une modification du dépôt doit être réservée à un plugin de commande lancé explicitement.

Pourquoi le plugin fonctionne-t-il localement mais échoue-t-il sur le Mac distant ?

Des fichiers résiduels peuvent masquer une sortie non déclarée, ou le générateur peut dépendre du dossier personnel, du répertoire courant ou d’un chemin absolu. Un dépôt propre et des chemins de build fixes révèlent ces dépendances.

Faut-il désactiver la sandbox pour débloquer la CI ?

Non, sauf diagnostic temporaire strictement contrôlé. La correction durable consiste à déclarer toutes les entrées et sorties et à limiter les écritures aux emplacements autorisés.

Nœud physique Apple Silicon dédié

Exécutez votre prochaine tâche Mac à distance sur un nœud dédié

Choisissez parmi deux configurations disponibles selon l’ampleur de la tâche, cinq nœuds et des cycles à la journée, à la semaine, au mois ou au trimestre. Vérifiez l’intégralité de la configuration et le montant en USD avant de commander.

Choisir une configuration et commander