Проверка архитектур Mach-O и версии iOS на облачном Mac

Проверка архитектур Mach-O и версии iOS на облачном Mac

Проект iOS, который без проблем работает в локальном симуляторе, может столкнуться с ошибками лишь на этапе установки, даже если Archive успешно создан на облачном Mac HopVM: в предсобранном Framework может оказаться срез x86_64, расширение может быть скомпоновано для неверной платформы, а вложенная динамическая библиотека — незаметно повысить минимальную требуемую версию системы. Вместо того чтобы разбираться с этим после передачи сборки, лучше сразу сканировать экспортированный пакет .app и превратить фактические параметры бинарных файлов в обязательную проверку конвейера.

Зачем проверять итоговый экспортированный пакет

Настройки проекта Xcode показывают лишь то, «как планируется выполнить сборку», тогда как итоговый пакет отражает то, «что действительно поставляется». Основное приложение, App Extension, Framework и файлы .dylib могут иметь собственные заголовки Mach-O. Бинарные пакеты, загруженные менеджером зависимостей, также не обязательно наследуют настройки основного проекта.

Такая проверка должна отвечать как минимум на три вопроса:

  1. Содержат ли бинарные файлы для реальных устройств iOS только разрешённые архитектуры.
  2. Указана ли для каждого Mach-O платформа iOS, а не симулятор или macOS.
  3. Не превышает ли минимальная версия системы во вложенных бинарных файлах версию, заявленную основным приложением.

Не считайте успешное создание Archive приёмкой поставки. Компоновщик проверяет лишь возможность собрать продукт для текущей цели, но не определяет за команду, соответствуют ли все вложенные файлы требованиям релиза.

Проверять нужно итоговый пакет, созданный командой xcodebuild -exportArchive, а не только содержимое DerivedData. При экспорте Framework, расширения и подписанные компоненты реорганизуются, поэтому именно этот пакет наиболее близок к фактически поставляемому продукту.

Фиксация входных данных и критериев приёмки

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

set -euo pipefail

ARCHIVE_PATH="$PWD/output/App.xcarchive"
EXPORT_PATH="$PWD/output/export"
REPORT_PATH="$PWD/output/macho-report.tsv"

rm -rf "$EXPORT_PATH"
mkdir -p "$EXPORT_PATH"

xcodebuild -exportArchive \
  -archivePath "$ARCHIVE_PATH" \
  -exportPath "$EXPORT_PATH" \
  -exportOptionsPlist "$PWD/ci/ExportOptions.plist"

APP_PATH="$(find "$EXPORT_PATH" -maxdepth 2 -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"

Базовые требования к продукту следует хранить в репозитории, а не распределять по настройкам интерфейса CI. Например, разрешённые архитектуры и минимальную версию можно записать в ci/macho-policy.env:

EXPECTED_ARCHS="arm64"
EXPECTED_PLATFORM="IOS"
DECLARED_MIN_IOS="17.0"

Значение DECLARED_MIN_IOS должно соответствовать реальному диапазону версий, поддерживаемому проектом. Приведённая здесь версия используется только как пример для скрипта — её нельзя копировать и автоматически принимать в качестве продуктового решения.

Рекурсивный поиск всех файлов Mach-O

Искать только по расширениям нельзя, поскольку основной бинарный файл Framework обычно не имеет расширения. Надёжнее перебрать все файлы, а затем с помощью file определить, относятся ли они к Mach-O.

: > "$REPORT_PATH"
failure=0

while IFS= read -r -d '' candidate; do
  if ! file -b "$candidate" | grep -q 'Mach-O'; then
    continue
  fi

  archs="$(lipo -archs "$candidate" 2>/dev/null || true)"
  build="$(vtool -show-build "$candidate" 2>/dev/null || true)"
  platform="$(awk '/platform/{print $2; exit}' <<<"$build")"
  minos="$(awk '/minos/{print $2; exit}' <<<"$build")"

  printf '%s	%s	%s	%s
' \
    "${candidate#"$APP_PATH"/}" "$archs" "$platform" "$minos" \
    >> "$REPORT_PATH"

  if [[ "$archs" != "$EXPECTED_ARCHS" ]]; then
    printf 'architecture mismatch: %s (%s)
' "$candidate" "$archs" >&2
    failure=1
  fi

  if [[ "$platform" != "$EXPECTED_PLATFORM" ]]; then
    printf 'platform mismatch: %s (%s)
' "$candidate" "$platform" >&2
    failure=1
  fi
done < <(find "$APP_PATH" -type f -print0)

exit "$failure"

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

Не изменяйте пакет непосредственно во время проверки

После обнаружения x86_64 может показаться удобным сразу выполнить lipo -remove, однако это изменит уже подписанное содержимое и скроет проблему в процессе подготовки зависимости. Правильный подход — определить, откуда поступил файл: из сборки исходного кода, бинарной зависимости или скрипта копирования. Затем нужно исправить источник проблемы и заново создать Archive.

Сравнение минимальных версий системы

Сначала считайте версию, заявленную основным приложением:

PLIST="$APP_PATH/Info.plist"
APP_MIN_IOS="$(/usr/libexec/PlistBuddy \
  -c 'Print :MinimumOSVersion' "$PLIST")"

printf 'declared minimum iOS: %s
' "$APP_MIN_IOS"

Затем получите значение minos из LC_BUILD_VERSION каждого файла Mach-O. Для проверки необходимо использовать семантическое сравнение версий, а не обычное сравнение строк, иначе 17.10 может быть ошибочно поставлена перед 17.9.

Объект проверки Источник значения Условие ошибки
Объявленные требования основного приложения MinimumOSVersion в Info.plist Не совпадает с базовым значением в репозитории
Основной исполняемый файл minos в LC_BUILD_VERSION Выше значения, заявленного продуктом
Framework и динамические библиотеки Собственный LC_BUILD_VERSION каждого файла Выше значения, заявленного продуктом
App Extension Info.plist расширения и Mach-O Заявленное или фактическое значение превышает базовый уровень

Для сравнения можно использовать встроенную в систему сортировку версий:

version_gt() {
  [[ "$1" != "$2" ]] &&
    [[ "$(printf '%s
%s
' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
}

if version_gt "$minos" "$APP_MIN_IOS"; then
  printf 'minimum iOS mismatch: %s requires %s
' \
    "$candidate" "$minos" >&2
  failure=1
fi

Если vtool не возвращает платформу или версию, нельзя молча пропускать файл. Сначала сохраните все команды загрузки с помощью otool -l, а затем выясните, является ли файл бинарным объектом старого формата, повреждённым файлом или результатом ошибочного определения скриптом.

Интеграция с CI и устранение типичных ложных срабатываний

Запускайте проверку после создания Archive и экспорта, но до загрузки продукта. Файл macho-report.tsv следует загружать всегда. Даже если задание завершилось ошибкой, отчёт должен сохраняться с помощью механизма публикации артефактов после сбоя, доступного в CI.

Типичные ложные срабатывания делятся на три основные категории:

Поэтому правила в скрипте нужно разделять по типам продуктов. Один набор правил не должен одновременно охватывать iOS, тестовые пакеты для симулятора и инструменты macOS. Если конвейер создаёт несколько типов продуктов, проверку необходимо запускать отдельно для каждого каталога экспорта, а в первом столбце отчёта указывать тип продукта.

В завершение ещё раз проверьте структуру подписи:

codesign --verify --deep --strict --verbose=2 "$APP_PATH"

Эта команда не заменяет проверку Mach-O, но позволяет обнаружить последующие проблемы, например изменение бинарного файла скриптом или недействительную подпись вложенного компонента. Полная последовательность должна выглядеть так: экспорт, сканирование, сравнение версий, проверка подписи и сохранение отчёта — и лишь затем продукт можно передавать. В таком случае каждая ошибка будет привязана к конкретному файлу, полю и способу исправления, а команде не придётся гадать о причине уже после сбоя установки.

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

Почему недостаточно проверить только основной файл приложения?

Framework, расширения и динамические библиотеки содержат собственные Mach-O. Ошибка даже в одном вложенном файле может привести к сбою установки или запуска.

Что делать, если в пакете iOS найден x86_64?

Нужно определить источник файла и исправить сборку зависимости либо этап архивации. Удалять срез командой lipo непосредственно перед поставкой не следует.

Где брать минимальную поддерживаемую версию iOS?

Следует сравнивать MinimumOSVersion из Info.plist с minos из LC_BUILD_VERSION. Значение вложенного бинарного файла не должно быть выше заявления основного приложения.

Выделенный физический узел

Воспроизводите рабочие процессы на облачном Mac, включая и выключая его по мере необходимости

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

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