Инженерные заметки

Сброс рабочей копии Cloud Mac клонами APFS

Сброс рабочей копии Cloud Mac клонами APFS

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

Какую проблему решают клоны APFS

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

Это не то же самое, что жёсткие ссылки: у клонированных файлов отдельные записи каталогов, поэтому изменение копии не затрагивает исходный файл. Клон также не является резервной копией, поскольку база и её копия по-прежнему находятся на одном томе. При повреждении тома, случайном удалении базы или потере всех данных компьютера клон не позволит выполнить удалённое восстановление.

Рассматривайте клоны APFS как механизм недорогого создания рабочих копий, а не как гарантию попадания в кеш, систему снимков или средство защиты данных.

Команда du -sh показывает логический размер каталога, поэтому по её результату нельзя напрямую определить, сколько дополнительного физического пространства занял клон. При контроле использования диска также проверяйте свободное место на томе APFS и задавайте порог очистки с учётом того, что задача продолжает записывать данные.

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

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

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
mkdir -p "$ROOT"

git -C "$BASE" status --porcelain
git -C "$BASE" submodule status --recursive
git -C "$BASE" rev-parse HEAD > "$BASE/.baseline-revision"
test -z "$(git -C "$BASE" status --porcelain)"

Файл .baseline-revision сам станет неотслеживаемым, поэтому его следует хранить за пределами репозитория или заранее добавить в согласованные командой правила игнорирования. Более надёжная структура — сохранять идентификатор коммита в $ROOT/metadata, чтобы $BASE всегда проходил проверку чистоты рабочего дерева.

Что не следует включать в базовую копию

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

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

Создавайте отдельный клон для каждой задачи

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

set -euo pipefail

ROOT="$HOME/ci-workspaces"
BASE="$ROOT/baseline"
JOB_ID="${BUILD_ID:?BUILD_ID is required}"

case "$JOB_ID" in
  *[!A-Za-z0-9._-]*) exit 64 ;;
esac

test "$(stat -f %T "$ROOT")" = "apfs"

WORK="$ROOT/jobs/$JOB_ID"
DERIVED="$ROOT/derived/$JOB_ID"

test ! -e "$WORK"
mkdir -p "$ROOT/jobs" "$DERIVED"
cp -cR "$BASE" "$WORK"

xcodebuild \
  -workspace "$WORK/App.xcworkspace" \
  -scheme App \
  -derivedDataPath "$DERIVED" \
  build

Команда cp -cR запрашивает клонирование, но фактическое поведение всё равно зависит от исходного и целевого путей, а также файловой системы. Исходный и целевой каталоги должны находиться на одном томе APFS. Если копирование выполняется между томами, его нельзя считать процессом копирования при записи. Перед внедрением можно создать тестовый каталог с большим файлом и проверить время копирования и изменение занятого пространства на томе.

Изолируйте параллельные задачи и изменяемое состояние

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

Задайте бюджет дискового пространства для параллельных задач

Сразу после создания клон занимает мало дополнительного места, однако объектные файлы компиляции, перезаписываемые зависимости и архивы быстро создают новые блоки. Ограничение параллелизма следует рассчитывать по максимальному объёму записываемых изменений, а не по логическому размеру базовой копии. Перед запуском задачи можно проверять свободное место на томе, а после завершения сборки — фиксировать изменение:

df -h "$ROOT"
du -sh "$DERIVED" "$WORK"

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

Безопасно удаляйте рабочие копии и регулярно обновляйте базу

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

set -euo pipefail

ROOT="$HOME/ci-workspaces"
WORK="$ROOT/jobs/${BUILD_ID:?BUILD_ID is required}"
DERIVED="$ROOT/derived/${BUILD_ID:?BUILD_ID is required}"

case "$WORK" in
  "$ROOT"/jobs/*) rm -rf -- "$WORK" ;;
  *) exit 64 ;;
esac

case "$DERIVED" in
  "$ROOT"/derived/*) rm -rf -- "$DERIVED" ;;
  *) exit 64 ;;
esac

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

Итоговая проверка включает четыре пункта: коммит базовой копии можно отследить, а её рабочее дерево остаётся чистым; у каждой задачи есть уникальное рабочее пространство и каталог генерируемых данных; свободного места на томе достаточно для максимального объёма параллельной записи; логика очистки принимает только контролируемые пути. Лишь при соблюдении этих условий клоны APFS становятся воспроизводимым этапом инженерного процесса, а не просто приёмом копирования, который однажды показался быстрым.

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

Может ли клон APFS заменить кэш сборки или резервную копию?

Нет. Клон ускоряет создание изменяемой копии на том же APFS, но изменённые блоки занимают новое место. Для восстановления после потери тома нужна отдельная резервная копия.

Почему DerivedData не следует хранить в базовом каталоге?

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

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

Продолжите проверку этого рабочего процесса на облачном Mac

Выберите подходящий объём памяти, хранилище, узел и срок аренды из трёх конфигураций Apple Silicon.

Выбрать конфигурацию и оформить заказ