Journal d’ingénierie

Réinitialiser un espace de build Mac avec des clones APFS

Réinitialiser un espace de build Mac avec des clones APFS

Lorsqu’un même Mac dans le cloud enchaîne les mises à niveau de dépendances, les migrations de projet et les scripts destructifs, la principale difficulté n’est souvent pas le build lui-même, mais le retour fiable à un espace de travail propre. Extraire de nouveau un dépôt volumineux oblige à relire une multitude de petits fichiers, tandis que réutiliser directement l’ancien répertoire risque de réintroduire des fichiers non suivis, des artefacts générés ou des permissions incorrectes. Si le répertoire de travail se trouve sur un volume APFS, il est possible de maintenir une base préalablement validée, puis d’utiliser des clones en copie sur écriture pour créer une copie modifiable par tâche.

Quel problème les clones APFS résolvent-ils ?

Avec un clone APFS, deux fichiers partagent initialement les mêmes blocs de données sous-jacents. Ce n’est que lorsque l’un d’eux est modifié que le système de fichiers alloue de nouveaux blocs aux parties concernées. Une base contenant le code source, les dépendances et les outils statiques peut ainsi être copiée rapidement dans un répertoire indépendant, sans que les modifications ultérieures soient répercutées sur la base.

Ce mécanisme diffère des liens physiques : les fichiers clonés possèdent des entrées de répertoire indépendantes, et modifier la copie ne change pas le fichier d’origine. Il ne s’agit pas non plus d’une sauvegarde, car la base et ses copies restent sur le même volume. En cas d’endommagement du volume, de suppression accidentelle de la base ou de perte complète des données de la machine, les clones n’offrent aucune possibilité de restauration hors site.

Considérez les clones APFS comme un mécanisme peu coûteux de création de copies de travail, et non comme une garantie de cache hit, un système d’instantanés ou une solution de protection des données.

du -sh affiche la taille logique d’un répertoire et ne permet pas de déterminer directement l’espace physique réellement ajouté par le clone. Pour surveiller l’évolution de l’espace utilisé, vérifiez également l’espace libre du volume APFS et définissez un seuil de nettoyage pour les tâches qui continuent à écrire des données.

Préparer une base vérifiable

La base ne doit pas être un répertoire laissé tel quel après un build. Commencez par extraire un commit déterminé, restaurez les dépendances, puis supprimez tout état propre à une exécution particulière. Vérifiez au minimum que l’arbre de travail est propre et que les sous-modules pointent vers les bons commits, puis consignez le commit de référence.

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
mkdir -p "$ROOT"

git -C "$BASE" status --porcelain
git -C "$BASE" submodule status --recursive
git -C "$BASE" rev-parse HEAD > "$BASE/.baseline-revision"
test -z "$(git -C "$BASE" status --porcelain)"

Le fichier .baseline-revision devient lui-même un fichier non suivi. Il faut donc le placer hors du dépôt ou l’ajouter au préalable à une règle d’exclusion approuvée par l’équipe. Une organisation plus robuste consiste à stocker le commit dans $ROOT/metadata, afin que $BASE satisfasse toujours le contrôle de propreté de l’arbre de travail.

Éléments à exclure de la base

N’intégrez pas à la base DerivedData, les bundles de résultats de test, les archives, les trousseaux temporaires, les journaux d’exécution ni les sockets actifs. Ces éléments contiennent souvent des chemins absolus, des états de processus ou des identifiants propres à une tâche. L’inclusion des répertoires de dépendances dépend de deux facteurs : leur contenu peut-il être déterminé à partir des fichiers de verrouillage, et les scripts d’installation écrivent-ils dans des chemins globaux à la machine ?

Seule la tâche chargée des mises à jour doit pouvoir écrire dans la base. Les comptes de build ordinaires peuvent la lire, mais ne doivent pas y exécuter de commandes de build. Une seule erreur suffirait autrement à contaminer tous les clones créés par la suite.

Créer un clone indépendant pour chaque tâche

Vérifiez d’abord que le répertoire racine se trouve bien sur APFS, puis utilisez l’identifiant de la tâche pour générer un répertoire unique. Le chemin de destination ne doit pas déjà exister, et l’identifiant doit être limité à des caractères sûrs afin de ne jamais incorporer directement une entrée externe dans un chemin de suppression.

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
JOB_ID="${BUILD_ID:?BUILD_ID is required}"

case "$JOB_ID" in
  *[!A-Za-z0-9._-]*) exit 64 ;;
esac

test "$(stat -f %T "$ROOT")" = "apfs"

WORK="$ROOT/jobs/$JOB_ID"
DERIVED="$ROOT/derived/$JOB_ID"

test ! -e "$WORK"
mkdir -p "$ROOT/jobs" "$DERIVED"
cp -cR "$BASE" "$WORK"

xcodebuild \
  -workspace "$WORK/App.xcworkspace" \
  -scheme App \
  -derivedDataPath "$DERIVED" \
  build

cp -cR demande une copie par clonage, mais le comportement réel dépend toujours des chemins source et cible ainsi que du système de fichiers. Le répertoire source et le répertoire cible doivent se trouver sur le même volume APFS. Si la copie traverse des volumes, elle ne doit pas être considérée comme un processus de copie sur écriture. Avant le déploiement, vous pouvez utiliser un répertoire de test contenant un fichier volumineux pour observer la durée de la copie et l’évolution de l’espace disponible sur le volume.

Isoler les tâches concurrentes et les états modifiables

Des espaces de travail distincts ne signifient pas que tous les états sont isolés. Les outils de build peuvent encore lire et écrire des caches, des répertoires temporaires et des fichiers de configuration dans le répertoire utilisateur. Attribuez au minimum un répertoire DerivedData et un répertoire de résultats distincts à chaque tâche. Évitez également que plusieurs tâches partagent le même ensemble d’appareils de simulateur ou le même chemin de sortie.

Définir un budget de capacité pour les tâches concurrentes

Lors de la création du clone, l’espace supplémentaire utilisé reste limité. Toutefois, les objets compilés, la réécriture des dépendances et les archives produisent rapidement de nouveaux blocs. La limite de concurrence doit être calculée à partir du volume maximal de données modifiables dans le pire des cas, et non à partir de la taille logique de la base. Vous pouvez vérifier l’espace du volume avant de lancer une tâche, puis consigner son évolution à la fin du build :

df -h "$ROOT"
du -sh "$DERIVED" "$WORK"

Si une tâche réécrit une grande quantité de fichiers de dépendances, l’avantage spatial du clonage diminue progressivement. Dans ce cas, conservez les dépendances immuables dans la base et déplacez les répertoires générés fréquemment modifiés vers des chemins propres à chaque tâche, plutôt que de partager des caches modifiables dans le seul but de préserver le taux de clonage.

Nettoyer en toute sécurité et reconstruire régulièrement la base

L’objectif principal du script de nettoyage n’est pas de supprimer rapidement, mais de ne jamais sortir du répertoire racine réservé aux tâches. Avant toute suppression, vérifiez à la fois le préfixe du chemin, l’existence du répertoire et l’identifiant de la tâche. N’exécutez jamais de suppression récursive directement sur une variable susceptible d’être vide.

set -euo pipefail

ROOT="$HOME/ci-workspaces"
WORK="$ROOT/jobs/${BUILD_ID:?BUILD_ID is required}"
DERIVED="$ROOT/derived/${BUILD_ID:?BUILD_ID is required}"

case "$WORK" in
  "$ROOT"/jobs/*) rm -rf -- "$WORK" ;;
  *) exit 64 ;;
esac

case "$DERIVED" in
  "$ROOT"/derived/*) rm -rf -- "$DERIVED" ;;
  *) exit 64 ;;
esac

La mise à jour de la base doit passer par un nouveau répertoire : extrayez le commit cible, installez les dépendances de manière déterministe, exécutez les contrôles de validation, puis remplacez la base actuelle. N’effectuez pas de mise à jour directement dans la base existante tout en continuant à l’utiliser, car une interruption laisserait un mélange d’anciens et de nouveaux états impossible à qualifier.

La vérification finale comporte quatre points : le commit de la base est traçable et l’arbre de travail reste propre ; chaque tâche possède un espace de travail et un répertoire de génération uniques ; l’espace libre du volume peut absorber les écritures correspondant à la concurrence maximale ; la logique de nettoyage n’accepte que des chemins contrôlés. Ce n’est qu’une fois ces conditions remplies que les clones APFS deviennent une véritable étape d’ingénierie reproductible, plutôt qu’une simple astuce de copie apparemment rapide.

Questions fréquentes

Un clone APFS remplace-t-il un cache de build ou une sauvegarde ?

Non. Il accélère la création d’une copie modifiable sur le même volume APFS, mais les blocs modifiés occupent de l’espace supplémentaire. Une sauvegarde indépendante reste nécessaire.

Pourquoi exclure DerivedData de la base ?

DerivedData contient des chemins absolus, des index et un état propre à chaque tâche. Un répertoire séparé par build limite les contaminations et facilite le diagnostic.

Nœud physique dédié

Poursuivez la validation de ce workflow sur un Mac dans le cloud

Choisissez la mémoire, le stockage, le nœud et la durée de location adaptés parmi trois configurations Apple Silicon.

Choisir une configuration et commander