feat(V2-5): macOS 빌드 지원 — 플랫폼 분기 + electron-builder mac 타겟 + CI
플랫폼 분기 (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 사용자 액션).
This commit is contained in:
parent
97eb886ec3
commit
77d514222e
12 changed files with 780 additions and 79 deletions
|
|
@ -1,15 +1,22 @@
|
|||
"""
|
||||
scripts/build-sidecar.py
|
||||
faster-whisper sidecar를 PyInstaller로 빌드한다.
|
||||
faster-whisper sidecar를 PyInstaller로 빌드한다. (크로스 플랫폼)
|
||||
|
||||
사용법:
|
||||
pip install pyinstaller
|
||||
pip install -r sidecar/requirements.txt
|
||||
python scripts/build-sidecar.py
|
||||
|
||||
출력:
|
||||
sidecar-dist/sidecar.exe (단일 디렉토리 모드)
|
||||
sidecar-dist/sidecar/sidecar(.exe) (onedir 모드)
|
||||
|
||||
플랫폼:
|
||||
Windows → --noconsole (콘솔 창 숨김)
|
||||
macOS → --windowed (.app 번들 형식은 여기서는 미사용, onedir만)
|
||||
Linux → 콘솔 옵션 없음
|
||||
"""
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
|
|
@ -20,10 +27,13 @@ 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():
|
||||
"""필수 도구가 설치되어 있는지 확인한다."""
|
||||
# PyInstaller
|
||||
try:
|
||||
import PyInstaller # noqa: F401
|
||||
except ImportError:
|
||||
|
|
@ -31,7 +41,6 @@ def check_prerequisites():
|
|||
print("실행: pip install pyinstaller")
|
||||
sys.exit(1)
|
||||
|
||||
# faster-whisper
|
||||
try:
|
||||
import faster_whisper # noqa: F401
|
||||
except ImportError:
|
||||
|
|
@ -47,24 +56,22 @@ def check_prerequisites():
|
|||
def build():
|
||||
"""PyInstaller로 sidecar를 빌드한다."""
|
||||
print("=" * 60)
|
||||
print("D3RO-VOICE Sidecar 빌드 시작")
|
||||
print(f"D3RO-VOICE Sidecar 빌드 시작 ({platform.system()} {platform.machine()})")
|
||||
print("=" * 60)
|
||||
|
||||
# 기존 빌드 정리
|
||||
if OUTPUT_DIR.exists():
|
||||
shutil.rmtree(OUTPUT_DIR)
|
||||
|
||||
# PyInstaller 실행 — onedir 모드 (onefile보다 시작이 빠름)
|
||||
cmd = [
|
||||
sys.executable, "-m", "PyInstaller",
|
||||
"--name", "sidecar",
|
||||
"--distpath", str(OUTPUT_DIR),
|
||||
"--workpath", str(PROJECT_ROOT / "build" / "sidecar-build"),
|
||||
"--specpath", str(PROJECT_ROOT / "build"),
|
||||
# onedir 모드 (단일 디렉토리)
|
||||
"--noconfirm",
|
||||
"--clean",
|
||||
# hidden imports (PyInstaller가 자동 감지 못하는 것)
|
||||
# hidden imports
|
||||
"--hidden-import", "faster_whisper",
|
||||
"--hidden-import", "ctranslate2",
|
||||
"--hidden-import", "huggingface_hub",
|
||||
|
|
@ -78,11 +85,20 @@ def build():
|
|||
"--hidden-import", "uvicorn.lifespan",
|
||||
"--hidden-import", "uvicorn.lifespan.on",
|
||||
"--hidden-import", "uvicorn.lifespan.off",
|
||||
# 콘솔 없음 (Windows)
|
||||
"--noconsole",
|
||||
str(MAIN_PY),
|
||||
]
|
||||
|
||||
# 플랫폼별 콘솔 옵션
|
||||
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))
|
||||
|
|
@ -92,27 +108,29 @@ def build():
|
|||
sys.exit(1)
|
||||
|
||||
# 빌드 결과 확인
|
||||
exe_path = OUTPUT_DIR / "sidecar" / "sidecar.exe"
|
||||
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_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 모드에서는 디렉토리 내부에 exe가 있음
|
||||
for exe in OUTPUT_DIR.rglob("*.exe"):
|
||||
print(f" 발견: {exe}")
|
||||
# onedir 모드에서는 디렉토리 내부에 바이너리가 있음
|
||||
pattern = "*.exe" if IS_WINDOWS else "sidecar"
|
||||
for item in OUTPUT_DIR.rglob(pattern):
|
||||
print(f" 발견: {item}")
|
||||
|
||||
print("\n빌드 완료!")
|
||||
print(f"electron-builder에서 이 경로를 extraResources로 지정하세요:")
|
||||
print(f" from: sidecar-dist/sidecar/")
|
||||
print(f" to: sidecar/")
|
||||
print("electron-builder에서 이 경로를 extraResources로 지정하세요:")
|
||||
print(" from: sidecar-dist/sidecar/")
|
||||
print(" to: sidecar/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
92
apps/desktop/scripts/install-sox.sh
Normal file
92
apps/desktop/scripts/install-sox.sh
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/install-sox.sh
|
||||
#
|
||||
# Mac/Linux에서 SoX 바이너리를 apps/desktop/resources/sox/로 복사한다.
|
||||
# electron-builder의 extraResources가 이 디렉토리를 번들 대상으로 잡는다.
|
||||
#
|
||||
# 사용법:
|
||||
# # 먼저 시스템에 SoX 설치
|
||||
# brew install sox # macOS
|
||||
# sudo apt install sox # Ubuntu/Debian
|
||||
#
|
||||
# # 그 다음 이 스크립트 실행 (apps/desktop 기준)
|
||||
# ./scripts/install-sox.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DESKTOP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
TARGET_DIR="${DESKTOP_DIR}/resources/sox"
|
||||
|
||||
OS="$(uname -s)"
|
||||
case "$OS" in
|
||||
Darwin*) PLATFORM="macos" ;;
|
||||
Linux*) PLATFORM="linux" ;;
|
||||
*)
|
||||
echo "Unsupported OS: $OS — Windows는 scripts/download-sox.ps1를 사용하세요."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "=== D3RO Voice: SoX 바이너리 번들 (${PLATFORM}) ==="
|
||||
|
||||
# 시스템 sox/rec 경로 찾기
|
||||
SOX_BIN="$(command -v sox || true)"
|
||||
REC_BIN="$(command -v rec || true)"
|
||||
|
||||
if [ -z "$SOX_BIN" ]; then
|
||||
echo "ERROR: 시스템에 sox가 설치되어 있지 않습니다."
|
||||
if [ "$PLATFORM" = "macos" ]; then
|
||||
echo " macOS: brew install sox"
|
||||
else
|
||||
echo " Linux: sudo apt install sox 또는 sudo yum install sox"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "sox: $SOX_BIN"
|
||||
if [ -n "$REC_BIN" ]; then
|
||||
echo "rec: $REC_BIN"
|
||||
fi
|
||||
|
||||
# 타겟 디렉토리 준비
|
||||
mkdir -p "${TARGET_DIR}"
|
||||
echo "target: ${TARGET_DIR}"
|
||||
|
||||
# 바이너리 복사
|
||||
cp "$SOX_BIN" "${TARGET_DIR}/sox"
|
||||
chmod +x "${TARGET_DIR}/sox"
|
||||
echo "복사: sox → ${TARGET_DIR}/sox"
|
||||
|
||||
if [ -n "$REC_BIN" ]; then
|
||||
cp "$REC_BIN" "${TARGET_DIR}/rec"
|
||||
chmod +x "${TARGET_DIR}/rec"
|
||||
echo "복사: rec → ${TARGET_DIR}/rec"
|
||||
fi
|
||||
|
||||
# 동적 라이브러리 의존성 복사 (macOS만)
|
||||
if [ "$PLATFORM" = "macos" ]; then
|
||||
echo "동적 라이브러리 의존성 복사 중..."
|
||||
|
||||
# otool로 의존성 확인 후 SoX와 함께 묶음
|
||||
LIBS=$(otool -L "$SOX_BIN" | awk 'NR>1 && $1 !~ /^\/System\// && $1 !~ /^\/usr\/lib\// {print $1}')
|
||||
|
||||
for lib in $LIBS; do
|
||||
if [ -f "$lib" ]; then
|
||||
lib_name="$(basename "$lib")"
|
||||
cp "$lib" "${TARGET_DIR}/${lib_name}"
|
||||
chmod +w "${TARGET_DIR}/${lib_name}"
|
||||
echo " lib: ${lib_name}"
|
||||
|
||||
# rpath를 @loader_path로 변경 (앱 번들 내부에서도 동작하도록)
|
||||
install_name_tool -change "$lib" "@loader_path/${lib_name}" "${TARGET_DIR}/sox" 2>/dev/null || true
|
||||
if [ -n "$REC_BIN" ]; then
|
||||
install_name_tool -change "$lib" "@loader_path/${lib_name}" "${TARGET_DIR}/rec" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "완료. electron-builder가 이 디렉토리를 extraResources로 번들합니다."
|
||||
ls -lh "${TARGET_DIR}/"
|
||||
Loading…
Add table
Add a link
Reference in a new issue