mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-30 12:15:56 +00:00
97 lines
3.4 KiB
Markdown
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.
|