Архив, одновременно содержащий приложение iOS и приложение watchOS, может оказаться непригодным для доставки даже после успешной сборки основного приложения. Приложение для часов могло не попасть в пакет, номер его сборки мог отстать на одну версию, расширение могло по-прежнему ссылаться на старый Bundle ID, а один из вложенных компонентов мог быть подписан неправильно. Такие проблемы не следует обнаруживать лишь на этапе экспорта или отправки. Надёжнее проверять итоговую структуру пакета на облачном Mac после каждого создания xcarchive и использовать результат как контрольный барьер конвейера.
Зачем проверять итоговый архив
Файлы проекта описывают ожидаемый результат, но реальным поставляемым артефактом является xcarchive. Профили, сценарии сборки и переменные окружения могут переопределять номер версии или идентификатор продукта. По одним только настройкам проекта нельзя доказать, что приложение watchOS действительно находится в каталоге Watch основного приложения, а расширение правильно связано с внешним приложением.
Объектом проверки рекомендуется сделать архив Release, а не промежуточный каталог DerivedData. Сначала создайте артефакт с теми же рабочей областью, Scheme и конфигурацией, которые используются в рабочем конвейере:
set -euo pipefail
ROOT="$PWD"
OUT="$ROOT/out"
ARCHIVE="$OUT/Client.xcarchive"
rm -rf "$ARCHIVE"
mkdir -p "$OUT"
xcodebuild \
-workspace Client.xcworkspace \
-scheme Client \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
clean archive
Команда clean archive подходит для базовой приёмочной проверки. В повседневном конвейере её можно адаптировать к стратегии кэширования, но сценарий проверки всегда должен читать архив, созданный текущим заданием, а не обращаться к какому-либо «последнему созданному» каталогу.
Контрольный барьер должен отвечать на вопрос «что находится в пакете, подготовленном к доставке сейчас», а не «что проект теоретически должен создать».
Формирование списка компонентов по структуре пакета
Типичный путь выглядит как Products/Applications/*.app. Внутренний каталог Watch содержит приложение watchOS, а каталог PlugIns приложения для часов — его расширение. Не задавайте имя продукта в сценарии жёстко: после переименования Target фиксированный путь легко перестаёт работать.
IOS_APP=$(find "$ARCHIVE/Products/Applications" \
-maxdepth 1 -type d -name '*.app' -print -quit)
test -n "${IOS_APP:-}" || {
printf '%s
' "iOS app not found"
exit 1
}
WATCH_APP=$(find "$IOS_APP/Watch" \
-maxdepth 1 -type d -name '*.app' -print -quit)
test -n "${WATCH_APP:-}" || {
printf '%s
' "watchOS app not found"
exit 1
}
WATCH_EXTENSION=$(find "$WATCH_APP/PlugIns" \
-maxdepth 1 -type d -name '*.appex' -print -quit)
printf 'ios=%s
watch=%s
extension=%s
' \
"$IOS_APP" "$WATCH_APP" "${WATCH_EXTENSION:-embedded}"
В некоторых новых проектах отдельного уровня .appex нет, поэтому отсутствие расширения само по себе нельзя считать ошибкой. Контрольный барьер должен учитывать структуру продукта, объявленную в репозитории: для старой структуры обязательны три уровня компонентов, а для более новой структуры с одним Target достаточно приложений iOS и watchOS. Вместо определения типа проекта по каталогам можно хранить в репозитории короткий список ожидаемых компонентов.
Проверка версий и связей между идентификаторами
Сначала выполните plutil -lint для каждого Info.plist, а затем прочитайте CFBundleIdentifier, CFBundleShortVersionString и CFBundleVersion. В рамках одного выпуска маркетинговые версии и номера сборок iOS и watchOS обычно должны полностью совпадать. Если команда использует независимые номера сборок, допустимые правила также следует явно зафиксировать в сценарии.
| Проверка | Рекомендуемое правило | Риск при нарушении |
|---|---|---|
| Маркетинговая версия | Одинакова для iOS и watchOS | Несогласованность версий в магазине |
| Номер сборки | Одинаков в рамках одного задания выпуска | Приложение для часов определяется как старая сборка |
| Идентификатор связанного приложения | Указывает на ID основного приложения iOS | После установки невозможно установить связь между приложениями |
| Принадлежность расширения | Указывает на ID текущего приложения watchOS | Расширение связано не с тем внешним приложением |
read_plist() {
/usr/libexec/PlistBuddy -c "Print :$2" "$1"
}
IOS_PLIST="$IOS_APP/Info.plist"
WATCH_PLIST="$WATCH_APP/Info.plist"
plutil -lint "$IOS_PLIST" "$WATCH_PLIST"
IOS_ID=$(read_plist "$IOS_PLIST" CFBundleIdentifier)
WATCH_ID=$(read_plist "$WATCH_PLIST" CFBundleIdentifier)
IOS_VERSION=$(read_plist "$IOS_PLIST" CFBundleShortVersionString)
WATCH_VERSION=$(read_plist "$WATCH_PLIST" CFBundleShortVersionString)
IOS_BUILD=$(read_plist "$IOS_PLIST" CFBundleVersion)
WATCH_BUILD=$(read_plist "$WATCH_PLIST" CFBundleVersion)
COMPANION_ID=$(read_plist "$WATCH_PLIST" WKCompanionAppBundleIdentifier)
test "$IOS_VERSION" = "$WATCH_VERSION"
test "$IOS_BUILD" = "$WATCH_BUILD"
test "$COMPANION_ID" = "$IOS_ID"
Не заменяйте точное сравнение правилом «Bundle ID watchOS должен начинаться с Bundle ID iOS». Команда может использовать явно заданные идентификаторы, а сходство префиксов не доказывает правильность связи. Ожидаемое значение следует читать из конфигурации выпуска и проверять полное содержимое WKCompanionAppBundleIdentifier.
Поддержка структуры с расширением
Если архив содержит .appex, дополнительно прочитайте его WKAppBundleIdentifier и потребуйте, чтобы это значение совпадало с Bundle ID текущего приложения watchOS. Также записывайте версию и номер сборки самого расширения, чтобы не пропустить Target, к которому не был применён общий сценарий версионирования.
Проверка подписи и архитектур исполняемых файлов
Успешная подпись внешнего приложения не означает, что каждый вложенный компонент пройдёт проверку. Вместо рекурсивной проверки только основного приложения лучше перечислить фактические компоненты, чтобы в журнале был однозначно указан объект сбоя:
verify_component() {
local item="$1"
codesign --verify --strict --verbose=2 "$item"
codesign -d --entitlements :- "$item" >/dev/null
}
verify_component "$IOS_APP"
verify_component "$WATCH_APP"
if test -n "${WATCH_EXTENSION:-}"; then
verify_component "$WATCH_EXTENSION"
fi
Затем найдите двоичный файл каждого компонента с помощью CFBundleExecutable и запишите его архитектуры командой file или lipo -archs. Необязательно навсегда фиксировать в контрольном барьере конкретный набор архитектур, поскольку цепочка инструментов и целевые версии развёртывания меняются. Надёжнее отклонять пустые исполняемые файлы и неожиданные артефакты симулятора, сохраняя текущий список архитектур для проверки.
Следует также проверять вложенные компоненты на символические ссылки, выходящие за границы пакета, повторяющиеся Bundle ID и дополнительные необъявленные расширения. Такие проблемы часто возникают из-за слишком широких шаблонов в сценариях копирования.
Превращение проверки в стабильный контрольный барьер
Код завершения сценария отвечает за блокировку, а отчёт — за объяснение причины. Для каждого задания следует сохранять относительные пути компонентов, Bundle ID, версии, номера сборок, архитектуры, статус проверки подписи и контрольную сумму архива. Не включайте в отчёт закрытые ключи сертификатов, полный набор переменных окружения или полные пути к пользовательским каталогам.
Рекомендуется разделить процесс на четыре этапа:
- Создать уникальный каталог архива и запретить повторное использование предыдущего артефакта.
- Обнаружить компоненты и сравнить их с ожидаемой структурой из репозитория.
- Проверить метаданные, поля связей, подписи и архитектуры исполняемых файлов.
- Сформировать текстовый отчёт или отчёт JSON, а затем запустить экспорт.
При первом включении контрольного барьера можно несколько раз запускать его только для регистрации результатов, без блокировки, чтобы убедиться в поддержке как старой, так и новой структуры проектов. После стабилизации правил отсутствие компонентов, несовпадение версий и ошибки подписи следует сделать критическими ошибками. Если один облачный Mac параллельно создаёт несколько архивов, каждому заданию необходимо назначить отдельные archivePath и каталог отчётов, чтобы одно задание не прочитало результаты другого.
Конечная цель состоит не в добавлении ещё одной формальной проверки, а в том, чтобы конвейер мог доказать: основное приложение, приложение для часов и его расширение действительно созданы в рамках одного выпуска, их взаимосвязи определены однозначно, а итоговый архив готов к переходу на этап экспорта.
Часто задаваемые вопросы
Достаточно ли проверить только настройки проекта?
Нет. Только готовый архив показывает результат подстановки конфигураций, работы скриптов, фактического встраивания компонентов и подписания.
Должны ли совпадать версии приложений iOS и watchOS?
Для одного выпуска обычно совпадают маркетинговая версия и номер сборки. Исключения следует явно описать и проверять отдельным правилом CI.
Запускайте рабочие процессы на Mac mini в облаке без перерывов
Выберите RunAMac M4, срок аренды и узел размещения, чтобы выполнять задачи разработки, сборки или тестирования на выделенном физическом узле.