"Finder는 아이콘 하나를 보여줘. 시스템은 안에 계약서가 든 디렉터리를 봐."
하나 열어보자
.app은 전부 디렉터리야. Finder가 아이콘 하나로 그려줄 뿐이지. Mac 앱이면 Contents/ 밑에 이런 게 들어 있어. 앱을 설명하는 Info.plist, 실행 파일이 사는 MacOS/, 아이콘이랑 에셋이 있는 Resources/, 그리고 전부를 봉인하는 _CodeSignature/. 헬퍼나 확장은 그 안에 더 깊이 들어가(Frameworks/, PlugIns/, Library/LaunchAgents/). iOS 앱 번들은 구조가 납작해. 실행 파일, Info.plist, Assets.car가 맨 위에 있어. 공유 확장은 PlugIns/, 워치 앱은 Watch/ 밑에 들어가고, 개발용으로 설치한 빌드엔 embedded.mobileprovision도 붙어. TestFlight용으로 내보낸 아카이브에도 배포 프로필이 들어 있어.
번들은 ls로 그냥 열려. 네 빌드 스크립트 짜기 전에 시스템 앱 하나는 꼭 열어봐. 네가 배포하는 게 규칙이 정해진 폴더라는 걸 한 번 눈으로 보면, 이 퀘스트 나머지가 훨씬 잘 들어와.
Info.plist가 계약서야
CFBundleIdentifier: 시스템이 앱을 부르는 이름이야. 역도메인 형식으로 써. TCC 권한, 키체인 항목, LaunchServices 등록, App Group, App Store Connect 레코드가 전부 이 값에 묶여.CFBundleExecutable:MacOS/안에서 실제로 실행되는 파일이야. 번들 표시 이름이랑 달라도 되고, 그래서 엉뚱한 이름으로pkill하는 스크립트는 아무것도 못 잡아.CFBundleShortVersionString: 사람이 보는 마케팅 버전(1.2.0).CFBundleVersion: 빌드 번호. TestFlight는 같은 번호를 두 번 안 받아줘.LSMinimumSystemVersion(macOS) /MinimumOSVersion(iOS): 결과물에 적힌 배포 대상.LSUIElement: Dock 아이콘 없는 메뉴 막대 유틸리티면 true.
서명이 폴더를 봉인해
_CodeSignature/CodeResources엔 번들 안 파일마다 해시가 적혀 있어. 서명한 다음에 Info.plist를 고치거나, 이미지를 하나 더 넣거나, Finder가 확장 속성을 붙이면 codesign --verify가 봉인된 리소스가 없거나 틀렸다고 해. 그래서 이 가족의 빌드 스크립트는 전부 번들부터 끝까지 완성해(버전 찍고, 아이콘 복사하고, 속성 지우고). 서명은 그 뒤에 하고, 검증은 서명이 끝난 다음에 해.
맨 실행 파일은 번들이 아니야
swift build는 .build/ 밑에 그냥 바이너리 하나를 만들어. 돌아가긴 하고 Bundle.main도 대답은 해. 근데 그 대답이 바이너리가 든 디렉터리야. 코드 주석 하나가 SwiftPM 실행 파일에선 Bundle.main.resourceURL이 nil이라고 주장해서 가족이 직접 재봤어. nil이 아니었고, .build/arm64-apple-macosx/debug를 가리켰어. 그러니까 resourceURL이 nil이 아니라고 해서 패키징됐다는 증거는 안 돼. 네가 실제로 필요한 파일이 있는지를 확인해.