동일한 커밋이 로컬에서는 정상적으로 빌드되지만, 계속 실행되는 클라우드 Mac에 올리면 간헐적으로 module file was created by a different version of the compiler 오류가 발생하고 재실행 시 다시 성공하는 경우가 있습니다. 이런 문제는 대개 Swift 소스 코드가 무작위로 바뀌어서가 아니라 서로 다른 툴체인, SDK 또는 병렬 작업이 동일한 사전 컴파일 모듈 캐시를 읽기 때문에 발생합니다. DerivedData 전체를 삭제하면 당장은 증상이 사라질 수 있지만, 가장 중요한 장애 현장 정보까지 지워지며 다음 오염도 막지 못합니다.
먼저 모듈 캐시 문제인지 확인하기
Clang은 시스템 헤더와 Objective-C 모듈을 PCM으로 컴파일하며, Swift 컴파일러도 자체 모듈 캐시를 관리합니다. 캐시를 생성할 때 사용한 컴파일러, SDK, 아키텍처 또는 컴파일 옵션이 현재 작업과 다르면 로드 단계에서 실패할 수 있습니다.
먼저 전체 로그에서 다음 특징을 검색합니다.
| 로그 특징 | 가능성이 높은 원인 | 첫 번째 확인 항목 |
|---|---|---|
different version of the compiler |
Xcode 전환 이력 | xcodebuild -version |
PCM file is out of date |
SDK 또는 헤더 파일 타임스탬프 변경 | 현재 SDK 경로 |
could not build module |
상위 모듈 손상 | 처음 실패한 모듈 |
| 동일 작업이 재실행 후 성공 | 병렬 쓰기 또는 공유 캐시 | 캐시 디렉터리 소유 관계 |
마지막 오류 한 줄만 발췌해서는 안 됩니다. 모듈 로드 실패는 수십 개의 연쇄 오류를 일으키는 경우가 많으므로, 로그를 거슬러 올라가 module, .pcm 또는 ModuleCache가 처음 나타난 위치를 찾아야 합니다.
캐시를 지우기 전에 실패 로그, Xcode 빌드 번호와 실제 빌드 설정을 먼저 보관하십시오. 이 정보가 없으면 다시 성공하더라도 툴체인 드리프트, 동시 실행에 따른 오염, 캐시 자체 손상 중 무엇이 원인이었는지 판단할 수 없습니다.
실패 작업에서 비교 가능한 현장 정보 보관하기
VMKeep의 클라우드 Mac에서는 스크립트가 사용하리라 예상한 버전만 확인하지 말고, 명령이 실제로 해석한 개발자 디렉터리를 먼저 기록해야 합니다.
set -o pipefail
mkdir -p artifacts/diagnostics
xcode-select -p | tee artifacts/diagnostics/developer-dir.txt
xcodebuild -version | tee artifacts/diagnostics/xcode-version.txt
xcrun --sdk iphoneos --show-sdk-path | tee artifacts/diagnostics/sdk-path.txt
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings \
> artifacts/diagnostics/build-settings.txt
그다음 실패한 명령의 SDKROOT, ARCHS, DEVELOPER_DIR, CLANG_MODULE_CACHE_PATH, SWIFT_MODULE_CACHE_PATH를 확인합니다. 작업이 sudo, 원격 세션 또는 스케줄러 프로세스를 통해 시작된다면 실행 사용자와 HOME도 비교해야 합니다. 같은 스크립트라도 실행 사용자가 바뀌면 기본 캐시 위치 역시 달라집니다.
툴체인 진입점 고정하기
빌드 스크립트는 DEVELOPER_DIR를 명시적으로 설정하고 실제 컴파일 전에 xcodebuild -version을 한 번 실행해야 합니다. 버전이 예상과 다르면 즉시 종료하고, 잘못된 툴체인으로 공유 캐시에 데이터를 기록하지 않도록 해야 합니다. Xcode를 업그레이드한 뒤에는 기존 디렉터리를 계속 사용하지 말고 새로운 캐시 키를 생성해야 합니다.
캐시 경계를 개별 작업 단위로 축소하기
신뢰할 수 있는 디렉터리 키에는 최소한 Xcode 빌드 번호, SDK, 아키텍처와 작업 식별자가 포함되어야 합니다. 브랜치 이름을 경로로 바로 사용해서는 안 되며, 먼저 특수 문자를 변환해야 합니다. 다음 방식은 각 작업에 독립적인 DerivedData와 모듈 캐시를 할당하면서 종속성 다운로드 디렉터리는 별도 위치에 유지합니다.
set -euo pipefail
JOB_KEY="$(printf '%s' "${CI_JOB_ID:-local}" | tr -cs 'A-Za-z0-9._-' '-')"
XCODE_BUILD="$(xcodebuild -version | awk '/Build version/{print $3}')"
SDK_BUILD="$(xcrun --sdk iphoneos --show-sdk-build-version)"
CACHE_KEY="${XCODE_BUILD}-${SDK_BUILD}-arm64-${JOB_KEY}"
ROOT="$HOME/BuildCaches/$CACHE_KEY"
DERIVED="$ROOT/DerivedData"
MODULES="$ROOT/Modules"
PACKAGES="$HOME/BuildCaches/SourcePackages"
mkdir -p "$DERIVED" "$MODULES/clang" "$MODULES/swift" "$PACKAGES"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath "$DERIVED" \
-clonedSourcePackagesDirPath "$PACKAGES" \
CLANG_MODULE_CACHE_PATH="$MODULES/clang" \
SWIFT_MODULE_CACHE_PATH="$MODULES/swift" \
build
장기 브랜치는 자체적으로 안정된 키를 재사용할 수 있지만, 임시 병합 요청은 작업별로 격리해야 합니다. 아직 실행 중인 두 xcodebuild 프로세스가 같은 모듈 디렉터리에 쓰도록 해서는 안 되며, 모듈 캐시를 툴체인 간에 복원할 수 있는 범용 압축 파일로 취급해서도 안 됩니다.
전체 초기화 대신 필요한 범위만 정밀하게 정리하기
모듈 캐시 문제로 확인되면 먼저 해당 디렉터리를 사용하는 빌드 프로세스를 중지한 뒤, 작은 범위부터 순서대로 처리합니다.
- 현재 작업의
Modules/clang과Modules/swift를 삭제합니다. SourcePackages는 유지하고 종속성 해석과 빌드를 다시 실행합니다.- 인덱스나 중간 산출물이 여전히 이전 모듈을 참조한다면 현재 작업의 DerivedData를 삭제합니다.
- 특정 서드파티 모듈만 실패한다면 해당 바이너리 산출물이 지원하는 아키텍처와 현재 SDK를 확인해야 합니다. 캐시를 반복해서 삭제해 호환성 문제를 감추면 안 됩니다.
정리 작업은 계산된 작업 디렉터리 내부로만 제한해야 합니다. rm -rf ~/Library/Developer/Xcode/DerivedData/*를 사용하면 다른 작업에도 동시에 영향을 주어 한 번의 국소적인 장애가 전체 시스템의 콜드 스타트로 확대될 수 있습니다.
사용 중인 디렉터리의 오삭제 방지하기
오래된 캐시를 회수할 때는 작업 잠금이나 실행 기록을 통해 디렉터리가 사용 중이 아님을 확인한 뒤 마지막 사용 시간을 기준으로 삭제해야 합니다. 디렉터리 생성 시간만 기준으로 삼으면 안 됩니다. 안정 브랜치는 이전에 생성된 캐시를 계속 재사용할 수 있기 때문입니다. 정리 스크립트는 대상 경로가 예상한 루트 디렉터리 아래에 있는지도 검증해야 하며, 변수가 비어 있거나 경로 계산에 실패하면 즉시 종료해야 합니다.
캐시 일관성을 빌드 게이트로 만들기
최종적으로는 파이프라인이 환경 드리프트를 능동적으로 드러내도록 해야 합니다. 빌드할 때마다 Xcode 빌드 번호, SDK 빌드 번호, 아키텍처, 캐시 키와 실행 사용자를 저장하고, 작업이 끝나면 실패 로그와 이 정보를 함께 보관합니다. 툴체인을 업그레이드할 때는 새로운 네임스페이스를 만들고 새 경로가 안정적임을 확인한 뒤 이전 캐시를 회수합니다.
모듈 캐시를 복원하지 않는 별도의 검증 작업을 추가할 수도 있습니다. 일반 작업은 실패하지만 클린 작업은 성공한다면 문제는 캐시 계층에 집중되어 있습니다. 둘 다 실패한다면 소스 코드, 종속성 또는 빌드 설정으로 돌아가 계속 조사해야 합니다. 이렇게 하면 모든 오류를 캐시 탓으로 돌리는 일을 피하면서도 반복 실행으로 운에 맡길 필요가 없습니다.
모듈 캐시는 본질적으로 컴파일러 입력의 일부입니다. 버전, 디렉터리 소유권과 동시 실행 경계를 스크립트에 명시하면 PCM 무효화는 무작위 장애가 아니라 원인을 추적하고 복구할 수 있는 엔지니어링 이벤트가 됩니다.
자주 묻는 질문
PCM 오류가 나면 DerivedData 전체를 삭제해야 하나요?
먼저 전체 삭제를 할 필요는 없습니다. 로그와 빌드 설정을 보존한 뒤 현재 작업의 ModuleCache.noindex만 지우고, 인덱스와 산출물까지 함께 손상된 경우에만 전용 DerivedData를 다시 만드세요.
여러 Xcode 버전이 같은 모듈 캐시를 사용해도 되나요?
권장하지 않습니다. 캐시 키에 Xcode 빌드 번호, SDK, 아키텍처와 작업 식별자를 포함해야 도구 체인 전환과 병렬 실행에서 호환되지 않는 PCM 재사용을 막을 수 있습니다.
클라우드 Mac에서 이 워크플로를 계속 검증하세요
세 가지 Apple Silicon 구성 중에서 필요한 메모리, 스토리지, 노드와 대여 기간을 선택하세요.