Инженерные заметки MiniDebug

Аудит Xcode Run Script на облачном Mac: входы, выходы и sandbox

Аудит Xcode Run Script на облачном Mac: входы, выходы и sandbox

Когда проект на облачном Mac переносят с локальной сборки, где всё работает, в автоматическое задание без участия пользователя, чаще всего упускают из виду не параметры компилятора, а Run Script в Build Phases. Скрипт может читать конфигурацию за пределами репозитория, записывать файлы в каталог исходного кода или запускаться при каждой сборке из-за отсутствия объявленных выходов. При одиночном запуске проблема может быть незаметна, но параллельные задания начинают перезаписывать файлы друг друга, а инкрементальная сборка постепенно теряет смысл. Устранять такие сбои следует не увеличением числа повторных попыток, а явным описанием файловых границ каждого скрипта с последующей проверкой в sandbox.

Создаём воспроизводимую базовую линию аудита

Сначала зафиксируйте проект, Scheme, конфигурацию сборки и каталог Derived Data. Во время аудита не используйте обычный рабочий каталог повторно: старые артефакты могут создать впечатление, что пропущенный шаг генерации по-прежнему выполняется корректно.

set -euo pipefail

ROOT="$PWD"
DERIVED="$ROOT/.audit-derived"

rm -rf "$DERIVED"

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED" \
  clean build | tee "$ROOT/audit-clean.log"

Если проект использует Workspace, замените -project на -workspace. Первая сборка нужна для проверки всей цепочки с нуля. Затем, не меняя исходный код, запустите ещё одну сборку без clean. Зафиксируйте количество запусков Run Script, общее время их выполнения и время изменения выходных файлов. Эти два журнала станут базовыми линиями для чистой и инкрементальной сборки соответственно.

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

Инвентаризируем все этапы Run Script

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

grep -nE "PBXShellScriptBuildPhase|shellScript =|inputPaths =|outputPaths =" \
  App.xcodeproj/project.pbxproj

Составьте таблицу для каждого этапа и не ограничивайтесь названием скрипта. Скрипты часто называются просто «Run Script», поэтому такое имя бесполезно при диагностике.

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

В начале скрипта рекомендуется использовать set -euo pipefail. Также проверьте команды, соединённые конвейерами: без pipefail ошибка предыдущей команды может быть скрыта успешным выполнением tee в конце.

Объявляем файловые границы через xcfilelist

Небольшое число путей можно указать непосредственно в Input Files и Output Files. Если файлов много, списки .xcfilelist удобнее проверять. По возможности формируйте пути на основе переменных сборки, таких как $(SRCROOT) и $(DERIVED_FILE_DIR), не закрепляя в них пользовательские каталоги.

Например, этап, который создаёт сводку на основе YAML-конфигурации, может использовать следующий список входов:

$(SRCROOT)/Config/app.yml
$(SRCROOT)/Scripts/generate-config.sh

В списке выходов указывайте только те файлы, которые скрипт действительно создаёт:

$(DERIVED_FILE_DIR)/Generated/config.sha256

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

set -euo pipefail

SOURCE="$SRCROOT/Config/app.yml"
OUTPUT="$DERIVED_FILE_DIR/Generated/config.sha256"
TEMP="$OUTPUT.tmp.$$"

mkdir -p "$(dirname "$OUTPUT")"
shasum -a 256 "$SOURCE" > "$TEMP"
mv "$TEMP" "$OUTPUT"

Не объявляйте весь каталог репозитория входом, а корневой каталог исходников — выходом. Чрезмерно широкие границы могут устранить ошибки, но тогда любое изменение файла будет запускать скрипт, а реальные зависимости останутся скрытыми. Создаваемые артефакты предпочтительно записывать в Derived Data. Если результат генерации кода действительно нужно вернуть в репозиторий, выполняйте этот шаг отдельно и проверяйте изменения через систему контроля версий.

Включаем sandbox и анализируем журналы отказов

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

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$PWD/.audit-derived" \
  ENABLE_USER_SCRIPT_SANDBOXING=YES \
  build | tee "$PWD/audit-sandbox.log"

При появлении sandbox deny сначала определите запрещённый путь, тип операции и соответствующий этап. Если скрипт читает необъявленную конфигурацию, добавьте её во входы. Если он создаёт или изменяет необъявленный файл, добавьте этот файл в выходы. Чтение инструментом системных библиотек среды выполнения обычно не требует включать в список весь системный каталог. В первую очередь проверяйте файлы проекта, конфигурационные файлы и пути к собственным инструментам, к которым скрипт обращается явно.

Распространённая ошибка — сразу отключить sandbox или добавить во входы весь домашний каталог пользователя. В первом случае неявные зависимости сохранятся, во втором возникнут неконтролируемые пересборки. Если скрипту нужен файл за пределами репозитория, скопируйте его в рабочий каталог задания, проверьте и только после этого используйте как явно объявленный вход.

Добавляем проверку инкрементальности и параллельности в CI

После исправлений выполните как минимум четыре группы тестов: чистую сборку с пустым Derived Data, вторую сборку без изменений в исходном коде, сборку после изменения одного объявленного входа и параллельную сборку с двумя независимыми каталогами Derived Data. Параллельные задания могут использовать общую копию исходников только для чтения, но не должны совместно использовать каталог выходов.

Проверяйте результат в следующем порядке:

  1. Чистая сборка создаёт с нуля все необходимые артефакты.
  2. При второй сборке скрипты с уже существующими стабильными выходами не запускаются безусловно.
  3. После изменения объявленного входа соответствующий этап запускается повторно.
  4. Изменение несвязанного файла не запускает этот этап.
  5. Два параллельных задания не перезаписывают один и тот же временный файл или отчёт.
  6. При ошибке скрипта xcodebuild возвращает ненулевой статус.
  7. В журнал не выводятся токены, содержимое закрытых ключей или полный набор переменных окружения.

В завершение сохраните настройку sandbox в конфигурации сборки, которую команда использует на практике, и оставьте отдельное задание для регулярной чистой сборки. Инкрементальная сборка обеспечивает скорость, а чистая выявляет пропущенные зависимости. Только если успешно проходят обе, файловые границы Run Script можно считать воспроизводимыми.

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

Почему Run Script выполняется при каждой сборке?

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

Что делать при сообщении sandbox deny?

Нужно найти в журнале запрещённый путь и тип операции, а затем добавить необходимый путь во входы, выходы или соответствующий xcfilelist. Отключение sandbox лишь скрывает зависимость.

Как проверить корректность инкрементальной сборки?

Выполните одинаковую сборку дважды, затем измените один объявленный вход. Второй неизменённый запуск должен пропустить фазу, а изменение входа должно обновить её выход.

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

Разместите следующую сборку iOS на MiniDebug M4.

В стандартную комплектацию входят M4, 16 ГБ ОЗУ и SSD на 256 ГБ. Доступна аренда на день, неделю, месяц или квартал, а выбрать можно один из пяти узлов: Сингапур, Токио, Южная Корея (Сеул), Гонконг или восток США. Фактическая доступность отображается в консоли в реальном времени.

Выбрать узел и оформить заказ