После переноса iOS-проекта на облачный Mac успешная сборка ещё не означает, что сведения о конфиденциальности указаны полностью. При обновлении зависимостей может появиться новый файл PrivacyInfo.xcprivacy, а из-за настроек копирования ресурсов манифест основного проекта может не попасть в готовое App. Надёжнее считать манифесты конфиденциальности частью артефактов сборки: сначала проверять исходный код, затем готовые артефакты, а после этого блокировать непроверенные изменения с помощью версионируемой базовой линии.
Сначала определите область проверки
Практическая проверка должна охватывать как минимум четыре уровня: возможность разобрать манифест как plist, корректность типов полей верхнего уровня, включение манифестов всех зависимостей в область проверки и наличие ожидаемых манифестов в готовом App. Недостаточно искать один файл в корне репозитория. Собственные манифесты могут находиться в зависимостях с исходным кодом, предварительно скомпилированных фреймворках и компонентах, загруженных менеджером пакетов.
Сначала создайте индекс манифестов в чистом рабочем каталоге:
find . \
-path './.git' -prune -o \
-path './DerivedData' -prune -o \
-name PrivacyInfo.xcprivacy -print \
| LC_ALL=C sort > privacy-manifests.current
Пути, которые явно контролируются проектом, рекомендуется записать в privacy-manifests.baseline. Конвейер должен сравнивать эти два файла. При добавлении, удалении или изменении пути проверка сначала завершается ошибкой, после чего владелец кода подтверждает изменение и обновляет базовую линию. Благодаря этому изменения деклараций, вызванные обновлением зависимостей, не останутся незамеченными.
Базовая линия не доказывает соответствие требованиям. Она подтверждает только то, что текущее изменение прошло явную проверку, но не заменяет оценку фактического поведения кода.
Проверьте plist и структуру верхнего уровня
Команда plutil -lint обнаруживает повреждённые XML- и бинарные plist-файлы, но не проверяет типы полей. Лёгкую проверку структуры можно добавить с помощью стандартной библиотеки Python, не устанавливая дополнительные зависимости:
import pathlib
import plistlib
import sys
allowed = {
"NSPrivacyTracking": bool,
"NSPrivacyTrackingDomains": list,
"NSPrivacyCollectedDataTypes": list,
"NSPrivacyAccessedAPITypes": list,
}
failed = False
files = sorted(pathlib.Path(".").rglob("PrivacyInfo.xcprivacy"))
if not files:
print("No privacy manifest found")
sys.exit(1)
for path in files:
try:
with path.open("rb") as stream:
data = plistlib.load(stream)
if not isinstance(data, dict):
raise TypeError("Root must be a dictionary")
for key, value in data.items():
expected = allowed.get(key)
if expected is None:
raise KeyError(f"Unknown top-level key: {key}")
if not isinstance(value, expected):
raise TypeError(f"{key} must be {expected.__name__}")
print(path)
except Exception as error:
failed = True
print(f"{path}: {error}")
sys.exit(1 if failed else 0)
Сохраните скрипт как Scripts/validate_privacy_manifests.py и запускайте его перед сборкой. Если проект использует внутренние расширенные поля организации, не следует сразу разрешать все неизвестные ключи. Каждый такой ключ нужно отдельно добавить в список разрешённых и задокументировать его назначение.
Создайте таблицу проверки Required Reason API
Корректная структура ещё не означает, что причина использования указана верно. Обращения к временным меткам файлов, времени запуска системы, объёму свободного места, настройкам и другим подобным данным также должны проверяться при ревью кода. Автоматическое сканирование даёт полезные подсказки, но макросы, обёртки и бинарные зависимости могут приводить как к пропускам, так и к ложным срабатываниям. Поэтому нельзя выбирать код причины только по результатам одного текстового поиска.
Для каждой декларации ведите отдельную строку с данными, пригодными для аудита:
| Проверяемый пункт | Что нужно записать | Условие ошибки |
|---|---|---|
| Категория API | Идентификатор категории из манифеста | Для категории нет соответствующего пути в коде |
| Место использования | Модуль, файл и ответственный | Невозможно определить источник вызова |
| Цель использования | Реальная пользовательская функция | Причина не соответствует поведению |
| Источник зависимости | Собственный код или конкретный компонент | Источник бинарного файла неясен |
| Условие повторной проверки | Изменение кода или версии зависимости | После изменения проверка не выполнена повторно |
Для бинарных компонентов с закрытым исходным кодом необходимо как минимум записывать версию компонента, хеш манифеста и бизнес-функцию, ради которой он был добавлен. Если происхождение декларации невозможно объяснить, не следует дополнять её догадками — нужно запросить подтверждение у ответственного за зависимость.
Повторно проверьте готовое App
Файл из исходного кода может не скопироваться из-за неверного target membership или неправильной настройки этапа ресурсов. Конвейер должен выполнить одну неподписанную Release-сборку, а затем проверить фактический артефакт:
rm -rf .build/privacy
xcodebuild \
-scheme "$SCHEME" \
-configuration Release \
-sdk iphoneos \
-derivedDataPath .build/privacy \
CODE_SIGNING_ALLOWED=NO \
build
APP_PATH="$(find .build/privacy/Build/Products -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
find "$APP_PATH" -name PrivacyInfo.xcprivacy -print | LC_ALL=C sort
Манифесты должны присутствовать в основном App, встроенных фреймворках и расширениях в соответствии со структурой проекта. Не задавайте случайный каталог DerivedData жёстко и не ограничивайтесь подсчётом файлов в исходном коде: во время сборки несколько исходных манифестов могут быть объединены, заменены или пропущены.
Сохраните инвентаризацию артефактов
Сохраните относительный путь и SHA-256 каждого манифеста внутри артефакта как вложение конвейера:
find "$APP_PATH" -name PrivacyInfo.xcprivacy -print0 |
while IFS= read -r -d '' file; do
relative="${file#"$APP_PATH"/}"
digest="$(shasum -a 256 "$file" | awk '{print $1}')"
printf '%s %s
' "$digest" "$relative"
done | LC_ALL=C sort > privacy-artifact.sha256
Если при проверке приложения возникнут вопросы, эта запись позволит ответить, что именно содержалось в конкретной сборке. Она надёжнее, чем просмотр только текущей ветки.
Включите условия ошибки в повседневный процесс
Проверки рекомендуется разделить на два уровня: быстрые и полные. Для запроса на слияние сначала запускайте поиск манифестов, разбор plist, проверку типов полей и сравнение с базовой линией. В основной ветке дополнительно выполняйте Release-сборку и проверку артефактов App. Быстрая проверка при ошибке обычно указывает проблемный путь за несколько десятков секунд, а полная выявляет проблемы на этапе обработки ресурсов и во встроенных фреймворках.
Перед выпуском сохраняйте следующие проверки:
- Индекс манифестов совпадает с утверждённой базовой линией.
- Каждый файл успешно проходит
plutil -lintи скрипт проверки структуры. - Для каждой новой категории API указаны место в коде, ответственный и фактическая цель использования.
- Различия в манифестах после обновления зависимостей проверены вручную.
- Манифесты в готовом App, расширениях и встроенных фреймворках соответствуют ожиданиям.
- Пути и хеши артефактов заархивированы вместе с текущей сборкой.
Цель такой проверки — не принимать за команду автоматические решения о соответствии требованиям, а превращать пропуски в явные ошибки сборки и обеспечивать связь каждого изменения деклараций с кодом, зависимостями и записями о проверке.
Часто задаваемые вопросы
Достаточно ли добавить PrivacyInfo.xcprivacy в репозиторий?
Нет. Файл должен корректно читаться, содержать поля нужных типов и попадать в итоговый артефакт приложения или фреймворка.
Может ли скрипт проверить корректность каждого основания Required Reason API?
Не полностью. Скрипт находит отсутствие файла, ошибки структуры и непроверенные изменения, но соответствие основания реальному коду оценивает его владелец.
Зачем обновлять базовую линию после изменения зависимости?
Зависимость может добавить или изменить собственный манифест. Базовую линию обновляют только после проверки различий и заявленного поведения.
Выберите выделенный облачный Mac для следующей сборки
Выбирайте модель Mac mini, срок аренды и регион под рабочую нагрузку. Каждый заказ соответствует отдельному физическому узлу, а фактическая доступность определяется в реальном времени через консоль.