Технический прогон

Аудит итогового Info.plist в Cloud Mac CI

Аудит итогового Info.plist в Cloud Mac CI

Один и тот же проект 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 внутри архива. Решение о выпуске принимают по второму результату.

MacVPSGo Облачный Mac

Нужна выделенная машина для сборки на Apple Silicon?

Арендуйте выделенный физический узел на день, неделю, месяц или квартал для сборки в Xcode, iOS CI, удалённой разработки и автоматизации.

Выбрать конфигурацию и заказать