From litestar-skills
Auto-activate for `uv build`, `hatch build`, pyapp invocations, bundler scripts, Hatchling force-include config, wheel packaging with bundled frontend assets, PyApp onefile binaries, GitHub Actions matrix workflows, `PYAPP_*` env vars, cargo-zigbuild, python-build-standalone. Produces Hatchling build configs that bundle Vite/Bun output into the wheel, Makefile build graphs (`build-assets` → `build-wheel` → `build-onefile`), PyApp hatch-binary targets, advanced PyApp pipelines with custom bundler and install-dir patching, GitHub Actions CI/release workflows with Python matrix + uv + Bun. Use when: building a Litestar wheel with embedded frontend assets, packaging an app as a single-file PyApp binary (standard or offline), customizing PyApp install location, setting up `uv` + `bun` CI, writing release pipelines that produce wheels + onefiles + container images. Not for runtime deployment — see litestar-deployment. Not for Vite plugin authoring — see litestar-vite.
npx claudepluginhub litestar-org/litestar-skills --plugin litestar-skillsThis skill uses the workspace's default tool permissions.
Build-side packaging patterns for Litestar applications: how to produce a **self-contained wheel** that embeds the Vite/Bun frontend, how to wrap that wheel in a **PyApp onefile** binary, and how to wire the whole pipeline into **GitHub Actions** CI and releases.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Builds scalable data pipelines, modern data warehouses, and real-time streaming architectures using Spark, dbt, Airflow, Kafka, and cloud platforms like Snowflake, BigQuery.
Builds production Apache Airflow DAGs with best practices for operators, sensors, testing, and deployment. For data pipelines, workflow orchestration, and batch job scheduling.
Build-side packaging patterns for Litestar applications: how to produce a self-contained wheel that embeds the Vite/Bun frontend, how to wrap that wheel in a PyApp onefile binary, and how to wire the whole pipeline into GitHub Actions CI and releases.
This skill is the counterpart to litestar-deployment — build is about producing artifacts, deployment is about running them.
A Litestar wheel is the single source of truth for a release. It contains:
src/py/app/ or app/)Once produced, that wheel can be:
pip installed into a container (litestar-deployment).dist/dma, dist/app-x86_64-linux-gnu) for zero-dep distribution.All three paths assume the wheel is already complete — no bun run build happens at deploy/install time.
| Property | Bundled wheel | External CDN |
|---|---|---|
| Deploy artifacts | 1 (.whl or binary) | 2+ (wheel + CDN upload) |
| Version alignment | Atomic — API and UI lock-step | Easy to skew; rollback is painful |
| PyApp onefile | Required — the binary embeds the wheel | Not possible — binary can't fetch CDN URLs at install time |
| Offline/air-gapped | Works | Doesn't |
| Dev server startup | Instant (files on disk next to package) | Fine |
| Frontend-only deploys | Rebuild + redeploy wheel | Push to CDN only |
For most Litestar apps that ship as a product (CLIs, internal tools, enterprise installers), bundled-in-wheel is correct. Projects like accelerator, litestar-fullstack-inertia, and litestar-fullstack-spa all bundle.
This is the piece most developers miss. The Vite/litestar-vite configs in the reference apps are deliberately set up so the Vite output lands inside the Python package directory — because that's what makes the wheel pick them up automatically.
litestar-fullstack-spa (src/js/web/vite.config.ts):
export default defineConfig({
build: {
outDir: path.resolve(__dirname, "../../py/app/server/static/web"), // ← inside src/py/app/ (the Python package)
emptyOutDir: true,
},
plugins: [
litestar({
bundleDir: path.resolve(__dirname, "../../py/app/server/static/web"),
hotFile: path.resolve(__dirname, "../../py/app/server/static/web/hot"),
}),
],
})
litestar-fullstack-inertia — the litestar-vite plugin resolves bundle_dir relative to the project root, and Python settings point it at a package-internal path:
# app/lib/settings.py
return ViteConfig(
paths=PathConfig(
root=BASE_DIR.parent,
bundle_dir=Path("app/domain/web/public"), # ← inside app/ (the Python package)
resource_dir=Path("resources"),
),
)
accelerator — same pattern: Vite and the offline-report build write to src/py/dma/server/public/ and src/py/dma/domain/web/static/reports/offline/, both under the dma package root.
Contrast with a naïve vite build that writes to ./dist/ at the repo root: those files are outside the package directory listed in [tool.hatch.build.targets.wheel] packages = [...], so Hatchling silently drops them. The wheel ships without a frontend.
Rule: Vite's outDir and litestar-vite's bundle_dir must point inside one of the Python packages that Hatchling is told to include. Everything else flows from that.
| Topic | Reference | Key Commands |
|---|---|---|
| Wheel build + asset bundling | references/wheel-assets.md | uv build --wheel, [tool.hatch.build.targets.wheel.force-include], ignore-vcs = true |
| PyApp — simple (hatch-binary) | references/pyapp-simple.md | uv run hatch build --target binary |
| PyApp — advanced (offline + custom install dir) | references/pyapp-advanced.md | tools/bundler.py build, cargo zigbuild |
| GitHub Actions CI (test matrix) | references/github-ci.md | astral-sh/setup-uv@v7, oven-sh/setup-bun@v2, composite actions |
| GitHub Actions release | references/github-release.md | matrix onefiles, cargo-zigbuild, gh release create |
| Upgrading Python / PyApp | references/upgrading.md | Files to edit in sync |
Every Litestar app with bundled assets has some variant of this:
.PHONY: install build-assets build-wheel build-onefile
install: ## Install Python + JS deps
@uv sync --all-groups
@cd src/js/web && bun install --frozen-lockfile
build-assets: ## Build frontend into the Python package
@uv run app assets install
@uv run app assets build
build-wheel: build-assets ## Self-contained Python wheel
@uv build --wheel
build-onefile: build-wheel ## Single-file PyApp binary
@./tools/scripts/build-onefile-package.sh
The dependency chain is load-bearing: build-onefile depends on build-wheel, which depends on build-assets. Running them out of order produces an empty or broken artifact.
Real projects have multiple JS build outputs that all need to land in the wheel:
js-build-all: js-build-web js-build-offline-report # accelerator pattern
build-wheel: generate-licenses build-templates js-build-all
@uv build --wheel
Each js-build-* target emits into a distinct subdirectory of the Python package (src/py/dma/server/public, src/py/dma/domain/web/static/reports/offline, etc.). Because they're all inside the package, a single uv build --wheel captures everything.
Open vite.config.ts. Set build.outDir to an absolute path inside your Python package (src/py/<pkg>/... or <pkg>/...). Set litestar({ bundleDir, hotFile }) to the same path. Do not let Vite default to ./dist/.
force-include (inertia): List the built-asset directory explicitly under [tool.hatch.build.targets.wheel.force-include]. Built assets stay .gitignored. Explicit, auditable.ignore-vcs = true (SPA, accelerator): Tell Hatchling to ignore .gitignore. All package files ship. Simpler; requires discipline to keep dev junk out of package dirs.See references/wheel-assets.md for full config.
Create install, build-assets, build-wheel. Make the wheel target depend on the asset target. Add any secondary generators (build-templates, generate-licenses) as additional wheel prerequisites.
Decide which flavor:
[tool.hatch.build.targets.binary] to pyproject.toml and run uv run hatch build --target binary. Good when end-users have PyPI access. See pyapp-simple.md.tools/bundler.py that pre-installs deps into a python-build-standalone archive, patches PyApp's src/app.rs for a custom install dir, then runs cargo zigbuild. Good for air-gapped distribution or bespoke install locations. See pyapp-advanced.md.Start with a reusable test.yml that accepts python-version + coverage inputs. Call it from ci.yml across a matrix. Use astral-sh/setup-uv@v7 and oven-sh/setup-bun@v2. See github-ci.md.
For larger projects, factor setup-python and setup-node into .github/actions/ composite actions (accelerator pattern).
Trigger on v* tags. Run the test matrix first. Then build the wheel once. Then build PyApp onefiles in a per-target matrix (x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, Apple, Windows). Upload to gh release create. See github-release.md.
build.outDir and litestar({ bundleDir }) to an absolute path under src/py/<pkg>/ or <pkg>/.uv build runs last. Assets, licenses, templates, OpenAPI TypeGen all run before uv build --wheel. Hatchling can't build Vite itself.force-include or ignore-vcs = true, not both. Mixing them causes duplicate-file warnings and unpredictable wheel contents.PYAPP_PROJECT_NAME, PYAPP_PYTHON_VERSION, PYAPP_DISTRIBUTION_EMBED are consumed when cargo build compiles PyApp — not when the resulting binary runs. Setting them at runtime does nothing.pyproject.toml, build-onefile-package.sh, .github/workflows/release.yml, tools/bundler.py. See upgrading.md.cargo-zigbuild for portable glibc. Plain cargo build on a modern Linux runner produces binaries that fail on older distros (glibc too new). Use cargo zigbuild --target x86_64-unknown-linux-gnu.2.17 to link against glibc 2.17 (CentOS 7-era). Required for broad compatibility.BZIP2_SYS_STATIC=1 and LZMA_API_STATIC=1 before cargo zigbuild, or patch Cargo.toml to add features = ["static"]. Otherwise the onefile fails to load on systems without matching libbz2.so / liblzma.so.uv and bun versions in CI. accelerator pins UV_VERSION=0.11.6 and BUN_INSTALL_VERSION=bun-v1.3.12. Drift in either breaks reproducible builds.app/domain/web/public or src/py/app/server/static/web doesn't exist at wheel-build time. CI jobs that don't build the frontend (lint, mypy, pyright) still need mkdir -p <asset-dir> before uv sync.bundle_dir paths in .gitignore. CI rebuilds them on every run. Reason: JS builds are non-deterministic across machines and cause noisy diffs.coverage.xml silently stomp each other. Pin it to one version in your matrix (if: matrix.python-version == '3.12').ubuntu-latest has ~30GB free; building wheels + PyApp + Docker images can blow past that. Aggressive cleanup before the build job (see accelerator ci.yml lines 222-249) is routine.Before claiming "the wheel builds":
make build-wheel succeeds in a clean checkout (after make install)unzip -l dist/*.whl | grep -E '\.(js|css|html)$' shows the built frontenduv pip install dist/*.whl in a fresh venv)python -c "import app; app.run()" (or equivalent) serves assets with no extra steps.gitignore excludes the built asset directorybuild.outDir is an absolute path inside a Python package dirforce-include OR ignore-vcs = trueBefore claiming "the PyApp binary works":
dist/<app> --help runs on the build machineldd dist/<app> shows ≤ libc / libm / libpthread (no libbz2, no liblzma)docker run --rm --network=none ghcr.io/.../distroless -- <app> --help succeeds (proves no runtime PyPI fetches)~/.dma/runtime/ or similar) is created on first run and re-used on second runBefore claiming "CI works":
make build-wheel runs in CI and the resulting wheel is uploaded as an artifactneeds: [lint, test])Everything in this skill is distilled from three production projects. Read these for the full picture:
/home/cody/code/litestar/litestar-fullstack-inertia) — monolithic app/ layout, Inertia.js + React 19, force-include bundling, hatch build --target binary for 4-platform PyApp./home/cody/code/litestar/litestar-fullstack-spa) — nested src/py/app/ + src/js/web/ layout, React + TanStack Router SPA, ignore-vcs = true bundling, React Email templates./home/cody/code/g/dma/accelerator) — advanced PyApp pipeline: custom bundler.py, Rust-source patching for custom install dir, cargo-zigbuild for portable glibc, offline-capable onefile, multi-arch distroless containers.PYAPP_* env vars)src/app.rs)uv build reference@dataclass settings that work both in-wheel and as a PyApp binary