Как надёжно разбирать вывод macOS и Xcode в облачном CI

DevOps и CI/CD ·~5 мин чтения

Как надёжно разбирать вывод macOS и Xcode в облачном CI

Один и тот же CI-скрипт может корректно работать в интерактивном терминале, но выбирать неверную схему Scheme на автоматизированном облачном Mac. Обычно причина не в самом Xcode, а в том, что скрипт использует человекочитаемый текст как программный интерфейс. Системный язык, версия инструмента, место появления предупреждений и даже один лишний пробел могут нарушить работу grep, cut и логики, зависящей от фиксированных номеров строк. Надёжнее по возможности читать JSON или plist, отдельно сохраняя код завершения, исходный вывод и результат разбора.

Найдите нестабильный разбор текста

Сначала найдите в конвейере потенциально опасные сочетания команд:

grep -RInE 'xcodebuild|simctl|system_profiler|sw_vers' ci scripts \
  | grep -E 'grep|awk|sed|cut|tail|head'

Не каждый конвейер команд ошибочен. Опасность возникает, когда скрипт зависит от естественного языка: например, ищет строку «Build Settings», извлекает второе поле после двоеточия или предполагает, что нужное устройство всегда находится в третьей строке. LC_ALL=C уменьшает влияние локали, но не фиксирует порядок полей, содержание предупреждений и формат будущих версий.

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

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

Преобразуйте настройки сборки в JSON-контракт

Команда xcodebuild -showBuildSettings -json позволяет получить настройки сборки цели напрямую. Сначала необходимо проверить код завершения команды и только затем разбирать файл; стандартный поток ошибок нельзя смешивать с JSON.

set -euo pipefail

mkdir -p artifacts/environment

if ! xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings \
  -json \
  > artifacts/environment/build-settings.json \
  2> artifacts/environment/build-settings.stderr; then
  cat artifacts/environment/build-settings.stderr >&2
  exit 1
fi

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

path = Path("artifacts/environment/build-settings.json")
if not path.exists() or path.stat().st_size == 0:
    raise SystemExit("build settings output is empty")

items = json.loads(path.read_text(encoding="utf-8"))
if len(items) != 1:
    raise SystemExit(f"expected one target, got {len(items)}")

settings = items[0].get("buildSettings", {})
required = ["PRODUCT_BUNDLE_IDENTIFIER", "SDKROOT", "CONFIGURATION_BUILD_DIR"]
missing = [key for key in required if not settings.get(key)]
if missing:
    raise SystemExit("missing build settings: " + ", ".join(missing))

print(settings["PRODUCT_BUNDLE_IDENTIFIER"])
PY

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

Зафиксируйте выбор устройства с помощью JSON от simctl

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

xcrun simctl list -j devices available \
  > artifacts/environment/simulators.json

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

data = json.loads(
    Path("artifacts/environment/simulators.json").read_text(encoding="utf-8")
)
matches = []
for runtime, devices in data.get("devices", {}).items():
    if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"):
        continue
    for device in devices:
        if device.get("isAvailable") and device.get("name") == "iPhone 16":
            matches.append((runtime, device["udid"]))

if len(matches) != 1:
    raise SystemExit(f"expected one matching simulator, got {len(matches)}")

print(matches[0][1])
PY

В production-скрипте ожидаемую версию среды выполнения также следует закрепить в конфигурации репозитория, а не выбирать автоматически «самую новую». Автоматический выбор последней версии позволяет обновлению системных компонентов напрямую изменить базовую среду тестирования. При фактическом запуске тестов передавайте UDID: это оставляет меньше неоднозначности, чем одно имя устройства.

Не игнорируйте пустой набор устройств

Отсутствие подходящих устройств должно считаться явной ошибкой. Перед временным созданием симулятора убедитесь, что требуемая среда выполнения действительно существует. После завершения задания удаляйте только устройства, созданные этим заданием, и не очищайте общий набор устройств. Для параллельных заданий предпочтительно задавать отдельные значения -derivedDataPath и независимые наборы устройств CoreSimulator.

Читайте сведения о хосте из plist

Сведения о версии macOS можно получить непосредственно из системного plist, не разбирая текстовые метки в выводе sw_vers:

SYSTEM_PLIST=/System/Library/CoreServices/SystemVersion.plist

product_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductVersion' "$SYSTEM_PLIST"
)
build_version=$(
  /usr/libexec/PlistBuddy -c 'Print :ProductBuildVersion' "$SYSTEM_PLIST"
)

printf '%s\n' "$product_version" > artifacts/environment/macos-version.txt
printf '%s\n' "$build_version" > artifacts/environment/macos-build.txt
xcodebuild -version > artifacts/environment/xcode-version.txt

Для чтения plist проекта предпочтительно использовать plutil -extract KEY raw -o - FILE. Если поле может отсутствовать, сначала проверьте его командой plutil -type KEY FILE, а затем явно определите в скрипте, допустимо ли его отсутствие. Не обрабатывайте XML регулярными выражениями: plist может быть в бинарном формате, а строки могут содержать экранированные символы.

Создайте диагностируемую границу сбора данных

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

Рекомендуется поручить каждому сборщику работу только с одним объектом и записывать вывод во временный файл. После успешной проверки синтаксиса и всех обязательных полей замените итоговый файл атомарной операцией mv. Тогда прерывание команды не оставит после себя неполный JSON.

Проверки перед коммитом

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

  • Скрипт не извлекает поля по фиксированным номерам строк или меткам на естественном языке.
  • Стандартный вывод и стандартный поток ошибок записываются в разные файлы.
  • Синтаксис JSON и plist проверяется до чтения данных.
  • Фильтрация массивов явно обрабатывает как отсутствие совпадений, так и несколько совпадений.
  • Версия Xcode, номер сборки macOS и UDID выбранного устройства архивируются вместе с заданием.
  • В журнал записываются только сведения о среде — без токенов, данных доступа и полного набора переменных окружения.
  • После обновления версий инструментов сначала запускается приёмочная проверка сбора данных, и лишь затем разрешаются production-задания сборки.

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

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

Достаточно ли установить LC_ALL=C?

Нет. Это уменьшает влияние локали, но не фиксирует порядок полей, пробелы, предупреждения и изменения формата между версиями. Для автоматизации нужны JSON или plist.

Что сохранять при ошибке структурированной команды?

Сохраняйте код возврата, stderr, исходный JSON или plist, версию Xcode и номер сборки macOS. Перед чтением проверяйте, что файл непустой и синтаксически корректный.

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

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

Выберите Berry M4 или Berry M4 Pro, проверьте пять узлов, срок аренды и дополнительные опции, затем оформите заказ.

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