uv-powered Python zipapp builder.
Project description
moonlit
moonlit is a CLI that bundles a uv-managed Python project (and optionally a uv workspace) into a single self-contained zipapp per PEP 441. The produced .pyz ships every transitive dependency from uv.lock; on the end user's machine it extracts to a per-build cache on first run, then dispatches the configured entry point.
It is similar to LinkedIn's shiv, with two differences:
- Built on
uv, notpip. Resolution is done byuv export --frozenagainstuv.lock; staging is done byuv pip install --target(no virtualenv); the target's wheel is built byuv build --wheel. - uv workspaces are first-class.
--package <member>selects a workspace target; transitive workspace deps are bundled automatically viauv build --all-packages.
Status
Pre-release (0.x). API and CLI surface are stabilizing toward 1.0; the produced .pyz runtime contract is pinned by the design specs under specs/.
Install
moonlit is not yet on PyPI. From source:
git clone <repo> moonlit
cd moonlit
uv sync
uv run moonlit --help
Quick start
In a uv-managed project (pyproject.toml + uv.lock):
uv run moonlit build -e myapp.cli:main -o myapp.pyz
python ./myapp.pyz
In a uv workspace:
uv run moonlit build --package shouter -e shouter.cli:main -o shouter.pyz
python ./shouter.pyz
The produced .pyz is self-contained: uv.lock's entire dependency closure is bundled, plus the target's own wheel. On first run it extracts site-packages/ to a per-build cache (%LOCALAPPDATA%\moonlit on Windows, ~/.moonlit on POSIX); subsequent runs hit the cache directly without unpacking.
How it works
The moonlit build pipeline runs ten ordered steps:
- Workspace detection. Parse
[tool.uv.workspace]frompyproject.toml; expandmembersglobs; applyexclude; PEP 503 normalize. - Target selection. Workspace +
--package <name>→ matched member; non-workspace → project root. uv exportwrites a frozen requirements file fromuv.lock.uv pip install --targetstages the third-party closure under a tempsite-packages/(no venv).uv build --wheel(or--all-packagesfor workspaces) builds the target's wheel.- Each produced wheel is installed into the same
site-packages/. - If
-c <script>was used, the entry point is resolved from staged*.dist-info/entry_points.txt. build_idis computed: a sorted SHA-256 over every file insite-packages/(excluding__pycache__/.pyc).- Archive assembly: shebang prefix, then a
ZIP_DEFLATEDarchive containingsite-packages/, the stdlib-only_bootstrap/package, the rendered__main__.py, andenv.json. - Atomic finalize: temp-then-rename to the output path; POSIX
chmod 0o755.
At runtime, the _bootstrap package reads env.json, derives a cache key from (name, build_id), takes either the lock-free fast path (cache hit) or the locked slow path (extract under O_CREAT|O_EXCL sentinel, atomic-replace into the cache via os.rename + os.replace), then calls site.addsitedir() and invokes the entry point.
Documentation
docs/index.md |
Overview and at-a-glance example. |
docs/getting-started.md |
Walkthroughs for single-package projects and uv workspaces. |
docs/cli-reference.md |
Every flag, every exit code, preflight order, stdout/stderr semantics. |
docs/runtime.md |
What runs inside the .pyz: cache layout, env vars, runtime exit codes, stale-lock recovery. |
The docs are built with zensical:
uv sync --group docs
uv run zensical serve # http://127.0.0.1:8000
Project layout
src/moonlit/
├── cli.py # Click frontend
├── builder.py # 10-step build pipeline orchestrator
├── resolver.py # the only module that calls `uv` subprocesses
├── workspace.py # parses [tool.uv.workspace]
├── hashing.py # deterministic build_id
├── errors.py # MoonlitError hierarchy with stable exit codes
├── _templates/main_py.tmpl
└── _bootstrap/ # SHIPPED INSIDE EVERY .pyz — stdlib-only
├── __init__.py # bootstrap() orchestrator
├── environment.py # env.json validation
├── extract.py # D4 atomic-replace, D14 fast path
├── locking.py # O_CREAT|O_EXCL sentinel lock
├── runner.py # site.addsitedir, entry-point resolution
└── errors.py
specs/ # Foundational design contracts (start here for hacking)
tests/
├── unit/ # 451 unit tests
└── e2e/ # 25 contract tests via subprocess
Status of features
| Feature | State |
|---|---|
| Build single-package projects | done |
| Build uv workspaces with transitive deps | done |
--entry-point (-e) and --console-script (-c) |
done |
Atomic .pyz output (temp-then-rename) |
done |
| First-run extraction + cache-hit fast path | done |
Cross-platform caching (%LOCALAPPDATA%, ~/.moonlit) |
done |
MOONLIT_ROOT, MOONLIT_FORCE_EXTRACT, MOONLIT_ENTRY_POINT, MOONLIT_DEBUG |
done |
--reproducible builds (zeroed mtimes, sorted entries) |
deferred to v0.2 |
--compile-pyc |
deferred to v0.2 |
--no-modify integrity verification |
deferred to v0.2 |
--windows-exe native launcher |
deferred to v0.2 |
Real flock/msvcrt locking |
deferred to v0.2 |
moonlit info <pyz> subcommand |
deferred to v0.2 |
Contributing
Read CLAUDE.md for development conventions and specs/ for the design contracts (start with specs/README.md, then specs/00-architecture.md).
uv run pytest # 476 tests, ~10s with e2e
uv run pytest tests/unit # unit only, <2s
uv run ruff format --check . # format check (CI gate)
uv run ruff check . # lints (CI gate)
uv run zensical build --strict # docs build (CI gate)
The e2e suite (tests/e2e/) shells out to real uv and produces real .pyz files; it skips automatically if uv is not on PATH.
CI runs all four gates on every pull request via .github/workflows/ci.yml.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file moonlit-0.1.0.tar.gz.
File metadata
- Download URL: moonlit-0.1.0.tar.gz
- Upload date:
- Size: 116.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2deb87d35518b24c2b51eee4d90c2e85523d78b9752a7ee8eb4797a2a1e025f1
|
|
| MD5 |
58ee8847384383eaec9510ff301e7ec5
|
|
| BLAKE2b-256 |
f9bae64f086ed3cba277167cadbff3fb6156aa0c26f8286d46b79ec79849d595
|
File details
Details for the file moonlit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: moonlit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f579c6f611a2aa945b2590da1c8584c0f6e245d401b2e422e9f616e720864ccc
|
|
| MD5 |
5f30bf37d3a2a79bfdbe6f88b84f832d
|
|
| BLAKE2b-256 |
e802769e122c2c4a944c92bb4c9eabffede7168700ef5f14dab7bcabc464e169
|