Практика разработки

Контроль лицензий зависимостей iOS в CI

Контроль лицензий зависимостей iOS в CI

Команда успешно создала архив после обновления зависимостей, но лишь перед выпуском обнаружила, что новый компонент не попал в перечень лицензий открытого ПО. Обычно проблема не в сборке, а в разрозненных источниках зависимостей: у SwiftPM есть файл блокировки, CocoaPods использует отдельную структуру каталогов, а исходный код или бинарные пакеты могут быть добавлены непосредственно в репозиторий. Проверка в облачном Mac CI позволяет формировать перечень, сопоставлять его с политиками и сохранять подтверждающие материалы для одного и того же проекта в фиксированном рабочем окружении — вместо того чтобы перед выпуском вручную просматривать каталоги.

Сначала определите, на какие вопросы должен отвечать контроль

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

Результаты рекомендуется разделить на машиночитаемый перечень и отчёт для проверки человеком. Перечень в формате JSON должен содержать имя компонента, источник, зафиксированную версию, тип лицензии, хеш текста и путь обнаружения. Отчёт в формате Markdown должен показывать добавленные, удалённые и изменённые элементы. Оба файла следует сохранять как артефакты конвейера.

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

Инвентаризация фактических зависимостей по результатам сборки

Сначала позвольте Xcode разрешить зависимости, а затем считайте каталоги, которые фактически использует рабочее пространство. Это исключает попадание в проверку устаревшего кеша с машины разработчика. Для каждой задачи используйте отдельный каталог DerivedData и очищайте его после завершения задачи.

set -euo pipefail

ROOT="$(pwd)"
OUT="$ROOT/build/license-audit"
DD="$ROOT/build/DerivedData"
mkdir -p "$OUT"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -resolvePackageDependencies \
  -derivedDataPath "$DD"

find "$DD/SourcePackages/checkouts" \
  -maxdepth 3 \
  -type f \
  \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
  -print0 | sort -z > "$OUT/swiftpm-license-files.bin"

find "$ROOT/Pods" \
  -maxdepth 4 \
  -type f \
  \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
  -print0 | sort -z > "$OUT/pods-license-files.bin"

Для бинарных зависимостей недостаточно проверять только расширение файла. Необходимо также регистрировать внутренний идентификатор компонента, соответствующий адресу загрузки, контрольную сумму, версию и поставляемую вместе с пакетом декларацию. Для исходного кода, скопированного в репозиторий, следует вести файл compliance/manual-components.json с явно указанным владельцем каждого каталога, чтобы при каждом сканировании не полагаться на неточное сопоставление.

Файлы блокировки также должны учитываться

Скопируйте Package.resolved, Podfile.lock и перечень вручную добавленных компонентов в каталог отчёта. Контроль должен проверять, что каждый компонент из перечня присутствует хотя бы в одном зафиксированном источнике. Обратная проверка также обязательна: прямые зависимости из файлов блокировки не должны исчезать из отчёта. Если проект содержит несколько рабочих пространств, зависимости необходимо разрешать для каждого из них, а не только для основного приложения.

Нормализация лицензий и вычисление отпечатков

Тексты лицензий нередко различаются только символами перевода строки или пробелами в конце строк. Перед вычислением SHA-256 можно привести их к кодировке UTF-8 и переводам строк LF, а также удалить пробельные символы в конце строк. Нельзя удалять абзацы основного текста, строки с авторскими правами или годы — иначе реальные изменения останутся незамеченными.

python3 - <<'PY'
from pathlib import Path
import hashlib
import json

root = Path("build/license-audit/licenses")
rows = []
for path in sorted(root.rglob("*")):
    if not path.is_file():
        continue
    text = path.read_text(encoding="utf-8", errors="strict")
    normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip() + "\n"
    digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
    rows.append({"path": str(path), "sha256": digest})
Path("build/license-audit/license-hashes.json").write_text(
    json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
    encoding="utf-8"
)
PY

Тип лицензии и отпечаток текста необходимо хранить отдельно. Тип используется для сопоставления с политикой, а отпечаток — для выявления незаметных изменений со стороны поставщика. Если надёжно определить лицензию невозможно, пометьте её как UNKNOWN; знакомое имя файла не является основанием для автоматической классификации.

Управление изменениями с помощью файла политик

Файл политик должен храниться в системе контроля версий, а изменения в него должны проходить явно определённую процедуру проверки кода. Для идентификаторов лицензий можно задать статусы allow, review и deny, а для отдельных компонентов — сохранить хеши одобренных текстов. Рекомендуется использовать следующие результаты проверки:

Ситуация Результат CI Дальнейшее действие
Известный компонент, версия и текст не изменились Пропустить Сохранить отчёт
Новая версия, отпечаток лицензии не изменился Проверить Подтвердить изменение компонента
Новый компонент или неизвестная лицензия Заблокировать Добавить источник и одобрение
Изменился текст лицензии существующего компонента Заблокировать Сравнить полные тексты
Компонент есть в файле блокировки, но отсутствует в отчёте Заблокировать Исправить правила обнаружения

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

Интеграция отчёта в процессы слияния и выпуска

Для обычных запросов на слияние можно выполнять разрешение зависимостей, проверку различий и формирование отчёта. В задачу выпуска следует дополнительно включить проверку содержимого архива, чтобы убедиться, что итоговый артефакт соответствует тому же файлу блокировки. Базовую версию рекомендуется хранить в compliance/license-baseline.json; CI должен сравнивать только структурированные поля, а не цветной вывод терминала.

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

Проверки перед выпуском

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

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

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

Достаточно ли проверять только Package.resolved?

Нет. Он фиксирует версии SwiftPM, но не подтверждает наличие текста лицензии и не охватывает CocoaPods, скопированный исходный код и двоичные зависимости.

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

Останавливайте выпуск при отсутствии лицензии у новой зависимости, неизвестном условии, расхождении с lock-файлами или неожиданном изменении существующего текста лицензии.

Облачный Mac ArmCLI

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

Выбирайте модель, дата-центр и срок аренды под задачу. Узлы стабильно работают 365 дней в году; актуальный статус доступности отображается в консоли в реальном времени.