Skip to content
cccskills
FreePublished on GitHub

litestar-build

Auto-activate for uv build, hatch build, PyApp, PYAPP_*, wheel assets, GitHub release matrices, cargo-zigbuild, or python-build-standalone. Not for runtime deployment.

The English summary is being prepared. Showing the original description.

Works withGitHub

Run score Not tested

See how to install

Files included (7)

  • SKILL.md20.2 KB
  • references/github-ci.md14.5 KB
  • references/github-release.md15.6 KB
  • references/pyapp-advanced.md19.5 KB
  • references/pyapp-simple.md6.7 KB
  • references/upgrading.md10.2 KB
  • references/wheel-assets.md13.6 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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 — 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/ or app/)
  • 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:

  1. pip installed into a container (litestar-deployment).
  2. Wrapped in a PyApp binary (dist/<app>, dist/app-x86_64-linux-gnu) for zero-dep distribution.
  3. 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)

PropertyBundled wheelExternal CDN
Deploy artifacts1 (.whl or binary)2+ (wheel + CDN upload)
Version alignmentAtomic — API and UI lock-stepEasy to skew; rollback is painful
PyApp onefileRequired — the binary embeds the wheelNot possible — binary can't fetch CDN URLs at install time
Offline/air-gappedWorksDoesn't
Dev server startupInstant (files on disk next to package)Fine
Frontend-only deploysRebuild + redeploy wheelPush 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 and litestar-fullstack 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 — in litestar-vite v0.30+, Python ViteConfig(paths=PathConfig(bundle_dir=...)) is the single source of truth and writes .litestar.json on startup/CLI invocation so litestar({ input: [...] }) in vite.config.ts inherits bundleDir, hotFile, and assetUrl automatically (and litestar assets build exports OpenAPI/routes metadata, runs TypeGenConfig codegen once with LITESTAR_VITE_SKIP_BUILD_TYPEGEN=1, installs node_modules if missing, and runs the Vite build):

from pathlib import Path
from litestar_vite.config import PathConfig, ViteConfig


def get_vite_config(base_dir: Path) -> ViteConfig:
    """Configure litestar-vite to emit assets inside the app/ Python package."""
    return ViteConfig(
        paths=PathConfig(
            root=base_dir.parent,
            bundle_dir=Path("app/domain/web/public"),
            resource_dir=Path("resources"),
        ),
    )

Advanced reference pattern — same approach: Vite and the offline-report build write to src/py/<app>/server/public/ and src/py/<app>/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.

Code Style Rules

  • Emit all compiled frontend bundles (PathConfig.bundle_dir / Vite build.outDir) inside a Python package directory included by Hatchling (app/... or src/py/<pkg>/...).
  • Choose exactly one Hatchling asset inclusion strategy ([tool.hatch.build.targets.wheel.force-include] OR ignore-vcs = true) — never combine both.
  • Guard custom Hatchling build hooks (BuildHookInterface.initialize) with if version == "editable": (warning to sys.stderr, creating the placeholder directory, and returning early when compiled assets do not exist yet) so uv sync succeeds before frontend assets are built, and exclude tools/build from mypy and pyright.
  • Run asset compilation, static template generation, and license collection before uv build --wheel --clear.
  • Treat PYAPP_* configuration variables (PYAPP_PROJECT_PATH, PYAPP_PYTHON_VERSION, PYAPP_DISTRIBUTION_EMBED, PYAPP_DISTRIBUTION_VARIANT_GIL, PYAPP_SKIP_INSTALL) as compile-time inputs to cargo build / cargo zigbuild; at runtime, use PYAPP_INSTALL_DIR_<PROJECT_NAME> (where <PROJECT_NAME> is the uppercased PYAPP_PROJECT_NAME with - replaced by _) to override the extraction directory and inspect os.getenv("PYAPP") ("1" or path when built with PYAPP_PASS_LOCATION=1) to detect PyApp execution.
  • Use cargo zigbuild --release --target <arch>-unknown-linux-gnu.2.17 with BZIP2_SYS_STATIC=1 and LZMA_API_STATIC=1 when building portable Linux PyApp binaries, and ensure both $(uv tool dir --bin) ($HOME/.local/bin) and $HOME/.cargo/bin are on PATH / $GITHUB_PATH so the build never silently falls back to cargo build --release and binds to the runner's host glibc.

Quick Reference

TopicReferenceKey Commands
Wheel build + asset bundlingreferences/wheel-assets.mduv build --wheel --clear, [tool.hatch.build.targets.wheel.force-include], ignore-vcs = true
PyApp — simple (hatch-binary)references/pyapp-simple.mduv run hatch build --target binary
PyApp — advanced (offline + custom install dir)references/pyapp-advanced.mdtools/bundler.py build, cargo zigbuild
GitHub Actions CI (test matrix)references/github-ci.mdastral-sh/setup-uv@v7, oven-sh/setup-bun@v2, composite actions
GitHub Actions releasereferences/github-release.mdmatrix onefiles, cargo-zigbuild, gh release create
Upgrading Python / PyAppreferences/upgrading.mdFiles 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 --clear

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 --clear

Each js-build-* target emits into a distinct subdirectory of the Python package (src/py/<app>/server/public, src/py/<app>/domain/web/static/reports/offline, etc.). Because they're all inside the package, a single uv build --wheel --clear captures everything.

<workflow>

Workflow

Step 1: Point Vite output inside the Python package

Configure ViteConfig(paths=PathConfig(bundle_dir=Path("app/domain/web/public"))) in Python so .litestar.json bridges the target directory to litestar-vite-plugin. If vite.config.ts overrides build.outDir directly (monorepo or standalone Vite build), set build.outDir and litestar({ bundleDir, hotFile }) to an absolute path inside your Python package (src/py/<pkg>/... or <pkg>/...). 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.
  • Optional custom Hatchling hook: If you add a BuildHookInterface subclass (e.g., tools/build/hatch_build.py) to validate or build frontend assets during uv build, guard initialize(version, build_data) with if version == "editable": (warning to sys.stderr, creating the placeholder directory, and returning early instead of raising ValueError or running a frontend build) so uv sync succeeds before frontend assets exist, and exclude tools/build from mypy and pyright.

See 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. Pass --clear to uv build --wheel --clear so stale wheels in dist/ never get picked up by downstream packaging steps.

Step 4: Add PyApp (if shipping a binary)

Decide which flavor:

  • Simple: add [tool.hatch.build.targets.binary] to pyproject.toml and run uv run hatch build --target binary (runtime install location can still be overridden via PYAPP_INSTALL_DIR_<PROJECT_NAME>). Good when end-users have PyPI access. See pyapp-simple.md.
  • Advanced: write a tools/bundler.py that pre-installs deps into a python-build-standalone archive, patches PyApp's src/app.rs for a custom compiled-in default install dir, then runs cargo zigbuild. Good for air-gapped distribution or bespoke default install locations. See 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.

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.

</workflow> <guardrails>

Guardrails

  • Vite/bun output must land inside a Python package directory. Otherwise Hatchling drops it. Set PathConfig.bundle_dir (and any explicit Vite build.outDir) to a path under src/py/<pkg>/ or <pkg>/.
  • uv build runs last. Assets, licenses, templates, OpenAPI TypeGen all run before uv build --wheel --clear.
  • Pick one bundling strategy. force-include or ignore-vcs = true, not both. Mixing them causes duplicate-file warnings and unpredictable wheel contents.
  • Skip or warn-only on editable installs in custom Hatchling hooks. uv sync installs the project in editable mode (version == "editable") before uv run app assets build can execute. If a custom BuildHookInterface runs in pyproject.toml (e.g., path = "tools/build/hatch_build.py"), short-circuit when version == "editable" (warning to sys.stderr, creating the placeholder directory, and returning early instead of raising ValueError or running bun run build) and exclude tools/build from mypy/pyright unless hatchling is in the typecheck environment.
  • PyApp PYAPP_* config vars are build-time (except PYAPP_INSTALL_DIR_<PROJECT_NAME>). PYAPP_PROJECT_NAME, PYAPP_PYTHON_VERSION, PYAPP_DISTRIBUTION_EMBED, PYAPP_DISTRIBUTION_VARIANT_GIL are consumed when cargo build compiles PyApp — not when the resulting binary runs. At runtime, the compiled binary reads PYAPP_INSTALL_DIR_<PROJECT_NAME> (uppercased PYAPP_PROJECT_NAME with - replaced by _; keep set for self remove / self restore) to override the extraction directory, and sets PYAPP=1 (or the executable path when PYAPP_PASS_LOCATION=1) inside the spawned Python process.
  • PyApp version upgrades touch multiple files. pyproject.toml, build-onefile-package.sh, .github/workflows/release.yml, tools/bundler.py. See upgrading.md.
  • cargo-zigbuild for portable glibc — and verify it is on PATH. Plain cargo build on a modern Linux runner links against the runner's host glibc (e.g. GLIBC_2.39 on Ubuntu 24.04), producing binaries that pass host smoke tests (./dist/app --help) but fail inside older distros or Debian Bookworm / distroless containers (GLIBC_2.36) with version 'GLIBC_2.39' not found. Use cargo zigbuild --target x86_64-unknown-linux-gnu.2.17 to link against glibc 2.17, add $(uv tool dir --bin) ($HOME/.local/bin) and $HOME/.cargo/bin to PATH / $GITHUB_PATH after uv tool install cargo-zigbuild, and never allow Linux CI builds to fall back silently to cargo build --release.
  • Static-link native deps in PyApp. Set BZIP2_SYS_STATIC=1 and LZMA_API_STATIC=1 before cargo zigbuild, or patch Cargo.toml to add features = ["static"]. (In PyApp v0.29.0, bzip2 = "0.6.0" uses the pure-Rust libbz2-rs-sys backend by default and has no xz/lzma dependency, so these flags are a harmless belt-and-suspenders safeguard that remain critical when pinning older PyApp releases.)
  • Pin uv and bun versions in CI. Use exact pinned versions (e.g., UV_VERSION=0.11.6 and BUN_INSTALL_VERSION=bun-v1.3.12). Drift in either breaks reproducible builds.
  • Create placeholder asset dirs in CI. Hatchling's force-include target fails if app/domain/web/public or src/py/app/server/static/web doesn't exist at wheel/editable-build time. CI jobs that don't build the frontend (lint, mypy, pyright) still need mkdir -p <asset-dir> before uv sync.
  • Never commit built frontend output or .litestar.json. Keep bundle_dir paths (including hot and hot.ssr) and .litestar.json 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.xml silently stomp each other. Pin it to one version in your matrix (if: matrix.python-version == '3.12').
  • Disk cleanup on CI runners (including PyApp scratch and runtime extraction dirs). GitHub's ubuntu-latest has ~30GB free; bundling large native wheels creates dist/.scratch (~1.2 GB) and dist/python-dist.tar.gz (~280 MB), and running ./dist/<app> --help on the runner host extracts ~750 MB into ~/.<app>/runtime. Remove dist/.scratch, dist/python-dist.tar.gz, and the extracted host runtime directory before running downstream docker/build-push-action steps.
</guardrails> <validation>

Validation Checkpoint

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 frontend
  • The wheel installs cleanly (uv 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 directory
  • PathConfig.bundle_dir (and any explicit Vite build.outDir) resolves inside a Python package dir
  • Hatchling config uses exactly one of force-include OR ignore-vcs = true

Before claiming "the PyApp binary works":

  • dist/<app> --help runs on the build machine
  • The binary is ≥ 50 MB (much smaller means it's not embedding Python)
  • On Linux, ldd dist/<app> shows ≤ libc / libm / libpthread (no libbz2, no liblzma)
  • A network-isolated docker run --rm --network=none gcr.io/distroless/cc-debian12:nonroot /opt/dist/<app> --help succeeds (proves no runtime PyPI fetches)
  • The install dir (~/.<app>/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-wheel runs 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
</validation> <example>

Example

[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]

[tool.hatch.build.targets.wheel]
packages = ["app"]

[tool.hatch.build.targets.wheel.force-include]
"app/domain/web/public" = "app/domain/web/public"

[tool.hatch.build.targets.binary]
pyapp-version = "0.29.0"
python-version = "3.13"
scripts = ["app"]

[tool.hatch.build.targets.binary.env]
PYAPP_DISTRIBUTION_EMBED = "1"
PYAPP_FULL_ISOLATION = "1"
PYAPP_UV_ENABLED = "1"

Example Projects

  • litestar-fullstack-inertia — monolithic app/ layout, Inertia.js + React 19, force-include bundling, hatch build --target binary for 4-platform PyApp.
  • litestar-fullstack — nested src/py/app/ + src/js/web/ layout, React + TanStack Router SPA, ignore-vcs = true bundling, React Email templates.
</example>

References Index

Official References

Cross-References

  • litestar-deployment — runtime deployment (Dockerfiles, K8s, Railway, Cloud Run, systemd) that consumes the artifacts this skill produces
  • litestar-vite — Vite plugin config (asset pipeline details, TypeGen, HMR)
  • litestar-granian — Granian ASGI server (what the wheel's entry-point starts)
  • litestar settings — env-driven @dataclass settings that work both in-wheel and as a PyApp binary

Shared Styleguide Baseline

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Auto-activate for advanced_alchemy, alembic/, SQLAlchemyAsyncRepositoryService, SQLAlchemyAsyncConfig, repository_type, service_class, filters, or storage. Not for raw SQLAlchemy — use sqlspec.

The English summary is being prepared. Showing the original description.

litestar-org/litestar-skills16Updated Oct 20, 2026

Auto-activate for Litestar, Controller, Router, @get/@post, FromPath, MsgspecDTO, OpenAPIConfig, Provide, FromDishka, Guard, ASGIMiddleware, HTTPException, ChannelsPlugin, or WebSocket. Not for standalone libs.

The English summary is being prepared. Showing the original description.

litestar-org/litestar-skills16Updated Oct 20, 2026

Auto-activate for Google ADK, LlmAgent, Runner, SQLSpecSessionService, google-genai, AgentRuntime, AgentGroup, SpecTree, DynamicWorkflow, FunctionTool, or SSE agent chats. Not for offline ML training.

The English summary is being prepared. Showing the original description.

litestar-org/litestar-skills16Updated Oct 20, 2026

Auto-activate for litestar_autowire, AutowirePlugin, AutowireConfig, domain_packages, AutowireIntegration, AutowireLoader, or clear_autowire_cache. Not for manual Router composition — use explicit routes.

The English summary is being prepared. Showing the original description.

litestar-org/litestar-skills16Updated Oct 20, 2026

Auto-activate for Dockerfile, compose, Railway, Cloud Run, GKE, systemd, Kubernetes, Terraform, deploy scripts, or granian/litestar run at runtime. Not for packaging artifacts.

The English summary is being prepared. Showing the original description.

litestar-org/litestar-skills16Updated Oct 20, 2026

Auto-activate for litestar_email, EmailPlugin, EmailConfig, EmailService, EmailMessage, SMTPConfig, ResendConfig, SendGridConfig, MailgunConfig, SESConfig. Not for marketing APIs — use vendor SDKs.

The English summary is being prepared. Showing the original description.

litestar-org/litestar-skills16Updated Oct 20, 2026

See all skills by litestar-org

Report a problem with this skill