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

3.4 KiB

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.

"""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:

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 for a complete working example.