Команда успешно создала архив после обновления зависимостей, но лишь перед выпуском обнаружила, что новый компонент не попал в перечень лицензий открытого ПО. Обычно проблема не в сборке, а в разрозненных источниках зависимостей: у 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 для следующей сборки
Выбирайте модель, дата-центр и срок аренды под задачу. Узлы стабильно работают 365 дней в году; актуальный статус доступности отображается в консоли в реальном времени.