Если графика корректно работает на машине разработчика, но сборка архива в CI завершается ошибкой компиляции Metal лишь на одном из последних этапов, причина обычно не в «недостаточной производительности машины». Скорее всего, шейдеры не управляются как самостоятельные входные данные сборки. Более надёжный подход — сначала офлайн создать на облачном Mac версии metallib для физического устройства и симулятора с помощью текущего инструментария Xcode, а затем запускать полную сборку проекта. Так синтаксические ошибки, неверно выбранный SDK и отсутствующие функции обнаруживаются раньше, а журналы сбоев становятся короче.
Зачем выносить шейдеры в отдельный этап проверки
Xcode автоматически обрабатывает добавленные в таргет файлы .metal, но ошибки часто теряются в длинных журналах, включающих разрешение зависимостей, копирование ресурсов и подписание кода. Отдельный этап проверки не заменяет сборку Xcode, а перед её запуском отвечает на три вопроса:
- Компилируются ли все шейдеры текущим инструментарием?
- Можно ли отдельно создать библиотеки для физического устройства и симулятора?
- Загружаются ли из библиотеки функции, на которые действительно ссылается приложение?
Не копируйте созданный на машине разработчика
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
Выберите модель, регион узла и срок аренды в зависимости от задачи. Каждый экземпляр размещается на выделенном физическом сервере, а не в виртуализированной среде.