UTF-8 и Unicode-имена файлов в Cloud Mac CI
Один и тот же проект iOS может успешно собираться в терминале разработчика, но завершаться ошибкой в неинтерактивной CI-среде на облачном Mac при чтении конфигурации на китайском языке, обработке имён ресурсов с диакритическими знаками или извлечении фрагментов журнала. Сложность в том, что такие сбои часто принимают за повреждение зависимостей: файл существует, но скрипт сообщает, что не может его найти; пути выглядят в журнале одинаково, однако Git считает их разными. Обычно причина не в самом UTF-8, а в том, что точка входа задания, языковая среда выполнения и имена файлов в репозитории по-разному интерпретируют кодировку.
Сначала определите, на каком уровне возникает расхождение
Не начинайте с изменения системных настроек. Сначала разделите проблему на четыре уровня: locale процесса задания, кодировка по умолчанию в среде выполнения скрипта, поведение отдельных команд в конвейере и форма нормализации Unicode в именах файлов.
Выполните следующие команды отдельно в интерактивном терминале и в начале 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]'
Сохраняйте вывод как обычный артефакт сборки, а не только в прокручиваемом журнале. Если в терминале отображается en_US.UTF-8, а в задании переменные пусты, проблема находится на границе запуска. Если locale совпадает, но Ruby по-прежнему читает файлы в другой кодировке, проверьте, не передаёт ли скрипт явно неверные параметры.
«Машина поддерживает UTF-8» не означает, что каждое задание использует UTF-8. Фактическая конфигурация CI — это окружение, полученное процессом, а не окружение, отображаемое в терминале входа.
Зафиксируйте UTF-8 в точке входа задания
Надёжнее всего задать окружение в скрипте-обёртке runner или непосредственно в точке входа конвейера, а не полагаться на .zshrc. Неинтерактивные оболочки обычно не загружают эти файлы так, как ожидает разработчик, а процессы, запущенные через launchd, могут получить другой набор переменных окружения.
#!/bin/zsh
set -euo pipefail
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
locale
exec "$@"
Сохраните скрипт как ci/run-utf8.zsh, сделайте его исполняемым и запускайте через него все команды сборки:
chmod +x ci/run-utf8.zsh
ci/run-utf8.zsh ./ci/build.zsh
Не выполняйте глобально export LC_ALL=C, пытаясь «исправить» журналы. Locale C подходит для отдельных команд, которым требуется стабильная побайтовая сортировка, но он также меняет классификацию символов, преобразование регистра и поведение регулярных выражений. Если он действительно нужен, ограничьте область действия одной командой, например LC_ALL=C sort input.txt.
Одновременно проверьте, как скрипты читают файлы
В Python следует явно указывать encoding="utf-8", а в Ruby можно использовать File.read(path, encoding: "UTF-8"). Для построчного чтения текста в Shell применяйте IFS= read -r, чтобы не изменять обратные косые черты и начальные или конечные пробелы. JSON, plist и YAML должны обрабатываться соответствующими парсерами — не пытайтесь угадывать их структуру с помощью grep и sed.
Проверьте нормализацию Unicode в именах файлов
Символ é может быть представлен одним кодовым пунктом либо буквой вместе с комбинируемым знаком. Внешне эти формы одинаковы, но их байтовое представление различается. Когда репозиторий перемещается между macOS, Linux и архивами, такие различия могут приводить к дублированию ресурсов, невозможности найти скрипт или некорректному применению коммитов, меняющих только регистр имени.
Добавьте в CI проверку только для чтения:
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)
Сохраните её как ci/check_unicode_paths.py и запускайте до установки зависимостей. Проверка должна только сообщать о проблемах, не переименовывая файлы автоматически, поскольку нормализованный путь может конфликтовать с уже существующим файлом. Перед исправлением проверьте реальные имена в индексе Git:
git -c core.quotepath=false ls-files
git -c core.quotepath=false status --short
Затем выполните переименование через git mv и оформите его отдельным коммитом. Если изменяется только регистр, сначала переместите файл во временное имя, а затем — в целевое. После этого обязательно клонируйте репозиторий в новый каталог и выполните сборку там. Не проверяйте исправление в старом рабочем каталоге, на который уже мог повлиять кеш файловой системы.
Сохраняйте исходные данные в журналах
После прохождения через tee, систему сбора журналов или JSON-экранирование исходные байты, вызвавшие ошибку кодировки, могут быть заменены, и в результате останутся только вопросительные знаки или символы замены. Во время диагностики сохраняйте как читаемый журнал, так и побайтовое представление ключевых файлов:
file -I Config/环境.json
xxd -g 1 -l 96 Config/环境.json
plutil -lint App/Info.plist
Не считайте результат file окончательным выводом: эта команда даёт лишь подсказку. Программа, которая действительно читает файл, всё равно должна явно использовать UTF-8 и завершаться с ненулевым кодом при ошибке декодирования. Скрипт сборки также не должен игнорировать код завершения первой команды после запуска cmd | tee build.log. В zsh можно проверить $pipestatus либо отделить критически важную команду от сбора журнала.
В журнале также следует сохранять экранированное представление пути, на котором произошёл сбой. Python repr() и Ruby String#dump лучше прямого вывода подходят для обнаружения комбинируемых символов, переводов строк и невидимых пробелов.
Настройте проверку перед слиянием и порядок исправления
Поддерживаемая система проверок должна устанавливать только проверяемые факты и не должна самостоятельно изменять репозиторий. Рекомендуемый порядок:
- Вывести locale и кодировки всех сред выполнения.
- Проверить, что пути в репозитории представлены в NFC.
- Проверить, не возникают ли совпадающие имена после нормализации.
- Прочитать ключевые текстовые конфигурации с явно заданной кодировкой.
- Выполнить минимальную сборку в новом рабочем каталоге.
- Сохранить данные окружения, отчёт о путях и журнал ошибки.
Если задание завершается ошибкой только на одном runner, сначала сравните данные окружения, а затем индекс Git — не очищайте сразу все кеши. Если отчёт о путях обнаружил проблему, сначала исправьте имена файлов и оформите это отдельным коммитом. Если пути корректны, но разбор содержимого не выполняется, проверьте объявленную кодировку файла и параметры чтения. Такой порядок позволяет раздельно диагностировать проблемы окружения, репозитория и инструментов.
В заданиях RentMini эти проверки также следует хранить в репозитории и версионировать вместе с проектом, а не полагаться на ручную настройку конкретного облачного Mac. Когда скрипт точки входа, проверка Unicode и диагностические данные проходят ревью вместе с кодом, пересоздание рабочего каталога или смена узла не меняют поведение сборки.
Часто задаваемые вопросы
Почему скрипт работает в Terminal, но искажает текст в CI?
Неинтерактивное задание может не читать пользовательские файлы настройки оболочки. Задайте LANG и LC_ALL в точке запуска и отдельно проверьте кодировку Ruby, Python и других используемых сред.
Можно ли автоматически переименовать все файлы не в форме NFC?
Сначала следует только сформировать отчёт и остановить проверку. После анализа конфликтов регистра и нормализации выполните git mv отдельным коммитом и повторите сборку из чистого клона.
Подготовьте отдельный физический Mac mini к следующей сборке
Выберите конфигурацию M4, срок аренды и узел. Каждый заказ включает отдельное физическое устройство; актуальный статус доступности отображается в консоли в реальном времени.