Проверка вариантов App Thinning на облачном Mac

Проверка вариантов App Thinning на облачном Mac

После передачи одного и того же архива 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>&lt;thin-for-all-variants&gt;</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, отсутствует отчёт о размере, основное приложение содержит недопустимую архитектуру, отсутствуют критически важные ресурсы, набор фреймворков изменился без утверждения либо размер варианта превышает установленный командой предел.

Одновременно сохраняйте следующие данные:

  1. версию Xcode и значение DEVELOPER_DIR;
  2. идентификатор пакета, версию и номер сборки архива;
  3. фактически использованный файл ExportOptions.plist;
  4. полный журнал экспорта;
  5. архитектуры, перечни фреймворков и файлов для каждого варианта;
  6. отчёт App Thinning о размере.

При сбое не удаляйте каталог экспорта сразу. Сначала упакуйте и загрузите собранные данные, а затем очистите рабочую область с помощью обработчика завершения задания. При параллельном выполнении на облачных Mac PingMini каждое задание должно использовать отдельный каталог, а доступную конфигурацию следует проверять в панели управления. Сама логика проверки не должна зависеть от конкретного фиксированного узла.

Ценность App Thinning заключается не только в уменьшении объёма загрузки: эта технология меняет конечный поставляемый артефакт. Только проверяя варианты для устройств как полноценные результаты сборки, можно ещё до выпуска получать воспроизводимые и локализуемые ошибки при изменении тегов ресурсов, архитектур или зависимостей.

Часто задаваемые вопросы

Что проверять: архив Xcode или экспортированные пакеты?

Проверять нужно оба результата. Архив подтверждает общий вход сборки, а экспортированные пакеты показывают фактические архитектуры, ресурсы и встроенные компоненты каждого варианта.

Разные хеши вариантов App Thinning означают ошибку сборки?

Нет. Исполняемые срезы, ресурсы устройств и данные подписи могут различаться. Сравнивайте списки файлов, разрешённые архитектуры, обязательные ресурсы и неожиданные добавления.

Какие отклонения должны останавливать задачу CI?

Задачу следует останавливать при отсутствии отчёта о размере, пустом экспорте, запрещённой архитектуре, пропавшем ресурсе, изменении набора фреймворков или превышении лимита размера.

Выберите выделенный физический Mac для следующего проекта

Выберите облачный Mac с одной из трёх фиксированных конфигураций для разработки, непрерывной интеграции или тестирования локальных моделей и настройте его на нужный срок.

Настроить облачный Mac