개발 머신에서는 그래픽 기능이 정상적으로 작동하지만 CI 아카이브 작업 막바지에 Metal 컴파일 오류가 발생한다면, 원인은 대개 “머신 성능 부족”이 아니라 셰이더가 독립적인 빌드 입력으로 관리되지 않았기 때문입니다. 더 안정적인 방법은 클라우드 Mac에서 현재 Xcode 도구 체인을 사용해 실제 기기용과 시뮬레이터용 metallib을 오프라인으로 먼저 생성한 다음 전체 프로젝트 빌드를 진행하는 것입니다. 이렇게 하면 구문 오류, 잘못 선택된 SDK, 누락된 함수를 더 일찍 발견할 수 있고 실패 로그도 짧아집니다.
셰이더를 별도 게이트로 분리해야 하는 이유
Xcode는 타깃에 추가된 .metal 파일을 자동으로 처리하지만, 오류는 의존성 분석, 리소스 복사, 코드 서명 단계까지 포함된 긴 로그 속에 묻히기 쉽습니다. 별도 게이트는 Xcode 빌드를 대체하는 것이 아니라 그 전에 다음 세 가지를 확인합니다.
- 현재 도구 체인으로 모든 셰이더를 컴파일할 수 있는가?
- 실제 기기용과 시뮬레이터용 라이브러리를 각각 생성할 수 있는가?
- 앱에서 실제로 참조하는 함수 이름을 라이브러리에서 불러올 수 있는가?
개발 머신에서 생성한
metallib을 장기간 사용하는 아티팩트 디렉터리에 그대로 복사하지 마세요. 도구 체인, SDK 또는 컴파일 옵션이 바뀌어도 기존 파일이 남아 있을 수 있으며, 이 파일은 더 이상 현재 소스 코드를 반영하지 않을 수 있습니다.
저장소에는 .metal 소스 코드와 컴파일 스크립트만 커밋하고, .air와 .metallib은 정리 가능한 빌드 디렉터리에 두는 것이 좋습니다. 팀에서 캐시를 사용해야 한다면 캐시 키에는 최소한 소스 해시, Xcode 경로, SDK 버전, 대상 플랫폼이 포함되어야 합니다.
이번 빌드에서 사용할 도구 체인 고정하기
클라우드 Mac에는 여러 버전의 Xcode가 설치되어 있을 수 있습니다. 스크립트는 GUI에서 마지막으로 선택한 버전에 의존하지 말고 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은 실패 로그와 함께 아카이브해야 합니다. 이 파일을 이용하면 “소스 코드 회귀”와 “러너에서 Xcode가 변경된 문제”를 빠르게 구분할 수 있습니다. 실제로 호출되는 SDK도 결과에 영향을 주므로 xcodebuild -version만 기록해서는 안 됩니다.
실제 기기용과 시뮬레이터용 아티팩트 각각 컴파일하기
먼저 각 소스 파일을 개별 .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파일의 소스 해시 - 두
metallib의 크기와 SHA-256 - 로드에 실패한 함수 이름, 테스트 타깃, 실행 타깃
- 이번 작업에 사용된 커밋 식별자
소스 해시는 find Shaders -name '*.metal' -print0 | sort -z | xargs -0 shasum -a 256 명령으로 생성할 수 있습니다. 도구 체인을 업그레이드해도 바이너리가 달라질 수 있으므로 아티팩트 해시 변경만으로 실패 처리해서는 안 됩니다. 올바른 방법은 먼저 도구 체인 지문을 비교한 다음 재컴파일, 로드, 파이프라인 생성 결과를 게이트 기준으로 사용하는 것입니다.
MacMLab의 전용 물리 노드에서 이런 작업을 실행하더라도 동일한 작업 공간이 연속 작업으로 오염될 수 있습니다. 빌드할 때마다 대상 출력 디렉터리를 비우고, 작업이 끝나면 메타데이터, 로그, 최종 라이브러리만 아카이브하세요. 이렇게 하면 이전 .air 파일이 새 라이브러리에 섞이는 것을 막을 수 있으며, 다음 실패를 재현 가능한 상태에서 조사할 수 있습니다.
자주 묻는 질문
Xcode가 Metal 파일을 컴파일하는데 별도 단계가 필요한가요?
전체 아카이브 전에 문법, SDK, 대상 차이를 발견하고 짧은 오류 로그를 얻을 수 있습니다. Xcode의 기본 셰이더 빌드도 그대로 유지합니다.
기기와 시뮬레이터가 같은 metallib을 사용해도 되나요?
공용으로 사용한다고 가정하면 안 됩니다. iphoneos와 iphonesimulator SDK로 각각 빌드한 뒤 해당 실행 환경에서 로딩을 확인합니다.
metallib 해시가 달라지면 빌드 오류인가요?
반드시 그렇지는 않습니다. Xcode 경로, SDK, 컴파일러 버전을 먼저 비교하고 로딩 및 스모크 테스트 결과로 유효성을 판단합니다.
전용 물리 노드
개발 및 빌드 작업을 전용 클라우드 Mac에서 실행하세요
작업에 맞는 모델, 노드 지역 및 대여 기간을 선택하세요. 각 인스턴스는 전용 물리 머신으로 제공되며 가상 머신 환경이 아닙니다.