UTF-8 et noms de fichiers Unicode en CI Mac cloud
Un même projet iOS peut se compiler correctement dans le terminal d’un développeur, puis échouer dans une CI non interactive sur Mac cloud lorsqu’il lit une configuration en chinois, analyse des noms de ressources contenant des caractères accentués ou extrait une partie des journaux. Le plus trompeur est que ces incidents sont souvent attribués à tort à des dépendances endommagées : le fichier existe, mais le script indique qu’il est introuvable ; deux chemins semblent identiques dans les journaux, alors que Git les considère comme distincts. En général, le problème ne vient pas d’UTF-8 lui-même, mais d’un désaccord entre le point d’entrée de la tâche, l’environnement d’exécution du langage et l’interprétation des noms de fichiers du dépôt.
Commencez par localiser la couche où apparaît l’écart
Ne modifiez pas immédiatement les réglages du système. Décomposez d’abord le problème en quatre couches : la locale du processus de la tâche, l’encodage par défaut de l’environnement d’exécution du script, le comportement de chaque commande dans le pipeline et la forme de normalisation Unicode des noms de fichiers.
Exécutez les commandes suivantes dans le terminal interactif, puis au début de la tâche CI :
printf 'shell=%s\n' "$SHELL"
locale
printf 'LANG=%s\nLC_ALL=%s\n' "${LANG:-unset}" "${LC_ALL:-unset}"
python3 -c 'import locale,sys; print(locale.getpreferredencoding(False), sys.getfilesystemencoding())'
ruby -e 'p [Encoding.default_external, Encoding.default_internal]'
Conservez la sortie comme un artefact de build ordinaire au lieu de la laisser uniquement dans un journal défilant. Si le terminal affiche en_US.UTF-8 alors que les variables sont vides dans la tâche, le problème se situe à la frontière de lancement. Si la locale est identique, mais que Ruby continue à lire les fichiers avec un autre encodage, vérifiez si le script transmet explicitement une option incorrecte.
« La machine prend en charge UTF-8 » ne signifie pas que chaque tâche utilise UTF-8. La véritable configuration de la CI correspond à l’environnement reçu par le processus, et non à celui affiché dans un terminal de connexion.
Fixez UTF-8 au point d’entrée de la tâche
La solution la plus fiable consiste à définir l’environnement dans le script d’encapsulation du runner ou au point d’entrée de la tâche du pipeline, plutôt que de dépendre de .zshrc. Les shells non interactifs ne chargent généralement pas ces fichiers comme les développeurs s’y attendent, et les processus lancés par launchd peuvent recevoir un autre environnement.
#!/bin/zsh
set -euo pipefail
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
locale
exec "$@"
Enregistrez ce script sous ci/run-utf8.zsh, rendez-le exécutable, puis faites passer toutes les commandes de build par ce point d’entrée :
chmod +x ci/run-utf8.zsh
ci/run-utf8.zsh ./ci/build.zsh
N’exécutez pas globalement export LC_ALL=C pour « corriger » les journaux. La locale C convient à une commande isolée nécessitant un tri stable octet par octet, mais elle modifie aussi la classification des caractères, la conversion entre majuscules et minuscules ainsi que le comportement des expressions régulières. Si elle est réellement nécessaire, limitez sa portée à la commande concernée, par exemple LC_ALL=C sort input.txt.
Vérifiez aussi la méthode de lecture propre à chaque script
En Python, indiquez explicitement encoding="utf-8". En Ruby, vous pouvez utiliser File.read(path, encoding: "UTF-8"). Pour lire du texte ligne par ligne dans le Shell, utilisez IFS= read -r afin de ne pas altérer les barres obliques inverses ni les espaces de début ou de fin. Confiez les fichiers JSON, plist et YAML aux analyseurs correspondants au lieu de tenter d’en deviner la structure avec grep et sed.
Vérifiez la normalisation Unicode des noms de fichiers
Le caractère é peut être représenté par un seul point de code ou par une lettre suivie d’un caractère combinatoire. Les deux formes paraissent identiques, mais leurs octets diffèrent. Lorsqu’un dépôt circule entre macOS, Linux et des archives, cet écart peut provoquer des ressources en double, rendre un script introuvable ou empêcher l’application correcte d’un commit qui ne modifie que la casse d’un nom.
Ajoutez à la CI ce contrôle en lecture seule :
from pathlib import Path
import sys
import unicodedata
bad = []
for path in Path(".").rglob("*"):
if ".git" in path.parts:
continue
raw = path.as_posix()
normalized = unicodedata.normalize("NFC", raw)
if raw != normalized:
bad.append((raw, normalized))
for raw, normalized in bad:
print(f"non-nfc: {raw!r} -> {normalized!r}")
sys.exit(1 if bad else 0)
Enregistrez-le sous ci/check_unicode_paths.py et exécutez-le avant l’installation des dépendances. Le contrôle doit uniquement signaler les anomalies, sans renommer automatiquement les fichiers, car le chemin normalisé pourrait entrer en conflit avec un fichier existant. Avant toute correction, vérifiez les noms réels enregistrés dans l’index Git :
git -c core.quotepath=false ls-files
git -c core.quotepath=false status --short
Effectuez ensuite le renommage avec git mv dans un commit séparé. Si seule la casse change, déplacez d’abord le fichier vers un nom temporaire, puis vers le nom cible. Une fois l’opération terminée, clonez impérativement le dépôt dans un nouveau répertoire et lancez-y le build. Ne validez pas la correction dans un ancien espace de travail déjà influencé par le cache du système de fichiers.
Conservez les preuves brutes dans les journaux
Après un passage par tee, un collecteur de journaux ou un échappement JSON, les octets à l’origine d’un problème d’encodage peuvent être remplacés. Il ne reste alors que des points d’interrogation ou des caractères de remplacement. Pendant le diagnostic, conservez à la fois un journal lisible et une vue des octets des fichiers essentiels :
file -I Config/环境.json
xxd -g 1 -l 96 Config/环境.json
plutil -lint App/Info.plist
Ne considérez pas le résultat de file comme une conclusion définitive : il fournit seulement un indice. Le programme qui lit réellement le fichier doit toujours spécifier explicitement UTF-8 et renvoyer un état non nul en cas d’échec du décodage. Le script de build ne doit pas non plus exécuter cmd | tee build.log puis ignorer le code de sortie de la première commande. Avec zsh, vous pouvez consulter $pipestatus ou séparer les commandes critiques de la collecte des journaux.
Les journaux doivent également enregistrer une représentation échappée du chemin en échec. Le repr() de Python et le String#dump de Ruby sont plus adaptés qu’un affichage direct pour repérer les caractères combinatoires, les retours à la ligne et les espaces invisibles.
Mettez en place un contrôle avant fusion et un ordre de correction
Un contrôle maintenable doit uniquement vérifier des faits déterministes et ne jamais modifier le dépôt de sa propre initiative. Exécutez de préférence les étapes dans cet ordre :
- Afficher la locale et l’encodage de chaque environnement d’exécution.
- Vérifier que les chemins du dépôt sont au format NFC.
- Vérifier si la normalisation produit des noms en double.
- Lire les principales configurations textuelles avec un encodage explicite.
- Exécuter un build minimal dans un nouvel espace de travail.
- Conserver les données d’environnement, le rapport sur les chemins et le journal d’échec.
Si la tâche échoue uniquement sur un runner donné, comparez d’abord les données d’environnement, puis l’index Git, au lieu de vider immédiatement tous les caches. Si le rapport sur les chemins signale une anomalie, corrigez d’abord les noms de fichiers dans un commit séparé. Si les chemins sont corrects mais que l’analyse échoue, vérifiez ensuite la déclaration d’encodage du contenu et les paramètres de lecture. Cette méthode permet de traiter séparément les problèmes d’environnement, de dépôt et d’outillage.
Sur RentMini, ces contrôles doivent eux aussi être stockés dans le dépôt et versionnés par le projet, plutôt que de dépendre de réglages manuels propres à un Mac cloud. Lorsque le script d’entrée, le contrôle Unicode et les éléments de diagnostic sont relus avec le code, la création d’un nouvel espace de travail ou le changement de nœud produit un comportement cohérent.
Questions fréquentes
Pourquoi un script correct dans Terminal produit-il du texte illisible en CI ?
Une tâche CI non interactive ne charge pas forcément les mêmes fichiers d’initialisation. Définissez LANG et LC_ALL au point d’entrée, puis vérifiez l’encodage réellement utilisé par chaque runtime.
Faut-il renommer automatiquement les fichiers qui ne sont pas normalisés en NFC ?
Non. Le contrôle doit d’abord produire un rapport et échouer. Vérifiez les collisions de casse et de normalisation, utilisez git mv dans un commit dédié, puis testez depuis un nouveau clone.
Préparez votre prochain build sur un Mac mini physique dédié
Choisissez la configuration M4, la durée de location et le nœud. Chaque commande correspond à un appareil physique dédié ; la disponibilité réelle est indiquée en temps réel dans la console.