Native on every platform: how MediaFind ships for Mac, Windows, and Linux
MediaFind started as a Mac-only app. Getting to native desktop installers on all three platforms required untangling three separate bugs — one per OS — and rethinking what "native" means for a Python ML app wrapped in a Tauri shell.
For a long time, the download page was simple: one button, one .dmg, one platform. That was a deliberate choice — Mac-first let us move fast, focus on the native app experience, and ship features without worrying about three operating systems at once. But "Mac-only" left a lot of people out. Video editors, researchers, and archivists on Windows and Linux were running MediaFind in a browser pointed at a terminal process, which is fine for power users and not fine for anyone else.
The goal was a real installer on every platform. Not a Docker container, not a Python package you pip-install — a double-click installer that puts a native app in the right place, launches it from the dock or start menu, and feels like software. Here is how we got there.
The architecture: engine + shell
MediaFind is two things stitched together: a Python ML engine that indexes your media, runs models, and serves a JSON API; and a native desktop shell that wraps the web UI in a proper window with menus, a dock icon, and OS integration. These two layers have different distribution needs, which is why they are packaged separately.
The engine is PyInstaller-frozen: a self-contained binary that ships every Python dependency, every ML library data file, and a bundled static ffmpeg — no Python installation required on the user's machine. The shell is a Tauri app: a small Rust binary that manages the window, menus, and OS chrome. On first launch, the shell starts the sidecar and opens the webview. From the user's perspective, it is one application.
This split has a useful property: the Python engine is platform-agnostic by design. If you can freeze a Python binary for a given OS, you have an engine that works there. The challenge was that for the first eighteen months, we only ever tried freezing it for macOS.
The first tagged build that worked on all three
The CI workflow triggers on version tags. When we pushed v0.1.5, it built the PyInstaller server binary on a macOS, Ubuntu, and Windows matrix, then published the Ubuntu and Windows artifacts to a GitHub release. That sounds simple. The reality was that every tagged build from v0.1.0 through v0.1.4 had failed on all three platforms. There were three bugs, each hidden behind the last.
Bug 1: the typing backport (all platforms)
resemblyzer — the library that computes speaker embeddings for voice diarization — has a transitive dependency on the obsolete typing package from PyPI. This package predates Python 3.5, exists only for compatibility with ancient code, and in modern Python it shadows the standard library's typing module. PyInstaller sees the import, finds the backport, and then fails to build because the real typing module is gone.
The fix is to uninstall the backport before freezing. Our build script now does exactly that: pip uninstall -y typing before invoking PyInstaller. It re-installs itself during pip install of the next package that depends on it, so the step has to run late in the build — right before the freeze — but it reliably clears the conflict.
Bug 2: Linux disk overflow
PyInstaller's --onefile mode assembles everything into a single executable. On Linux, the default PyPI torch wheel includes CUDA: roughly 2.5 GB of GPU libraries that MediaFind does not use. Trying to objcopy a 2.5 GB blob into a single binary on a GitHub Actions runner — which has around 14 GB of free space shared across the entire build job — hits the disk wall.
The fix is to install the CPU-only torch wheel before anything else:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
That drops the torch footprint from ~2.5 GB to ~250 MB. The macOS and Windows PyPI wheels are already CPU-only, so this step only matters on Linux — but on Linux it is the difference between a build that finishes and one that dies with No space left on device partway through linking.
Bug 3: Windows help output crash
MediaFind's CLI has a --help flag. The help text contains a → glyph to label a path step. On Windows, the default console code page is cp1252, which does not include that character. Python's attempt to print it raises a UnicodeEncodeError before the app ever starts. From a user's perspective: they double-click the installer, the app opens, and immediately crashes on the very first output.
The fix is a one-time UTF-8 reconfiguration in the entry point:
import sys
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
This runs before any output is produced. The hasattr guard handles the frozen app case where stdout is None (no console attached). After this, every glyph in the app's output renders correctly regardless of the system locale.
The Tauri shell and what "native" actually means
Getting the engine to build on three platforms was a prerequisite. The second layer of the problem was making the experience feel native — not just "runs on Windows" but "feels like a Windows app." The Tauri shell is where that work lives.
The core of the shell is small: a Rust binary that reads a config for the sidecar path, spawns the engine process, waits for it to be ready (polling the /health endpoint), then opens a WebviewWindow pointing at localhost. The web UI inside the window is the same HTML, CSS, and JavaScript that works in a browser — no platform-specific UI code at all. Tauri provides the native window chrome, and the web UI fills it.
A few things make this feel genuinely native rather than just "a browser window in a box":
- The sidecar is named by Rust target triple. When Tauri bundles the Python binary as a sidecar, it appends the target triple to the filename (e.g.
mediafind-x86_64-pc-windows-msvc.exe). The Rust code references the sidecar by the triple-suffixed name at compile time. This means the same Tauri source correctly finds the right binary on each platform without any runtime configuration. - The webview is the platform's own engine. On macOS it is WebKit; on Windows it is WebView2 (Chromium-based); on Linux it is WebKitGTK. Each is the native rendering engine for that OS, not a bundled Electron-style Chromium.
- Menus are declared in Rust. File, Edit, and the application menu are wired at the Tauri layer, so they follow OS conventions: keyboard shortcuts, system fonts, the right menu bar location. The web UI doesn't handle any of that.
The installer formats
Different platforms have different conventions for what an "installer" looks like, and deviating from those conventions is a fast way to make users distrust your software.
- macOS: a signed and notarized
.dmgcontaining a.appbundle. Gatekeeper will block anything that isn't notarized, so there is no shortcut here. The signing and notarization step runs locally (not in CI) because it requires a Developer ID certificate and an Apple notarization profile stored in the build machine's keychain. The.dmgdrops the app into/Applicationsvia a symlink, which is what macOS users expect. - Windows: an NSIS
.exeinstaller, generated by Tauri's built-in NSIS bundler. It creates a Start Menu entry, handles uninstallation cleanly, and is what Windows users reach for. The binary is currently unsigned — a code-signing certificate is the next step for Windows — but it runs fine once the user clicks through the SmartScreen warning. - Linux: both an
.AppImage(portable, no installation needed, runs on any distribution with FUSE) and a.deb(integrates with apt, creates a desktop entry, installs to/opt). The two formats serve different users: AppImage for people who want to try it without touching their system; deb for people who want it integrated.
The release pipeline
Because macOS signing is owner-gated (the certificate lives on one machine), the release process is split in two. CI handles Linux and Windows: push a version tag, the matrix build runs, and the artifacts publish to a GitHub release. macOS is built locally with a dedicated script that handles the signing, packaging, notarization, and stapling sequence, then the .dmg is manually uploaded alongside the CI artifacts.
The finished release then mirrors to a separate public repository. The primary repository is private; GitHub releases on a private repo require authentication to download, which makes the URL useless for a website download button. The public mirror holds the same assets with public read access, and the site's config.js points each platform's download button at the right asset there.
The site now shows three download buttons: macOS (signed, primary), Windows (beta), and Linux (beta). Each links to the format most appropriate for that platform. The macOS button has always been there; the Windows and Linux buttons are new. That's what v0.1.7 shipped.
What "beta" means here
The Windows and Linux installers are labeled beta for two concrete reasons, not as a hedge. First, the Windows binary is unsigned, which means SmartScreen will warn on first run. Second, the Linux build is tested on Ubuntu 22.04 — other distributions, especially those with older WebKitGTK or different glibc versions, have not been validated. Neither of these is a fundamental problem; both are addressable with a code-signing certificate (Windows) and broader distribution testing (Linux). The Mac build has neither limitation, which is why it is not labeled beta.
The engine itself — the indexing, search, ML, and everything under the hood — is identical on all three platforms. The beta label is about the installer and integration story, not the feature set.
What is next
The three-platform installer story unlocked a set of follow-on work that wasn't practical before:
- Windows code signing. An EV certificate or a Microsoft-trusted certificate eliminates the SmartScreen warning and is the right next step for a production Windows release.
- Native macOS chrome. The Tauri shell has a branch that implements a proper overlay titlebar (the translucent macOS window bar with inline traffic lights, no separate title bar row) and a full native menu bar with Cmd+, for Settings and the standard View/Window submenus. This work is code-complete but blocked on a Tauri/tao version bump for macOS 26 Tahoe compatibility — the current stack doesn't apply the overlay titlebar on Tahoe. Once the stack is upgraded, that branch merges and the macOS app gets the last layer of native polish.
- Linux distribution breadth. Testing and packaging for Fedora, Arch, and other distributions beyond the Debian/Ubuntu family.
- Auto-update. Tauri has a built-in updater; wiring it to the release pipeline means users get updates without manually downloading a new installer.
The goal throughout has been the same as the Mac experience: you download an installer, run it, and MediaFind is on your computer. No Python environment, no terminal, no configuration. Everything runs locally, all models stay on your machine, and search is fast because nothing is going to a server. That experience is now available on all three platforms.
Download for your platform
Native installers for macOS, Windows, and Linux — all on-device, all private.
Download MediaFind