Один и тот же коммит может успешно собираться на машине разработчика, но разрешаться с другим набором зависимостей на другом облачном Mac. Обычно это не случайный сбой компилятора. Чаще всего файл разрешения, версия Xcode и границы кэша не были зафиксированы вместе. Особенно часто проблема проявляется на узлах с краткосрочной арендой: машина предоставляется с чистым каталогом, но конвейер может по умолчанию повторно использовать старый кэш или незаметно перезаписывать версии зависимостей во время сборки.
Определите, что считать одной и той же сборкой
Воспроизводимость не означает, что между запусками нужно сохранять весь рабочий каталог. Она означает, что одинаковые входные данные должны давать одинаковый набор зависимостей. Как минимум фиксируйте четыре параметра: коммит исходного кода, содержимое Package.resolved, номер сборки Xcode и архитектуру процессора. Одной основной версии Xcode недостаточно: версии исправлений набора инструментов могут различаться даже в пределах одного основного выпуска.
Сначала сохраните снимок окружения на узле:
set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD
Для приложений файл Package.resolved, как правило, следует хранить в репозитории. В рабочем пространстве он обычно находится по пути App.xcworkspace/xcshareddata/swiftpm/Package.resolved. Если есть только файл проекта, этот файл также может располагаться во внутреннем каталоге рабочего пространства проекта. Не поддерживайте одновременно две копии файла разрешения. Сначала выясните, какую из них фактически использует конвейер.
Критерий прост: если после выполнения сценария сборки команда
git status --porcelainпоказывает изменение файла разрешения, такую сборку нельзя считать проверкой исходного коммита.
Фиксируйте результат разрешения, а не только диапазоны версий
Диапазоны версий в Package.swift определяют, какие версии SwiftPM разрешено выбирать. Файл Package.resolved фиксирует, что именно было выбрано для конкретной сборки. При первичной подготовке узла зависимости можно разрешить явно, но рабочая сборка должна принимать только уже зафиксированные версии.
set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$CACHE"
git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=macOS' \
-clonedSourcePackagesDirPath "$CACHE" \
-onlyUsePackageVersionsFromResolvedFile \
build
Для цели iOS замените destination на универсальную цель устройства или симулятора, уже заданную в конвейере. Не позволяйте сценарию автоматически выбирать устройство, запущенное в данный момент. Для чистых пакетов Swift можно использовать swift package resolve и swift build, однако проекты и рабочие пространства следует единообразно собирать через xcodebuild, чтобы два разных способа разрешения не создавали разное состояние каталогов.
Разделяйте кэш по дайджесту входных данных
Кэш предназначен только для сокращения времени загрузки и извлечения исходного кода. Он не должен становиться источником версий зависимостей. Надёжный ключ кэша должен включать архитектуру, сведения о сборке Xcode и дайджест файла разрешения:
set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
| Изменение входных данных | Повторно использовать рабочий кэш? | Причина |
|---|---|---|
| Изменился только исходный код приложения | Да | Набор зависимостей не изменился |
Изменился Package.resolved |
Нет | Изменились извлекаемые версии или ревизии |
| Изменился номер сборки Xcode | Нет | Набор инструментов и форматы артефактов могут отличаться |
Переход между arm64 и другой архитектурой |
Нет | Двоичные артефакты нельзя смешивать |
При работе на нескольких узлах не разрешайте нескольким машинам одновременно записывать данные в один каталог. Можно хранить доступные только для чтения архивы кэша по ключам, а затем копировать нужный архив в локальный рабочий каталог каждого физического узла. Перед повторной попыткой после сбоя также следует очистить текущую рабочую копию, а не удалять все исторические архивы.
Немедленно завершайте задачу при дрейфе зависимостей
Проверка состояния репозитория до и после сборки позволяет обнаружить как перезапись файла разрешения, так и случайное попадание сгенерированных артефактов в каталог исходного кода. Рабочая задача должна запускаться из чистой копии репозитория:
set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
-clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
-onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"
После сбоя не снимайте ограничения версий и не запускайте разрешение заново. Это лишь замаскирует ошибку конфигурации под успешную сборку. Сохраните журнал разрешения, снимок окружения и ключ кэша, а затем определите причину: в репозитории отсутствует зафиксированный результат, ревизия зависимости стала недействительной или на узле выбрана неправильная версия Xcode.
Проверяйте типовые причины сбоев по порядку
Тайм-аут на этапе разрешения
Сначала проверьте доступ узла к источникам зависимостей, затем свободное место на диске и права доступа к каталогу кэша. После восстановления сети повторите попытку с тем же файлом разрешения, не обновляя предварительно зависимости. Если сбой каждый раз происходит на одном и том же пакете, отдельно проверьте его объявление и убедитесь, что указанная ревизия по-прежнему ему соответствует.
Разрешение выполнено, но компиляция завершается с ошибкой
Сначала сравните результаты xcode-select -p, xcodebuild -version и uname -m на двух узлах. Для двоичных зависимостей также убедитесь, что они содержат срез для текущей архитектуры и поддерживают минимальную версию системы, заданную в настройках проекта. Удаление локальной копии для соответствующего ключа кэша с последующей повторной сборкой поможет отличить повреждение кэша от несовместимости исходного кода.
Контрольный список перед запуском
- В репозитории находится только один фактически используемый файл
Package.resolved. - В команде сборки включён параметр
-onlyUsePackageVersionsFromResolvedFile. - Ключ кэша содержит архитектуру, сведения о сборке Xcode и дайджест файла разрешения.
- Каждый узел использует отдельный рабочий каталог с правом записи.
- Дайджест файла разрешения до и после сборки совпадает.
- В журналах сохраняются идентификатор коммита, ключ кэша и версия набора инструментов, но не учётные данные.
Часто задаваемые вопросы
Нужно ли добавлять Package.resolved в репозиторий?
Для приложений и развёртываемых сервисов обычно нужно, а сборка должна использовать только записанные версии. Для библиотеки допустима другая политика, но диапазон версий следует проверять в чистом каталоге.
Можно ли нескольким облачным Mac записывать данные в общий кэш SwiftPM?
Одновременная запись в один каталог нежелательна. Ключ кэша должен учитывать архитектуру процессора, номер сборки Xcode и хеш Package.resolved, а каждому узлу нужна отдельная рабочая копия.
Что проверять, если зависимости разрешились, но компиляция завершилась ошибкой?
Сначала проверьте выбранный путь и версию Xcode, затем пересоздайте рабочий кэш. После этого сопоставьте архитектуры двоичных зависимостей, минимальную версию системы и состояние Package.resolved.
Сохраняйте воспроизводимую среду на одном облачном Mac
Выберите фиксированные модель, регион и срок аренды, а затем используйте полноценный графический интерфейс macOS и командную строку для разработки или автоматизации.