From PyInstaller binaries to portable Python distributions
Architectural decisions & lessons learned from embedding a Python AI agent framework in a cross-platform desktop app
Built with Electron — JavaScript all the way down. Great for cross-platform UI. But JavaScript isn't Python.
Python through and through. A modular AI agent framework that relies deeply on Python's packaging ecosystem.
Starts the Amplifier runtime as a child process, passes a port number via CLI args
Amplifier runtime binds to localhost and signals readiness via stdout
The Node.js backend discovers the port and proxies requests to Amplifier
React frontend receives SSE events and renders agent output in real time
On paper: bundle Amplifier's Python packages into a frozen binary, ship it as an Electron resource, spawn at runtime. The plan was clean. The execution… was not.
The Amplifier agent never loaded. The runtime binary started, then immediately crashed before initializing.
Notice /tmp/_MEI81mChB/ — that's PyInstaller's temporary extraction directory at runtime.
The path points to a .dist-info directory, not a Python package. Something fundamental is broken.
importlib.metadataHow amplifier-core discovers modules at startup:
dist-info/RECORD
Inspects the installed package manifest via importlib.metadata
Looks for .py package entries, skips .dist-info entries
Falls back to using the .dist-info directory itself as the module root
__init__.py
Tries to find a Python package inside a dist-info folder. Fails.
Runtime crashes before any agent loads
PyInstaller freezes Python — converts source to bytecode, strips the filesystem structure. But importlib.metadata relies on that filesystem structure being real.
Amplifier discovers modules via Python entry points and importlib.metadata. You can pip install amplifier-module-provider-anthropic and it just appears — no configuration needed.
The modular, swappable architecture depends on Python packaging being alive and intact — real dist-info directories, real RECORD files, real site-packages paths on disk.
The question shifted: from "how do we freeze Python?" → "how do we ship a real Python installation that users don't have to install themselves?"
A portable Python distribution that finds its standard library relative to itself — no system-wide installation, no PATH dependencies, no conflicts with a user's own Python.
Install Amplifier's packages normally into site-packages. Full metadata, full RECORD files, full entry points. importlib.metadata works perfectly.
Copy the entire directory tree as an Electron extraResource. Spawn by absolute path at runtime.
uv?uv python install downloads exactly python-build-standalone distributionsuv use the standalone Python as baseimportlib.metadata works correctly before bundling. If verification fails, the build fails. Ship only what you've proven works.
Not "works on my machine" — the exact metadata structure used at runtime is validated in CI before packaging.
Recording how many files are in each package's RECORD catches partial installs and truncated metadata.
If any package lacks valid metadata, the build exits non-zero. Users never see the RuntimeError.
| Dimension | PyInstaller Binary | Portable Venv |
|---|---|---|
| Bundle size | ~43 MB compact | ~150–160 MB larger but functional |
| Module discovery | ✗ Broken RECORD paths invalid | ✓ Works full importlib.metadata |
| Upgrade one module | ✗ Full recompile rebuild entire binary | ✓ Replace one package in site-packages |
| Amplifier bundles | ✗ Fragile entry points break | ✓ Full support all bundles load |
| Debugging crashes | ✗ Opaque frozen bytecode, no source | ✓ Real Python files readable source |
| Startup speed | Extracts to /tmp on each launch |
Direct execution, no extraction step |
| CI reproducibility | Compiled binary, hard to inspect | ✓ Verifiable file hashes + metadata check |
The size tradeoff is real — but a broken 43 MB binary is less useful than a working 156 MB one.
When integrating Amplifier across language boundaries, its core abstractions map cleanly to HTTP primitives:
Amplifier's architecture is about mechanisms, not policies — pluggable systems, clear interfaces, composable modules. HTTP is just another boundary to cross. The philosophy travels.
Amplifier's module discovery depends on real dist-info directories and intact RECORD files. Any distribution approach that strips or virtualizes this will break module loading.
python-build-standalone gives you a portable, self-contained Python without PyInstaller's tradeoffs. The extra ~110 MB buys you a fully functional runtime that behaves exactly like development.
Running Amplifier as a child process with an HTTP API cleanly bridges the language gap. Electron speaks HTTP. Amplifier speaks HTTP. No FFI, no shared memory, no complexity.
Mechanisms, not policies — even when your host app is JavaScript. The protocol boundary maps naturally to HTTP. The modularity is preserved. The sidecar is still Amplifier.
"Amplifier's modular design works because it trusts Python's package ecosystem. Work with it, not around it."
github.com/indygreg/python-build-standalone
docs.astral.sh/uv — fast Python tooling in Rust
Technical Case Study · Cross-Platform Desktop Integration · Amplifier AI Agent Framework