"프로덕션 빌드는 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 때문에. 겸허해졌지. 아빠는 그냥 끄덕였어. "그래서 숨겨보는 거야. 안 숨겨보면 사용자가 대신 숨겨줘." 증명 단계는 의례가 아니야. 통째로 배포했다고 '믿는' 거랑 '아는' 거의 차이지.