facett's Python wheels — built in a container, verified, and served
nornir:class=guide
0.2.0 (2026-10-02): two PyPI packages.
facet-wrapped-webis nowfacett(importfacett;facet_wrapped_webstays as an alias for one release; Streamlit glue infacett.streamlitbehind the[streamlit]extra; wheel only), and the Streamlit demo isfacett-demo(importfacett_demo, commandfacett-demo, live korp data behind[korp]). Recipe rowsfacett-wheel+facett-demo-wheel. Build, check and upload:cargo ops publish-pip,cargo ops verify-demo-install,cargo ops publish-pypi— the owner's steps are.nornir/pypi-publish.md. The sections below keep the road's history; where they namefacet-wrapped-web, readfacett.
facett ships two Python distributions. This document is how they are built today.
| wheel | what it is | built by | artefact |
facet-wrapped-web |
the delivery package: the compiled facett component wasm + a host-agnostic loader page. No rendering code, no UI-framework glue. | cargo xtask wrap-wasm → cargo xtask publish-pip (in a container) |
facet_wrapped_web-0.1.19-py3-none-any.whl + .tar.gz in pkg-facet-wrapped/dist/ |
facett-native |
the headless companion: pyo3 bindings over the SAME component registry — no browser, no wasm, no event loop | maturin (own detached workspace, facett-py/) |
facett_native-0.1.19-cp39-abi3-*.whl |
They are not twins. facet-wrapped-web delivers the interactive canvas; facett-native calls the registry directly from CPython so a host can enumerate components and read each one's state_json. One registry, two front doors.
What this document used to say. It described a
facett-streamlitcrate built bycargo xtask release-streamlit, producingfacett_streamlit-0.1.13. That crate and that verb were both removed. The runbook was not, so its step 2 was a command the binary does not have — someone following it gotunknown commandwith no way to tell whether they had typed it wrong or were reading about a world that had ended. It also ranpython -m buildon the host, which is the exact path whose predecessor forked ~10 000 processes and OOM-killed oden on 2026-07-31.
xtask/tests/no_ghost_paths_or_verbs.rsnow fails if any document instructs the reader to run acargo xtaskverb the dispatcher does not have.
0a. The declaration, and the door this repo opens to it
Everything below is a hand-typed verb, in order, from memory. That order is also declared — facett-build-thing is the Rust impl BuildThing, and .nornir/build-variants.toml is its emission. Ask it what exists:
cargo xtask build --list # every declared row + the exact cargo line
cargo xtask build --variant <name> # build one
cargo run -p facett-build-thing -- emit # re-emit the description after a code change
Seven rows, three groups:
| row | kind | what builds it |
facet-wrapped-wasm |
wasm | cargo xtask wrap-wasm (bindgen + the manifest capture) |
facet-wrapped-web |
package | cargo xtask publish-pip (in nordisk-pip-toolchain) |
facett-demo |
exe | cargo xtask build --variant facett-demo |
facett-demo-wasm |
wasm | cargo xtask demo-wasm → deploy-pages |
nordisk-pip-toolchain |
container | cargo xtask publish-pip builds it first |
nordisk-pip-consumer |
container | cargo xtask verify-install builds it first |
nordisk-streamlit |
container | cargo xtask streamlit / test-demo build it first |
cargo xtask build compiles the cargo rows and REPORTS the other four as skipped. It does not pretend to build a wheel or an image: those need stages cargo does not have (wasm-bindgen, a manifest capture, python -m build inside a pinned container), and those stages are the verbs in the rest of this document. The same declaration is what dwarves buildthing facett --variant <name> reads from the forge — one description, two doors.
What is deliberately NOT declared in the build space: running anything. cargo xtask streamlit starts the demo and cargo xtask test-demo runs its suite; neither is a build. The ORDER of the release chain is declared separately, as a pipeline:
DWARVES_REPO_ROOT=$PWD dwarves run facett.runa.ron
facett.runa.ron is sections 1–3 below written down — bundle → wheel → publish → prove → demo, each stage naming either a declared variant or the xtask verb that already exists, chained by needs in the order the artifacts require. It re-implements nothing. xtask/tests/the_pipeline_names_things_that_exist.rs parses it with the forge's own loader and checks every variant against the declaration and every verb against the dispatcher, so a stage naming something that does not exist fails HERE rather than red on the forge.
0. The law this runbook obeys
Rickard, 2026-09-05: "make build environment so it can build pip packages, ready for release, use a CONTAINER for the horrible python language."
No python runs on the host. Not to build a wheel, not to check one, not to publish one. The host side is pure Rust: it stages bytes, drives podman through draupnir::container (the REST-over-bollard door), copies artefacts out, and reads the zip back. There is no Command::new("podman") and no shell script in the path.
1. facett (+ facett-demo) — build
cargo xtask wrap-wasm # the REAL components → wasm32 + wasm-bindgen + manifest.json
cargo xtask publish-pip # → a wheel, built INSIDE nordisk-pip-toolchain, verified back
publish-pip builds a digest-pinned image (debian@sha256:6788062a…), stages the package into target/pip-build/pkg/ — never into the source tree — and runs python3 -m build --wheel --sdist --no-isolation with --network none.
It then reads the produced bytes back: every member checked against the wheel's own RECORD (sha256 and size), nothing unrecorded riding along, and all four shipped assets present, non-empty, with the wasm actually beginning \0asm. A zero-byte wasm is present, is recorded, hashes correctly, and is not a component.
Measured 2026-09-05 at facet-wrapped 0.1.19, with the GPU lanes on:
| asset | bytes |
facet_wrapped_bg.wasm |
10 078 599 |
facet_wrapped.js |
104 112 |
manifest.json (27 components) |
4 682 |
index.html (the loader; draws nothing) |
9 277 |
2. facett + facett-demo — publish and prove
cargo xtask publish-holger # pack into holger's PEP 503 archive (znippy as a LIBRARY)
holger-server start --config target/pip-build/holger-facett-pip.ron
cargo xtask verify-install # pip install in a container that has never seen this repo
publish-holger extracts the finished archive and compares byte for byte against what went in — a compressor exiting 0 and writing a non-zero file is a state round-trip, not an applied output. The config is rendered from the checkout you are in, so a worktree cannot publish its wheel and then serve the shared tree's stale archive.
verify-install is the one that matters: a real pip, a real PEP 503 index, an image carrying only python3/pip/venv with no source mounted. The verdict is made in Rust, against the wheel that was built — not by the container's exit code, which would be a python script asserting on itself.
3. The demo
cargo xtask streamlit # the Streamlit app, in a container, on the INSTALLED wheel
cargo xtask test-demo # its python suite, in the same container
The wheel is installed at run time from the local index, never baked into the image: an image carrying it could not tell us whether publishing works.
4. facett-native — the pyo3 wheel
facett-py/ is a detached workspace and is deliberately not a member of the root one: it links libpython and must never enter a normal cargo build. Built with maturin. Its version tracks the workspace version, held there by xtask/tests/python_versions_track_the_workspace.rs — it once carried 0.1.15 in its Cargo.toml and 0.1.13 in its pyproject.toml, the same artefact declaring two versions of itself.
5. Publishing to PyPI
Nothing here has been uploaded to PyPI. The wheels are served from holger's local PEP 503 index. A public upload is a separate, credentialed, one-way act and is Rickard's call, not a step in this runbook.
Where the detail lives
.nornir/facet-wrapped-wheel.md — the rows, what each one measured, and what is still open (the sdist's timestamps, deb.debian.org vs a snapshot, cold-store image reproducibility).