Packaging

It works on my Mac: shipping ML in a frozen app without silent failures

The scariest bugs aren't crashes — those announce themselves. The scary ones are features that quietly do nothing. Packaging a Python ML stack into a double-clickable Mac app produced a whole family of them. Here's the bug class, and the test that put an end to it.

In development, MediaFind runs from source with a full Python environment — every library importable, everything works. Users don't get that. They get a frozen bundle: a PyInstaller-built .app with a snapshot of exactly the dependencies the packager decided to include. The gap between “my dev machine” and “the bundle” is a narrow, treacherous place — and it's where features went to die without a sound.

The bug class: lazy imports meet tree-shaking

PyInstaller bundles what it can see. It traces imports statically, and to keep the app from ballooning it leaves out what it thinks is unused. That collides with two completely reasonable habits:

Put them together in a frozen app and you get the trap: the heavy dep gets tree-shaken out, the lazy import fails at runtime, the try/except catches it, and the feature falls back to a stub that returns empty — no crash, no error dialog, just a feature that mysteriously produces nothing.

DEV · from source import (lazy) insightface real backend ✓ faces found FROZEN · the .app dep tree-shaken (not bundled) import fails → try/except stub backend returns [] 0 faces no error · looks broken
Same code, two outcomes. The frozen app drops the dependency, the lazy import fails, the fallback stub runs — and the feature returns empty with no error. That's what makes the bug so hard to spot.

The casualties

This one pattern took down four flagship features, each looking like an unrelated product bug:

FeatureDropped dependencySilent symptom
Semantic searchsentence-transformers / sklearnHashing fallback → wrong vector size → “No matches” for everything
Face recognitioninsightfaceStub backend → 0 faces detected
Speaker diarizationresemblyzerWeak fallback embedder → everyone collapses into one speaker
Web downloaderyt-dlp“Couldn't find any videos” on sites that clearly have them

Every one of them worked perfectly in development. Every one of them was dead in the shipped app. And because the failure was silent, each got triaged as “the model is bad” or “the feature is broken” before anyone realized the real cause lived in the packaging spec.

The immediate fix: bundle what's actually needed

The direct remedy is to tell the packager about the deps it can't see — collect_all() the fragile libraries in the PyInstaller spec so their modules and their data files come along. Alongside that, a few related packaging decisions matter:

The durable fix: a test that fails when a dep is dropped

Fixing four spec entries is easy. Making sure a fifth never silently breaks the same way — that's the real win. So there's a unit test that parses the PyInstaller specs themselves: it walks the spec files' syntax tree and asserts that every known-fragile dependency (the embeddings stack, insightface, resemblyzer, the ANN library, yt-dlp, and the rest) is still collected.

Why this is the highest-leverage test in the repo: it converts a release-day landmine — “we cut a build, signed it, notarized it, and only then discovered faces are dead” — into a two-second failure on every pull request. The bug class can't reach users anymore, because it can't get past CI. The cheapest place to catch a packaging bug is before you ever package.

The lesson, beyond MediaFind

Two principles fall out of this. First: silent fallbacks are a liability — if a real backend is missing, say so loudly rather than quietly degrading, because a noisy failure gets fixed and a silent one ships. Second: guard the boundary you can't see across. The frozen app is a different world from your dev shell, and the only way to trust it is to test the thing that builds it. Once we did, “works on my machine” stopped being a punchline.

Download the build that actually ships

Every feature bundled, verified in CI, and notarized for macOS.

Download for macOS