Add macOS app bundle packaging, native launcher, and MACOS_APP_PACKAGING guide
This commit is contained in:
@@ -6,3 +6,6 @@ swift/speech-helper
|
|||||||
swift/llm-helper
|
swift/llm-helper
|
||||||
swift/test_foundation
|
swift/test_foundation
|
||||||
swift/test_foundation.swift
|
swift/test_foundation.swift
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Packaging Python Applications into Native macOS Apps (`.app`) with Custom Privacy Entitlements
|
||||||
|
|
||||||
|
This guide details how to transform a CLI Python application into a native macOS `.app` bundle that requests macOS Transparency, Consent, and Control (TCC) privacy permissions (Microphone, Speech Recognition, Camera, Input Monitoring) under its **own custom app identity** instead of `Terminal.app` or `python3.11`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problem Overview
|
||||||
|
|
||||||
|
When launching a Python script via Terminal or a shell launcher (`#!/bin/bash`), macOS TCC attributes privacy permissions to either:
|
||||||
|
1. `Terminal.app` (if executed inside a terminal window), or
|
||||||
|
2. `python3.11` / `python3.1` (if `exec /path/to/python` is called from a script).
|
||||||
|
|
||||||
|
To ensure macOS prompts display your custom application name (e.g., *"VoiceAgent would like to access the microphone"*) and show up under **System Settings → Privacy & Security → Microphone**, you must follow Apple's native bundle layout and executable architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Key Architectural Requirements
|
||||||
|
|
||||||
|
```
|
||||||
|
VoiceAgent.app/
|
||||||
|
└── Contents/
|
||||||
|
├── Info.plist # App Identity & Custom Privacy Descriptions
|
||||||
|
├── MacOS/
|
||||||
|
│ └── VoiceAgent # Compiled Mach-O Swift Launcher Binary
|
||||||
|
└── Resources/
|
||||||
|
├── src/ # Packaged Python Source Code
|
||||||
|
└── swift/ # Native Helper Binaries (speech-helper, llm-helper)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Standard macOS `.app` Directory Layout**:
|
||||||
|
The bundle must strictly use the `Contents/` directory layout.
|
||||||
|
|
||||||
|
2. **Compiled Native Mach-O Executable (Swift/C)**:
|
||||||
|
- macOS TCC identifies process ownership through the executable binary image (`CFBundleExecutable`).
|
||||||
|
- Shell scripts (`/bin/bash`) cause macOS to inspect the underlying `python` image and attribute permissions to `python3.1`.
|
||||||
|
- **Solution**: Compile a native Swift executable (`swift/VoiceAgentLauncher.swift`) into a Mach-O 64-bit arm64 binary (`Contents/MacOS/VoiceAgent`). When Finder launches this binary, macOS registers the process under `com.voiceagent.mac`. Subprocesses spawned by this binary inherit the bundle's TCC identity.
|
||||||
|
|
||||||
|
3. **Privacy Entitlements in `Info.plist` & Code Signing**:
|
||||||
|
- `Info.plist` defines `CFBundleExecutable`, `CFBundleIdentifier`, and the `NS*UsageDescription` keys.
|
||||||
|
- The app bundle must be deeply code-signed (`codesign --deep`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementation Workflow
|
||||||
|
|
||||||
|
### Step 1: Native Swift Launcher (`swift/VoiceAgentLauncher.swift`)
|
||||||
|
|
||||||
|
The Swift launcher sets environment variables, redirects logs, and spawns the Python process:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct VoiceAgentLauncher {
|
||||||
|
static func main() {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let projDir = "/Users/adolforeyna/Projects/VoiceAgent1"
|
||||||
|
|
||||||
|
let bundleResPath = Bundle.main.resourcePath ?? ""
|
||||||
|
let bundledSrcPath = "\(bundleResPath)/src"
|
||||||
|
|
||||||
|
let workDir = fileManager.fileExists(atPath: projDir) ? projDir : bundledSrcPath
|
||||||
|
let pythonBin = "\(projDir)/.venv/bin/python"
|
||||||
|
let fallbackPython = "/usr/bin/python3"
|
||||||
|
let targetPython = fileManager.fileExists(atPath: pythonBin) ? pythonBin : fallbackPython
|
||||||
|
let targetScript = "\(workDir)/app_main.py"
|
||||||
|
|
||||||
|
setenv("SSL_CERT_FILE", "/etc/ssl/cert.pem", 1)
|
||||||
|
setenv("REQUESTS_CA_BUNDLE", "/etc/ssl/cert.pem", 1)
|
||||||
|
|
||||||
|
let logDir = fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Logs/VoiceAgent")
|
||||||
|
try? fileManager.createDirectory(at: logDir, withIntermediateDirectories: true)
|
||||||
|
let logFile = logDir.appendingPathComponent("voiceagent.log")
|
||||||
|
|
||||||
|
if !fileManager.fileExists(atPath: logFile.path) {
|
||||||
|
fileManager.createFile(atPath: logFile.path, contents: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: targetPython)
|
||||||
|
process.arguments = [targetScript]
|
||||||
|
process.currentDirectoryURL = URL(fileURLWithPath: workDir)
|
||||||
|
|
||||||
|
if let logHandle = try? FileHandle(forWritingTo: logFile) {
|
||||||
|
logHandle.seekToEndOfFile()
|
||||||
|
process.standardOutput = logHandle
|
||||||
|
process.standardError = logHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
} catch {
|
||||||
|
print("Failed to run VoiceAgent: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Build & Package Script (`build_app.sh`)
|
||||||
|
|
||||||
|
Automate Swift binary compilation, bundle structure creation, `Info.plist` injection, installation to `/Applications`, and code signing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$HERE"
|
||||||
|
|
||||||
|
# 1. Build Swift Helpers
|
||||||
|
bash swift/build.sh
|
||||||
|
|
||||||
|
APP_NAME="VoiceAgent"
|
||||||
|
DIST_DIR="$HERE/dist"
|
||||||
|
APP_BUNDLE="$DIST_DIR/$APP_NAME.app"
|
||||||
|
CONTENTS_DIR="$APP_BUNDLE/Contents"
|
||||||
|
MACOS_DIR="$CONTENTS_DIR/MacOS"
|
||||||
|
RESOURCES_DIR="$CONTENTS_DIR/Resources"
|
||||||
|
SRC_DIR="$RESOURCES_DIR/src"
|
||||||
|
|
||||||
|
# 2. Setup Bundle Directory Structure
|
||||||
|
rm -rf "$APP_BUNDLE"
|
||||||
|
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR/swift" "$SRC_DIR"
|
||||||
|
|
||||||
|
# 3. Compile Native Mach-O Launcher
|
||||||
|
swiftc -O -parse-as-library "$HERE/swift/VoiceAgentLauncher.swift" -o "$MACOS_DIR/VoiceAgent"
|
||||||
|
chmod +x "$MACOS_DIR/VoiceAgent"
|
||||||
|
|
||||||
|
# 4. Copy Swift Helper Binaries & Python Source Files
|
||||||
|
cp "$HERE/swift/speech-helper" "$RESOURCES_DIR/swift/"
|
||||||
|
cp "$HERE/swift/llm-helper" "$RESOURCES_DIR/swift/"
|
||||||
|
cp "$HERE"/*.py "$SRC_DIR/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 5. Generate Info.plist
|
||||||
|
cat << 'EOF' > "$CONTENTS_DIR/Info.plist"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>VoiceAgent</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.voiceagent.mac</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>VoiceAgent</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>VoiceAgent</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>14.0</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>VoiceAgent requires access to your microphone to listen to your voice commands.</string>
|
||||||
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
|
<string>VoiceAgent uses on-device speech recognition to process your spoken input.</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 6. Sign & Install to /Applications
|
||||||
|
codesign -s - --deep --force "$APP_BUNDLE"
|
||||||
|
rm -rf /Applications/VoiceAgent.app
|
||||||
|
cp -R "$APP_BUNDLE" /Applications/
|
||||||
|
codesign -s - --deep --force /Applications/VoiceAgent.app
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Verification Matrix
|
||||||
|
|
||||||
|
| Launch Mechanism | Process Ownership | Permission Prompt Identity | System Settings Category |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Terminal Script (`python bot.py`) | `Terminal.app` | *"Terminal would like to access..."* | Listed under Terminal |
|
||||||
|
| Shell Script (`#!/bin/bash exec`) | `python3.11` | *"python3.1 would like to access..."* | Listed under `python3.1` |
|
||||||
|
| **Native Swift Launcher (`VoiceAgent`)** | **`com.voiceagent.mac`** | ***"VoiceAgent would like to access..."*** | **Listed under `VoiceAgent`** |
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Main entrypoint for VoiceAgent macOS App bundle."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure CA certificates are configured for macOS SSL
|
||||||
|
os.environ["SSL_CERT_FILE"] = "/etc/ssl/cert.pem"
|
||||||
|
os.environ["REQUESTS_CA_BUNDLE"] = "/etc/ssl/cert.pem"
|
||||||
|
|
||||||
|
import bot
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(asyncio.run(bot.main()))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(0)
|
||||||
+20
-2
@@ -13,6 +13,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -41,8 +42,25 @@ _NOISE_TRANSCRIPTS = {
|
|||||||
"[silence]",
|
"[silence]",
|
||||||
}
|
}
|
||||||
|
|
||||||
HERE = Path(__file__).parent
|
def get_helper_path(binary_name: str) -> Path:
|
||||||
LLM_HELPER_PATH = HERE / "swift" / "llm-helper"
|
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||||
|
p1 = Path(sys._MEIPASS) / binary_name
|
||||||
|
if p1.exists():
|
||||||
|
return p1
|
||||||
|
p2 = Path(sys._MEIPASS) / "swift" / binary_name
|
||||||
|
if p2.exists():
|
||||||
|
return p2
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
res_dir = Path(sys.executable).parent.parent / "Resources"
|
||||||
|
p1 = res_dir / binary_name
|
||||||
|
if p1.exists():
|
||||||
|
return p1
|
||||||
|
p2 = res_dir / "swift" / binary_name
|
||||||
|
if p2.exists():
|
||||||
|
return p2
|
||||||
|
return Path(__file__).parent / "swift" / binary_name
|
||||||
|
|
||||||
|
LLM_HELPER_PATH = get_helper_path("llm-helper")
|
||||||
|
|
||||||
|
|
||||||
def probe_apple_llm() -> tuple[bool, str]:
|
def probe_apple_llm() -> tuple[bool, str]:
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# build_app.sh - Build standalone macOS VoiceAgent.app bundle
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$HERE"
|
||||||
|
|
||||||
|
echo "=== 1. Building Swift Helper & Launcher Binaries ==="
|
||||||
|
bash swift/build.sh
|
||||||
|
|
||||||
|
APP_NAME="VoiceAgent"
|
||||||
|
DIST_DIR="$HERE/dist"
|
||||||
|
APP_BUNDLE="$DIST_DIR/$APP_NAME.app"
|
||||||
|
CONTENTS_DIR="$APP_BUNDLE/Contents"
|
||||||
|
MACOS_DIR="$CONTENTS_DIR/MacOS"
|
||||||
|
RESOURCES_DIR="$CONTENTS_DIR/Resources"
|
||||||
|
SRC_DIR="$RESOURCES_DIR/src"
|
||||||
|
|
||||||
|
echo "=== 2. Creating macOS App Bundle Structure ==="
|
||||||
|
rm -rf "$APP_BUNDLE"
|
||||||
|
mkdir -p "$MACOS_DIR"
|
||||||
|
mkdir -p "$RESOURCES_DIR/swift"
|
||||||
|
mkdir -p "$SRC_DIR"
|
||||||
|
|
||||||
|
echo "=== 3. Compiling Native App Bundle Launcher ==="
|
||||||
|
swiftc -O -parse-as-library "$HERE/swift/VoiceAgentLauncher.swift" -o "$MACOS_DIR/VoiceAgent"
|
||||||
|
chmod +x "$MACOS_DIR/VoiceAgent"
|
||||||
|
|
||||||
|
echo "=== 4. Copying Source Files, Swift Helpers, and Assets ==="
|
||||||
|
cp "$HERE/swift/speech-helper" "$RESOURCES_DIR/swift/"
|
||||||
|
cp "$HERE/swift/llm-helper" "$RESOURCES_DIR/swift/"
|
||||||
|
chmod +x "$RESOURCES_DIR/swift/speech-helper" "$RESOURCES_DIR/swift/llm-helper"
|
||||||
|
|
||||||
|
# Copy python files to bundle resources
|
||||||
|
cp "$HERE"/*.py "$SRC_DIR/" 2>/dev/null || true
|
||||||
|
if [ -f "$HERE/vocabulary.example.txt" ]; then
|
||||||
|
cp "$HERE/vocabulary.example.txt" "$RESOURCES_DIR/"
|
||||||
|
fi
|
||||||
|
if [ -f "$HERE/corrections.example.txt" ]; then
|
||||||
|
cp "$HERE/corrections.example.txt" "$RESOURCES_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 5. Generating Info.plist with Entitlements ==="
|
||||||
|
cat << 'EOF' > "$CONTENTS_DIR/Info.plist"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>VoiceAgent</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.voiceagent.mac</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>VoiceAgent</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>VoiceAgent</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>14.0</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>VoiceAgent requires access to your microphone to listen to your voice commands.</string>
|
||||||
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
|
<string>VoiceAgent uses on-device speech recognition to process your spoken input.</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== 6. Code-signing App Bundle ==="
|
||||||
|
codesign -s - --deep --force "$APP_BUNDLE"
|
||||||
|
|
||||||
|
echo "=== 7. Installing to /Applications ==="
|
||||||
|
rm -rf /Applications/VoiceAgent.app
|
||||||
|
cp -R "$APP_BUNDLE" /Applications/
|
||||||
|
codesign -s - --deep --force /Applications/VoiceAgent.app
|
||||||
|
|
||||||
|
echo "=========================================================="
|
||||||
|
echo "Successfully built and installed VoiceAgent.app to:"
|
||||||
|
echo "1. /Applications/VoiceAgent.app"
|
||||||
|
echo "2. $APP_BUNDLE"
|
||||||
|
echo "=========================================================="
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""py2app setup configuration for building VoiceAgent.app."""
|
||||||
|
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
APP = ["app_main.py"]
|
||||||
|
DATA_FILES = [
|
||||||
|
("swift", ["swift/speech-helper", "swift/llm-helper"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
OPTIONS = {
|
||||||
|
"argv_emulation": False,
|
||||||
|
"iconfile": None,
|
||||||
|
"plist": {
|
||||||
|
"CFBundleName": "VoiceAgent",
|
||||||
|
"CFBundleDisplayName": "VoiceAgent",
|
||||||
|
"CFBundleIdentifier": "com.voiceagent.mac",
|
||||||
|
"CFBundleVersion": "1.0.0",
|
||||||
|
"CFBundleShortVersionString": "1.0.0",
|
||||||
|
"NSMicrophoneUsageDescription": "VoiceAgent requires access to your microphone for voice interaction.",
|
||||||
|
"NSSpeechRecognitionUsageDescription": "VoiceAgent uses on-device speech recognition to process your spoken input.",
|
||||||
|
"NSHumanReadableCopyright": "Copyright © 2026 Adolfo Reyna. All rights reserved.",
|
||||||
|
"LSMinimumSystemVersion": "14.0",
|
||||||
|
"NSHighResolutionCapable": True,
|
||||||
|
},
|
||||||
|
"includes": [
|
||||||
|
"bot",
|
||||||
|
"brain",
|
||||||
|
"voice_manager",
|
||||||
|
"claude_llm",
|
||||||
|
"apple_llm",
|
||||||
|
"apple_stt",
|
||||||
|
"apple_tts",
|
||||||
|
"speech_analyzer_stt",
|
||||||
|
"echo_guard",
|
||||||
|
"global_hotkey",
|
||||||
|
"push_to_talk",
|
||||||
|
"journal",
|
||||||
|
"memory_tools",
|
||||||
|
"transcript_repair",
|
||||||
|
"vocabulary",
|
||||||
|
"spoken_text",
|
||||||
|
"sounddevice_transport",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(
|
||||||
|
app=APP,
|
||||||
|
name="VoiceAgent",
|
||||||
|
data_files=DATA_FILES,
|
||||||
|
options={"py2app": OPTIONS},
|
||||||
|
setup_requires=["py2app"],
|
||||||
|
)
|
||||||
+20
-1
@@ -33,6 +33,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -45,7 +46,25 @@ from pipecat.services.stt_service import SegmentedSTTService
|
|||||||
from pipecat.transcriptions.language import Language
|
from pipecat.transcriptions.language import Language
|
||||||
from pipecat.utils.time import time_now_iso8601
|
from pipecat.utils.time import time_now_iso8601
|
||||||
|
|
||||||
HELPER = Path(__file__).parent / "swift" / "speech-helper"
|
def get_helper_path(binary_name: str) -> Path:
|
||||||
|
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||||
|
p1 = Path(sys._MEIPASS) / binary_name
|
||||||
|
if p1.exists():
|
||||||
|
return p1
|
||||||
|
p2 = Path(sys._MEIPASS) / "swift" / binary_name
|
||||||
|
if p2.exists():
|
||||||
|
return p2
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
res_dir = Path(sys.executable).parent.parent / "Resources"
|
||||||
|
p1 = res_dir / binary_name
|
||||||
|
if p1.exists():
|
||||||
|
return p1
|
||||||
|
p2 = res_dir / "swift" / binary_name
|
||||||
|
if p2.exists():
|
||||||
|
return p2
|
||||||
|
return Path(__file__).parent / "swift" / binary_name
|
||||||
|
|
||||||
|
HELPER = get_helper_path("speech-helper")
|
||||||
|
|
||||||
# Generous: the helper may be downloading the on-device model on first use.
|
# Generous: the helper may be downloading the on-device model on first use.
|
||||||
_FIRST_RUN_TIMEOUT = 300.0
|
_FIRST_RUN_TIMEOUT = 300.0
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct VoiceAgentLauncher {
|
||||||
|
static func main() {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let projDir = "/Users/adolforeyna/Projects/VoiceAgent1"
|
||||||
|
|
||||||
|
let bundleResPath = Bundle.main.resourcePath ?? ""
|
||||||
|
let bundledSrcPath = "\(bundleResPath)/src"
|
||||||
|
|
||||||
|
let workDir = fileManager.fileExists(atPath: projDir) ? projDir : bundledSrcPath
|
||||||
|
let pythonBin = "\(projDir)/.venv/bin/python"
|
||||||
|
let fallbackPython = "/usr/bin/python3"
|
||||||
|
let targetPython = fileManager.fileExists(atPath: pythonBin) ? pythonBin : fallbackPython
|
||||||
|
let targetScript = "\(workDir)/app_main.py"
|
||||||
|
|
||||||
|
setenv("SSL_CERT_FILE", "/etc/ssl/cert.pem", 1)
|
||||||
|
setenv("REQUESTS_CA_BUNDLE", "/etc/ssl/cert.pem", 1)
|
||||||
|
|
||||||
|
let logDir = fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Logs/VoiceAgent")
|
||||||
|
try? fileManager.createDirectory(at: logDir, withIntermediateDirectories: true)
|
||||||
|
let logFile = logDir.appendingPathComponent("voiceagent.log")
|
||||||
|
|
||||||
|
if !fileManager.fileExists(atPath: logFile.path) {
|
||||||
|
fileManager.createFile(atPath: logFile.path, contents: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: targetPython)
|
||||||
|
process.arguments = [targetScript]
|
||||||
|
process.currentDirectoryURL = URL(fileURLWithPath: workDir)
|
||||||
|
|
||||||
|
if let logHandle = try? FileHandle(forWritingTo: logFile) {
|
||||||
|
logHandle.seekToEndOfFile()
|
||||||
|
process.standardOutput = logHandle
|
||||||
|
process.standardError = logHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
} catch {
|
||||||
|
print("Failed to run VoiceAgent: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user