Технические заметки VMArm

Изоляция кешей Metro и Watchman для React Native на облачном Mac

Изоляция кешей Metro и Watchman для React Native на облачном Mac

Когда один и тот же облачный Mac последовательно выполняет сборки React Native, сложнее всего диагностировать не ошибку компиляции, а ситуацию, когда «код уже изменён, но результат выглядит так, будто изменений не было». Локальный перезапуск проходит нормально, однако задача непрерывной интеграции время от времени использует старый модуль. После удаления всего репозитория проблема временно исчезает, а через несколько дней возвращается. Обычно причина в том, что не разделены четыре уровня состояния: рабочее дерево Git, отслеживание Watchman, кеш преобразований Metro и Xcode DerivedData.

Сначала определите уровень загрязнения кеша

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

Собирайте диагностические данные каждой задачи

Для каждой задачи записывайте как минимум хеш коммита, абсолютный путь к рабочему каталогу, версии Node и Xcode, команду запуска Metro и путь к DerivedData. Также сохраняйте вывод следующих команд:

set -euo pipefail

printf 'commit=%s\n' "$(git rev-parse HEAD)"
printf 'workspace=%s\n' "$PWD"
node --version
xcodebuild -version
watchman watch-list || true
find "$PWD" -maxdepth 2 -name metro.config.js -print

Если коммит и исходные файлы верны, но JavaScript-бандл остался прежним, в первую очередь проверьте Metro. Если в архиве не обновился нативный код, проверьте DerivedData, конфигурацию сборки и фактически открытый workspace. Если Watchman сообщает об ошибке или отслеживает родительский каталог, сначала исправьте границы отслеживания.

Очистка — это способ устранения проблемы, а не результат диагностики. Сначала определите, на каком уровне произошёл сбой, и только затем удаляйте состояние этого уровня.

Выделяйте каждой задаче отдельный рабочий каталог

Параллельные задачи не должны переключать ветки в одном и том же каталоге checkout. Кроме того, не размещайте несколько задач в общем родительском каталоге, отслеживаемом Watchman. Рекомендуется включать в путь номер конвейера и номер задачи, например /Users/ci/work/4821/ios-release. После завершения задачи весь каталог можно независимо архивировать или удалить.

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

JOB_ROOT="/Users/ci/work/${PIPELINE_ID}/${JOB_ID}"
WORKSPACE="${JOB_ROOT}/repo"
DERIVED_DATA="${JOB_ROOT}/DerivedData"
METRO_CACHE="${JOB_ROOT}/metro-cache"

mkdir -p "$WORKSPACE" "$DERIVED_DATA" "$METRO_CACHE"
export TMPDIR="${JOB_ROOT}/tmp/"
mkdir -p "$TMPDIR"

Используйте в именах каталогов только стабильные символы ASCII, избегая пробелов и динамических символических ссылок. Отдельный корневой каталог для каждой задачи также упрощает аудит использования диска и ответственности за очистку.

Ограничьте область отслеживания Watchman

После перехода в рабочий каталог выполните watchman watch-project "$WORKSPACE" и убедитесь, что возвращённое значение watch соответствует ожидаемому корню проекта. Если Watchman поднимает корень отслеживания до общего родительского каталога, это обычно означает, что границы репозитория определены неясно или в родительском каталоге находится конфигурация, влияющая на их распознавание.

Поместите .watchmanconfig в корень репозитория и исключите крупные каталоги, которые не участвуют в графе зависимостей JavaScript:

{
  "ignore_dirs": [
    ".git",
    "DerivedData",
    "build",
    "artifacts"
  ]
}

После изменения этого файла заново настройте отслеживание текущего проекта. Не выполняйте регулярно watchman watch-del-all на машине с параллельными задачами: эта команда удалит отслеживание и для других задач. Безопасный подход — использовать каждый рабочий каталог как отдельный корень отслеживания, а после завершения задачи выполнять только:

watchman watch-del "$WORKSPACE" || true

Если команда сообщает, что указанный каталог не является корнем отслеживания, сначала проверьте watchman watch-list. Не пытайтесь угадать путь и не удаляйте отслеживаемые каталоги массово.

Управляйте кешами Metro и Xcode явно

Кеш Metro должен быть привязан к задаче или конкретному состоянию кода, а не зависеть от файлов неизвестного происхождения в системном временном каталоге. Скрипт запуска должен явно передавать расположение кеша. Точное имя параметра может зависеть от версии Metro, поэтому сверяйтесь с выводом справки для версии, зафиксированной в проекте. Независимо от того, используется параметр командной строки или конфигурация проекта, кеш должен находиться в ${METRO_CACHE}, а не в общем каталоге по умолчанию для нескольких задач.

Параметр --reset-cache следует использовать только для однократного восстановления после подтверждённого повреждения кеша преобразований. Если кеш сбрасывается в каждой задаче, значит изоляция ещё не реализована полностью. Для Xcode аналогично задайте расположение DerivedData в каталоге задачи:

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DATA" \
  build

Для Pods, пакетов Swift и зависимостей JavaScript можно использовать проверенный кеш загрузок, однако «кеш загрузок» необходимо отделять от «состояния сборки». Первый можно повторно использовать по ключу, сформированному из lock-файлов, тогда как второе должно принадлежать только одной задаче. После восстановления кеша как минимум сверьте хеши lock-файлов — совпадения одного лишь имени ветки недостаточно.

Используйте поуровневую очистку вместо полного сброса

При возникновении аномалии начинайте с уровня, очистка которого оказывает наименьшее влияние. Сначала остановите текущий процесс Metro, удалите кеш Metro этой задачи и перезапустите процесс. Если проблема не исчезла, удалите отслеживание Watchman для текущего рабочего каталога и зарегистрируйте его заново. DerivedData задачи следует удалять только при несоответствии результатов нативной компиляции. Повторный checkout репозитория рассматривайте в последнюю очередь.

Перед очисткой убедитесь, что ни один процесс больше не использует каталог:

pgrep -af "$WORKSPACE" || true
lsof +D "$JOB_ROOT" 2>/dev/null | head -n 50 || true

Скрипт завершения задачи также должен проверять, что путь к рабочему каталогу находится внутри ожидаемого корня. Это предотвращает случайное удаление пользовательского каталога, если переменная окажется пустой. Можно проверить префикс пути по белому списку с помощью case, а затем выполнить rm -rf -- "$JOB_ROOT".

Для проверки дважды подряд соберите один и тот же коммит. Во второй сборке можно повторно использовать кеш загрузок, но рабочий каталог, состояние Metro и DerivedData всё равно должны оставаться изолированными на уровне задачи. Если результаты двух сборок различаются, сначала сравните сгенерированные файлы, переменные окружения и журналы сборки, не расширяя немедленно область очистки. Такой процесс не только устраняет конкретную проблему с кешем, но и сохраняет достаточно данных, чтобы объяснить следующее расхождение.

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

Нужно ли запускать Metro с reset-cache при каждой сборке?

Нет. Используйте отдельный каталог кеша для каждого задания, а полный сброс выполняйте только при подтверждённом повреждении графа модулей или кеша преобразований.

Можно ли нескольким заданиям использовать один корень Watchman?

Не рекомендуется. Рабочие каталоги параллельных заданий не должны быть вложены друг в друга, а каждый каталог следует регистрировать и удалять отдельно.

Выделенный физический узел Mac

Выберите облачный Mac, который будет постоянно доступен для следующей сборки Xcode

Проверьте чип, память, хранилище, расчётный период и регион узла, затем перейдите к настройке. Фактическая доступность отображается в консоли в реальном времени.

Выбрать конфигурацию облачного Mac