UTF-8 und Unicode-Dateinamen in Cloud-Mac-CI

UTF-8 und Unicode-Dateinamen in Cloud-Mac-CI

Dasselbe iOS-Projekt kann im Terminal eines Entwicklers problemlos kompilieren und dennoch in der nicht interaktiven CI auf einem Cloud-Mac scheitern – etwa beim Lesen einer Konfiguration mit chinesischen Zeichen, beim Auflösen von Ressourcennamen mit Akzenten oder beim Ausschneiden von Logabschnitten. Besonders tückisch ist, dass solche Fehler häufig für beschädigte Abhängigkeiten gehalten werden: Die Datei ist nachweislich vorhanden, doch das Skript meldet, sie nicht finden zu können. Zwei Pfade sehen im Log identisch aus, werden von Git aber als unterschiedliche Pfade behandelt. Die eigentliche Ursache ist meist nicht UTF-8 selbst, sondern eine uneinheitliche Interpretation der Kodierung durch den Job-Einstiegspunkt, die Sprachlaufzeit und die Dateinamen im Repository.

Zuerst die betroffene Ebene ermitteln

Ändern Sie nicht sofort die Systemeinstellungen. Teilen Sie das Problem zunächst in vier Ebenen auf: die Locale des Job-Prozesses, die Standardkodierung der Skriptlaufzeit, das Verhalten einzelner Befehle in einer Pipeline und die Unicode-Normalisierungsform der Dateinamen.

Führen Sie die folgenden Befehle sowohl im interaktiven Terminal als auch am Anfang des CI-Jobs aus:

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]'

Speichern Sie die Ausgabe als reguläres Build-Artefakt, statt sie nur im fortlaufenden Log zu belassen. Zeigt das Terminal en_US.UTF-8, während die Variablen im Job leer sind, liegt das Problem an der Startgrenze. Stimmen die Locale-Einstellungen überein, liest Ruby Dateien aber weiterhin mit einer anderen Kodierung, sollte geprüft werden, ob das Skript ausdrücklich eine falsche Option übergibt.

„Der Rechner unterstützt UTF-8“ bedeutet nicht, dass jeder Job UTF-8 verwendet. Die tatsächliche CI-Konfiguration ist die Umgebung, die der Prozess erhält – nicht die Umgebung, die im Anmeldeterminal angezeigt wird.

UTF-8 am Job-Einstiegspunkt verbindlich setzen

Am zuverlässigsten ist es, die Umgebung im Wrapper-Skript des Runners oder direkt am Einstiegspunkt des Pipeline-Jobs zu setzen, statt sich auf .zshrc zu verlassen. Nicht interaktive Shells laden solche Dateien üblicherweise nicht so, wie Entwickler es erwarten. Auch von launchd gestartete Prozesse können eine andere Umgebung erhalten.

#!/bin/zsh
set -euo pipefail

export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

locale
exec "$@"

Speichern Sie das Skript als ci/run-utf8.zsh, machen Sie es ausführbar und führen Sie anschließend alle Build-Befehle darüber aus:

chmod +x ci/run-utf8.zsh
ci/run-utf8.zsh ./ci/build.zsh

Führen Sie nicht global export LC_ALL=C aus, um Logs zu „reparieren“. Die Locale C eignet sich für einzelne Befehle, die eine stabile Sortierung nach Bytes benötigen, verändert jedoch auch Zeichenklassen, Groß-/Kleinschreibung und das Verhalten regulärer Ausdrücke. Falls sie tatsächlich erforderlich ist, begrenzen Sie ihre Gültigkeit auf den jeweiligen Befehl, beispielsweise mit LC_ALL=C sort input.txt.

Auch die Leseweise der Skripte prüfen

In Python sollte encoding="utf-8" ausdrücklich angegeben werden. In Ruby kann File.read(path, encoding: "UTF-8") verwendet werden. Liest eine Shell Text zeilenweise ein, verhindert IFS= read -r, dass Backslashes sowie führende oder nachgestellte Leerzeichen verändert werden. JSON, plist und YAML sollten mit den jeweils passenden Parsern verarbeitet werden. Versuchen Sie nicht, ihre Struktur mit grep und sed zu erraten.

Unicode-Normalisierung von Dateinamen prüfen

Das Zeichen é kann aus einem einzigen Codepunkt oder aus einem Buchstaben und einem kombinierenden Zeichen bestehen. Beide Formen sehen gleich aus, bestehen aber aus unterschiedlichen Bytes. Wenn ein Repository zwischen macOS, Linux und Archiven übertragen wird, kann dieser Unterschied zu doppelten Ressourcen, nicht auffindbaren Skripten oder fehlerhaft angewendeten Commits führen, die lediglich die Groß-/Kleinschreibung ändern.

Ergänzen Sie die CI um folgende schreibgeschützte Prüfung:

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)

Speichern Sie die Datei als ci/check_unicode_paths.py und führen Sie sie vor der Installation der Abhängigkeiten aus. Das Prüfprogramm meldet Abweichungen lediglich und benennt nichts automatisch um, da ein normalisierter Pfad mit einer vorhandenen Datei kollidieren könnte. Prüfen Sie vor der Korrektur die tatsächlichen Namen im Git-Index:

git -c core.quotepath=false ls-files
git -c core.quotepath=false status --short

Nehmen Sie die Änderung anschließend mit git mv in einem separaten Commit vor. Ändert sich nur die Groß-/Kleinschreibung, kann die Datei zuerst auf einen temporären Namen und danach auf den Zielnamen verschoben werden. Anschließend muss das Repository in ein neues Verzeichnis geklont und dort gebaut werden. Ein alter Workspace, der bereits durch Dateisystem-Caches beeinflusst wurde, eignet sich nicht für die Abnahme.

Originalbelege in den Logs erhalten

Wenn ein Kodierungsfehler tee, einen Log-Collector oder JSON-Escaping durchläuft, können die ursprünglichen Bytes ersetzt werden, sodass am Ende nur Fragezeichen oder Ersatzzeichen übrig bleiben. Während der Analyse sollten deshalb sowohl lesbare Logs als auch Byte-Ansichten wichtiger Dateien erhalten bleiben:

file -I Config/环境.json
xxd -g 1 -l 96 Config/环境.json
plutil -lint App/Info.plist

Betrachten Sie die Einschätzung von file nicht als abschließenden Beweis. Sie liefert lediglich einen Hinweis. Das Programm, das die Datei tatsächlich liest, sollte UTF-8 weiterhin ausdrücklich vorgeben und bei einem Dekodierungsfehler einen von null verschiedenen Exit-Status zurückgeben. Build-Skripte dürfen außerdem nicht mit cmd | tee build.log arbeiten und anschließend den Exit-Code des vorherigen Befehls ignorieren. In zsh lässt sich $pipestatus auswerten; alternativ können kritische Befehle und die Log-Erfassung getrennt werden.

Im Log sollte außerdem die maskierte Darstellung des fehlgeschlagenen Pfads aufgezeichnet werden. Pythons repr() und Rubys String#dump eignen sich besser als eine direkte Ausgabe, um kombinierende Zeichen, Zeilenumbrüche und unsichtbare Leerzeichen zu erkennen.

Merge-Gate und Reihenfolge der Korrekturen festlegen

Ein wartbares Gate sollte nur überprüfbare Fakten kontrollieren und das Repository nicht eigenmächtig verändern. Empfehlenswert ist folgende Reihenfolge:

  1. Locale und Kodierung jeder Laufzeit ausgeben.
  2. Prüfen, ob die Repository-Pfade in NFC vorliegen.
  3. Prüfen, ob durch die Normalisierung doppelte Namen entstehen.
  4. Kritische Textkonfigurationen mit ausdrücklich angegebener Kodierung lesen.
  5. Einen minimalen Build in einem vollständig neuen Workspace ausführen.
  6. Umgebungsdiagnose, Pfadbericht und Fehlerlogs speichern.

Schlägt ein Job nur auf einem bestimmten Runner fehl, vergleichen Sie zuerst die Umgebungsdiagnosen und danach die Git-Indizes, statt sofort sämtliche Caches zu leeren. Enthält der Pfadbericht Auffälligkeiten, korrigieren Sie zunächst die Dateinamen und erstellen dafür einen separaten Commit. Sind die Pfade unauffällig, schlägt aber das Parsen fehl, prüfen Sie die Kodierungsdeklaration des Dateiinhalts und die beim Lesen verwendeten Parameter. So lassen sich Umgebungs-, Repository- und Werkzeugprobleme getrennt behandeln.

Auch Jobs auf RentMini sollten diese Prüfungen im Repository ablegen und zusammen mit dem Projekt versionieren, statt von manuellen Einstellungen eines bestimmten Cloud-Macs abhängig zu sein. Werden Einstiegsskript, Unicode-Prüfprogramm und Logbelege gemeinsam mit dem Code geprüft, bleibt das Verhalten auch nach dem Neuaufbau des Workspace oder einem Wechsel des Knotens konsistent.

Häufig gestellte Fragen

Warum funktioniert ein Skript im Terminal, zeigt in der CI aber beschädigte Zeichen?

Nicht interaktive Jobs laden häufig andere oder gar keine Shell-Startdateien. Setzen Sie LANG und LC_ALL am Einstieg des Jobs und prüfen Sie zusätzlich die gemeldete Kodierung jeder Laufzeit.

Soll die CI nicht NFC-normalisierte Dateien automatisch umbenennen?

Nein. Sie sollte zunächst einen Bericht erzeugen und fehlschlagen. Prüfen Sie Groß-/Kleinschreibung und Normalisierung auf Kollisionen, nutzen Sie git mv in einem eigenen Commit und testen Sie einen frischen Checkout.

Cloud-Mac nach Bedarf nutzen

Exklusiven physischen Mac mini für den nächsten Build vorbereiten

Wählen Sie Konfiguration, Mietdauer und Standort. Jede Bestellung umfasst ein exklusives physisches Gerät. Der tatsächlich verfügbare Status wird in Echtzeit über die Konsole angezeigt.

Konfiguration auswählen und mieten