Инженерная статья

Аудит динамических библиотек и экспортируемых символов iOS в CI

Аудит динамических библиотек и экспортируемых символов iOS в CI

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

Сначала определите, какие изменения нужно блокировать

Такой контроль не должен решать, «хороша» или «плоха» конкретная библиотека. Вместо этого он должен отвечать на три проверяемых вопроса:

  1. От чего фактически зависят основное приложение и встроенные Framework?
  2. Действительно ли собственные библиотеки, на которые указывает @rpath, находятся внутри пакета App?
  3. Относятся ли новые глобальные символы к ожидаемому интерфейсу?

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

Проверку рекомендуется выполнять после создания Release Archive. В Debug-сборке могут присутствовать диагностические библиотеки, а результаты оптимизации могут отличаться, поэтому она не отражает окончательную границу поставки. На первом этапе формируйте только отчёт, не блокируя сборку. После того как команда проверит содержимое эталона, настройте завершение задания с ошибкой при обнаружении различий.

Извлечение проверяемых объектов из Archive

Сначала создайте отдельный архив, чтобы не использовать старые артефакты с локального компьютера разработчика:

rm -rf out/App.xcarchive
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$PWD/out/App.xcarchive" \
  clean archive

Если подпись выполняется отдельным последующим заданием, с учётом особенностей проекта можно задать CODE_SIGNING_ALLOWED=NO. Не следует механически добавлять это переопределение во все проекты. Если проект использует определённые capabilities или пользовательские этапы сборки, сначала убедитесь, что архив без подписи формируется полностью.

Основное приложение в архиве обычно находится в Products/Applications. Проверка должна как минимум охватывать главный исполняемый файл App и исполняемые файлы всех Framework из каталога Frameworks. Расширения также нужно проверять отдельно, поскольку у каждого из них собственная граница зависимостей.

Объект Основные проверки Типичные отклонения
Главный исполняемый файл App Системные библиотеки, встроенные Framework, глобальные символы Абсолютные пути вне системных каталогов
Динамический Framework Транзитивные зависимости, установочное имя, открытые символы Зависимость не включена в пакет
App Extension Отдельные эталоны зависимостей и символов Ошибочное повторное использование допущений для основного приложения

Формирование списков зависимостей и символов

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

set -euo pipefail

archive="${1:?archive path required}"
app=$(find "$archive/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)
test -n "$app"

mkdir -p audit/current
main=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Info.plist")
printf '%s
' "$app/$main" > audit/current/binaries.txt

if test -d "$app/Frameworks"; then
  while IFS= read -r framework; do
    executable=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$framework/Info.plist")
    printf '%s
' "$framework/$executable"
  done < <(find "$app/Frameworks" -maxdepth 1 -name '*.framework' -type d | sort)
fi >> audit/current/binaries.txt

: > audit/current/dependencies.txt
: > audit/current/symbols.txt

while IFS= read -r binary; do
  name=${binary#"$app/"}
  otool -L "$binary" |
    sed '1d' |
    awk -v n="$name" '{$1=$1; print n "	" $1}' \
    >> audit/current/dependencies.txt

  xcrun nm -gU "$binary" |
    awk -v n="$name" 'NF {print n "	" $NF}' \
    >> audit/current/symbols.txt
done < audit/current/binaries.txt

LC_ALL=C sort -u -o audit/current/dependencies.txt audit/current/dependencies.txt
LC_ALL=C sort -u -o audit/current/symbols.txt audit/current/symbols.txt

При добавлении эталона в репозиторий сохраняйте только относительные имена объектов и не записывайте рабочий каталог облачного Mac. Затем в конвейере выполняются команды:

diff -u audit/baseline/dependencies.txt audit/current/dependencies.txt
diff -u audit/baseline/symbols.txt audit/current/symbols.txt

Превращение отчёта в надёжный контрольный барьер

Полное текстовое сравнение может оказаться слишком чувствительным. Практичнее сначала применять жёсткие правила, а затем проверять изменения относительно эталона. Обычно допустимы зависимости, начинающиеся с /System/Library/Frameworks/, /usr/lib/ или контролируемого @rpath/. Любые другие абсолютные пути должны немедленно приводить к ошибке. Для @rpath/Example.framework/Example скрипт также должен проверять наличие соответствующего Framework внутри пакета: одного правильного префикса недостаточно.

Изменения символов лучше проверять вручную. Имена Swift после манглинга могут быть очень длинными, поэтому нельзя оценивать риск только по длине строки. Особое внимание следует уделять внезапному появлению большого количества глобальных символов, вспомогательным отладочным интерфейсам, точкам входа для тестов, а также символам C или Objective-C, которые должны быть скрыты, но оказались видимы между модулями.

Снижение числа ложных срабатываний

Зафиксируйте версию Xcode и конфигурацию Release и всегда сравнивайте один и тот же scheme. Если проект содержит несколько расширений, храните отдельный эталон для каждого бинарного файла, а не объединяйте все результаты в один набор. Если обновление инструментов изменило системные символы, обновите эталон в запросе на слияние с этим обновлением, чтобы впоследствии источник различий оставался понятным.

Поиск причины сбоя по артефактам

При появлении новой зависимости сначала с помощью otool -L определите, какой бинарный файл её добавил, а затем проверьте настройки линковки соответствующей цели, условную компиляцию и этап встраивания зависимостей. Если цель @rpath отсутствует, в первую очередь убедитесь, что Framework не только участвует в линковке, но и копируется в пакет. При появлении неожиданного символа найдите соответствующий объект с помощью nm -gU, а затем проверьте объявления видимости, связующий код и настройки экспорта линкера.

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

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

Почему недостаточно проверить зависимости, объявленные в проекте?

Условная сборка, параметры компоновщика и сценарии зависимостей могут изменить готовый Archive, поэтому проверять нужно итоговые файлы Mach-O.

Нужно ли навсегда блокировать выпуск при любом изменении символов?

Нет. Проверка требует осознанного просмотра разницы; после подтверждённого изменения API или обновления инструментов эталон можно обновить.

Позволяет ли список символов обнаружить встроенные секреты?

Сам по себе нет. Для секретов нужна отдельная проверка исходников, конфигурации, ресурсов и журналов сборки.

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

Перенесите задачи разработки и сборки на выделенный облачный Mac

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

Выбрать тариф облачного Mac