Один и тот же проект iOS может без проблем архивироваться локально, но в Release-конвейере на облачном Mac получить неверный Bundle ID, отладочную URL Scheme или лишиться описания разрешения на доступ к камере. Обычно проблема не в файле Info.plist, который хранится в репозитории, а в итоговой конфигурации, записанной Xcode в App после объединения настроек сборки, сгенерированной конфигурации и свойств target. Надёжный CI должен проверять собранный артефакт, а не ограничиваться текстовым анализом исходного файла.
Сначала проверьте конфигурацию фактически поставляемого продукта
В современных проектах Xcode файл Info.plist может создаваться автоматически, а значения могут поступать из разных target, configuration и файлов .xcconfig. Файл, на который указывает INFOPLIST_FILE, — лишь один из источников. Такие параметры, как PRODUCT_BUNDLE_IDENTIFIER, MARKETING_VERSION и CURRENT_PROJECT_VERSION, также участвуют в объединении во время сборки.
Сначала выполните сборку, явно указав workspace, scheme, configuration и каталог вывода:
set -euo pipefail
ROOT="$(pwd)"
DERIVED_DATA="$ROOT/.ci/DerivedData"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED_DATA" \
CODE_SIGNING_ALLOWED=NO \
build
Отключение подписи здесь подходит только для быстрой проверки конфигурации. Это не означает, что подпись следует отключать и при создании релизного архива. После завершения сборки не пытайтесь угадать путь к App: получите каталог и имя продукта из настроек сборки.
SETTINGS="$(mktemp)"
xcodebuild \
-workspace Example.xcworkspace \
-scheme Example \
-configuration Release \
-sdk iphoneos \
-derivedDataPath "$DERIVED_DATA" \
-showBuildSettings > "$SETTINGS"
BUILD_DIR="$(awk -F ' = ' '/ TARGET_BUILD_DIR = /{print $2; exit}' "$SETTINGS")"
WRAPPER_NAME="$(awk -F ' = ' '/ WRAPPER_NAME = /{print $2; exit}' "$SETTINGS")"
APP_PATH="$BUILD_DIR/$WRAPPER_NAME"
PLIST_PATH="$APP_PATH/Info.plist"
test -d "$APP_PATH"
test -f "$PLIST_PATH"
plutil -lint "$PLIST_PATH"
Проверять нужно App, созданный текущим заданием. Если повторно использовать фиксированный путь с результатами предыдущей сборки, обязательная проверка может вынести ошибочное решение на основе устаревшего артефакта.
Храните ожидаемые значения в системе контроля версий
Не следует жёстко задавать множество ожидаемых значений в интерфейсе CI-платформы. Более удобный для проверки подход — хранить небольшой JSON-файл для каждого релизного окружения, например ci/plist-release.json:
{
"bundleIdentifier": "com.example.product",
"urlSchemes": ["example"],
"requiredUsageKeys": [
"NSCameraUsageDescription",
"NSPhotoLibraryUsageDescription"
],
"allowedBackgroundModes": ["remote-notification"]
}
В этот файл следует включать только правила, которые можно безопасно хранить в открытом репозитории. Токенам, закрытым ключам и паролям для подписи в нём не место. Версия и номер сборки обычно формируются из параметров конвейера, поэтому скрипт должен проверять их формат и соответствие входным данным сборки, а не постоянно сравнивать их с одним жёстко заданным числом.
Для каждого App target, расширения и тестового хоста нужны отдельные правила. Нельзя применять правило Bundle ID основного App к Widget или использовать для расширения без доступа к камере список, требующий наличия описания такого разрешения.
Напишите скрипт аудита, способный завершиться ошибкой
Команда plutil -extract удобна для чтения словарей и массивов, а PlistBuddy — для простых скалярных значений. Ниже приведён минимальный каркас скрипта:
#!/bin/bash
set -euo pipefail
PLIST_PATH="${1:?missing Info.plist path}"
EXPECTED_BUNDLE_ID="${EXPECTED_BUNDLE_ID:?missing bundle id}"
EXPECTED_VERSION="${EXPECTED_VERSION:?missing version}"
EXPECTED_BUILD="${EXPECTED_BUILD:?missing build number}"
read_key() {
/usr/libexec/PlistBuddy -c "Print :$1" "$PLIST_PATH" 2>/dev/null
}
require_key() {
local key="$1"
local value
value="$(read_key "$key" || true)"
if [[ -z "$value" ]]; then
printf 'Missing required key: %s
' "$key" >&2
exit 1
fi
}
[[ "$(read_key CFBundleIdentifier)" == "$EXPECTED_BUNDLE_ID" ]]
[[ "$(read_key CFBundleShortVersionString)" == "$EXPECTED_VERSION" ]]
[[ "$(read_key CFBundleVersion)" == "$EXPECTED_BUILD" ]]
require_key NSCameraUsageDescription
require_key NSPhotoLibraryUsageDescription
SCHEMES_JSON="$(plutil -extract CFBundleURLTypes json -o - "$PLIST_PATH")"
printf '%s' "$SCHEMES_JSON" | grep -q '"example"'
MODES="$(plutil -extract UIBackgroundModes raw -o - "$PLIST_PATH" 2>/dev/null || true)"
if [[ "$MODES" == *"audio"* ]]; then
printf 'Unexpected background mode: audio
' >&2
exit 1
fi
Рабочий скрипт также должен выводить имя ключа, ожидаемое и фактическое значения, но не весь plist. Описания разрешений, проверяемые Scheme и настройки сторонних компонентов могут содержать внутренние идентификаторы. Публикация этих данных в логах без обработки увеличивает риск их раскрытия.
Используйте одновременно разрешающие и запрещающие правила
Правила вида «обязательно должно присутствовать» позволяют обнаружить пропущенные настройки, но не выявляют утечку отладочных параметров. Рекомендуется дополнительно вести список запретов: например, не разрешать в Release-артефакте признаки тестового сервера, отладочные URL Scheme, включённый общий доступ к файлам или неодобренные фоновые режимы.
Не используйте простую проверку вхождения строки как окончательный способ сравнения полей-массивов. Надёжнее получить JSON командой plutil -extract ... json, а затем сравнить элементы с помощью Ruby, Python или уже существующего скрипта проекта. При этом нужно явно обрабатывать три ситуации: отсутствие поля, неверный тип и повторяющиеся значения.
Проверяйте App, расширения и архив в два этапа
Один архив может одновременно содержать основной App, Widget, расширение службы уведомлений и другие .appex. Если проверять только основной App, можно пропустить ошибки в идентификаторах, номерах версий или разрешениях расширений. После создания архива можно перебрать все bundle:
find "$ARCHIVE_PATH/Products/Applications" \
\( -name "*.app" -o -name "*.appex" \) -print0 |
while IFS= read -r -d '' bundle; do
plist="$bundle/Info.plist"
plutil -lint "$plist"
printf 'Auditing %s
' "$bundle"
done
Рекомендуется настроить два обязательных этапа. После обычного Release build выполняйте быструю проверку для ранней обратной связи, а после завершения archive повторно проверяйте артефакты внутри архива и только затем принимайте решение о выпуске. Второй этап нельзя пропускать, поскольку архивирование может использовать другую configuration, другие параметры экспорта или переменные конвейера.
| Объект проверки | Ключевые поля | Действие при ошибке |
|---|---|---|
| Основной App | Идентификатор, версия, описания разрешений, URL Scheme | Блокировать архивирование |
| App Extension | Префикс идентификатора, версия, тип расширения | Блокировать архивирование |
| Итоговый Archive | Все bundle и параметры, запрещённые в production | Блокировать выпуск |
Устраняйте распространённые ложные срабатывания и сохраняйте доказательства
Чаще всего ложные срабатывания связаны с пустыми строками, типом логических значений и порядком элементов массива. Для описания разрешения недостаточно проверить наличие ключа: после удаления пробелов в начале и конце значение должно оставаться непустым. Логические значения нужно читать с учётом типа plist, не принимая строку "false" за логическое значение. URL Scheme и фоновые режимы обычно сравниваются как множества, поэтому проверка не должна зависеть от порядка элементов массива.
Если скрипт завершается ошибкой, сохраняйте обезличенный отчёт об аудите. В него должны входить scheme, configuration, SDK, путь к App, имя проблемного ключа и хеш. Не сохраняйте всё окружение сборки и не выводите в журнал все переменные окружения. Если на одном облачном Mac выполняется несколько заданий, используйте отдельный DerivedData для каждого из них и перед аудитом проверяйте, что время изменения артефакта соответствует текущему заданию.
Наконец, сопровождайте скрипт аудита как обычный код: изменения правил должны проходить проверку перед слиянием, а для сценариев «отсутствующий ключ», «неверный тип», «запрещённое значение» и «пропущенное расширение» следует подготовить отдельные тестовые примеры. Тогда проблемы Info.plist будут превращаться в понятные и воспроизводимые ошибки прямо в конвейере, а не обнаруживаться на этапе архивирования или отправки, когда причины приходится выяснять вручную.
Часто задаваемые вопросы
Почему недостаточно проверить исходный Info.plist?
Xcode объединяет генерируемые значения, настройки сборки и файлы отдельных конфигураций. Реально поставляется Info.plist, встроенный в собранный App.
Какие ключи сначала включить в проверку CI?
Начните с CFBundleIdentifier, версии, номера сборки, описаний разрешений, URL-схем и UIBackgroundModes. Отсутствующие и запрещённые значения должны останавливать сборку.
Когда запускать аудит относительно archive?
После обычной сборки его запускают для быстрой обратной связи, а после archive повторяют для App внутри архива. Решение о выпуске принимают по второму результату.
Нужна выделенная машина для сборки на Apple Silicon?
Арендуйте выделенный физический узел на день, неделю, месяц или квартал для сборки в Xcode, iOS CI, удалённой разработки и автоматизации.