Один и тот же коммит успешно собирается локально, но на постоянно работающем облачном Mac время от времени появляется ошибка module file was created by a different version of the compiler. Иногда повторный запуск снова проходит успешно. Обычно причина не в случайных изменениях исходного кода Swift, а в том, что разные наборы инструментов, SDK или параллельные задачи обращаются к одному и тому же кэшу предварительно скомпилированных модулей. Полное удаление DerivedData может временно устранить симптом, но при этом уничтожит важнейшие данные для диагностики и не предотвратит повторное загрязнение кэша.
Сначала убедитесь, что проблема связана с кэшем модулей
Clang компилирует системные заголовки и модули Objective-C в PCM-файлы, а компилятор Swift поддерживает собственный кэш модулей. Если компилятор, SDK, архитектура или параметры сборки, использованные при создании кэша, отличаются от параметров текущей задачи, загрузка модулей может завершиться ошибкой.
Сначала найдите в полном журнале следующие признаки:
| Признак в журнале | Наиболее вероятная причина | Что проверить в первую очередь |
|---|---|---|
different version of the compiler |
Выполнялось переключение между версиями Xcode | xcodebuild -version |
PCM file is out of date |
Изменились SDK или временные метки заголовочных файлов | Путь к текущему SDK |
could not build module |
Повреждён вышестоящий модуль | Первый модуль, завершившийся ошибкой |
| Повторный запуск той же задачи проходит успешно | Параллельная запись или общий кэш | Владельца каталога кэша |
Не ограничивайтесь последней строкой ошибки. Сбой загрузки модуля нередко вызывает десятки каскадных ошибок. Просмотрите журнал выше и найдите первое упоминание module, .pcm или ModuleCache.
Перед очисткой кэша сохраните журнал неудачной сборки, номер сборки Xcode и фактические параметры сборки. Без этих данных даже после успешного восстановления невозможно определить, чем была вызвана проблема: сменой набора инструментов, загрязнением из-за параллельного доступа или повреждением самого кэша.
Сохраните сопоставимые диагностические данные неудачной задачи
На облачном Mac от VMKeep сначала зафиксируйте каталог разработчика, в который фактически разрешается команда, а не только версию, которую предполагается использовать в сценарии.
set -o pipefail
mkdir -p artifacts/diagnostics
xcode-select -p | tee artifacts/diagnostics/developer-dir.txt
xcodebuild -version | tee artifacts/diagnostics/xcode-version.txt
xcrun --sdk iphoneos --show-sdk-path | tee artifacts/diagnostics/sdk-path.txt
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings \
> artifacts/diagnostics/build-settings.txt
Затем проверьте значения SDKROOT, ARCHS, DEVELOPER_DIR, CLANG_MODULE_CACHE_PATH и SWIFT_MODULE_CACHE_PATH в завершившейся ошибкой команде. Если задача запускается через sudo, удалённый сеанс или процесс-планировщик, также сравните пользователя выполнения и значение HOME. При запуске одного и того же сценария от имени другого пользователя меняется и стандартное расположение кэша.
Зафиксируйте точку входа в набор инструментов
Сценарий сборки должен явно задавать DEVELOPER_DIR и выполнять xcodebuild -version перед началом фактической компиляции. Если версия не соответствует ожидаемой, немедленно завершите задачу, чтобы она не записывала данные в общий кэш с использованием неправильного набора инструментов. После обновления Xcode необходимо создать новый ключ кэша, а не продолжать использовать старый каталог.
Ограничьте кэш одной задачей
Надёжный ключ каталога должен включать как минимум номер сборки Xcode, SDK, архитектуру и идентификатор задачи. Имя ветки нельзя напрямую использовать в качестве пути: сначала необходимо преобразовать специальные символы. В следующем варианте каждая задача получает собственные каталоги DerivedData и кэша модулей, а каталог загрузки зависимостей хранится отдельно.
set -euo pipefail
JOB_KEY="$(printf '%s' "${CI_JOB_ID:-local}" | tr -cs 'A-Za-z0-9._-' '-')"
XCODE_BUILD="$(xcodebuild -version | awk '/Build version/{print $3}')"
SDK_BUILD="$(xcrun --sdk iphoneos --show-sdk-build-version)"
CACHE_KEY="${XCODE_BUILD}-${SDK_BUILD}-arm64-${JOB_KEY}"
ROOT="$HOME/BuildCaches/$CACHE_KEY"
DERIVED="$ROOT/DerivedData"
MODULES="$ROOT/Modules"
PACKAGES="$HOME/BuildCaches/SourcePackages"
mkdir -p "$DERIVED" "$MODULES/clang" "$MODULES/swift" "$PACKAGES"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath "$DERIVED" \
-clonedSourcePackagesDirPath "$PACKAGES" \
CLANG_MODULE_CACHE_PATH="$MODULES/clang" \
SWIFT_MODULE_CACHE_PATH="$MODULES/swift" \
build
Долгоживущие ветки могут повторно использовать собственный стабильный ключ, а временные запросы на слияние следует изолировать на уровне задачи. Не допускайте, чтобы два одновременно работающих процесса xcodebuild записывали данные в один каталог модулей. Также не следует сохранять кэш модулей в виде универсального архива, пригодного для восстановления с разными наборами инструментов.
Выполняйте точечную очистку вместо полного сброса
Убедившись, что проблема связана с кэшем модулей, сначала остановите использующие этот каталог процессы сборки, а затем последовательно расширяйте область очистки:
- Удалите каталоги
Modules/clangиModules/swiftтекущей задачи. - Сохраните
SourcePackages, затем повторно выполните разрешение зависимостей и сборку. - Если индекс или промежуточные артефакты по-прежнему ссылаются на старые модули, удалите DerivedData текущей задачи.
- Если ошибка возникает только для одного стороннего модуля, проверьте, поддерживает ли его бинарный артефакт текущие архитектуру и SDK. Не маскируйте проблему совместимости многократной очисткой кэша.
Очистка должна быть строго ограничена вычисленным каталогом задачи. Команда rm -rf ~/Library/Developer/Xcode/DerivedData/* затронет и другие задачи, из-за чего локальный сбой может привести к холодному запуску всей машины.
Защитите используемые каталоги от случайного удаления
Перед удалением старого кэша убедитесь с помощью блокировки задачи или журнала выполнения, что каталог больше не используется. Не полагайтесь только на время создания каталога: стабильная ветка может долго повторно использовать ранее созданный кэш. Сценарий очистки также должен проверять, что целевой путь находится внутри ожидаемого корневого каталога, и немедленно завершаться при пустой переменной или ошибке вычисления пути.
Превратите согласованность кэша в обязательную проверку сборки
В конечном итоге конвейер должен самостоятельно выявлять расхождения. Для каждой сборки сохраняйте номер сборки Xcode, номер сборки SDK, архитектуру, ключ кэша и пользователя выполнения. После завершения задачи архивируйте эти сведения вместе с журналом ошибки. При обновлении набора инструментов создавайте новое пространство имён, а старый кэш удаляйте только после подтверждения стабильности нового процесса.
Можно также добавить проверочную задачу, которая не восстанавливает кэш модулей. Если обычная задача завершается ошибкой, а чистая проходит успешно, проблема сосредоточена на уровне кэша. Если завершаются ошибкой обе задачи, продолжите диагностику исходного кода, зависимостей или параметров сборки. Такой подход не позволяет безосновательно списывать все ошибки на кэш и избавляет от попыток добиться успеха повторными запусками.
По сути, кэш модулей является частью входных данных компилятора. Если зафиксировать в сценарии версии, владельцев каталогов и границы параллельного доступа, сбои PCM перестанут быть случайными и превратятся в инженерные инциденты, которые можно локализовать и устранить.
Часто задаваемые вопросы
Нужно ли при ошибке PCM удалять весь DerivedData?
Нет. Сначала сохраните журнал и настройки сборки, затем удалите только ModuleCache.noindex текущей задачи. Пересоздавать весь каталог стоит лишь при одновременном повреждении индекса, артефактов и модулей.
Можно ли разным версиям Xcode использовать общий кэш модулей?
Не рекомендуется. Ключ каталога должен включать номер сборки Xcode, SDK, архитектуру и идентификатор задачи, иначе параллельные процессы могут прочитать несовместимый PCM.
Продолжите проверку этого рабочего процесса на облачном Mac
Выберите подходящий объём памяти, хранилище, узел и срок аренды из трёх конфигураций Apple Silicon.