콘텐츠로 이동

릴리스 빌드

이 페이지는 CLAUDE.md의 구현 노트를 미러합니다. 서브시스템 변경 시 양쪽 다 업데이트하세요.

Cue는 서명되고 노타리된 .app / DMG (macOS)와 PyInstaller 빌드된 .exe + Inno Setup installer (Windows)로 ship. macOS 경로가 더 복잡 — hardened runtime + Apple notary 요구사항 때문.

macOS — Nuitka + Developer ID + 노타리제이션

단일 명령:

bash scripts/build_nuitka.sh

스크립트가 하는 일:

  1. conda cue env 활성화 (uv 빌드 env는 dev용; 빌드 env는 Python / PyGObject / GStreamer / GStreamer 플러그인 간 ABI 일관성 위해 conda). scripts/environment.yml 참고.
  2. Nuitka로 .app 번들로 컴파일.
  3. vendoring된 owa 패키지의 dist-info 번들 — 그래야 importlib.metadata.entry_points()가 플러그인 등록 발견.
  4. 시스템 경로에서 GStreamer.framework 해결 (번들 안 함 — 너무 큼; 사용자가 시스템 전역 GStreamer 설치). PyGObject가 GNU iconv* 심볼셋 찾도록 작은 libiconv 호환성 shim 빌드.
  5. 온디바이스 추론용 llama-server 바이너리를 Cue.app/Contents/MacOS/bin/llama-b<tag>/ 아래 번들 (스크립트가 없으면 자동 다운로드).
  6. Codesign 모든 Mach-O 바이너리 — Developer ID Application 인증서, hardened runtime, embedded entitlements (scripts/release/entitlements.plist), secure timestamp로 serial. 일시적 timestamp-server hiccup에 retry.
  7. Notarize .app을 keychain profile (한 번 xcrun notarytool store-credentials로 설정)으로 xcrun notarytool submit --wait.
  8. notary ticket을 .appstaple.
  9. DMG 빌드, 같은 Developer ID 신원으로 서명, notary 제출, staple.

산출물은 dist/Cue.appdist/Cue-<version>.dmg로 land.

Hardened runtime entitlements

scripts/release/entitlements.plist:

  • com.apple.security.cs.disable-library-validation — 필수, GStreamer.framework dylib이 우리 team-id로 서명되지 않아서.
  • com.apple.security.cs.allow-dyld-environment-variablesInfo.plistLSEnvironment (아래 참고)가 시스템 GStreamer.framework를 가리키도록 DYLD_LIBRARY_PATH / GST_PLUGIN_PATH / GI_TYPELIB_PATH 주입. Hardened runtime이 이 entitlement 없으면 그런 env var 제거.
  • com.apple.security.cs.allow-jit — GStreamer의 orc SIMD JIT 컴파일러가 pthread_jit_write_protect_np 호출하여 W^X 메모리 토글하기 때문에 필수.

LSEnvironment + sticky 권한

Info.plistLSEnvironment dict — Launch Services가 프로세스 시작 DYLD_LIBRARY_PATH / GST_PLUGIN_PATH / GI_TYPELIB_PATH / GST_PLUGIN_SCANNER를 주입. dyld가 첫 execve에 이를 보므로 run.py는 프로세스를 재실행 하지 않음.

이게 중요한 이유: 같은 Mach-O를 재실행하면 codesign 식별자가 바뀌지 않더라도 macOS Accessibility TCC grant가 매 launch마다 드롭됨. 화면 기록은 재실행에서 살아남지만 Accessibility는 안 살아남음. LSEnvironment 없으면 사용자가 매 launch마다 Accessibility 재부여해야 하고 글로벌 핫키가 깨짐.

터미널에서 Cue.app/Contents/MacOS/run 직접 실행은 레거시 os.execv 재실행 경로로 fallback (LSEnvironment는 Launch Services 밖에서 적용 안 됨). dev / 진단 케이스.

Apple Developer 사전 요구사항

  • Apple Developer Program 멤버십.
  • login keychain에 설치된 Developer ID Application 인증서 (security find-identity -v -p codesigning로 검증).
  • Apple ID + app-specific password + Team ID로 xcrun notarytool store-credentials cue-notarize 한 번 실행.
  • 인증서에 security set-key-partition-list -S apple-tool:,apple: 한 번 실행 — 그래야 codesign이 매 바이너리마다 keychain access 프롬프트 안 함.

빌드 스크립트가 security find-identity로 Developer ID 신원 자동 감지. 없으면 ad-hoc 서명 (--sign -)으로 fallback — 배포 안 하는 로컬 스모크 테스트에 유용.

Windows — Nuitka + Inno Setup

scripts\build_nuitka.bat

스크립트가 하는 일:

  1. dist/build/를 hard-clean (PyInstaller 잔여물 회피).
  2. 빌드 타임 deps (nuitka, ordered-set, Pillow)를 uv pip install로 설치 — uv venv는 pip이 없음.
  3. assets/icon.pngassets/Cue.ico (multi-res 16-256).
  4. spaCy en_core_web_sm 확인.
  5. ggml-org/llama.cpp에서 llama-<tag>-bin-win-cpu-x64.zip 다운로드 후 vendor/llama-bin/llama-b<tag>/에 압축 해제.
  6. Nuitka standalone 컴파일 (~5-15분 첫 실행; 이후는 ccache로 빠름). --enable-plugin=tk-inter 사용 — customtkinter / Settings 창 / API 키 프롬프트 동작 위해.
  7. build/run.distdist/Cue, run.exeCue.exe 리네임.
  8. owa.* 네임스페이스 패키지의 dist-info 수동 복사 (owa-core, owa-env-desktop, owa-msgs, ocap-windows, mcap-owa-support). Nuitka의 --include-distribution-metadata= 플래그는 PEP 420 네임스페이스 dist를 거부 (top-level importable 이름과 안 맞음). 런타임 entry-point 로더에 dist-info 디렉토리가 있어야 함.
  9. Nuitka의 --include-data-dir이 스킵하는 GStreamer 네이티브 에셋 복구: site-packages의 각 gstreamer_* 패키지 내부를 walk 하면서 *.exe, *.dll, *.typelib 모두 dist 트리에 복사 (+ vendor/ocap-windows/의 커스텀 gst-plugins/*.py). ocap-windows가 하는 것 그대로.
  10. vendor/llama-bin/llama-b<tag>/dist/Cue/bin/llama-b<tag>/ 복사.
  11. Inno Setup (scripts/release/cue_setup.iss) 호출해 dist/Cue/dist/Cue-Setup-<version>.exe로 wrap (LZMA2 압축 후 ~300-500 MB).

산출물: dist/Cue/Cue.exe (트리 ~700MB-1.2GB)와 dist/Cue-Setup-<version>.exe.

런타임 GStreamer 환경 셋업

run.py_setup_windows_gstreamer_env()를 어떤 gi / owa.gstreamer / owa.ocap_windows import 보다도 먼저 호출. 번들 모드 감지 (exe 옆 gstreamer_libs/ 존재 여부), 모든 gstreamer_*/bin/PATH에 prepend, 각각에 os.add_dll_directory() 호출 (Win10+ secure DLL search에 belt-and-suspenders), 그리고 GI_TYPELIB_PATH, GST_PLUGIN_PATH{,_1_0}, GST_PLUGIN_SYSTEM_PATH_1_0, GST_PLUGIN_SCANNER_1_0, PYGI_DLL_DIRS 설정. 함수 상단에 assert "gi" not in sys.modules tripwire — Nuitka가 이른 GStreamer import를 env 셋업 앞으로 hoist 하면 의문스러운 DLL load 실패 대신 첫 실행에서 즉시 알게 됨.

GStreamer 출처

gstreamer-bundle>=1.28.0,<1.29 (ocap-windows가 쓰는 동일한 PyPI distribution)이 GStreamer 1.0 + PyGObject + 모든 plugin을 import 가능한 파이썬 패키지로 ship. 시스템 GStreamer 설치 불필요. 1.29 라인은 alpha — pin은 1.28에 hold.

PyInstaller fallback

레거시 PyInstaller 경로는 parity 테스트용으로 유지:

scripts\build_windows.bat               # PyInstaller .exe
python scripts/build_installer.py       # Inno Setup wrap

regression이 Nuitka에서만 발생하는지 진단할 때 유용.

코드 서명

스킵. installer는 unsigned — Windows SmartScreen이 첫 실행에 경고; 사용자는 "More info → Run anyway" 클릭. Authenticode 인증서로 SignTool 추가는 follow-up.

Windows GStreamer 파이프라인 — Nuitka에서 GDI fallback

vendor/ocap-windows의 기본 파이프라인은 d3d11screencapturesrc + d3d11scale / d3d11download / d3d11convert 체인을 하드코딩. Nuitka standalone 하에서 Gst.parse_launch 안에서 어떤 d3d11* element라도 인스턴스화하면 element 생성 즉시 access violation 발생 — Cue.exe --selftest=gst_init로 검증:

videotestsrc          - parses
gdiscreencapsrc       - parses
dx9screencapsrc       - parses
d3d11screencapturesrc - SIGSEGV

같은 파이프라인이 dev 모드 CPython에서는 잘 parses 되므로, 이는 Nuitka-vs-D3D11 상호작용 문제. 우회하기 위해 cue.ocap_launcher.patched_on_configure가 Windows에서 자체 GDI 기반 파이프라인 구성: gdiscreencapsrcvideoconvertvideoscale name=d3d11scale0teefpsdisplaysink / x265enc (CPU H.265) / pruner appsink. macOS는 변경 없음.

트레이드 오프: GPU 대신 순수 CPU 캡처 + 인코딩, 5 fps에서 약 1코어 부담. ocap-windows의 자체 gui 바이너리 (자기네 scripts/build_exe.py로 Nuitka 빌드)는 각 녹화를 fresh multiprocessing.Process로 wrap해서 이 문제를 회피 — Cue의 launcher는 ocap의 recorder.main()으로 직접 route하므로 ocap-windows의 gui가 안 겪는 이 버그를 우리는 겪음.

첫 빌드 트러블슈팅

증상 원인 해결
Nuitka: Could not find C compiler MSVC Build Tools 없음 --assume-yes-for-downloads가 자동 pull; 막히면 Build Tools 2022 수동 설치.
assert "gi" not in sys.modules 발생 import 순서 가드 트립 이른 gi/owa import 찾아 _setup_windows_gstreamer_env() 뒤로 이동.
시작 시 ImportError: gi gstreamer-bundle 없음 uv pip install -e ".[windows,on-device]"; python -c "import gi"로 확인.
트레이 아이콘 없음 pystray Win32 backend 미번들 flag list에 --include-package=pystray 이미 있음; 그래도 없으면 --include-module=pystray._win32 추가.
핫키 안 발동 pynput Win32 backend 없음 --include-module=pynput.keyboard._win32pynput.mouse._win32 (이미 flag list에 있음).
API 키 프롬프트 / Settings 창 빈 화면 Tkinter 미번들 --enable-plugin=tk-inter (이미 flag list — 없으면 이게 원인).
MissingDependencyException: en_core_web_sm spaCy 모델 없음 --spacy-language-model=en_core_web_sm--include-package=en_core_web_sm 둘 다 있는지 확인.
llama-server.exe 못 찾음 번들 레이아웃 drift dist\Cue\bin\llama-b<tag>\llama-server.exe 확인; Cue.exe --selftest=llama_server_health.
Inno Setup "missing source" Phase 7 rename 안 됨 ISCC 호출 전에 dist\Cue\Cue.exe 존재 확인.

Selftest

Frozen 빌드는 사용자가 hit하기 전에 누락된 native lib을 잡기 위한 selftest 지원:

Cue.app/Contents/MacOS/run --selftest=llama_server_health
Cue.app/Contents/MacOS/run --selftest=llama_server_import

CI 단계가 빌드 후 이를 실행.

번들된 llama-server

온디바이스 디지스트 백엔드는 llama-cpp-python이 아닌 외부 멀티모달 추론 바이너리 사용. 이유는 온디바이스 비전 참고.

빌드 매트릭스:

  • macOS arm64 / x64ggml-org/llama.cppllama-<tag>-bin-macos-{arm64,x64}.tar.gz 릴리스 아티팩트.
  • Windows x64llama-<tag>-bin-win-cpu-x64.zip 릴리스 아티팩트.

scripts/build_nuitka.sh (macOS), scripts/build_nuitka.py (Windows), scripts/build_installer.py (legacy PyInstaller fallback)이 이 바이너리를 다운로드 + 검증 + 번들로 추출. 핀된 릴리스 태그는 scripts/의 매니페스트 편집으로 업데이트.

CI 통합

.github/workflows/ci.yml이 매 push에 ruff lint + format 실행. 앱 릴리스 빌드는 CI에서 안 함 (CI에 없는 keychain 액세스와 Apple Developer 인증서 필요).

.github/workflows/docs.yml은 별도 워크플로:

  • 매 PR + main push에 uv pip install -e ".[docs]"uv run --no-sync mkdocs build --strict 실행. 깨진 nav, 누락된 앵커, mkdocstrings import regression을 deploy 전에 잡음.
  • main push 시에는 cloudflare/wrangler-action@v3CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID repo secret을 사용해 빌드된 site/cue Cloudflare Pages 프로젝트에 push (pages deploy site --project-name=cue --branch=main).
  • 이 워크플로에서 서브모듈 fetch는 의도적으로 off — docs 빌드는 ocap이나 owa가 필요 없고, Cloudflare git-connect 플로우는 어차피 우리 private vendor 서브모듈을 못 읽음. 그게 deploy를 Cloudflare Git 통합 대신 GitHub Actions로 보낸 이유.

공개 docs URL: https://cue-aif.pages.dev/.

버전

단일 진실 공급원: pyproject.tomlversion. main.py_VERSION과 README 푸터도 업데이트.

더 보기