mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-30 12:15:56 +00:00
Add error recovery hooks and PyInstaller frozen build recipes (#1388)
* Add error recovery hooks and PyInstaller frozen build recipes * fixed datas to data
This commit is contained in:
96
cookbook/copilot-sdk/python/pyinstaller-frozen-build.md
Normal file
96
cookbook/copilot-sdk/python/pyinstaller-frozen-build.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user