릴리스 빌드¶
이 페이지는 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
스크립트가 하는 일:
- conda
cueenv 활성화 (uv 빌드 env는 dev용; 빌드 env는 Python / PyGObject / GStreamer / GStreamer 플러그인 간 ABI 일관성 위해 conda).scripts/environment.yml참고. - Nuitka로
.app번들로 컴파일. - vendoring된 owa 패키지의 dist-info 번들 — 그래야
importlib.metadata.entry_points()가 플러그인 등록 발견. - 시스템 경로에서 GStreamer.framework 해결 (번들 안 함 — 너무 큼;
사용자가 시스템 전역 GStreamer 설치). PyGObject가 GNU
iconv*심볼셋 찾도록 작은libiconv호환성 shim 빌드. - 온디바이스 추론용
llama-server바이너리를Cue.app/Contents/MacOS/bin/llama-b<tag>/아래 번들 (스크립트가 없으면 자동 다운로드). - Codesign 모든 Mach-O 바이너리 — Developer ID Application
인증서, hardened runtime, embedded entitlements
(
scripts/release/entitlements.plist), secure timestamp로 serial. 일시적 timestamp-server hiccup에 retry. - Notarize
.app을 keychain profile (한 번xcrun notarytool store-credentials로 설정)으로xcrun notarytool submit --wait. - notary ticket을
.app에 staple. - DMG 빌드, 같은 Developer ID 신원으로 서명, notary 제출, staple.
산출물은 dist/Cue.app과 dist/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-variables—Info.plist의LSEnvironment(아래 참고)가 시스템 GStreamer.framework를 가리키도록DYLD_LIBRARY_PATH/GST_PLUGIN_PATH/GI_TYPELIB_PATH주입. Hardened runtime이 이 entitlement 없으면 그런 env var 제거.com.apple.security.cs.allow-jit— GStreamer의orcSIMD JIT 컴파일러가pthread_jit_write_protect_np호출하여 W^X 메모리 토글하기 때문에 필수.
LSEnvironment + sticky 권한¶
Info.plist에 LSEnvironment 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
스크립트가 하는 일:
dist/와build/를 hard-clean (PyInstaller 잔여물 회피).- 빌드 타임 deps (
nuitka,ordered-set,Pillow)를uv pip install로 설치 — uv venv는 pip이 없음. assets/icon.png→assets/Cue.ico(multi-res 16-256).- spaCy
en_core_web_sm확인. ggml-org/llama.cpp에서llama-<tag>-bin-win-cpu-x64.zip다운로드 후vendor/llama-bin/llama-b<tag>/에 압축 해제.- Nuitka standalone 컴파일 (~5-15분 첫 실행; 이후는 ccache로 빠름).
--enable-plugin=tk-inter사용 — customtkinter / Settings 창 / API 키 프롬프트 동작 위해. build/run.dist→dist/Cue,run.exe→Cue.exe리네임.- 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 디렉토리가 있어야 함. - Nuitka의
--include-data-dir이 스킵하는 GStreamer 네이티브 에셋 복구: site-packages의 각gstreamer_*패키지 내부를 walk 하면서*.exe,*.dll,*.typelib모두 dist 트리에 복사 (+vendor/ocap-windows/의 커스텀gst-plugins/*.py). ocap-windows가 하는 것 그대로. vendor/llama-bin/llama-b<tag>/→dist/Cue/bin/llama-b<tag>/복사.- 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
기반 파이프라인 구성: gdiscreencapsrc → videoconvert →
videoscale name=d3d11scale0 → tee → fpsdisplaysink /
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._win32와 pynput.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 / x64 —
ggml-org/llama.cpp의llama-<tag>-bin-macos-{arm64,x64}.tar.gz릴리스 아티팩트. - Windows x64 —
llama-<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, 누락된 앵커,mkdocstringsimport regression을 deploy 전에 잡음. - main push 시에는
cloudflare/wrangler-action@v3로CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_IDrepo secret을 사용해 빌드된site/를cueCloudflare 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.toml → version. main.py의
_VERSION과 README 푸터도 업데이트.
더 보기¶
- 개발 환경 — 로컬 dev 워크플로.
- 서브모듈 / 벤더링.
- 크로스 플랫폼 규칙.
- 온디바이스 비전.