Auto-activate for advanced_alchemy, alembic/, SQLAlchemyAsyncRepositoryService, SQLAlchemyAsyncConfig, repository_type, service_class, filters, or storage. Not for raw SQLAlchemy — use sqlspec.
日本語の概要は準備中です。原文の説明を表示しています。
Auto-activate for uv build, hatch build, PyApp, PYAPP_*, wheel assets, GitHub release matrices, cargo-zigbuild, or python-build-standalone. Not for runtime deployment.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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/<app>, 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 litestar-fullstack-inertia and litestar-fullstack 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 (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 and hotFile automatically:
from pathlib import Path
from litestar_vite.config import PathConfig, ViteConfig
def get_vite_config() -> 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.
PathConfig.bundle_dir / Vite build.outDir) inside a Python package directory included by Hatchling (app/... or src/py/<pkg>/...).[tool.hatch.build.targets.wheel.force-include] OR ignore-vcs = true) — never combine both.BuildHookInterface.initialize) with if version == "editable": return so uv sync succeeds before frontend assets are built, and exclude tools/build from mypy and pyright.uv build --wheel --clear.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, inspect os.getenv("PYAPP") ("1" or path when running under Pass-Through) to detect PyApp execution.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.| Topic | Reference | Key Commands |
|---|---|---|
| Wheel build + asset bundling | references/wheel-assets.md | uv build --wheel --clear, [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 --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.
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.
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/.
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.BuildHookInterface subclass (e.g., tools/build/hatch_build.py) to run bun run build during uv build, guard initialize(version, build_data) with if version == "editable": return so uv sync does not trigger a frontend build during editable installs, and exclude tools/build from mypy and pyright.See references/wheel-assets.md for full config.
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.
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.
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.
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.force-include or ignore-vcs = true, not both. Mixing them causes duplicate-file warnings and unpredictable wheel contents.BuildHookInterface runs in pyproject.toml (e.g., path = "tools/build/hatch_build.py"), short-circuit when version == "editable" and exclude tools/build from mypy/pyright unless hatchling is in the typecheck environment.PYAPP_* config vars are build-time, not runtime. 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, PyApp sets PYAPP=1 (or the executable path when PYAPP_PASS_LOCATION=1) inside the spawned Python process.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. 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.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.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 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 directoryPathConfig.bundle_dir (and any explicit Vite build.outDir) resolves 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 gcr.io/distroless/cc-debian12:nonroot /opt/dist/<app> --help succeeds (proves no runtime PyPI fetches)~/.<app>/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])[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"
app/ layout, Inertia.js + React 19, force-include bundling, hatch build --target binary for 4-platform PyApp.src/py/app/ + src/js/web/ layout, React + TanStack Router SPA, ignore-vcs = true bundling, React Email templates.PYAPP_* env vars)src/app.rs)uv build reference@dataclass settings that work both in-wheel and as a PyApp binaryまだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Auto-activate for advanced_alchemy, alembic/, SQLAlchemyAsyncRepositoryService, SQLAlchemyAsyncConfig, repository_type, service_class, filters, or storage. Not for raw SQLAlchemy — use sqlspec.
日本語の概要は準備中です。原文の説明を表示しています。
Auto-activate for Litestar, Controller, Router, @get/@post, FromPath, MsgspecDTO, OpenAPIConfig, Provide, FromDishka, Guard, ASGIMiddleware, HTTPException, ChannelsPlugin, or WebSocket. Not for standalone libs.
日本語の概要は準備中です。原文の説明を表示しています。
Auto-activate for Google ADK, LlmAgent, Runner, SQLSpecSessionService, google-genai, AgentRuntime, SpecTree, DynamicWorkflow, FunctionTool, or SSE agent chats. Not for offline ML training.
日本語の概要は準備中です。原文の説明を表示しています。
Auto-activate for litestar_autowire, AutowirePlugin, AutowireConfig, domain_packages, AutowireIntegration, AutowireLoader, or clear_autowire_cache. Not for manual Router composition — use explicit routes.
日本語の概要は準備中です。原文の説明を表示しています。
Auto-activate for Dockerfile, compose, Railway, Cloud Run, GKE, systemd, Kubernetes, Terraform, deploy scripts, or granian/litestar run at runtime. Not for packaging artifacts.
日本語の概要は準備中です。原文の説明を表示しています。
Auto-activate for litestar_email, EmailPlugin, EmailConfig, EmailService, EmailMessage, SMTPConfig, ResendConfig, SendGridConfig, MailgunConfig, SESConfig. Not for marketing APIs — use vendor SDKs.
日本語の概要は準備中です。原文の説明を表示しています。