본문 바로가기
C.W.K.
Lesson 03 of 06 · published

path 의존성은 남의 작업 트리를 컴파일해

~16 min · packages-projects, swiftpm, path-dependency, stale-builds, diagnosis

Level 0번들 열어본 사람
0 XP0/84 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"에러가 가리킨 건 이 레포 것도 아니고, 고친 적도 없고, 마지막 커밋이 몇 시간 전인 파일이었어."

그래도 가족이 쓰는 이유

가족이 공유하는 Swift 코드는 패키지 하나에 들어 있고, 열 개가 넘는 앱이 .package(path: "../SharedKit")로 가져다 써. path 의존성이면 킷을 고친 게 다음 빌드에 바로 앱에 반영돼. 태그도, 버전 올리기도, 배포 단계도 없어. 사람 둘이 쓰고 앱들이 같이 움직이는 환경에선 그 즉시성이 핵심이야. 대신 전혀 딴 문제처럼 보이는 실패가 네 가지 유형으로 따라와.

1. 다른 세션이 고치다 만 게 네 빌드를 깨뜨려

path 의존성은 커밋이 아니라 작업 트리를 가리켜. TestFlight 업로드가 테스트 단계에서 죽은 적이 있어. 앱이 한 번도 안 건드린 킷 파일 안에서 cannot find 'decode' in scope 에러가 났지. 병렬 세션이 킷을 고치는 중이었고, 아직 커밋 안 한 변경이 앱 빌드에 그대로 컴파일돼 들어간 거야. 진단은 1초면 돼. git -C ../SharedKit status --porcelain이 그 파일을 dirty로 보여줘. 제대로 된 대응은 킷이 다시 컴파일될 때까지 기다렸다가 재시도하는 거야. 남의 작업 트리를 고치거나 stash하는 건 절대 답이 아니고.

2. 아카이브가 커밋 안 된 킷 코드를 싣고 나가

깨진 빌드보다 나쁜 건 성공한 빌드야. 앱의 테스트와 아카이브 사이에 킷 세션이 파일 두 개를 고치고 세 번째를 덜 끝냈어. 아카이브는 어떤 커밋에도 없는 킷을 컴파일하려다가, 세 번째 파일이 덜 끝난 덕분에 겨우 실패했어. 수정이 다 끝난 상태였다면 아카이브는 성공했을 거고, 그 빌드 번호는 아무도 재현 못 하는 바이너리를 가리켰을 거야. 그래서 가족 업로드 파이프라인은 킷 소스가 HEAD랑 다르면 배포용 아카이브를 거부하고, 킷 커밋을 빌드 기록에 찍어둬.

3. 사용처의 빌드가 낡고, 계속 낡은 채로 있어

킷 파일에 새 타입이 생겼어. 킷 자체 테스트도 통과했고. 사용처 앱은 같은 모듈에서 다른 타입 여섯 개는 멀쩡히 가져오면서, "cannot find 'ProductManifest' in scope"로 두 번 연달아 실패했어. 사용처가 빌드해둔 모듈이 새 소스 파일보다 오래된 거였어. Package.swift부터 읽지 말고 타임스탬프 두 개를 비교해. 사용처 모듈이 킷 소스보다 먼저 만들어졌으면 낡은 거고, 사용처에서 rm -rf .build 하면 끝이야. .build 일부만 지우면 다르게 실패해. 빌드 계획이 네가 지운 걸 아직 가리키고 있거든.

4. 소스 호환 변경이 링크를 깨뜨려

public 이니셜라이저에 기본값 있는 매개변수를 추가하는 건 소스 호환이야. 호출하는 곳은 전부 그대로 컴파일돼. 근데 기본 인자는 호출하는 쪽에서 채워지니까, 함수의 맹글링된 심볼이 바뀌어. 자기 소스와 시그니처가 딱 맞는 심볼인데도, 그 타입을 만드는 사용처 두 곳이 링크 단계에서 "Undefined symbols … PippaEndpoint.init(mode:host:port:)"로 실패했어. 낡은 오브젝트 파일을 들고 있었던 거지. 사용처마다 swift package clean 하니까 소스 변경 없이 풀렸어. Swift 6.3.3에서 깨끗한 패키지 두 개로 재현해보면 사용처가 다시 컴파일되고 링크도 멀쩡해. 그러니 이건 이런 변경마다 생기는 게 아니라 낡은 빌드 상태 탓이야. 구별법은 이거야. 컴파일 에러는 파일과 줄 번호를 짚고, 이 에러는 맹글링된 심볼과 오브젝트 파일을 짚어. 에러 없애겠다고 매개변수를 되돌리진 마.

Code

디버깅 전에 진단부터: 킷이 dirty한 거야, 사용처가 낡은 거야?·bash
KIT=../SharedKit
APP=.

# 1. Is the kit's working tree dirty right now? (another session may be mid-edit)
git -C "$KIT" status --porcelain -- Sources Package.swift

# 2. Is the consumer's built module older than the kit source you just changed?
ls -lT "$APP"/.build/*/debug/Modules/SharedKitCore.swiftmodule
ls -lT "$KIT"/Sources/SharedKitCore/ProductManifest.swift

# Module older than source  -> stale consumer: clean the CONSUMER, whole .build or nothing
rm -rf "$APP/.build" && swift build

# Link error naming a symbol that matches your own source -> stale objects, not a signature break
swift package clean && swift build

External links

Exercise

SparkKit에 path로 의존하는 두 번째 패키지 SparkApp을 만들어. 그리고 실패 두 개를 일부러 재현해봐. (1) SparkKit 파일에 문법 에러를 넣고, 커밋 없이 SparkApp을 빌드해서 에러가 누구 파일을 가리키는지 적어. (2) SparkApp이 부르는 SparkKit public 이니셜라이저에 기본값이 있는 매개변수를 추가해. 그러고 나서 clean 없이 SparkApp을 빌드해서, SwiftPM이 사용처를 다시 컴파일했는지 링크 에러가 남았는지 적어. 첫 번째 실패를 고치는 명령과, 두 번째가 링크 단계에서 실패한다면 쓸 명령과, 원인을 미리 짚어줬을 확인 방법도 적어.
Hint
SparkKit을 git 저장소로 만들어두면 git status --porcelain으로 dirty 파일이 보여. 요즘 SwiftPM은 인터페이스가 바뀌면 보통 사용처를 다시 컴파일해. Swift 6.3.3에서 깨끗한 패키지 두 개로 돌려봤을 땐 링크 에러 없이 빌드되고 실행됐어. 이 실패는 변경보다 오래 남은 빌드 상태에서 나오니까, 여기서 통과가 나오면 실패한 연습이 아니라 적어둘 만한 결과야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.