본문으로 건너뛰기
cccskills
무료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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구GitHub

실행 점수 미검증

설치 방법 보기

포함된 파일(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(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

litestar-org/litestar-skills162026년 10월 20일 업데이트

litestar

무료

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

litestar-org/litestar-skills162026년 10월 20일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

litestar-org/litestar-skills162026년 10월 20일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

litestar-org/litestar-skills162026년 10월 20일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

litestar-org/litestar-skills162026년 10월 20일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

litestar-org/litestar-skills162026년 10월 20일 업데이트

litestar-org의 스킬 모두 보기

이 스킬의 문제 신고