본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

closure 를 통째로 번들하고 서명해

~14 min · self-contained, dylib, codesign, bundling

Level 0릴 입문자
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"프로덕션 빌드는 pin 된 libmpv dylib 과 필요한 의존성 closure 를 번들하고, 런타임 경로를 고치고, 라이선스 고지를 포함하고, 모든 중첩 바이너리를 앱보다 먼저 서명해."

libmpv 는 혼자 안 와

libmpv 를 번들할 때, 넌 파일 하나를 번들하는 게 아니야. libmpv 는 다른 dylib 에 의존하고, 걘 또 다른 것에 의존하고, 쭉 밑바닥까지 내려가. 이게 의존성 closure 야. libmpv 는 실었는데 그게 transitive 하게 필요한 하나를 빠뜨리면, 아무것도 안 번들한 것만큼 확실하게 앱이 실행 시 죽어. 자립은 네가 처음 떠올린 라이브러리만이 아니라 전체 closure 가 앱 안에서 여행한다는 뜻이야.

거기 도달하는 건 특정한 네 단계 춤이고, 단계 하나하나가 다 중요해.

  • Pin. 모든 dylib 을 정확한 빌드로, 기록된 체크섬이랑 라이선스 고지와 함께. 재현성이랑 attribution 은 옵션이 아니야.
  • Copy. 전체 closure 를 번들의 Frameworks 디렉토리로.
  • Rewrite. 각 install name 을 고쳐서, 바이너리가 의존성을 Homebrew 경로가 아니라 번들 안(@rpath)에서 로드하게.
  • Sign. 모든 중첩 바이너리를 먼저, 앱을 마지막에. 서명은 바깥으로 중첩되니까.
전체 transitive closure 가 여행해, 경로가 고쳐지고 안에서 바깥으로 서명된 채로. 번들 안 한 의존성은 테스트 안 한 실행 실패야. closure 를 끝까지 걸어, 모든 install name 을 번들 안으로 재배치하고, 안에서 바깥으로 서명해.

쉘로 된 춤이 여기 있어. 대부분이 잊는 단계, 그러니까 증명까지 포함해서.

서명 순서는 디테일이 아니야. 중첩 바이너리가 앱보다 먼저 서명돼야 해. code signature 는 중첩되거든. 앱의 서명이 그 내용을 덮으니까, 앱을 먼저 서명하고 나서 중첩 dylib 을 다시 서명하면 바깥 서명을 무효화한 거야. 항상 제일 깊은 바이너리를 먼저 서명하고 앱 번들을 마지막으로, 안에서 바깥으로 나가. 순서 틀리면 Gatekeeper 가 통째로 거절하고.

목발을 숨겨서 증명해

마지막 단계가 바람이랑 증명을 가르는 거야. 개발 Homebrew prefix 를 잠깐 숨기고 번들을 실행해봐. 여전히 돌면 closure 가 진짜 여행하는 거고, 죽으면 dylib 하나를 빠뜨린 거야. 사용자 대신 네 머신에서 방금 알아낸 거지. 이 '목발 걷어차고 서 있나 보기' 테스트가 트랙의 핵심이야. 설치된 앱은 개발자의 cellar 나, 쓰기 가능한 소스 체크아웃이나, 서명 안 되고 번들 안 된 라이브러리 하나에 의존하면 안 되거든.

Ashen Reel 이 자립했다고 처음 생각했을 때, 증명하려고 Homebrew 를 숨겼더니 앱이 즉시 죽었어. 내가 의식적으로 의존한 적도 없는, 두 단계 아래 transitive dylib 때문에. 겸허해졌지. 아빠는 그냥 끄덕였어. "그래서 숨겨보는 거야. 안 숨겨보면 사용자가 대신 숨겨줘." 증명 단계는 의례가 아니야. 통째로 배포했다고 '믿는' 거랑 '아는' 거의 차이지.

Code

복사하고, 재배치하고, 안에서 바깥으로 서명하고, Homebrew 숨겨서 증명·bash
APP="build/Ashen Reel.app"
FW="$APP/Contents/Frameworks"

# 1. Copy the WHOLE closure (libmpv + every dylib it transitively needs).
mkdir -p "$FW"
cp "$(brew --prefix)/opt/mpv/lib/libmpv.2.dylib" "$FW/"
#   ...repeat for each transitive dependency the closure walk finds...

# 2. Rewrite install names to load from INSIDE the bundle, not Homebrew.
install_name_tool -change /opt/homebrew/opt/mpv/lib/libmpv.2.dylib \
  @rpath/libmpv.2.dylib "$APP/Contents/MacOS/AshenReel"

# 3. Sign INNER binaries first, the app LAST (signatures nest outward).
codesign --force --options runtime --sign "$CERT" "$FW/"*.dylib
codesign --force --options runtime --sign "$CERT" "$APP"

# 4. THE PROOF (throwaway test rig only): hide the dev crutch and launch.
sudo mv /opt/homebrew /opt/homebrew.hidden
open "$APP"        # must still launch and play
sudo mv /opt/homebrew.hidden /opt/homebrew   # restore

External links

Exercise

네이티브 의존성을 번들하는 뭔가를 배포하면, 그 전체 transitive closure 를 걸어봐. 최상위 라이브러리만 말고, 그게 필요로 하는 전부. 그다음 네 버전의 '목발 숨기기' 테스트를 찾거나 만들어. 그 의존성의 개발 소스를 잠깐 없애놓고 네 아티팩트가 여전히 도는지 확인해. 쉽게 못 하겠으면, 네 번들이 자립인지 그냥 운 좋은 건지 아직 모르는 거야.
Hint
두 가지가 사람들을 넘어뜨려. 하나, closure 는 transitive 야. 번들한 라이브러리가 생각도 못 한 라이브러리들을 끌어와. 둘, 서명은 안에서 바깥으로 가야 해. 제일 깊은 바이너리 먼저, 컨테이너 마지막. '개발 사본 숨기고 실행' 테스트가 closure 가 완전한지 확신하는 유일한 방법이야. 여기선 믿음이 증명이 아니거든.

Progress

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

댓글 0

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

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