# 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" CFBundleExecutable VoiceAgent CFBundleIdentifier com.voiceagent.mac CFBundleName VoiceAgent CFBundleDisplayName VoiceAgent CFBundlePackageType APPL CFBundleShortVersionString 1.0.0 CFBundleVersion 1.0.0 LSMinimumSystemVersion 14.0 NSMicrophoneUsageDescription VoiceAgent requires access to your microphone to listen to your voice commands. NSSpeechRecognitionUsageDescription VoiceAgent uses on-device speech recognition to process your spoken input. NSHighResolutionCapable 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`** |