После передачи одного и того же архива Xcode в процесс распространения содержимое, которое в итоге попадает на разные устройства, может отличаться. App Thinning выбирает исполняемые срезы и варианты ресурсов с учётом возможностей устройства. Обычная сборка подтверждает лишь успешное создание архива, поэтому легко пропустить ошибочные теги ресурсов, лишние архитектуры, изменения во встроенных фреймворках или отсутствие отчёта о размере. Надёжнее разделить на облачном Mac создание архива и проверку вариантов для устройств на два независимых этапа.
В этой статье используется существующий проект и конфигурация архива, пригодная для распространения. Систему подписывания менять не нужно, как и заново компилировать приложение при каждой проверке. Цель — экспортировать результаты App Thinning из одного файла .xcarchive, сформировать перечень содержимого и настроить CI так, чтобы при явно заданных аномалиях он завершался с ненулевым кодом.
Сначала зафиксируйте входные данные проверки
Перед началом проверки запишите путь к Xcode, версию проекта, номер сборки и расположение архива. Задание не должно зависеть от набора инструментов, выбранного по умолчанию в интерактивной оболочке. На облачном Mac могут одновременно находиться несколько версий Xcode, поэтому CI должен явно выбирать версию, утверждённую для текущего конвейера.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_DIR="$PWD/output/thinning"
REPORT_DIR="$PWD/output/evidence"
xcodebuild -version
test -d "$ARCHIVE_PATH"
rm -rf "$EXPORT_DIR" "$REPORT_DIR"
mkdir -p "$EXPORT_DIR" "$REPORT_DIR"
Затем прочитайте сведения о приложении из архива и убедитесь, что CI не использует артефакт, оставшийся от предыдущего задания.
INFO="$ARCHIVE_PATH/Info.plist"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleIdentifier" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$INFO"
/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$INFO"
Рекомендуется привязать рабочий каталог к номеру задания и создавать пустой каталог при его запуске. Не используйте общий каталог output с фиксированным глобальным путём для архивов нескольких параллельных заданий.
Входными данными для проверки App Thinning должен быть один файл
.xcarchive, уже прошедший стандартную проверку архива. Если нет уверенности в самих входных данных, подробное сравнение вариантов для устройств не имеет смысла.
Экспортируйте все совместимые варианты для устройств
Подготовьте отдельный файл ExportOptions.plist. Значение method должно соответствовать версии Xcode, которую использует команда, и существующему процессу распространения. Ключевой момент в следующем примере — параметр thinning, настроенный на экспорт всех совместимых вариантов.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>thinning</key>
<string><thin-for-all-variants></string>
<key>stripSwiftSymbols</key>
<true/>
</dict>
</plist>
При экспорте сохраняйте и результаты, и журнал в каталоге текущего задания:
xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$PWD/ExportOptions.plist" \
2>&1 | tee "$REPORT_DIR/export.log"
find "$EXPORT_DIR" -maxdepth 3 -type f -print \
| LC_ALL=C sort \
> "$REPORT_DIR/export-files.txt"
Если существующий конвейер использует другой метод экспорта, не меняйте его ради точного повторения примера. Сохраните исходное значение method, проверяйте конфигурацию thinning только в изолированной ветке и архивируйте использованный plist вместе с остальными артефактами.
Проверьте архитектуры и встроенное содержимое
Успешный экспорт ещё не означает, что результат корректен. Сначала найдите установочные пакеты, распакуйте каждый из них и проверьте архитектуры основного исполняемого файла. Скрипт не должен предполагать, что каталог приложения всегда имеет одно и то же имя: файл .app можно найти внутри Payload.
mkdir -p "$REPORT_DIR/unpacked"
while IFS= read -r ipa; do
name="$(basename "$ipa" .ipa)"
dest="$REPORT_DIR/unpacked/$name"
mkdir -p "$dest"
ditto -x -k "$ipa" "$dest"
app="$(find "$dest/Payload" -maxdepth 1 -name '*.app' -type d | head -n 1)"
test -n "$app"
executable="$(/usr/libexec/PlistBuddy \
-c 'Print :CFBundleExecutable' "$app/Info.plist")"
printf '%s: ' "$name"
lipo -archs "$app/$executable"
find "$app/Frameworks" -maxdepth 1 -type d -name '*.framework' 2>/dev/null \
| sed "s#^$app/##" \
| LC_ALL=C sort \
> "$REPORT_DIR/$name-frameworks.txt"
done < <(find "$EXPORT_DIR" -type f -name '*.ipa' | LC_ALL=C sort)
Для текущего результата распространения на устройства iOS наличие архитектуры симулятора в основном приложении должно немедленно приводить к ошибке. Перечень фреймворков следует сравнивать с утверждённой базовой версией в репозитории. Новый фреймворк не обязательно указывает на ошибку, но такое изменение должно пройти проверку кода, чтобы в пакет поставки не попали отладочные компоненты или временные зависимости.
Не сравнивайте хеши установочных пакетов целиком
Варианты для разных устройств изначально могут содержать разные ресурсы и исполняемые срезы, а данные подписи также создают бинарные различия. Требование одинакового SHA-256 для всех IPA ошибочно расценит нормальную оптимизацию как сбой.
Более полезно сравнивать:
- разрешённые архитектуры основного исполняемого файла;
- списки относительных путей в
FrameworksиPlugIns; - обязательные локализации, шрифты и файлы данных;
- отладочные конфигурации, тестовые ресурсы и файлы исходного кода, которые не должны попадать в готовый продукт;
- размер каждого варианта после распаковки и его изменение относительно исторического базового уровня.
Превратите ресурсы и размеры в проверяемые данные
Сначала убедитесь, что процесс экспорта действительно создал отчёт о размере, а затем сформируйте перечень файлов для каждого варианта. Размер файлов зависит от оптимизации ресурсов, поэтому базовый уровень следует хранить отдельно для каждого варианта.
SIZE_REPORT="$(find "$EXPORT_DIR" -type f \
-name 'App Thinning Size Report.txt' | head -n 1)"
test -n "$SIZE_REPORT"
cp "$SIZE_REPORT" "$REPORT_DIR/"
for app in "$REPORT_DIR"/unpacked/*/Payload/*.app; do
variant="$(basename "$(dirname "$(dirname "$app")")")"
(
cd "$app"
find . -type f -print0 \
| xargs -0 stat -f '%z %N' \
| LC_ALL=C sort
) > "$REPORT_DIR/$variant-files-and-sizes.txt"
done
Не ограничивайте контроль размера одним абсолютным значением, которое никогда не меняется. Рекомендуется одновременно использовать два критерия: утверждённый командой жёсткий предел и допустимый рост относительно последнего стабильного базового уровня. Жёсткий предел предотвращает очевидный выход размера из-под контроля, а относительный базовый уровень выявляет аномальный рост, вызванный отдельным коммитом.
Для каталогов ресурсов также следует проверять наличие критически важных для продукта файлов. Например, полноту офлайн-шаблонов, шрифтов первого экрана или ресурсов определённого языка нельзя подтверждать только тем, что приложение запускается. Храните пути к таким файлам в текстовом списке внутри репозитория и выполняйте test -e для каждого элемента. Такой подход легче проверять, чем множество разрозненных условий в скрипте.
Задайте в CI однозначные условия сбоя
Итоговый скрипт должен как минимум блокировать процесс в следующих случаях: не экспортирован ни один IPA, отсутствует отчёт о размере, основное приложение содержит недопустимую архитектуру, отсутствуют критически важные ресурсы, набор фреймворков изменился без утверждения либо размер варианта превышает установленный командой предел.
Одновременно сохраняйте следующие данные:
- версию Xcode и значение
DEVELOPER_DIR; - идентификатор пакета, версию и номер сборки архива;
- фактически использованный файл
ExportOptions.plist; - полный журнал экспорта;
- архитектуры, перечни фреймворков и файлов для каждого варианта;
- отчёт App Thinning о размере.
При сбое не удаляйте каталог экспорта сразу. Сначала упакуйте и загрузите собранные данные, а затем очистите рабочую область с помощью обработчика завершения задания. При параллельном выполнении на облачных Mac PingMini каждое задание должно использовать отдельный каталог, а доступную конфигурацию следует проверять в панели управления. Сама логика проверки не должна зависеть от конкретного фиксированного узла.
Ценность App Thinning заключается не только в уменьшении объёма загрузки: эта технология меняет конечный поставляемый артефакт. Только проверяя варианты для устройств как полноценные результаты сборки, можно ещё до выпуска получать воспроизводимые и локализуемые ошибки при изменении тегов ресурсов, архитектур или зависимостей.
Часто задаваемые вопросы
Что проверять: архив Xcode или экспортированные пакеты?
Проверять нужно оба результата. Архив подтверждает общий вход сборки, а экспортированные пакеты показывают фактические архитектуры, ресурсы и встроенные компоненты каждого варианта.
Разные хеши вариантов App Thinning означают ошибку сборки?
Нет. Исполняемые срезы, ресурсы устройств и данные подписи могут различаться. Сравнивайте списки файлов, разрешённые архитектуры, обязательные ресурсы и неожиданные добавления.
Какие отклонения должны останавливать задачу CI?
Задачу следует останавливать при отсутствии отчёта о размере, пустом экспорте, запрещённой архитектуре, пропавшем ресурсе, изменении набора фреймворков или превышении лимита размера.
Выберите выделенный физический Mac для следующего проекта
Выберите облачный Mac с одной из трёх фиксированных конфигураций для разработки, непрерывной интеграции или тестирования локальных моделей и настройте его на нужный срок.