Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/python-packaging-publish-runbook.md

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-web is now facett (import facett; facet_wrapped_web stays as an alias for one release; Streamlit glue in facett.streamlit behind the [streamlit] extra; wheel only), and the Streamlit demo is facett-demo (import facett_demo, command facett-demo, live korp data behind [korp]). Recipe rows facett-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 name facet-wrapped-web, read facett.

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-streamlit crate built by cargo xtask release-streamlit, producing facett_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 got unknown command with no way to tell whether they had typed it wrong or were reading about a world that had ended. It also ran python -m build on 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.rs now fails if any document instructs the reader to run a cargo xtask verb 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).