Build Hook¶
The primary way to use Smelt is as a Hatchling build hook. Once configured, every pip install / build of your project automatically compiles the native extensions and entrypoints you declared - no manual step required, and no extra system dependency (Smelt uses Zig as its compiler, which ships as a standalone toolchain through the ziglang PyPI package).
Minimal setup¶
Add smelt to your build requirements, select hatchling as the build backend, and declare the smelt build hook:
[build-system]
requires = ["hatchling", "smelt"]
build-backend = "hatchling.build"
[project]
name = "my-project"
version = "0.1.0"
dependencies = ["click"]
[project.scripts]
my-cli = "my_project.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/my_project"]
[tool.hatch.build.hooks.smelt]
packages_location = { "my_project" = "src/my_project" }
[[tool.hatch.build.hooks.smelt.mypyc_modules]]
import_path = "my_project.fib"
[[tool.hatch.build.hooks.smelt.nuitka_modules]]
import_path = "my_project.cli"
[[tool.hatch.build.hooks.smelt.c_extensions]]
import_path = "my_project.hello"
sources = ["src/my_project/hello.c"]
[[tool.hatch.build.hooks.smelt.zig_modules]]
import_path = "my_project.zighello"
name = "zighello"
folder = "src/my_project/zighello"
[[tool.hatch.build.hooks.smelt.cython_modules]]
import_path = "my_project.fib_cython"
source = "src/my_project/fib_cython.pyx"
Every module is compiled to a native .so/.pyd sitting right next to its pure-Python counterpart, which it then shadows - so the rest of your codebase, tooling, and imports don't need to change.
Configuration reference¶
All of the following keys live under [tool.hatch.build.hooks.smelt].
| Key | Type | Description |
|---|---|---|
packages_location |
dict[str, str] |
Maps a package's top-level import name to the folder it lives in (e.g. {"my_project" = "src/my_project"}). Used to resolve import_paths declared below to actual files on disk. |
mypyc_modules |
list of tables | Modules to compile with mypyc. Each entry has an import_path and an optional source (path override) and extras. |
cython_modules |
list of tables | Modules to compile with Cython. Each entry has an import_path, an optional source (defaults to a .py/.pyx file matching the import path) and extras. |
nuitka_modules |
list of tables | Modules to compile with Nuitka, as thin per-module extensions sharing one runtime library. See Nuitka internals for why this needs special handling. Each entry has an import_path, an optional source, and extras. |
c_extensions |
list of tables | Handwritten C/C++ extensions, compiled with Zig's C compiler (zig cc). Each entry has an import_path, a sources list, and extras. |
zig_modules |
list of tables | Handwritten Zig extensions, built with zig build. Each entry has an import_path, a name (the library name Zig produces), a folder (containing a build.zig), optional flags passed to zig build, and extras. |
entrypoints |
table of tables | Customizes the standalone-binary build for one entrypoint (see CLI). Keyed by the script name from [project.scripts], or by an explicit module.path/module.path:func. Every [project.scripts] entry is picked up automatically with default options; declaring it here only lets you override those options (include-modules, include-package, include-package-data, extra_flags) or declare an additional entrypoint that isn't in [project.scripts]. |
auto_mode |
"off" | "package" | "all" |
Instead of listing modules by hand, walk the package tree and propose every .py file as a compile target (see limitation below). |
backend_priority_order |
list of "nuitka"/"mypyc"/"cython" |
Fallback order used to pick a backend for modules discovered by auto_mode. Defaults to ["nuitka"]. |
platforms |
list of str | Restricts the hook to run only on the listed host platforms; the build is skipped (not failed) on any other platform. |
debug |
bool | Enables verbose logging from the build hook. Also settable via the SMELT_DEBUG environment variable. |
report_path |
str | If set, writes a build report to this path once the hook completes. Also settable via the SMELT_REPORT environment variable. |
extras-gated modules¶
Any of the module tables above (mypyc_modules, cython_modules, nuitka_modules, c_extensions, zig_modules) accepts an extras list. If any of the listed [project.optional-dependencies] extras isn't installed in the current environment, the build hook silently skips that module instead of failing - handy for optional native acceleration that shouldn't block a plain install:
[[tool.hatch.build.hooks.smelt.mypyc_modules]]
import_path = "my_project.fast_path"
extras = ["speedups"]
auto_mode and packages¶
auto_mode ("package"/"all") currently never proposes a package's own __init__.py, at any depth - only its concrete submodules. This isn't an oversight: see Nuitka internals for why whole-package compilation doesn't fit Smelt's per-module model yet.
Standalone [tool.smelt] config¶
If you don't use Hatchling, the same schema can be declared under [tool.smelt] instead of [tool.hatch.build.hooks.smelt]. This form isn't wired into any build backend automatically - it's read by the CLI (smelt build-extensions, smelt build-standalone-binary, ...) so you can drive the build yourself, e.g. from a Makefile or CI step. The two forms are mutually exclusive: Smelt raises an error if both are present in the same pyproject.toml.