Install
$ agentstack add skill-litestar-org-litestar-skills-litestar-build ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
litestar-build
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](../litestar-deployment/SKILL.md) — build is about producing artifacts, deployment is about running them.
The Core Idea: One Wheel, Self-Contained
A Litestar wheel is the single source of truth for a release. It contains:
- Python code (
src/py/app/orapp/) - SQL migrations, Jinja templates, INI configs
- The built Vite/Bun frontend bundle (JS, CSS, HTML, images)
- Email templates rendered from React/MJX to static HTML
Once produced, that wheel can be:
pip installed into a container (litestar-deployment).- Wrapped in a PyApp binary (
dist/,dist/app-x86_64-linux-gnu) for zero-dep distribution. - Uploaded to PyPI or a private index.
All three paths assume the wheel is already complete — no bun run build happens at deploy/install time.
Why bundle assets into the wheel (and not serve from a CDN)
| 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 [litestar-fullstack-inertia](#example-projects) and [litestar-fullstack](#example-projects) all bundle.
Why litestar-vite configs look the way they do in reference apps
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 (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"),
),
)
Advanced reference pattern — same approach: Vite and the offline-report build write to src/py//server/public/ and src/py//domain/web/static/reports/offline/, both under the 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.
Quick Reference
| Topic | Reference | Key Commands | | --- | --- | --- | | Wheel build + asset bundling | [references/wheel-assets.md](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](references/pyapp-simple.md) | uv run hatch build --target binary | | PyApp — advanced (offline + custom install dir) | [references/pyapp-advanced.md](references/pyapp-advanced.md) | tools/bundler.py build, cargo zigbuild | | GitHub Actions CI (test matrix) | [references/github-ci.md](references/github-ci.md) | astral-sh/setup-uv@v7, oven-sh/setup-bun@v2, composite actions | | GitHub Actions release | [references/github-release.md](references/github-release.md) | matrix onefiles, cargo-zigbuild, gh release create | | Upgrading Python / PyApp | [references/upgrading.md](references/upgrading.md) | Files to edit in sync |
Canonical Makefile Build Graph
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.
The two-variant story
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
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//server/public, src/py//domain/web/static/reports/offline, etc.). Because they're all inside the package, a single uv build --wheel captures everything.
Workflow
Step 1: Point Vite output inside the Python package
Open vite.config.ts. Set build.outDir to an absolute path inside your Python package (src/py//... or /...). Set litestar({ bundleDir, hotFile }) to the same path. Do not let Vite default to ./dist/.
Step 2: Choose a Hatchling bundling strategy
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): 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](references/wheel-assets.md) for full config.
Step 3: Wire Makefile targets
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.
Step 4: Add PyApp (if shipping a binary)
Decide which flavor:
- Simple: add
[tool.hatch.build.targets.binary]to pyproject.toml and runuv run hatch build --target binary. Good when end-users have PyPI access. See [pyapp-simple.md](references/pyapp-simple.md). - Advanced: write a
tools/bundler.pythat pre-installs deps into apython-build-standalonearchive, patches PyApp'ssrc/app.rsfor a custom install dir, then runscargo zigbuild. Good for air-gapped distribution or bespoke install locations. See [pyapp-advanced.md](references/pyapp-advanced.md).
Step 5: Add GitHub Actions CI
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](references/github-ci.md).
For larger projects, factor setup-python and setup-node into .github/actions/ composite actions.
Step 6: Add release workflow
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](references/github-release.md).
Guardrails
- Vite/bun output must land inside a Python package directory. Otherwise Hatchling drops it. Set
build.outDirandlitestar({ bundleDir })to an absolute path undersrc/py//or/. uv buildruns last. Assets, licenses, templates, OpenAPI TypeGen all run beforeuv build --wheel. Hatchling can't build Vite itself.- Pick one bundling strategy.
force-includeorignore-vcs = true, not both. Mixing them causes duplicate-file warnings and unpredictable wheel contents. - PyApp envs are build-time, not runtime.
PYAPP_PROJECT_NAME,PYAPP_PYTHON_VERSION,PYAPP_DISTRIBUTION_EMBEDare consumed whencargo buildcompiles PyApp — not when the resulting binary runs. Setting them at runtime does nothing. - PyApp version upgrades touch multiple files.
pyproject.toml,build-onefile-package.sh,.github/workflows/release.yml,tools/bundler.py. See [upgrading.md](references/upgrading.md). cargo-zigbuildfor portable glibc. Plaincargo buildon a modern Linux runner produces binaries that fail on older distros (glibc too new). Usecargo zigbuild --target x86_64-unknown-linux-gnu.2.17to link against glibc 2.17 (CentOS 7-era). Required for broad compatibility.- Static-link native deps in PyApp. Set
BZIP2_SYS_STATIC=1andLZMA_API_STATIC=1beforecargo zigbuild, or patchCargo.tomlto addfeatures = ["static"]. Otherwise the onefile fails to load on systems without matchinglibbz2.so/liblzma.so. - Pin
uvandbunversions in CI. Use exact pinned versions (e.g.,UV_VERSION=0.11.6andBUN_INSTALL_VERSION=bun-v1.3.12). Drift in either breaks reproducible builds. - Create placeholder asset dirs in CI. Hatchling's wheel target fails if
app/domain/web/publicorsrc/py/app/server/static/webdoesn't exist at wheel-build time. CI jobs that don't build the frontend (lint, mypy, pyright) still needmkdir -pbeforeuv sync. - Never commit built frontend output. Keep
bundle_dirpaths in.gitignore. CI rebuilds them on every run. Reason: JS builds are non-deterministic across machines and cause noisy diffs. - Coverage on one Python version only. Multiple versions uploading the same
coverage.xmlsilently stomp each other. Pin it to one version in your matrix (if: matrix.python-version == '3.12'). - Disk cleanup on self-hosted runners. GitHub's
ubuntu-latesthas ~30GB free; building wheels + PyApp + Docker images can blow past that. Aggressive cleanup before the build job is routine.
Validation Checkpoint
Before claiming "the wheel builds":
- [ ]
make build-wheelsucceeds in a clean checkout (aftermake install) - [ ]
unzip -l dist/*.whl | grep -E '\.(js|css|html)$'shows the built frontend - [ ] The wheel installs cleanly (
uv pip install dist/*.whlin a fresh venv) - [ ]
python -c "import app; app.run()"(or equivalent) serves assets with no extra steps - [ ]
.gitignoreexcludes the built asset directory - [ ] Vite's
build.outDiris an absolute path inside a Python package dir - [ ] Hatchling config uses exactly one of
force-includeORignore-vcs = true
Before claiming "the PyApp binary works":
- [ ]
dist/ --helpruns on the build machine - [ ] The binary is ≥ 50 MB (much smaller means it's not embedding Python)
- [ ] On Linux,
ldd dist/shows ≤ libc / libm / libpthread (nolibbz2, noliblzma) - [ ] A network-isolated
docker run --rm --network=none ghcr.io/.../distroless -- --helpsucceeds (proves no runtime PyPI fetches) - [ ] The install dir (
~/./runtime/or similar) is created on first run and re-used on second run
Before claiming "CI works":
- [ ] Python matrix covers minimum + stable + latest (e.g., 3.11, 3.12, 3.13)
- [ ]
make build-wheelruns in CI and the resulting wheel is uploaded as an artifact - [ ] Pre-commit / ruff / mypy / pyright / slotscheck run on every PR
- [ ] Release workflow is gated on CI (
needs: [lint, test]) - [ ] A tag push produces wheel + onefiles + GitHub release in one run
Example Projects
Everything in this skill is distilled from three production projects. Read these for the full picture:
- litestar-fullstack-inertia — monolithic
app/layout, Inertia.js + React 19,force-includebundling,hatch build --target binaryfor 4-platform PyApp. - litestar-fullstack — nested
src/py/app/+src/js/web/layout, React + TanStack Router SPA,ignore-vcs = truebundling, React Email templates.
Official References
- — PyApp documentation (all
PYAPP_*env vars) - — PyApp source (patch target:
src/app.rs) - — Hatchling build config
- — Hatch binary builder (simple PyApp)
- —
uv buildreference - — Portable Python archives
- — cargo-zigbuild for portable glibc
- — Bun install and lockfile
Cross-References
- [litestar-deployment](../litestar-deployment/SKILL.md) — runtime deployment (Dockerfiles, K8s, Railway, Cloud Run, systemd) that consumes the artifacts this skill produces
- [litestar-vite](../litestar-vite/SKILL.md) — Vite plugin config (asset pipeline details, TypeGen, HMR)
- [litestar-granian](../litestar-granian/SKILL.md) — Granian ASGI server (what the wheel's entry-point starts)
- [litestar settings](../litestar-settings/references/settings.md) — env-driven
@dataclasssettings that work both in-wheel and as a PyApp binary
Shared Styleguide Baseline
- [General Principles](../litestar-styleguide/references/general.md)
- [Python](../litestar-styleguide/references/python.md)
- [CI/CD](../litestar-styleguide/references/ci-cd.md)
- [Litestar](../litestar-styleguide/references/litestar.md)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: litestar-org
- Source: litestar-org/litestar-skills
- License: MIT
- Homepage: https://github.com/litestar-org/litestar-skills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.