Инженерные практики

Как выявлять несовместимые изменения Swift-фреймворка до слияния

Как выявлять несовместимые изменения Swift-фреймворка до слияния

Если один Swift-фреймворк используется несколькими приложениями, достаточно удалить публичный метод, сузить тип параметра или добавить обязательное требование в публичный протокол, чтобы зависимые проекты перестали компилироваться после обновления. Модульные тесты обычно проверяют поведение реализации, но не сообщают напрямую, что изменение нарушило публичный интерфейс. На облачных сборочных узлах Mac от OnceMac Swift API Digester можно включить в проверку перед слиянием: сначала сохранить снимок API выпущенной версии, затем создать снимок кандидата с помощью того же набора инструментов и сравнить их.

Сначала зафиксируйте условия сравнения

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

set -euo pipefail

xcodebuild -version
xcrun swift --version
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
echo "SDK_PATH=$SDK_PATH"

В CI следует явно выбирать Xcode, а для базовой и кандидатной веток использовать один и тот же образ или шаблон узла. Целевая тройка также должна совпадать — например, в обоих случаях можно использовать arm64-apple-ios17.0-simulator. Не создавайте интерфейс для физического устройства с одной стороны и для симулятора с другой, иначе различия платформенных условий будут ошибочно приняты за изменения кода.

Базовый снимок API — это контракт выпуска, а не кэш компиляции. Его следует хранить в системе контроля версий и проверять в ходе ревью; обычные задачи сборки не должны автоматически его перезаписывать.

Соберите Framework для анализа

В следующем примере предполагается, что scheme и module называются MyKit. Сначала очищается отдельный каталог Derived Data, после чего выполняется сборка в конфигурации Release. Параметр BUILD_LIBRARY_FOR_DISTRIBUTION=YES создаёт файлы интерфейса, необходимые для стабильности модулей, и приближает условия проверки к сценарию распространения бинарной библиотеки.

DERIVED_DATA="$PWD/.build/api-dd"
PRODUCTS="$DERIVED_DATA/Build/Products/Release-iphonesimulator"

rm -rf "$DERIVED_DATA"

xcodebuild build \
  -scheme MyKit \
  -configuration Release \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_DATA" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO \
  CODE_SIGNING_ALLOWED=NO

После сборки сначала убедитесь, что модуль действительно существует. Не позволяйте Digester работать с неверным путём поиска и выдавать малопонятную ошибку «module not found».

test -d "$PRODUCTS/MyKit.framework"
find "$PRODUCTS/MyKit.framework/Modules" -maxdepth 3 -type f -print

Если зависимости проекта управляются через workspace, добавьте -workspace. Если scheme не является общей, CI не сможет её обнаружить: сначала сделайте её общей в настройках проекта, а не пытайтесь угадывать путь в скрипте.

Создайте и сравните снимки API

Отдельно соберите текущую базовую версию выпуска и кандидатный код, а затем сохраните их интерфейсы в JSON. Базовый файл рекомендуется хранить в каталоге api-baselines/ репозитория и указывать платформу в имени файла. Путь на конкретной машине и номер сборки записывать не нужно.

mkdir -p api-baselines .build/api-report

xcrun swift-api-digester \
  -dump-sdk \
  -module MyKit \
  -sdk "$SDK_PATH" \
  -target arm64-apple-ios17.0-simulator \
  -F "$PRODUCTS" \
  -o ".build/api-report/current-ios-simulator.json"

xcrun swift-api-digester \
  -diagnose-sdk \
  -input-paths "api-baselines/MyKit-ios-simulator.json" \
  -input-paths ".build/api-report/current-ios-simulator.json" \
  > ".build/api-report/diagnostics.txt"

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

Какие изменения требуют особого внимания

Изменение Оценка по умолчанию Что делать
Удаление публичного типа или метода Несовместимое Восстановить интерфейс либо оформить явное изменение основной версии
Изменение параметра, возвращаемого значения или ограничения generic-типа Несовместимое Добавить совместимую перегрузку и запланировать период устаревания
Добавление обязательного требования в публичный протокол Высокий риск Рассмотреть реализацию по умолчанию
Добавление публичного метода или типа Обычно совместимое Проверить имя, видимость и платформенные атрибуты
Изменение только внутренней реализации Не должно появляться Проверить, не был ли случайно расширен уровень доступа

public не всегда означает намеренное обязательство поддерживать интерфейс. Если объявление не должно использоваться внешним кодом, лучше ограничить уровень доступа, а не поддерживать правила исключения в течение длительного времени.

Превратите диагностику в поддерживаемый барьер CI

Процесс рекомендуется разделить на четыре этапа: «сборка», «создание снимка», «сравнение» и «загрузка отчёта». В скрипте включите set -euo pipefail, а Derived Data размещайте в отдельном каталоге каждой задачи, чтобы параллельные задания не читали артефакты друг друга. В репозитории с несколькими модулями лучше поддерживать список модулей и обрабатывать их по очереди, не используя повторно путь PRODUCTS от предыдущего модуля.

Если проверка завершается неудачно, ревьюерам нужны три группы данных: версии Xcode и Swift, базовый и кандидатный снимки, а также полный текст диагностики. Обновлять базовый снимок в том же запросе на слияние следует только после подтверждения, что изменение соответствует принятой стратегии версионирования. Если разрешить неуспешной задаче самостоятельно перезаписывать базовый файл, проверка всегда будет проходить и потеряет смысл.

Порядок проверки ложных срабатываний

Сначала сверьте набор инструментов, затем SDK и целевую тройку, после этого — параметры сборки и флаги условной компиляции. Лишь в последнюю очередь следует решать, является ли различие шумом в выводе Digester. Сократить поиск причины поможет следующий список:

  1. Полностью ли совпадает вывод xcodebuild -version;
  2. Совпадают ли scheme, конфигурация и destination;
  3. Включён ли BUILD_LIBRARY_FOR_DISTRIBUTION с обеих сторон;
  4. Указывает ли путь поиска модуля на артефакты текущей задачи;
  5. Получен ли базовый снимок из последнего выпущенного интерфейса, а не из произвольного исторического коммита;
  6. Не попали ли в созданные файлы изменчивые абсолютные пути, например путь к рабочему каталогу.

Завершите процесс стратегией версионирования

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

Наиболее надёжный процесс выглядит так: ветка выпуска хранит базовый снимок, запрос на слияние создаёт только кандидатный снимок, CI формирует машинную диагностику, а сопровождающие принимают решение с учётом стратегии версионирования. Такой подход не запрещает без разбора любые изменения API и одновременно не позволяет случайному изменению public немедленно затронуть все зависимые проекты.

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

Заменяет ли Swift API Digester модульные тесты?

Нет. Он анализирует структуру публичного Swift API, но не проверяет поведение программы, бизнес-логику и интерфейсы Objective-C. Эти проверки нужно выполнять параллельно.

Когда следует обновлять базовый снимок API?

Только после подтверждения, что изменение соответствует правилам версионирования проекта. Снимок должен проходить ревью вместе с кодом и не должен автоматически перезаписываться в CI.

Почему одинаковый код иногда даёт разные результаты?

Причиной обычно становятся разные версии Xcode, Swift, SDK, целевая архитектура или конфигурация сборки. Для снимка и сравнения нужна одна зафиксированная среда.

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

Запустите следующую сборку на выделенном физическом узле.

Выберите чип, объем памяти, хранилище, узел и срок аренды и получите облачный Mac с ресурсами, не разделяемыми с другими клиентами.

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