Устранение блокировок DerivedData в параллельных сборках Xcode
Если на одном облачном Mac одновременно работают два конвейера Xcode, периодические ошибки database is locked, повреждение базы индекса или ошибки компоновки, которые не удаётся воспроизвести локально, часто не связаны с изменениями кода. Сначала проверьте, не используют ли обе задачи стандартный каталог ~/Library/Developer/Xcode/DerivedData. В нём хранятся не только кэши, но и база сборки, индекс, кэш модулей и промежуточные данные, которые постоянно перезаписываются во время выполнения задачи. Если считать этот каталог общим кэшем, пригодным для параллельной записи, сбой становится лишь вопросом времени.
Сначала определите источник конфликта
Не удаляйте все кэши сразу после первого сбоя. Сначала сохраните полную команду xcodebuild завершившейся с ошибкой задачи, время её запуска, рабочий каталог и список процессов. Затем найдите в журналах строки с locked, database, unable to attach, malformed и сообщениями о повторном создании одних и тех же артефактов.
pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log
Если обе задачи используют одинаковый -derivedDataPath или вообще не задают этот параметр явно, основной риск уже обнаружен. Также проверьте, не завершает ли конвейер при превышении лимита времени только внешний скрипт, оставляя xcodebuild или дочерние процессы компилятора записывать данные в каталог.
Успешный повторный запуск не доказывает, что проблема устранена. Если окно конкурирующей записи сократилось, вторая сборка может случайно завершиться успешно, хотя несколько процессов по-прежнему используют один каталог для записи.
Отличайте конфликт блокировок от обычной ошибки компиляции
Ошибки компиляции исходного кода обычно стабильно возникают в одном и том же файле и на одной и той же строке. Конфликты блокировок сильнее зависят от порядка выполнения параллельных процессов и могут случайным образом проявляться при разрешении зависимостей, создании модулей или компоновке. Если после отключения параллельных задач несколько сборок подряд проходят успешно, а при возврате параллельного выполнения снова возникают ошибки, сначала проверьте принадлежность каталогов, а не изменяйте код приложения.
Выделите каждой задаче отдельное рабочее пространство
Надёжный подход — использовать уникальный идентификатор задачи в путях к рабочей копии репозитория, DerivedData, пакетам результатов и временным каталогам. В конвейере таким идентификатором может служить номер задачи, а при локальной проверке — идентификатор процесса.
set -euo pipefail
JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"
mkdir -p "$DERIVED_DATA"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath "$RESULT_BUNDLE" \
build-for-testing
test -d "$RESULT_BUNDLE"
Каталог должен создаваться конкретной задачей и удаляться только ею. Не используйте название проекта как единственное имя каталога: две ветки одного проекта всё равно будут конфликтовать. Скрипты очистки также не должны применять нечёткие шаблоны — например, удалять все каталоги DerivedData, в названии которых встречается App.
Отделите разрешение зависимостей от компиляции
Если после изоляции DerivedData несколько задач продолжают одновременно изменять один каталог с исходным кодом зависимостей, конфликт переместится на этап разрешения пакетов. Сначала разрешите зависимости, а затем запретите автоматически менять их версии во время основной сборки. Проверенный файл блокировки должен храниться в репозитории, а конвейер не должен незаметно обновлять его.
PACKAGES="$ROOT/SourcePackages"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$PACKAGES"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "$PACKAGES" \
-disableAutomaticPackageResolution \
build
Если кэширование необходимо, сохраняйте результаты загрузки или проверенные неизменяемые снимки, а не разрешайте нескольким задачам записывать данные в один активный каталог. Ключ кэша должен включать как минимум контрольную сумму файла блокировки, версию Xcode и целевую архитектуру. Даже после успешного получения данных из кэша необходимо убедиться, что файл блокировки не изменился.
Составьте таблицу с однозначными критериями приёмки
Нельзя считать исправление подтверждённым после одного успешного запуска. Одновременно выполните несколько задач для одного коммита и проверьте границы каталогов, процессов и артефактов.
Рекомендуется одновременно запустить как минимум две полностью одинаковые сборки, а затем группу сборок из разных веток. Первый тест проверяет изоляцию записи, второй помогает обнаружить рабочие каталоги, кэши модулей или пути артефактов, которые по-прежнему совместно используются на основании одного лишь названия проекта.
Сохраняйте данные для анализа и безопасно выполняйте очистку
После сбоя не следует сразу уничтожать данные задачи. Сначала заархивируйте журнал сборки, пакет результатов, версию Xcode, контрольную сумму файла блокировки, объём свободного места на диске и путь задачи. Исходный код, переменные окружения и токены в журналах необходимо предварительно обезличить. Данные успешной задачи можно удалить после подтверждения, что артефакты скопированы в отдельный каталог публикации.
Очистку лучше ограничить корневым каталогом задачи и дополнить проверкой префикса пути. Если задача была принудительно завершена, последующий процесс очистки должен удалить оставшиеся каталоги по идентификатору задачи, а не очищать каталог разработчика на всей машине. При одновременном запуске нескольких конвейеров на NUMACS также следует сначала проверить в консоли доступные конфигурации, а затем установить лимит задач в соответствии с требуемым уровнем параллелизма. Увеличение параллелизма не заменяет изоляцию записи.
Конечная цель — не сделать ошибки «менее частыми», а установить чёткие инварианты: у каждой параллельной задачи есть только один процесс записи, версии зависимостей не меняются во время сборки, состояние после сбоя можно исследовать, а очистка не затрагивает другие задачи. Только после выполнения этих четырёх условий DerivedData перестаёт быть источником случайных сбоев и снова становится управляемым набором данных сборки.
Часто задаваемые вопросы
Нужен ли отдельный DerivedData для каждого задания?
Да. Все одновременно выполняемые задания должны получать уникальный путь через -derivedDataPath. Общий путь допустим только при строгом последовательном запуске.
Исчезнет ли ускорение от кэша после изоляции?
Нет. Загруженные зависимости и проверенные неизменяемые артефакты можно хранить отдельно, не разделяя записываемый DerivedData между заданиями.
Можно ли просто повторить сборку после database is locked?
Повтор не устраняет причину и может снова прочитать смешанные данные. Сначала изолируйте пути и завершите оставшиеся процессы сборки.
Numacs облачный Mac
Перенесите задачи сборки на выделенную физическую рабочую станцию
Две конфигурации Apple Silicon доступны на выделенных физических машинах, а не на виртуальных, с арендой на день, неделю, месяц или квартал в Сингапуре, Токио, Сеуле и Гонконге.