Move Along, Python

This content was originally published on the Tiles Blog: tiles.run/blog/move-along-python .

We have been working on Tiles, a private and secure AI assistant for everyday use.

The Python Problem

Right now, we have a polyglot architecture where the control pane and CLI are written in Rust, while local model inference runs through a Python server as a daemon. Ideally, when we ship Tiles, we should also ship the required artifacts needed to run Python on the user's system.

Since Python servers cannot be compiled into a single standalone binary, the user's system must have a Python runtime available. More importantly, it must be a deterministic Python runtime so that the server runs exactly on the version developers expect.

In earlier releases of Tiles (before 0.4.0), we packed the server files into the final release tarball. During installation, we extracted them to the user's system, downloaded uv (a Python package manager), installed Python 3.13 if it was not already present, and then ran the server as a daemon.

This approach had several issues:

Downloading development-related tools such as uv onto the user's system

Relying on uv at install time to manage dependencies and run the server

Increased chances of failures due to dependency or runtime non-determinism

Requiring internet access to download all of the above tools

Lack of a fully deterministic runtime across operating systems

One of the long-term goals of Tiles is complete portability. The previous approach did not align with that vision.

Portable Runtimes

To address these issues, we decided to ship the runtime along with the release tarball. We are now using venvstacks by LM Studio to achieve this.

Venvstacks allows us to build a layered Python environment with three layers:

Runtimes

Frameworks

Applications

Similar to Docker, each layer depends on the layer beneath it. A change in any layer requires rebuilding that layer and the ones above it.

All components within a layer share the layers beneath them. For example, every framework uses the same Python runtime defined in the runtimes layer. Likewise, if we have multiple servers in the applications layer and both depend on MLX, they will share the exact deterministic MLX dependency defined in frameworks, as well as the same Python runtime defined in runtimes.

We define everything inside a venvstacks.toml file. Here is the venvstacks.toml used in Tiles.

Because we pin dependency versions in the TOML file, we eliminate non-determinism.

Internally, venvstacks uses uv to manage dependencies. Once the TOML file is defined, we run:

venvstacks lock venvstacks.toml

This resolves dependencies and creates the necessary folders, lock files, and metadata for each layer.

Next:

venvstacks build venvstacks.toml

This builds the Python runtime and environments based on the lock files.

Finally:

venvstacks publish venvstacks.toml

This produces reproducible tarballs for each layer. These tarballs can be unpacked on external systems and run directly.

We bundle the venvstack runtime artifacts into the final installer using this bundler script . During installation, this installer script extracts the venvstack tarballs into a deterministic directory.

Our Rust CLI can then predictably start the Python server using:

stack_export_prod/app-server/bin/python -m server.main

What's Next

We tested version 0.4.0 on clean macOS virtual machines to verify portability, and the approach worked well.

For now, we are focusing only on macOS. When we expand support to other operating systems, we will revisit this setup and adapt it as needed.

Packaging the runtime and dependencies increases the size of the final installer. We are exploring ways to reduce that footprint.

We also observed that changes in lock files can produce redundant application tarballs when running the publish command. More details are tracked in this issue .

Overall, we are satisfied with this approach for now.

Till then, keep on tiling.