Precompiled Wheels¶
pytanga compiles C++ binding modules on demand via JIT compilation. For
users without a C++ compiler, precompiled extension modules (.so / .pyd)
can be bundled into platform-specific wheels. This document covers the tooling
and workflows for building, cleaning, and uploading precompiled wheels.
Tools¶
| Script | Purpose |
|---|---|
tools/build-precompiled.py |
Compile the seven common algebra bindings and harvest compiled extension files into precompiled/ |
tools/clean-precompiled.py |
Remove precompiled artifacts to restore a pure Python wheel build |
tools/fix-wheel-tag.py |
Rewrite the wheel filename and metadata with the correct platform tag |
tools/upload-pypi.py |
Inspect a wheel and upload it to PyPI via twine |
All scripts are run from the repo root with uv run.
Workflow: Build a Precompiled Wheel¶
Use this when you want to ship a wheel that works without a C++ compiler.
# 1. Ensure dev dependencies are installed (includes [compile] extras)
uv sync --group dev
# 2. Compile and bundle the seven common algebra bindings
uv run python tools/build-precompiled.py
# 3. Build the wheel with correct platform tag (one-step via helper script)
# Linux/macOS:
uv run bash tools/build-precompiled-wheel.sh
# Windows:
uv run powershell tools/build-precompiled-wheel.ps1
# 4. Verify
uv run python tools/upload-pypi.py --check
Output: A platform-specific wheel (e.g. cp312-cp312-manylinux_2_35_x86_64
for Linux, cp312-cp312-win_amd64 for Windows)
with precompiled extension modules for seven bindings (float64 unless noted):
(2,0) E2, (3,0) E3/P2, (4,0) P3, (4,8) N2/PGA2, (5,16) N3/PGA3,
plus (3,0) E3 modular (int64) and (10,0) G(10,0) sparse (int64).
The helper scripts tools/build-precompiled-wheel.sh (Linux/macOS) and
tools/build-precompiled-wheel.ps1 (Windows) build to a temp directory,
fix the platform tag via fix-wheel-tag.py, move the result to dist/,
and clean up — all in one step.
About the Wheel Tag¶
hatchling produces py3-none-any because the extension files in precompiled/
are not recognized as Python extension modules. build-precompiled-wheel.sh
runs fix-wheel-tag.py automatically after the build.
For official manylinux compliance, use auditwheel in a manylinux Docker
container instead. The fix-wheel-tag script is intended for local and
ad-hoc builds.
What build-precompiled.py Does¶
- Calls
pytanga.codegen._cache.get_or_build()for each of the seven bindings. This triggers the same JIT compilation pipeline that users would normally pay on first import — but once, at build time. - Walks the cache directory (
~/.cache/pytanga/) to find the compiled extension for each module name. - Copies each compiled extension into
precompiled/at the repo root. - Writes
precompiled/manifest.jsonrecording platform, ABI, compiler info, and a cache key (SHA-256 hash of all C++ headers and codegen Python files) for each algebra.
The precompiled/ directory is included in the wheel via hatchling's
[tool.hatch.build.targets.wheel.force-include] in pyproject.toml.
Workflow: Build a Pure Python Wheel¶
Use this when you want to build a py3-none-any wheel with no precompiled
binaries (the default, portable wheel).
# 1. Remove precompiled artifacts (if any)
uv run python tools/clean-precompiled.py
# 2. Build the wheel
uv build --wheel
Output: A py3-none-any wheel containing only Python code and C++
headers (_ga_src/). Users need the [compile] extra to use this wheel.
clean-precompiled.py removes everything from precompiled/ except
.gitkeep, so the directory stays tracked by git but the next wheel
build produces a pure wheel.
Workflow: Upload to PyPI¶
# 1. Build and tag the wheel (precompiled or pure, see above)
uv build --wheel
uv run python tools/fix-wheel-tag.py # only needed for precompiled wheels
# 2. Inspect the wheel
uv run python tools/upload-pypi.py --check
# 3. Upload (interactive — prompts for confirmation)
uv run python tools/upload-pypi.py
# Or upload to Test PyPI first:
uv run python tools/upload-pypi.py --repo testpypi
upload-pypi.py inspects the newest wheel in dist/, reports whether it
is a pure wheel or contains precompiled extension modules, and prompts for
confirmation before running twine upload.
Prerequisites: twine must be installed and PyPI credentials configured.
Add twine via uv add --group dev twine, then set up a PyPI API token in
~/.pypirc or via TWINE_USERNAME / TWINE_PASSWORD environment variables.
Runtime Behavior¶
When a user imports an algebra (e.g. pytanga.Algebra(3, 0)), the cache
layer runs this priority chain:
- Cache hit — compiled extension already exists in
~/.cache/pytanga/<key>/→ load instantly. - Precompiled check — Look for a matching entry in
pytanga/precompiled/manifest.json. If found, compare the cache key. If the key matches (C++ headers haven't changed since the wheel was built), copy the extension into the cache and load it. If the key doesn't match, skip the precompiled binary. - JIT compilation — Generate, compile, and load the binding. This requires
the
[compile]extras (cmake, ninja, pybind11) and a C++ compiler.
The key-based check means a precompiled binary is automatically invalidated if the C++ headers change between releases. Users with a compiler experience self-healing behavior: the outdated precompiled binary is skipped, and JIT compilation produces a correct binding.
Directory Layout¶
precompiled/ # ← at repo root, git-ignored except .gitkeep
├── .gitkeep # keep directory tracked when empty
├── manifest.json # metadata: platform, ABI, keys, algebra list
├── binding_dim2_sig0_float64.cpython-312-x86_64-linux-gnu.so
├── binding_dim3_sig0_float64.cpython-312-x86_64-linux-gnu.so
├── binding_dim4_sig0_float64.cpython-312-x86_64-linux-gnu.so
├── binding_dim4_sig8_float64.cpython-312-x86_64-linux-gnu.so
├── binding_dim5_sig16_float64.cpython-312-x86_64-linux-gnu.so
├── binding_dim3_sig0_int64.cpython-312-x86_64-linux-gnu.so
└── binding_dim10_sig0_int64.cpython-312-x86_64-linux-gnu.so
The extension filenames include Python ABI tags (e.g. .cpython-312-x86_64-linux-gnu.so
on Linux, .cp312-win_amd64.pyd on Windows), which pip uses to determine platform
compatibility. The runtime loader matches files by module name prefix, ignoring
the ABI suffix.
The pyproject.toml force-include maps precompiled/ into the wheel at
pytanga/precompiled/. In a dev checkout, the loader also checks the
repo-root precompiled/ directory (for testing before building the wheel).
Manifest Format¶
{
"version": 1,
"platform": "Linux-7.0.0-28-generic-x86_64-with-glibc2.39",
"python_abi": "cp312",
"compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0",
"algebras": {
"binding_dim3_sig0_float64": {
"dim": 3,
"sig": 0,
"dtype": "float64",
"key": "1cbb8d45fab331a3cb84bf02ae01424b1494608970cb58c4577890d298615f9c"
}
}
}
The key field is the SHA-256 digest from pytanga.codegen._cache._make_key().
It covers the algebra identity (dim, sig, dtype), all C++ headers under
TANGA_SOURCE, the binding template _template.cpp, and all Python files
in the codegen/ package.
Related Files¶
| File | Role |
|---|---|
py/pytanga/codegen/_cache.py |
_load_precompiled() runtime loader |
py/pytanga/codegen/_build.py |
JIT compilation pipeline (build_and_load()) |
pyproject.toml |
force-include for precompiled/ and _ga_src/ |
.gitignore |
Ignores precompiled/* except .gitkeep |
dev/todos/precompiled-wheels.md |
Full implementation plan |