Files
awesome-copilot/cookbook/copilot-sdk/python/pyinstaller-frozen-build.md
Tilak Patel 5f69546969 Add error recovery hooks and PyInstaller frozen build recipes (#1388)
* Add error recovery hooks and PyInstaller frozen build recipes

* fixed datas to data
2026-04-28 12:08:25 +10:00

97 lines
3.4 KiB
Markdown

# Deploying Copilot SDK Apps with PyInstaller
Package a Copilot SDK application into a standalone executable using PyInstaller (or Nuitka).
## Problem
When you freeze a Python SDK application with PyInstaller, three things break:
1. **CLI binary resolution** — The SDK locates its CLI via `__file__`, which points inside the PYZ archive in a frozen build.
2. **SSL certificates** — On macOS, the frozen app can't find system CA certs, so the CLI subprocess fails TLS handshakes.
3. **Execute permissions** — The bundled CLI binary may lose its `+x` bit when extracted from the archive.
## Solution
Resolve the CLI path by searching both the SDK's normal location and PyInstaller's `_MEIPASS` temp directory. Fix SSL by injecting `certifi`'s CA bundle into the environment. Restore execute permissions on Unix before launching.
```python
"""Frozen-build compatibility for Copilot SDK applications."""
import os, sys
from pathlib import Path
from copilot import CopilotClient, SubprocessConfig
def resolve_cli_path() -> str | None:
"""Find the Copilot CLI binary in a frozen build."""
candidates = []
binary = "copilot.exe" if sys.platform == "win32" else "copilot"
# 1. SDK's normal resolution
try:
import copilot as pkg
candidates.append(Path(pkg.__file__).parent / "bin" / binary)
except Exception:
pass
# 2. PyInstaller _MEIPASS fallback
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
meipass = Path(sys._MEIPASS)
candidates.append(meipass / "copilot" / "bin" / binary)
candidates.append(meipass.parent / "copilot" / "bin" / binary)
for c in candidates:
if c.exists():
if sys.platform != "win32" and not os.access(str(c), os.X_OK):
os.chmod(str(c), c.stat().st_mode | 0o755)
return str(c)
return None
def ensure_ssl_certs():
"""Set SSL env vars for the CLI subprocess (macOS frozen builds)."""
if os.environ.get("SSL_CERT_FILE"):
return
try:
import certifi
ca = certifi.where()
if Path(ca).is_file():
os.environ["SSL_CERT_FILE"] = ca
os.environ["REQUESTS_CA_BUNDLE"] = ca
os.environ.setdefault("NODE_EXTRA_CA_CERTS", ca)
except ImportError:
pass # CLI will use platform defaults
async def create_frozen_client():
"""Create a CopilotClient that works in both normal and frozen builds."""
ensure_ssl_certs()
kwargs = {"log_level": "info", "use_stdio": True}
if getattr(sys, "frozen", False):
cli = resolve_cli_path()
if cli:
kwargs["cli_path"] = cli
client = CopilotClient(SubprocessConfig(**kwargs), auto_start=True)
await client.start()
return client
```
## PyInstaller Spec
Include the SDK's binary directory in your `.spec` file so PyInstaller bundles it:
```python
from PyInstaller.utils.hooks import collect_data_files
data += collect_data_files('copilot', include_py_files=False)
```
## Tips
- **Test the frozen build on a clean machine** — `_MEIPASS` extraction behaves differently than your dev environment.
- **Pin `certifi`** in your requirements so the CA bundle is always available.
- **Nuitka** uses a different extraction model (`--include-package-data=copilot`), but the same `resolve_cli_path` logic works.
## Runnable Example
See [`recipe/pyinstaller_frozen_build.py`](recipe/pyinstaller_frozen_build.py) for a complete working example.