동일한 커밋이 개발 머신에서는 정상적으로 빌드되지만 다른 클라우드 Mac에서는 서로 다른 의존성으로 해석된다면, 대개 컴파일러가 무작위로 오작동한 것이 아니라 해석 파일, Xcode 버전, 캐시 경계를 함께 고정하지 않았기 때문입니다. 단기 임대 노드에서는 이런 문제가 특히 잘 드러납니다. 새 머신은 빈 디렉터리 상태로 제공되지만, 파이프라인 스크립트가 기존 캐시를 기본적으로 재사용하거나 빌드 중에 의존성 버전을 조용히 다시 기록할 수 있기 때문입니다.
동일한 빌드의 기준부터 정의하기
재현 가능한 빌드란 매번 전체 작업 디렉터리를 보존한다는 뜻이 아니라, 입력이 같으면 동일한 의존성 집합을 얻어야 한다는 의미입니다. 최소한 코드 커밋, Package.resolved 내용, Xcode 빌드 버전, 프로세서 아키텍처의 네 가지 항목을 기록해야 합니다. Xcode의 메이저 버전만 기록해서는 충분하지 않습니다. 같은 메이저 버전이라도 툴체인 패치 버전이 다를 수 있습니다.
먼저 노드에서 환경 스냅샷을 저장합니다.
set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD
일반적으로 앱 프로젝트에서는 Package.resolved를 저장소에 커밋해야 합니다. 워크스페이스에서는 보통 App.xcworkspace/xcshareddata/swiftpm/Package.resolved 경로를 사용합니다. 프로젝트 파일만 있는 경우에는 프로젝트 내부의 워크스페이스 디렉터리에 위치할 수도 있습니다. 해석 파일 두 개를 동시에 관리하지 말고, 파이프라인이 실제로 어느 파일을 읽는지 먼저 확인해야 합니다.
판단 기준은 간단합니다. 빌드 스크립트를 실행한 뒤
git status --porcelain에 해석 파일 변경 사항이 나타난다면, 해당 빌드는 원래 커밋에 대한 검증으로 간주할 수 없습니다.
버전 범위가 아닌 해석 결과 고정하기
Package.swift의 버전 범위는 “어떤 버전을 선택할 수 있는지”를 나타내고, Package.resolved는 “이번에 실제로 어떤 버전을 선택했는지”를 기록합니다. 노드를 처음 준비할 때는 의존성을 명시적으로 해석할 수 있지만, 실제 빌드에서는 이미 기록된 버전만 허용해야 합니다.
set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$CACHE"
git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=macOS' \
-clonedSourcePackagesDirPath "$CACHE" \
-onlyUsePackageVersionsFromResolvedFile \
build
대상이 iOS라면 destination을 파이프라인에서 이미 정의한 일반 디바이스 또는 시뮬레이터 대상으로 변경해야 합니다. 스크립트가 현재 부팅된 디바이스를 자동으로 선택하게 두지 마십시오. 순수 Swift 패키지에는 swift package resolve와 swift build를 사용할 수 있지만, 프로젝트와 워크스페이스는 일관되게 xcodebuild를 사용해야 합니다. 해석 진입점을 두 개로 나누면 디렉터리 상태가 달라질 수 있습니다.
입력 해시로 캐시 경계 나누기
캐시는 다운로드와 체크아웃 시간을 줄이기 위한 수단일 뿐, 의존성 버전의 출처가 되어서는 안 됩니다. 안정적인 캐시 키에는 아키텍처, Xcode 빌드 정보, 해석 파일 해시가 포함되어야 합니다.
set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
| 입력 변경 | 작업 캐시 재사용 여부 | 이유 |
|---|---|---|
| 애플리케이션 소스 코드만 변경 | 가능 | 의존성 집합이 변경되지 않음 |
Package.resolved 변경 |
불가능 | 체크아웃 버전 또는 리비전이 변경됨 |
| Xcode 빌드 버전 변경 | 불가능 | 툴체인과 산출물 형식이 달라질 수 있음 |
arm64와 다른 아키텍처 간 전환 |
불가능 | 바이너리 산출물을 혼용할 수 없음 |
여러 노드에서 실행할 때는 여러 머신이 동일한 디렉터리에 동시에 쓰지 않도록 해야 합니다. 키별로 보관된 읽기 전용 캐시를 저장한 뒤, 각 물리 노드가 이를 로컬 작업 디렉터리로 복사해 사용할 수 있습니다. 실패한 작업을 재시도할 때도 모든 이전 아카이브를 삭제하지 말고 현재 작업 복사본만 먼저 정리해야 합니다.
의존성 드리프트 발생 시 작업 즉시 중단하기
빌드 전후에 저장소 상태를 각각 확인하면 해석 파일이 다시 작성되는 문제와 스크립트 산출물이 소스 디렉터리에 잘못 생성되는 문제를 함께 감지할 수 있습니다. 실제 작업은 깨끗한 체크아웃 상태에서 시작해야 합니다.
set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
-clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
-onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"
실패 직후 버전 제한을 해제하고 다시 해석해서는 안 됩니다. 그렇게 하면 구성 오류가 성공한 빌드처럼 위장됩니다. 올바른 방법은 해석 로그, 환경 스냅샷, 캐시 키를 보존한 뒤 저장소에 잠금 결과가 누락되었는지, 의존성 리비전이 유효하지 않은지, 또는 노드에서 잘못된 Xcode를 선택했는지 확인하는 것입니다.
일반적인 실패를 순서대로 진단하기
해석 단계 시간 초과
먼저 노드에서 의존성 소스에 접근할 수 있는지 확인한 다음, 디스크 공간과 캐시 디렉터리 권한을 점검합니다. 네트워크가 복구되면 동일한 해석 파일로 다시 시도해야 하며, 의존성부터 업그레이드해서는 안 됩니다. 매번 같은 패키지에서 실패한다면 해당 의존성 선언과 리비전이 여전히 일치하는지 별도로 검증해야 합니다.
해석은 성공하지만 컴파일 실패
먼저 두 노드의 xcode-select -p, xcodebuild -version, uname -m 결과를 비교합니다. 바이너리 의존성이 있다면 해당 바이너리 슬라이스가 현재 아키텍처를 포함하고 있는지, 프로젝트에 설정된 최소 운영체제 버전을 충족하는지도 확인해야 합니다. 해당 캐시 키의 로컬 복사본을 삭제한 뒤 다시 빌드하면 캐시 손상과 소스 코드 비호환 문제를 구분할 수 있습니다.
커밋 전 체크리스트
- 저장소에는 실제로 적용되는
Package.resolved가 하나만 있어야 합니다. - 빌드 명령에
-onlyUsePackageVersionsFromResolvedFile이 활성화되어 있어야 합니다. - 캐시 키에는 아키텍처, Xcode 빌드 정보, 해석 파일 해시가 포함되어야 합니다.
- 각 노드는 독립적으로 쓰기 가능한 작업 디렉터리를 사용해야 합니다.
- 빌드 전후의 해석 파일 해시가 동일해야 합니다.
- 로그에는 커밋 번호, 캐시 키, 툴체인 버전을 남기되 자격 증명은 기록하지 않아야 합니다.
자주 묻는 질문
Package.resolved를 저장소에 커밋해야 하나요?
애플리케이션과 배포형 서비스는 일반적으로 커밋하고 기록된 버전만 사용해 빌드해야 합니다. 라이브러리는 다른 정책을 선택할 수 있지만 배포 전 깨끗한 환경에서 선언한 버전 범위를 검증해야 합니다.
여러 클라우드 Mac이 하나의 SwiftPM 캐시에 동시에 쓸 수 있나요?
동일한 디렉터리에 동시 쓰기는 피해야 합니다. CPU 아키텍처, Xcode 빌드 번호, Package.resolved 해시로 캐시 키를 만들고 각 노드에는 독립된 작업 복사본을 제공합니다.
의존성 해석은 성공했지만 컴파일이 실패하면 무엇을 확인해야 하나요?
선택된 Xcode 경로와 빌드 번호를 먼저 확인하고 해당 키의 캐시를 다시 만듭니다. 이후 바이너리 의존성의 아키텍처, 최소 시스템 버전, Package.resolved 변경 여부를 점검합니다.
재현 가능한 환경을 같은 클라우드 Mac에 유지
고정된 기종, 리전 및 대여 기간을 선택하고 완전한 macOS 그래픽 인터페이스와 명령줄을 사용해 개발 또는 자동화 작업을 수행하세요.