Auditer les liens symboliques en CI Mac cloud

Auditer les liens symboliques en CI Mac cloud

Un même projet peut s’archiver sans problème sur le Mac d’un développeur, puis signaler une ressource introuvable sur une CI Mac dans le cloud. Les liens symboliques figurent parmi les causes les plus faciles à négliger. Un fichier qui paraît ordinaire dans le dépôt peut en réalité pointer vers un répertoire local, une cible non versionnée ou un emplacement situé hors de l’espace de travail. Xcode n’y accède parfois qu’au moment de copier les ressources, d’exécuter un script ou d’empaqueter les dépendances. Le problème ne se révèle alors que dans la seconde moitié du build, souvent sous la forme d’une banale erreur « fichier introuvable ».

Définir d’abord les liens autorisés

Avant d’écrire un script d’analyse, commencez par formaliser la politique à appliquer. Pour une intégration continue, les règles par défaut se résument généralement à trois points : le lien doit utiliser un chemin relatif, sa cible résolue doit exister et cette cible doit rester dans l’espace de travail courant. Certains liens internes créés par des outils peuvent faire exception, mais chaque exception doit désigner un chemin relatif complet et précis.

L’objectif du contrôle des liens symboliques n’est pas de les éliminer, mais de supprimer toute dépendance implicite à l’arborescence d’une machine particulière.

Par exemple, Config/current.json -> release.json peut être récupéré de manière fiable avec le dépôt. En revanche, SDK/current -> /Users/dev/SDK introduit l’état de la machine locale dans le pipeline. Il ne suffit pas non plus d’examiner le texte de ../Shared/file : la cible peut se trouver encore dans le dépôt, comme elle peut sortir du répertoire de travail attribué par la CI. Il faut donc résoudre et normaliser le chemin avant de le comparer aux limites autorisées.

Point contrôlé À autoriser À bloquer
Forme du lien Chemin relatif interne au dépôt Chemin absolu
État de la cible Cible existante et lisible Lien rompu ou boucle de liens
Limites du chemin Chemin résolu dans l’espace de travail Cible hors de l’espace de travail
Règles d’exception Chemin relatif complet Préfixe de répertoire trop large

Confirmer la nature des liens depuis l’index Git

Vérifier le mode plutôt que l’extension

Git enregistre les liens symboliques avec le mode 120000, tandis que leur cible est stockée comme contenu du fichier. Commencez par les répertorier depuis l’index afin de ne pas les confondre avec des fichiers texte ordinaires ou des liens générés après le build.

git ls-files -s | awk '$1 == "120000" { print $4 }'
git config --show-origin --get core.symlinks || true
git status --porcelain=v1

Sous macOS, les objets extraits doivent réellement être des liens. Si l’état du dépôt indique soudain qu’un lien a été remplacé par un fichier ordinaire, vérifiez d’abord si une étape d’empaquetage, de décompression ou de synchronisation a modifié son type. Le contrôle doit également exécuter git diff --exit-code après une extraction propre, afin de confirmer qu’aucun script d’initialisation n’a discrètement modifié le texte des liens enregistré dans l’index.

Repérer les cibles non suivies

Le fait qu’un lien soit versionné ne signifie pas que sa cible l’est aussi. Pour chaque lien suivi par Git, vérifiez que la cible existe, puis utilisez git ls-files --error-unmatch pour déterminer si une cible située dans le dépôt est elle-même sous contrôle de version. Si la cible doit être produite par le build, déplacez le contrôle après l’étape de génération, tout en conservant une liste précise des cibles autorisées à être temporairement absentes avant cette étape.

Bloquer les liens rompus et les sorties de périmètre avec un script

Le script ci-dessous analyse récursivement le répertoire indiqué. Il refuse les liens absolus, les cibles impossibles à résoudre et les cibles dont le chemin résolu se trouve hors du répertoire racine. Enregistrez-le dans ci/check_symlinks.py, puis exécutez-le après l’installation des dépendances et avant l’appel à xcodebuild.

import os
import sys
from pathlib import Path

root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
failures = []

for path in root.rglob("*"):
    if not path.is_symlink():
        continue

    raw = os.readlink(path)
    relative = path.relative_to(root)

    if os.path.isabs(raw):
        failures.append((relative, "absolute", raw))
        continue

    try:
        target = path.resolve(strict=True)
    except (FileNotFoundError, RuntimeError, OSError) as error:
        failures.append((relative, "broken", str(error)))
        continue

    try:
        target.relative_to(root)
    except ValueError:
        failures.append((relative, "outside", str(target)))

for item in failures:
    print("\t".join(map(str, item)))

sys.exit(1 if failures else 0)

Exécutez-le comme suit :

python3 ci/check_symlinks.py "$PWD"

N’utilisez pas une simple comparaison de préfixes de chaînes pour les chemins. /tmp/job-10 commence par la chaîne /tmp/job-1, sans pour autant être l’un de ses sous-répertoires. Seule une comparaison hiérarchique effectuée après normalisation des chemins évite ce type de faux résultat.

Effectuer un contrôle avant et après le build

L’analyse des sources ne détecte que les problèmes déjà présents dans le dépôt. Les gestionnaires de dépendances, les générateurs de code et les phases de build personnalisées peuvent encore créer de nouveaux liens. Il est donc recommandé de prévoir deux points de contrôle.

Contrôler l’espace de travail avant le build

Une fois la résolution des dépendances et la génération de code terminées, analysez l’ensemble de l’espace de travail, en excluant uniquement les répertoires de cache qui relèvent clairement du fonctionnement interne des outils et qui ne seront pas intégrés au livrable. Les règles d’exclusion doivent elles aussi être figées dans le dépôt ; elles ne doivent pas pouvoir être élargies arbitrairement par une variable d’environnement temporaire du runner. Enregistrez ensuite la liste des liens afin d’identifier ceux qui apparaissent pendant le build.

find "$PWD" -type l -print | LC_ALL=C sort > "$TMPDIR/symlinks-before.txt"
python3 ci/check_symlinks.py "$PWD"

Contrôler le livrable après le build

Une fois l’archivage terminé, exécutez de nouveau le même script sur le fichier .app exporté ou sur le répertoire racine du livrable décompressé. Pour être valide, un lien doit à la fois conserver sa cible à l’intérieur du livrable et correspondre à un chemin figurant dans la liste d’autorisation. Si un framework tiers contient réellement des liens internes, autorisez leurs chemins précis plutôt que l’ensemble du répertoire Frameworks.

Comparez ensuite les listes obtenues avant et après le build. Tout nouveau lien créé pendant une phase de script doit pouvoir être rattaché à une commande de génération clairement identifiée. Si son origine ne peut pas être expliquée, bloquez d’abord la livraison, puis recherchez son créateur dans les journaux de build.

Traiter les faux positifs courants et finaliser le contrôle

Les faux positifs les plus fréquents proviennent des répertoires temporaires, des caches de dépendances et de la structure interne des archives. Commencez par réduire la racine analysée, puis ajoutez des exceptions précises, et n’envisagez l’exclusion de répertoires qu’en dernier recours. Ne sautez pas un arbre de dépendances entier sous prétexte qu’une dépendance crée un lien : de véritables liens rompus seraient alors masqués en même temps.

En cas d’échec, le contrôle doit au minimum afficher le chemin relatif du lien, le type d’erreur et la cible résolue, puis transmettre directement à la CI le code de sortie du script d’analyse. Après correction, validez le résultat depuis une nouvelle extraction complète ; ne relancez pas uniquement le contrôle dans un espace de travail où des scripts d’initialisation ont déjà été exécutés. La liste finale peut se résumer à six critères : mode Git correct, cible sous contrôle, chemin du lien relatif, cible résolue sans sortie de périmètre, nouveaux liens du build explicables et exceptions d’archive correspondant à des chemins précis. Une fois ces six conditions remplies, les liens symboliques cessent d’être une source aléatoire de problèmes d’environnement et deviennent un contrat d’ingénierie vérifiable.

Questions fréquentes

Pourquoi un lien valide en local casse-t-il sur un Mac cloud ?

Il peut dépendre d’un chemin absolu local, viser un fichier non suivi ou supposer un emplacement de checkout différent. Il faut contrôler le texte du lien et sa destination résolue.

Faut-il interdire tous les liens symboliques du dépôt ?

Non. Les liens relatifs stables qui restent dans le dépôt sont acceptables. Refusez les liens cassés, absolus ou sortant de l’espace de travail, puis limitez strictement les exceptions.

Comment autoriser un lien légitime dans une archive ?

Vérifiez d’abord que sa destination reste sous la racine de l’archive, puis autorisez son chemin relatif exact. Une règle large par nom ou préfixe affaiblit le contrôle.

Lancer la prochaine tâche

Choisissez un Mac dans le cloud selon votre charge de travail

Vérifiez la puce, la mémoire, le stockage, la durée de location et le nœud, puis commandez une machine physique dédiée Apple Silicon.

Choisir un modèle et commander