Инженерная статья

Офлайн-компиляция и проверка Metal-шейдеров в Cloud Mac CI

Офлайн-компиляция и проверка Metal-шейдеров в Cloud Mac CI

Если графика корректно работает на машине разработчика, но сборка архива в CI завершается ошибкой компиляции Metal лишь на одном из последних этапов, причина обычно не в «недостаточной производительности машины». Скорее всего, шейдеры не управляются как самостоятельные входные данные сборки. Более надёжный подход — сначала офлайн создать на облачном Mac версии metallib для физического устройства и симулятора с помощью текущего инструментария Xcode, а затем запускать полную сборку проекта. Так синтаксические ошибки, неверно выбранный SDK и отсутствующие функции обнаруживаются раньше, а журналы сбоев становятся короче.

Зачем выносить шейдеры в отдельный этап проверки

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

  1. Компилируются ли все шейдеры текущим инструментарием?
  2. Можно ли отдельно создать библиотеки для физического устройства и симулятора?
  3. Загружаются ли из библиотеки функции, на которые действительно ссылается приложение?

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

Рекомендуется хранить в репозитории только исходные файлы .metal и сценарии компиляции, а .air и .metallib помещать в очищаемый каталог сборки. Если команда использует кеширование, ключ кеша должен включать как минимум хеш исходного кода, путь к Xcode, версию SDK и целевую платформу.

Фиксация инструментария для текущей сборки

На облачном Mac может быть установлено несколько версий Xcode. Сценарий не должен зависеть от версии, которая в последний раз была выбрана в графическом интерфейсе. Вместо этого необходимо явно проверять DEVELOPER_DIR. Путь определяется фактической конфигурацией узла; не используйте в разных заданиях одно и то же значение, если оно не было проверено.

set -euo pipefail

: "${DEVELOPER_DIR:?DEVELOPER_DIR is required}"

xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version
xcrun --sdk iphonesimulator --show-sdk-version
xcrun --sdk iphoneos --find metal
xcrun --sdk iphoneos metal --version

mkdir -p build/metal/metadata
{
  echo "developer_dir=$DEVELOPER_DIR"
  xcodebuild -version
  echo "iphoneos=$(xcrun --sdk iphoneos --show-sdk-version)"
  echo "iphonesimulator=$(xcrun --sdk iphonesimulator --show-sdk-version)"
  xcrun --sdk iphoneos metal --version
} > build/metal/metadata/toolchain.txt

Файл toolchain.txt следует архивировать вместе с журналами сбоя. Он позволяет быстро отличить регрессию исходного кода от ситуации, когда на исполнителе CI переключилась версия Xcode. Недостаточно записать только xcodebuild -version, поскольку на результат также влияет фактически используемый SDK.

Раздельная компиляция артефактов для устройства и симулятора

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

set -euo pipefail

sources=(Shaders/*.metal)
if [ ! -e "${sources[0]}" ]; then
  echo "No Metal source files found" >&2
  exit 1
fi

for sdk in iphoneos iphonesimulator; do
  out="build/metal/$sdk"
  rm -rf "$out"
  mkdir -p "$out/air"

  for src in "${sources[@]}"; do
    name="$(basename "$src" .metal)"
    xcrun --sdk "$sdk" metal -c "$src" \
      -o "$out/air/$name.air"
  done

  xcrun --sdk "$sdk" metallib "$out"/air/*.air \
    -o "$out/AppShaders.metallib"

  test -s "$out/AppShaders.metallib"
  shasum -a 256 "$out/AppShaders.metallib" \
    > "$out/AppShaders.metallib.sha256"
done

Два набора артефактов необходимо сохранять в разных каталогах, чтобы задание, выполняемое позже, не перезаписало предыдущий набор. Если исходный код различает платформы с помощью макросов препроцессора C/C++, соберите их параметры в одном месте сценария и убедитесь, что в Xcode Build Phase используются те же определения.

Объект проверки Таргет физического устройства Таргет симулятора
SDK iphoneos iphonesimulator
Промежуточные файлы Отдельные .air Отдельные .air
Выходная библиотека Архивируется отдельно Архивируется отдельно
Итоговая проверка Физическое устройство или штатное архивирование Дымовой тест в симуляторе

Проверка контракта функций с помощью теста загрузки

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

import Metal
import XCTest

final class ShaderLibraryTests: XCTestCase {
    func testRequiredFunctionsExist() throws {
        let device = try XCTUnwrap(MTLCreateSystemDefaultDevice())
        let url = try XCTUnwrap(
            Bundle(for: Self.self).url(
                forResource: "AppShaders",
                withExtension: "metallib"
            )
        )
        let library = try device.makeLibrary(URL: url)

        for name in ["imageVertex", "imageFragment", "toneMapKernel"] {
            XCTAssertNotNil(
                library.makeFunction(name: name),
                "Missing Metal function: \(name)"
            )
        }
    }
}

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

Какие данные сохранять при сбое

При диагностике сбоев Metal в CI полезнее сохранить не весь рабочий каталог, а небольшой набор данных, которые можно сопоставить:

  • версии DEVELOPER_DIR, Xcode и обоих SDK;
  • фактически выполненные команды компиляции и стандартный поток ошибок;
  • хеш исходного кода каждого файла .metal;
  • размеры и SHA-256 двух файлов metallib;
  • имя функции, которую не удалось загрузить, тестовый таргет и таргет запуска;
  • идентификатор коммита, использованного в текущем задании.

Хеши исходного кода можно создать командой find Shaders -name '*.metal' -print0 | sort -z | xargs -0 shasum -a 256. Изменение хеша артефакта само по себе не должно приводить к сбою, поскольку обновление инструментария также может изменить бинарный файл. Правильная стратегия — сначала сравнить отпечатки инструментария, а затем использовать результаты повторной компиляции, загрузки и создания конвейера в качестве критериев проверки.

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

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

Зачем отдельно компилировать Metal-шейдеры, если это уже делает Xcode?

Отдельный этап раньше обнаруживает ошибки синтаксиса, SDK и целевой платформы и дает короткий журнал. Штатный этап Xcode при этом сохраняется.

Можно ли использовать один metallib для устройства и симулятора?

Не следует. Библиотеки нужно собирать отдельно через SDK iphoneos и iphonesimulator, а затем проверять загрузку в соответствующей среде.

Изменение хеша metallib всегда означает неисправную сборку?

Нет. Сначала сравнивают DEVELOPER_DIR, версию SDK и компилятора, после чего оценивают загрузку библиотеки и результат короткого теста.

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

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

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

Выбрать тариф облачного Mac