"swift build 통과는 약속일 뿐이야. 증명해 주는 건 설치되고 검증된 번들이지."
스크립트가 Xcode 빌드 단계를 대신해
Xcode 프로젝트가 없으면 SwiftPM 바이너리를 앱으로 바꿔주는 게 아무것도 없어. 그래서 가족 Mac 앱마다 그 일을 하는 scripts/build-app.sh가 있고, 전부 같은 이유로 같은 순서를 따라.
- 릴리스로 빌드하고, 결과물이 어디 생겼는지 SwiftPM에 물어봐(
--show-bin-path). Spark.app/Contents/{MacOS,Resources}를 조립해. 바이너리랑 Info.plist를 넣고, 아이콘이 있으면 그것도 넣는 거야.- 버전을 정하는 파일이 하나뿐이도록, 제품 버전 파일의 마케팅 버전을
PlistBuddy로 Info.plist에 찍어. - 확장 속성을 지워(
xattr -cr). 번들 안의 Finder 메타데이터는 코드 서명을 깨뜨려. - 다 만든 번들을 서명해. 다 만들기 전엔 절대 안 돼. 서명이 파일 전부를 봉인하거든.
- 검증해.
codesign --verify --deep --strict를 돌리고, designated requirement가 cdhash가 아니라 인증서 기반인지 확인해. - 대기시키고, 바꿔 끼우고, 다시 검증하고, 실패하면 되돌려.
/Applications옆에 숨긴 대기 이름으로 복사한 다음, 옛 앱을 옆으로 치우고 새 앱을 넣어. 설치된 사본을 검증하고, 하나라도 실패하면 이전 앱을 되돌려놔. - 시스템이 새 빌드를 바로 알아보도록 LaunchServices에 등록해(
lsregister -f).
덮어쓰지 않고 대기시키는 이유
커널은 서명된 실행 파일의 코드 서명을 파일마다(inode마다) 캐시해. 서명된 바이너리 파일 위에 그대로 덮어쓰면, 새 바이트에 옛 서명이 캐시된 채로 남을 수 있어. 그러면 서명이 잘못됐다며 실행이 죽는 식으로 드러나. 완성된 새 번들을 따로 쓴 다음 이름을 바꿔 넣으면, 커널은 그걸 새 파일로 받아들여. 게다가 바꿔 끼우는 방식이라, 설치가 실패해도 잘 돌던 이전 앱이 원래 있던 곳에 그대로 남아.
한 번 당하면 잊지 못할 디테일 두 가지
- 명령줄 헬퍼에도 Info.plist가 필요해. 개인정보 권한을 요청하는 도구(가족의 캘린더 브리지는 EventKit을 읽어)는 Info.plist를 담을 번들이 없어서, macOS가 대화상자 한 번 안 띄우고 요청을 거절해. 해결책은 바이너리 안에 plist를 넣는 거야.
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist로 넣고 서명해. - 안쪽 코드는 안에서 밖으로 서명해. 헬퍼, 번들에 넣은 실행 파일, 번들한 dylib는 그걸 담는 번들보다 먼저 서명해. 애플 가이드는 서명할 때
codesign --deep에 기대지 말고 안쪽 항목을 하나씩 명시적으로 서명하라고 해.--deep은 검증할 땐 괜찮아. 서드파티 dylib를 번들하고 불러오는 경로를 고쳐 쓰는 건 그 자체로 긴 이야기라서/cwk-quests/ashen-reel-quest에서 다뤄.