iOS 재현 가능한 아카이브와 두 번의 빌드 차이 분석
동일한 커밋을 개발자 컴퓨터에서는 성공적으로 아카이브하고, 클라우드 Mac이나 야간 파이프라인에서도 성공으로 표시되지만 두 산출물의 해시가 서로 다를 수 있습니다. 이 문제를 곧바로 Xcode 캐시 탓으로 돌려서는 안 됩니다. 서명, 압축 시각, 생성된 소스 코드, 스크립트 출력 순서, 절대 경로가 모두 산출물에 포함될 수 있습니다. 더 효과적인 방법은 동일한 ArmVMS 노드에 완전히 격리된 빌드 디렉터리 두 개를 만들고, 서명하지 않은 아카이브를 연속으로 두 번 생성한 뒤 바깥쪽부터 안쪽으로 차이를 비교하는 것입니다.
먼저 ‘재현 가능’의 검사 범위 정의하기
서명된 IPA는 서명 데이터, 프로비저닝 프로파일, 패키징 메타데이터가 달라질 수 있으므로 바이트 단위로 완전히 동일하게 만들기 어렵습니다. 프로젝트 검사는 다음 두 계층으로 나누어야 합니다.
- 콘텐츠 계층: 동일한 커밋, 도구 체인, 종속성 잠금 파일, 빌드 매개변수로 동일한 앱 리소스와 실행 파일을 생성합니다.
- 배포 계층: 내보내기 방식, 서명 구성, 권한 선언, 패키지 내부 파일이 배포 요구 사항을 충족해야 하지만 전체 IPA의 해시가 같을 필요는 없습니다.
먼저 서명되지 않은 앱 콘텐츠가 안정적인지 확인한 다음 서명과 내보내기를 검사해야 합니다. 두 IPA의 전체 해시를 바로 비교하면 ‘서로 다르다’는 사실만 확인할 수 있을 뿐, 차이가 어디에서 발생했는지는 알 수 없습니다.
시작하기 전에 커밋 해시, Xcode 경로, SDK, Scheme, Configuration과 Package.resolved, Podfile.lock 등의 잠금 파일을 기록합니다. 작업 공간은 깨끗한 상태여야 하며, 커밋되지 않은 생성 파일도 검사 대상에 포함해야 합니다.
격리된 디렉터리에서 아카이브 두 번 실행하기
두 빌드가 DerivedData를 공유해서는 안 됩니다. 공유하면 두 번째 빌드가 첫 번째 빌드에서 남긴 중간 산출물을 읽을 수 있습니다. 다음 스크립트는 언어 환경과 시간대를 고정하고, 각 아카이브에 별도의 디렉터리를 할당하며, 코드 서명을 비활성화합니다.
#!/bin/zsh
set -euo pipefail
export LC_ALL=C
export LANG=C
export TZ=UTC
ROOT="$PWD"
SCHEME="ExampleApp"
for RUN in A B; do
rm -rf "$ROOT/.repro/$RUN"
mkdir -p "$ROOT/.repro/$RUN"
xcodebuild archive \
-workspace "$ROOT/ExampleApp.xcworkspace" \
-scheme "$SCHEME" \
-configuration Release \
-destination "generic/platform=iOS" \
-derivedDataPath "$ROOT/.repro/$RUN/DerivedData" \
-archivePath "$ROOT/.repro/$RUN/App.xcarchive" \
CODE_SIGNING_ALLOWED=NO \
COMPILER_INDEX_STORE_ENABLE=NO
done
프로젝트가 스크립트 단계에서 환경 변수를 읽어야 한다면 안정적인 값을 명시적으로 전달하고, 대화형 Shell의 임시 구성을 상속하지 않아야 합니다. 기본 Xcode 경로가 바뀌지 않도록 두 번의 아카이브에서 동일한 DEVELOPER_DIR도 사용해야 합니다.
파일 목록부터 비교해 차이 범위 좁히기
처음부터 바이너리를 16진수로 비교하지 마십시오. 먼저 두 .app에 동일한 경로가 포함되어 있는지 확인합니다.
APP_A=".repro/A/App.xcarchive/Products/Applications/ExampleApp.app"
APP_B=".repro/B/App.xcarchive/Products/Applications/ExampleApp.app"
find "$APP_A" -type f | sed "s#^$APP_A/##" | LC_ALL=C sort > /tmp/files-a.txt
find "$APP_B" -type f | sed "s#^$APP_B/##" | LC_ALL=C sort > /tmp/files-b.txt
diff -u /tmp/files-a.txt /tmp/files-b.txt
파일 수가 다르다면 일반적으로 Run Script 단계, 리소스 생성기 또는 복사 규칙에 불안정한 입력이 있다는 의미입니다. 경로가 일치하면 파일별로 해시를 계산하고, 변경되는 것으로 알려진 서명 디렉터리는 제외합니다.
(
cd "$APP_A"
find . -type f ! -path "./_CodeSignature/*" -print0 |
sort -z | xargs -0 shasum -a 256
) > /tmp/hash-a.txt
(
cd "$APP_B"
find . -type f ! -path "./_CodeSignature/*" -print0 |
sort -z | xargs -0 shasum -a 256
) > /tmp/hash-b.txt
diff -u /tmp/hash-a.txt /tmp/hash-b.txt
| 첫 번째 차이 위치 | 우선 확인할 항목 |
|---|---|
| Info.plist | 자동으로 기록된 날짜, 호스트 이름, 경로 또는 파이프라인 번호 |
| Assets.car | 리소스 입력, 도구 체인 버전, 리소스 디렉터리 순서 |
| 실행 파일 | 컴파일 매개변수, 생성된 소스 코드, 오브젝트 파일과 링크 순서 |
| Framework | 종속성 잠금 파일, 바이너리 종속성 버전과 임베드 스크립트 |
| 텍스트 또는 JSON | 정렬되지 않은 컬렉션, 무작위 식별자와 로캘 형식 |
구성과 Mach-O 심층 검사하기
속성 목록은 키 순서만 달라도 바이트 차이가 발생할 수 있습니다. 파일을 복사한 뒤 plutil -convert xml1으로 정규화하고 텍스트 비교를 수행합니다. 이후 증거가 오염될 수 있으므로 아카이브 원본을 직접 수정해서는 안 됩니다.
실행 파일이 서로 다르면 먼저 UUID를 확인합니다.
dwarfdump --uuid "$APP_A/ExampleApp"
dwarfdump --uuid "$APP_B/ExampleApp"
xcodebuild \
-workspace ExampleApp.xcworkspace \
-scheme ExampleApp \
-configuration Release \
-showBuildSettings > /tmp/build-settings.txt
UUID가 다르다는 것은 링크 결과가 다르다는 의미일 뿐, 비즈니스 코드가 변경되었다는 사실을 자동으로 입증하지는 않습니다. 계속해서 OTHER_SWIFT_FLAGS, OTHER_LDFLAGS, 검색 경로, 배포 대상, 실제 도구 경로를 확인해야 합니다. 생성 코드에 현재 날짜, 임시 디렉터리 또는 정렬되지 않은 딕셔너리 순회 결과가 기록되면 이후의 전체 컴파일 결과도 달라질 수 있습니다.
#filePath, 디버그 매핑, 스크립트 출력에 포함된 작업 공간의 절대 경로를 특히 주의해서 확인하십시오. CI 작업 디렉터리가 달라지는 경우 체크아웃 경로를 통일하거나 디버그 접두사 매핑을 적절히 설정하여 경로 차이를 줄일 수 있습니다. 다만 해시를 일치시키기 위해 디버깅에 필요한 정보를 삭제해서는 안 됩니다.
검사를 유지보수 가능한 게이트로 만들기
게이트에서 단순히 “두 IPA의 해시가 반드시 같아야 한다”라고 규정해서는 안 됩니다. 더 안정적인 전략은 차이 보고서를 보존하고 계층별로 판정하는 것입니다.
반드시 차단해야 하는 변경 사항
동일한 커밋에서 파일 누락, 리소스 콘텐츠 변경, 권한 선언 변경, 임베드된 Framework 버전 변경이 발생하거나 동일한 입력에서 주 실행 파일이 불안정하다면 즉시 차단하고 두 아카이브를 모두 보존해야 합니다.
별도로 검토할 수 있는 변경 사항
서명 디렉터리, 내보내기 로그, 명시적으로 표시된 패키징 시각은 배포 계층의 차이로 분류하여 별도로 검사할 수 있습니다. 하지만 변경을 허용할 경로를 명확히 제한해야 하며, “모든 plist 무시”와 같은 광범위한 규칙을 사용해서는 안 됩니다.
수정한 뒤에는 이전 DerivedData를 재사용하지 말고, 비어 있는 디렉터리 두 개를 다시 만들어 검증해야 합니다. 대규모 프로젝트에서는 모든 커밋마다 이중 아카이브를 실행할 필요가 없습니다. Xcode 버전, 종속성 잠금 파일, 생성기, Run Script 또는 배포 구성이 변경될 때 실행하도록 설정할 수 있습니다. 이렇게 하면 취약한 해시 규칙 하나가 아니라 차이가 어느 계층에서 발생했는지 보여 주는 빌드 증거 체계를 구축할 수 있습니다.
자주 묻는 질문
서명된 IPA 두 개의 해시만 비교하면 안 되나요?
서명 데이터와 패키징 시간이 달라질 수 있으므로 전체 IPA 해시만으로는 판단하기 어렵습니다. 먼저 서명 전 앱 내용을 비교해야 합니다.
Mach-O UUID가 다르면 소스 코드도 달라진 것인가요?
반드시 그렇지는 않습니다. 링크 입력 순서, 생성 코드, 빌드 인수, 도구 경로가 달라져도 UUID가 바뀔 수 있습니다.
이 검사를 모든 커밋에서 실행해야 하나요?
대규모 프로젝트라면 도구 체인, 잠금 파일, 빌드 스크립트 또는 배포 설정이 바뀔 때 실행하는 방식이 효율적입니다.
다음 빌드를 위한 독점 Apple Silicon 물리 노드 선택
ArmVMS M4 및 ArmVMS M4 Pro를 일·주·월·분기 단위로 대여하세요. 주문을 만들기 전에 구성, 노드 및 추가 항목을 하나씩 확인할 수 있습니다.