Инженерная статья

Контроль дрейфа генерации Swift-кода в облачном Mac CI

Контроль дрейфа генерации Swift-кода в облачном Mac CI

Один и тот же коммит не создаёт изменений на компьютере разработчика, но после запуска в облачном Mac CI в каталоге Generated/ появляются десятки строк различий. Хуже того, проект по-прежнему успешно компилируется, поэтому в сборку могут попасть устаревшие константы, пропущенные ключи ресурсов или неактуальные mock-объекты. Чтобы решить эту проблему, недостаточно просто добавить ещё один запуск генератора: версия инструмента, набор входных данных, каталог вывода и правила проверки должны вместе стать частью контракта сборки.

Сначала определите источник дрейфа

Результат работы SwiftGen, Sourcery и подобных инструментов зависит от четырёх составляющих: версии исполняемого файла, конфигурации и шаблонов, входных файлов и среды выполнения. Зафиксировать только конфигурационный файл недостаточно. Версия инструмента в PATH на машине разработчика может оказаться новее, чем в CI; порядок обхода файлов может зависеть от состояния каталога, а региональные настройки — влиять на сортировку или формат дат.

Сначала зафиксируйте минимальный набор сведений о среде локально и в CI:

set -euo pipefail

sw_vers
xcodebuild -version
swiftgen --version
sourcery --version
printf 'LANG=%s
' "${LANG:-unset}"
printf 'PWD=%s
' "$PWD"
git status --short

Цель здесь не в том, чтобы собрать как можно больше информации, а в том, чтобы установить, какие данные фактически использовал процесс генерации. Если шаблон записывает текущее время, абсолютный путь или случайный идентификатор, сначала исключите эти недетерминированные входные данные. Иначе каждый запуск будет создавать объяснимые, но бесполезные различия.

«Возможность повторно сгенерировать файлы» не означает «возможность получить тот же результат». CI-проверка должна определять, совпадают ли результаты побайтно при одинаковых входных данных.

Создайте воспроизводимую базовую линию генерации

Каталог генерации должен полностью принадлежать инструментам. Не смешивайте в нём автоматически созданные файлы с поддерживаемыми вручную файлами Swift. Удаление старого каталога перед каждым запуском позволяет обнаружить типы, которые уже исключены из входных данных, но сохранились из-за инкрементальной генерации. Для конфигураций, шаблонов и входных каталогов используйте однозначные пути относительно корня репозитория, не зависящие от текущего каталога вызывающего процесса.

Распределить ответственность можно следующим образом:

Компонент Способ фиксации Проверка в CI
Генератор Зафиксировать и выводить версию Совпадает с версией, объявленной в репозитории
Конфигурация и шаблоны Хранить в системе контроля версий В рабочем дереве нет временных изменений
Входные данные Явно указать каталоги и расширения Отсортированный набор остаётся стабильным
Выходные данные Использовать отдельный каталог Очистить перед генерацией
Среда Зафиксировать региональные настройки Не записывать время и абсолютные пути

Способ установки инструментов может различаться в разных командах, но источник объявления версии должен быть только один. Нельзя допускать, чтобы скрипт указывал одну версию, документация для разработчиков — другую, а облачный Mac использовал третью из PATH.

Проверяйте различия до компиляции

Разместите генерацию перед компиляцией: так сбой обнаружится быстрее, а ошибки компиляции не скроют истинную причину. Следующий скрипт предполагает, что все сгенерированные файлы находятся в Generated/ и уже добавлены в репозиторий:

set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

export LANG=C
export LC_ALL=C

rm -rf Generated
mkdir -p Generated build

swiftgen config run --config swiftgen.yml
sourcery --config sourcery.yml

find Generated -type f -print |
  LC_ALL=C sort |
  while IFS= read -r file; do
    shasum -a 256 "$file"
  done > build/generated.sha256

git diff --exit-code -- Generated
git status --short --untracked-files=all Generated

Команда git diff видит изменения только в отслеживаемых файлах, поэтому необходимо также проверять неотслеживаемые файлы. В реальной CI-проверке любой вывод git status должен приводить к явному сбою, а патч различий и файл generated.sha256 следует сохранять как артефакты сборки. Список хешей не заменяет проверку различий, но позволяет точно определить, какой набор файлов был создан в конкретном запуске CI.

Различайте три типа сбоев

Если изменилось только форматирование, проверьте переносы строк в шаблонах, версию инструмента и порядок запуска форматтера. Если файлы добавились или исчезли, проверьте набор входных данных и логику очистки. Если изменились значения в содержимом, проверьте ресурсы, описания интерфейсов и параметры шаблонов. Сначала классифицируйте сбой и устраните его первопричину. Не перезаписывайте результаты прямо в CI с последующим продолжением компиляции.

Избегайте проблем параллельного выполнения на этапах сборки Xcode

Если несколько target используют один каталог генерации, добавление команды генерации в отдельный Run Script Phase каждого target может привести к одновременной записи в один и тот же файл. Иногда это лишь дублирует работу, но в других случаях создаёт обрезанные или временно отсутствующие файлы. Надёжнее сделать генерацию отдельным предварительным этапом CI и запускать все задачи xcodebuild только после её завершения.

Если генерацию необходимо выполнять на этапе сборки Xcode, у неё должен быть только один владелец с полным списком входных и выходных файлов. Сам скрипт генерации не должен изменять файлы проекта или очищать каталог генерации одновременно с его чтением. Если параллельным сборкам требуются независимые выходные данные, создавайте временный каталог для каждого идентификатора задачи, а после успешной проверки публикуйте результат в постоянный каталог одной атомарной заменой.

Ещё одна распространённая ошибка — запускать форматтер сразу после генерации, когда локальный pre-commit hook выполняет эти операции в обратном порядке. Генерация, форматирование и проверка различий должны всегда выполняться в одном порядке через один и тот же входной скрипт.

Контрольный список перед объединением и при диагностике сбоев

При изменении генератора или входных данных во время code review необходимо проверить как минимум следующее:

  1. Синхронно ли обновлено объявление версии генератора.
  2. Включены ли изменения конфигурации, шаблонов и входных данных в один коммит.
  3. Действительно ли соответствующие сгенерированные файлы исчезают после удаления входных данных.
  4. Не попали ли в результаты время, имя пользователя или абсолютный путь.
  5. Остаётся ли второй запуск без различий после двух последовательных локальных запусков.
  6. Проверяет ли CI отслеживаемые и неотслеживаемые файлы до компиляции.
  7. Записывают ли параллельные задачи результаты в изолированные друг от друга каталоги.
  8. Сохраняются ли при сбое сведения о версиях, патч различий и список хешей.

При диагностике не запускайте сначала автоматическое исправление с перезаписью исходного состояния. Сначала сохраните git status, версии инструментов и различия, а затем повторно запустите единый процесс генерации из чистой рабочей копии. Если результаты различаются даже в чистой копии, причина связана со средой или незафиксированными входными данными. Если сбой возникает только в повторно используемом рабочем каталоге, в первую очередь проверьте старые файлы, кеши и параллельную запись. Итоговый критерий прост: при двух последовательных запусках генерации одного и того же коммита в любой задаче облачного Mac второй запуск не должен создавать наблюдаемых различий.

Часто задаваемые вопросы

Нужно ли хранить сгенерированные Swift-файлы в репозитории?

Да, если они нужны разработчикам или проверке кода без запуска генератора. В таком случае CI должен заново создать файлы и завершиться ошибкой при наличии различий.

Почему локальный результат отличается от результата в CI?

Обычно расходятся версии генератора, порядок входных файлов, локаль, окончания строк или рабочий каталог. Все эти параметры следует сделать явными и проверить до компиляции.

VMMini M4

Запустите следующую задачу на выделенном физическом узле

Выберите узел и период оплаты, затем начните развертывание с фиксированной конфигурацией M4, 16GB RAM и 256GB SSD. Фактическая доступность отображается в консоли в реальном времени.

Арендовать облачный Mac сейчас