플랫폼 분기 (paths.ts): - EXE_SUFFIX 상수로 sox/sidecar/ffmpeg 실행파일 확장자 통합 - Windows에선 .exe 자동 부착, Mac/Linux에선 빈 문자열 - 미사용 getProjectRoot 헬퍼 제거 런타임 서비스 Mac 분기: - SoundEffectService: darwin → /usr/bin/afplay, linux → aplay 분기 추가 (execFile로 안전하게) - ScreenContextService._getActiveWindowInfo: win32 → PowerShell + user32.dll (기존), darwin → osascript (System Events frontmost process + 윈도우 타이틀) Linux는 미지원 (null) electron-builder.yml: - mac 타겟 추가 (dmg + zip, arm64 + x64 매트릭스) - hardenedRuntime, gatekeeperAssess, entitlements 설정 - extendInfo로 NSMicrophoneUsage / NSCameraUsage / NSAppleEvents / NSSystemAdministration 권한 메시지 - dmg 레이아웃 (드래그 to /Applications) - linux AppImage placeholder - notarize: false 기본, NOTARIZE 환경변수로 활성화 build/entitlements.mac.plist: - allow-jit, allow-unsigned-executable-memory (Electron 필수) - audio-input, camera, network.client - automation.apple-events (활성 윈도우 조회용) - files.user-selected.read-write - allow-dyld-environment-variables (sox/ffmpeg 라이브러리 로드) scripts/build-sidecar.py: - IS_WINDOWS / IS_MACOS / EXE_SUFFIX 도입 - Windows에서만 --noconsole 플래그 - 빌드 결과 경로 + size 출력 플랫폼 통합 scripts/install-sox.sh (신규): - Mac/Linux용 SoX 번들 스크립트 - macOS는 otool로 dylib 의존성 식별 후 함께 복사, install_name_tool로 rpath를 @loader_path로 변경 - electron-builder의 extraResources 대상 디렉토리에 배치 resources/icons/ (신규): - README.md만 커밋, 실제 아이콘 파일은 분리 - sips/iconutil/imagemagick으로 .icns/.ico/.png 생성 가이드 .github/workflows/build-mac.yml (신규): - macos-14 runner (Apple Silicon), arm64/x64 matrix - brew sox, npm install, @electron/rebuild, install-sox.sh, build-sidecar.py, electron-builder dist - CSC/NOTARIZE 환경변수 자동 처리 - artifact 업로드 (dmg + zip, retention 7일) docs/v2/phase-V2-5-mac-guide.md (신규): - 사전 조건, 시스템 의존성, dev 실행, dist 빌드, Code signing + Notarization, CI 트리거, 트러블슈팅 검증 (Windows에서): - typecheck 통과 (Mac 분기 추가에도 회귀 없음) - build 통과 - dev 런타임 정상 Mac 검증은 사용자 본인 Mac에서 수행 (V2-5 사용자 액션).
138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""
|
|
scripts/build-sidecar.py
|
|
faster-whisper sidecar를 PyInstaller로 빌드한다. (크로스 플랫폼)
|
|
|
|
사용법:
|
|
pip install pyinstaller
|
|
pip install -r sidecar/requirements.txt
|
|
python scripts/build-sidecar.py
|
|
|
|
출력:
|
|
sidecar-dist/sidecar/sidecar(.exe) (onedir 모드)
|
|
|
|
플랫폼:
|
|
Windows → --noconsole (콘솔 창 숨김)
|
|
macOS → --windowed (.app 번들 형식은 여기서는 미사용, onedir만)
|
|
Linux → 콘솔 옵션 없음
|
|
"""
|
|
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
SIDECAR_DIR = PROJECT_ROOT / "sidecar"
|
|
MAIN_PY = SIDECAR_DIR / "main.py"
|
|
OUTPUT_DIR = PROJECT_ROOT / "sidecar-dist"
|
|
|
|
IS_WINDOWS = platform.system() == "Windows"
|
|
IS_MACOS = platform.system() == "Darwin"
|
|
EXE_SUFFIX = ".exe" if IS_WINDOWS else ""
|
|
|
|
|
|
def check_prerequisites():
|
|
"""필수 도구가 설치되어 있는지 확인한다."""
|
|
try:
|
|
import PyInstaller # noqa: F401
|
|
except ImportError:
|
|
print("PyInstaller가 설치되어 있지 않습니다.")
|
|
print("실행: pip install pyinstaller")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
import faster_whisper # noqa: F401
|
|
except ImportError:
|
|
print("faster-whisper가 설치되어 있지 않습니다.")
|
|
print("실행: pip install -r sidecar/requirements.txt")
|
|
sys.exit(1)
|
|
|
|
if not MAIN_PY.exists():
|
|
print(f"sidecar 소스를 찾을 수 없습니다: {MAIN_PY}")
|
|
sys.exit(1)
|
|
|
|
|
|
def build():
|
|
"""PyInstaller로 sidecar를 빌드한다."""
|
|
print("=" * 60)
|
|
print(f"D3RO-VOICE Sidecar 빌드 시작 ({platform.system()} {platform.machine()})")
|
|
print("=" * 60)
|
|
|
|
# 기존 빌드 정리
|
|
if OUTPUT_DIR.exists():
|
|
shutil.rmtree(OUTPUT_DIR)
|
|
|
|
cmd = [
|
|
sys.executable, "-m", "PyInstaller",
|
|
"--name", "sidecar",
|
|
"--distpath", str(OUTPUT_DIR),
|
|
"--workpath", str(PROJECT_ROOT / "build" / "sidecar-build"),
|
|
"--specpath", str(PROJECT_ROOT / "build"),
|
|
"--noconfirm",
|
|
"--clean",
|
|
# hidden imports
|
|
"--hidden-import", "faster_whisper",
|
|
"--hidden-import", "ctranslate2",
|
|
"--hidden-import", "huggingface_hub",
|
|
"--hidden-import", "tokenizers",
|
|
"--hidden-import", "uvicorn.logging",
|
|
"--hidden-import", "uvicorn.protocols.http",
|
|
"--hidden-import", "uvicorn.protocols.http.auto",
|
|
"--hidden-import", "uvicorn.protocols.http.h11_impl",
|
|
"--hidden-import", "uvicorn.protocols.websockets",
|
|
"--hidden-import", "uvicorn.protocols.websockets.auto",
|
|
"--hidden-import", "uvicorn.lifespan",
|
|
"--hidden-import", "uvicorn.lifespan.on",
|
|
"--hidden-import", "uvicorn.lifespan.off",
|
|
]
|
|
|
|
# 플랫폼별 콘솔 옵션
|
|
if IS_WINDOWS:
|
|
cmd.append("--noconsole")
|
|
elif IS_MACOS:
|
|
# macOS에서 --windowed는 .app 번들 생성을 의미.
|
|
# onedir 모드로도 .app이 만들어질 수 있어서 명시적으로는 쓰지 않음.
|
|
# electron-builder가 extraResources로 Binary만 포함하므로 기본 콘솔 형태 유지.
|
|
pass
|
|
# Linux: 옵션 없음
|
|
|
|
cmd.append(str(MAIN_PY))
|
|
|
|
print(f"\n실행 명령:\n {' '.join(cmd)}\n")
|
|
|
|
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT))
|
|
|
|
if result.returncode != 0:
|
|
print(f"\nPyInstaller 빌드 실패 (exit code: {result.returncode})")
|
|
sys.exit(1)
|
|
|
|
# 빌드 결과 확인
|
|
exe_path = OUTPUT_DIR / "sidecar" / f"sidecar{EXE_SUFFIX}"
|
|
if exe_path.exists():
|
|
size_mb = exe_path.stat().st_size / (1024 * 1024)
|
|
print(f"\n빌드 성공!")
|
|
print(f" 경로: {exe_path}")
|
|
print(f" 크기: {size_mb:.1f} MB")
|
|
|
|
total_size = sum(
|
|
f.stat().st_size for f in (OUTPUT_DIR / "sidecar").rglob("*") if f.is_file()
|
|
)
|
|
total_mb = total_size / (1024 * 1024)
|
|
print(f" 전체 디렉토리: {total_mb:.1f} MB")
|
|
else:
|
|
print(f"\n빌드 출력을 찾을 수 없습니다: {exe_path}")
|
|
# onedir 모드에서는 디렉토리 내부에 바이너리가 있음
|
|
pattern = "*.exe" if IS_WINDOWS else "sidecar"
|
|
for item in OUTPUT_DIR.rglob(pattern):
|
|
print(f" 발견: {item}")
|
|
|
|
print("\n빌드 완료!")
|
|
print("electron-builder에서 이 경로를 extraResources로 지정하세요:")
|
|
print(" from: sidecar-dist/sidecar/")
|
|
print(" to: sidecar/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
check_prerequisites()
|
|
build()
|