facett-ops/src/holger.rs
//! **Publishing the wheel to holger's PEP 503 index — in Rust, as a library.**
//!
//! This replaces `pkg-facet-wrapped/publish_to_holger.sh`, which was a LAW breach in
//! two ways at once: it was a shell script doing what Rust owns, and it shelled out
//! to `znippy-cli` and `holger-server` **binaries** that had to already exist. On
//! oden today neither did:
//!
//! ```text
//! MEASURED 2026-09-05:
//! $HOME/git/znippy/target/release/znippy-cli — does not exist
//! $HOME/git/holger/target/release/holger-server — does not exist
//! ```
//!
//! So the script's happy path had not been runnable on this box, and its failure
//! would have been `ZNIPPY: No such file or directory` — a message about a missing
//! binary, from a script whose job is publishing a wheel. Linking `znippy-compress`
//! as a **library** removes that whole class: the packer is compiled into the xtask
//! and cannot be absent.
//!
//! ## What is being built
//!
//! There is deliberately **no index implemented here**. holger-ds already IS the
//! suite's Python index — a PEP 503 HTML index whose links carry `#sha256=`, a PEP
//! 691 JSON view, and a `/packages/…` download route, all served by `PipRepoZnippy`
//! out of a znippy archive. This module only packs facett's wheels into that
//! archive in the layout the backend serves:
//!
//! ```text
//! packages/{pep503-normalized-name}/{original filename} → znippy → .znippy
//! ```
//!
//! and holger serves it. Anything more would be a second index, which is exactly
//! what we are not doing.
use std::path::{Path, PathBuf};
/// The archive holger's `pip` repository is configured to read
/// (`pkg-facet-wrapped/holger-facett-pip.ron`, `ron_archive_path`). The path is the
/// contract between this module and that config; changing one without the other is
/// how an index silently serves yesterday's wheels.
pub const ARCHIVE_REL: &str = "dist/holger-pip/facett-pip.znippy";
/// The staging tree that becomes the archive.
pub const STAGE_REL: &str = "dist/holger-pip/src";
/// **PEP 503 name normalization** — `re.sub(r"[-_.]+", "-", name).lower()`.
///
/// ⚠ The shell script this replaces did `tr '[:upper:]' '[:lower:]' | tr '_.' '--'`,
/// and that is NOT the same function. `tr` maps character-for-character, so it
/// cannot collapse a RUN: `facet__wrapped` normalizes to `facet-wrapped` under PEP
/// 503 and to `facet--wrapped` under `tr`. A wheel packed under the second name is
/// invisible to `pip install facet-wrapped`, because holger normalizes the request
/// the right way and looks for a directory that is not there.
///
/// Today's distribution (`facet-wrapped-web`) has no doubled separator, so the bug
/// was latent — the script worked for the one name it was ever run with. That is
/// the most expensive kind of wrong: it passes until someone adds a package.
pub fn normalize_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut last_was_sep = false;
for c in name.chars() {
if c == '-' || c == '_' || c == '.' {
if !last_was_sep {
out.push('-');
}
last_was_sep = true;
} else {
out.extend(c.to_lowercase());
last_was_sep = false;
}
}
out
}
/// The PEP 427 / PEP 625 distribution name carried by an artifact FILENAME.
///
/// A wheel is `{distribution}-{version}-…`, an sdist is `{distribution}-{version}`.
/// Both put the distribution first, so the first `-`-separated field is it — with
/// the caveat that the distribution's own `_` separators are part of that field
/// (`facet_wrapped_web-0.1.19-py3-none-any.whl` → `facet_wrapped_web`), which is
/// exactly what [`normalize_name`] then folds.
pub fn distribution_of(file_name: &str) -> Option<String> {
let stem = file_name
.strip_suffix(".whl")
.or_else(|| file_name.strip_suffix(".tar.gz"))?;
let first = stem.split('-').next()?;
if first.is_empty() {
return None;
}
Some(first.to_string())
}
/// Where one artifact goes inside the archive.
pub fn archive_path_for(file_name: &str) -> Option<String> {
let dist = distribution_of(file_name)?;
Some(format!("packages/{}/{}", normalize_name(&dist), file_name))
}
/// What a publish laid down, so the caller reports facts.
#[derive(Debug, Clone)]
pub struct HolgerPublish {
/// The `.znippy` on disk.
pub archive: PathBuf,
/// `(path inside the archive, byte length)` for every artifact packed IN.
pub packed: Vec<(String, u64)>,
/// The same list, read BACK OUT of the finished archive and compared byte for
/// byte against the originals. This is the field worth believing: `packed` says
/// what we handed the compressor, `verified` says what holger will actually
/// serve.
pub verified: Vec<(String, u64)>,
/// The archive's own size.
pub archive_bytes: u64,
/// The rendered holger config that serves THIS checkout's archive.
pub config: PathBuf,
/// The index URL a consumer installs from — printed so nobody has to reconstruct
/// it from a config file.
pub index_url: String,
}
/// Lay the artifacts out under `stage` in the layout `PipRepoZnippy` serves.
///
/// Returns the packed paths. Refuses an artifact whose filename it cannot read a
/// distribution out of, rather than packing it somewhere `pip` will never look —
/// a silently mislaid wheel is an index that resolves and 404s.
pub fn stage_index(artifacts: &[PathBuf], stage: &Path) -> Result<Vec<(String, u64)>, String> {
if artifacts.is_empty() {
return Err("no artifacts to publish — run `cargo ops publish-pip` first".into());
}
if stage.exists() {
std::fs::remove_dir_all(stage).map_err(|e| format!("clear {}: {e}", stage.display()))?;
}
let mut packed = Vec::new();
for src in artifacts {
let name = src
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("unreadable artifact name: {}", src.display()))?;
let rel = archive_path_for(name).ok_or_else(|| {
format!(
"`{name}` is neither a wheel nor an sdist — refusing to pack it, because \
an artifact under a path pip does not look in is an index that resolves \
and then 404s"
)
})?;
let dst = stage.join(&rel);
std::fs::create_dir_all(dst.parent().unwrap())
.map_err(|e| format!("create {}: {e}", dst.parent().unwrap().display()))?;
let n = std::fs::copy(src, &dst)
.map_err(|e| format!("stage {} → {}: {e}", src.display(), dst.display()))?;
packed.push((rel, n));
}
packed.sort();
Ok(packed)
}
/// Pack the staged tree into the `.znippy` holger reads.
///
/// `znippy_compress::compress_dir` as a LIBRARY CALL — no `znippy-cli` binary, no
/// shell. The `python` handler is the same one `znippy compress --format python`
/// selects, taken from `znippy_cli::handlers` rather than re-implemented, so the
/// archive's recorded package type is what every `--type python` consumer expects.
pub fn pack_index(stage: &Path, archive: &Path) -> Result<u64, String> {
std::fs::create_dir_all(archive.parent().unwrap())
.map_err(|e| format!("create {}: {e}", archive.parent().unwrap().display()))?;
if archive.exists() {
std::fs::remove_file(archive).map_err(|e| format!("remove old archive: {e}"))?;
}
// The SAME `NativePythonPlugin` that `znippy compress --format python` selects,
// taken from the leaf crate that defines it. Going through the CLI's handler
// registry would have been the same plugin plus cranelift and wasmtime, because
// that registry also hosts the wasm plugins.
let handler: Box<dyn znippy_common::plugin::ArchiveTypePlugin> =
Box::new(znippy_plugin_python::NativePythonPlugin);
let registry = znippy_common::plugin::PluginRegistry::with_plugin(handler);
znippy_compress::compress_dir(
&stage.to_path_buf(),
&archive.to_path_buf(),
false,
Some(®istry),
Some("facett"),
None,
)
.map_err(|e| format!("znippy compress {}: {e}", stage.display()))?;
let n = std::fs::metadata(archive)
.map_err(|e| format!("the compressor reported success but wrote no archive: {e}"))?
.len();
if n == 0 {
return Err(format!("{} is 0 bytes — nothing was packed", archive.display()));
}
Ok(n)
}
/// Pack every artifact of BOTH releases (`facett` and `facett-demo`, each in its
/// cell's `out_dir`) into holger's pip archive.
pub fn publish_to_holger(root: &Path) -> Result<HolgerPublish, String> {
let mut artifacts: Vec<PathBuf> = Vec::new();
for r in crate::pip::Release::ALL {
let dist = r.dist_dir(root)?;
artifacts.extend(
std::fs::read_dir(&dist)
.map_err(|e| format!("{} — run `cargo ops publish-pip` first ({e})", dist.display()))?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
let n = p.to_string_lossy();
n.ends_with(".whl") || n.ends_with(".tar.gz")
}),
);
}
artifacts.sort();
// Every wheel is re-verified HERE, not just when it was built. The archive is
// what a consumer installs from, and a wheel can rot between the two steps —
// a partial copy, a truncated write, an editor saving over it. Re-reading costs
// milliseconds and is the difference between publishing bytes and publishing a
// memory of bytes.
for a in &artifacts {
let name = a.file_name().and_then(|s| s.to_str()).unwrap_or_default();
if name.ends_with(".whl") {
let release = crate::pip::Release::of_wheel(name)
.ok_or_else(|| format!("refusing to publish {}: not a wheel of either release", a.display()))?;
release
.verify(a)
.map_err(|f| format!("refusing to publish {}: {f}", a.display()))?;
}
}
let stage = root.join(STAGE_REL);
let packed = stage_index(&artifacts, &stage)?;
let archive = root.join(ARCHIVE_REL);
let archive_bytes = pack_index(&stage, &archive)?;
// LAW 2: the compressor exiting 0 is not evidence. Extract it back and compare
// the bytes holger would serve against the bytes that went in.
let checked = verify_archive_round_trip(
&archive,
&artifacts,
&root.join("target").join("pip-build").join("holger-verify"),
)?;
if checked.len() != packed.len() {
return Err(format!(
"packed {} artifact(s) but only {} came back out of the archive",
packed.len(),
checked.len()
));
}
// The served config must name THIS checkout, not the path baked into the
// template — a worktree that publishes its wheel and then serves the shared
// tree's stale archive is a green that means nothing.
let config = render_index_config(root)?;
Ok(HolgerPublish {
archive,
packed,
verified: checked,
archive_bytes,
index_url: "http://127.0.0.1:8455/pip/simple/".to_string(),
config,
})
}
/// **Read the archive back and compare bytes.**
///
/// `pack_index` returning `Ok` means the compressor did not error and wrote a
/// non-zero file. That is a state round-trip, not an applied output: an archive
/// whose index is right and whose payload is short still has a size, and holger
/// would serve it, and `pip` would download a truncated wheel and fail its own hash
/// check with a message about the network.
///
/// So the archive is EXTRACTED to a scratch directory and every artifact is compared
/// **byte for byte** against the file that went in. That is the only claim worth
/// making about a publish: what comes out is what went in.
pub fn verify_archive_round_trip(
archive: &Path,
originals: &[PathBuf],
scratch: &Path,
) -> Result<Vec<(String, u64)>, String> {
if scratch.exists() {
std::fs::remove_dir_all(scratch).map_err(|e| format!("clear {}: {e}", scratch.display()))?;
}
std::fs::create_dir_all(scratch).map_err(|e| format!("create {}: {e}", scratch.display()))?;
let report = znippy_decompress::decompress_archive(archive, scratch)
.map_err(|e| format!("extract {}: {e}", archive.display()))?;
if report.corrupt_files != 0 {
return Err(format!(
"{} extracted with {} corrupt file(s) of {} — the archive holger would serve is \
damaged",
archive.display(),
report.corrupt_files,
report.total_files
));
}
let mut checked = Vec::new();
for src in originals {
let name = src.file_name().and_then(|s| s.to_str()).unwrap_or_default();
let rel = archive_path_for(name)
.ok_or_else(|| format!("`{name}` has no archive path"))?;
// The extract is rooted at the staged tree's own layout; find the file by
// its relative path under whichever root the unpacker chose.
let found = find_under(scratch, &rel).ok_or_else(|| {
format!("`{rel}` is not in the extracted archive — it was reported packed and is not there")
})?;
let want = std::fs::read(src).map_err(|e| format!("re-read {}: {e}", src.display()))?;
let got = std::fs::read(&found).map_err(|e| format!("read {}: {e}", found.display()))?;
if want.len() != got.len() {
return Err(format!(
"`{rel}` came out of the archive at {} bytes, went in at {} — a truncated \
wheel that holger would serve and pip would blame the network for",
got.len(),
want.len()
));
}
if want != got {
return Err(format!("`{rel}` came out of the archive with DIFFERENT bytes"));
}
checked.push((rel, got.len() as u64));
}
checked.sort();
Ok(checked)
}
/// Locate `rel` anywhere under `root` — the unpacker may root the tree at the
/// staged directory's own name, and hard-coding which would make this check depend
/// on an implementation detail of the packer rather than on the bytes.
fn find_under(root: &Path, rel: &str) -> Option<PathBuf> {
let direct = root.join(rel);
if direct.is_file() {
return Some(direct);
}
let entries = std::fs::read_dir(root).ok()?;
for e in entries.flatten() {
if e.file_type().ok()?.is_dir() {
let p = e.path().join(rel);
if p.is_file() {
return Some(p);
}
}
}
None
}
// ─────────────────────────────────────────────────────────────────────────────
// ROW 1c — actually INSTALL it, in a container that has never seen this repo.
// ─────────────────────────────────────────────────────────────────────────────
/// The clean-consumer image: `python3` + `pip` + `venv`, and nothing else — the
/// recipe's `[[build.container]]` name.
pub use facett_build_thing::PIP_CONSUMER_IMAGE;
/// Its recipe, compiled in.
pub const PIP_CONSUMER_CONTAINERFILE: &str =
include_str!("containerfiles/pip-consumer.Containerfile");
/// The line the in-container check prints, which the host side parses and asserts
/// on. A prefix rather than "the last line": pip is chatty and a bare JSON line
/// would be indistinguishable from something pip decided to print.
pub const CHECK_MARKER: &str = "NORDISK_INSTALL_CHECK ";
/// What a real `pip install` off the index produced, read out of the installed
/// package INSIDE the container.
#[derive(Debug, Clone)]
pub struct InstallProof {
/// The image the install ran in, and its id when the engine can name it.
pub image_ref: String,
pub image_id: Option<String>,
/// `facett.__version__` as the INSTALLED package reports it.
pub version: String,
/// `assets_dir()` inside the container — must be a site-packages path, or we
/// checked the source tree by accident.
pub assets_dir: String,
/// `(asset, bytes)` for each required asset, as installed.
pub assets: Vec<(String, u64)>,
/// First four bytes of the installed wasm, hex — the magic, read after a real
/// download and unpack.
pub wasm_magic: String,
/// How many components the installed manifest declares.
pub components: u64,
/// Whether `streamlit` is importable in the venv after installing `facett` ALONE.
/// It must not be: Streamlit is the `[streamlit]` extra, and a plain install that
/// drags it in is a dependency a non-Streamlit host never asked for.
pub streamlit_installed: bool,
/// What `import facett.streamlit` said without Streamlit — it must name the extra.
pub glue_refusal: String,
}
/// The script the consumer container runs. Pure consumer verbs: make a venv, pip
/// install from the index, import the package, print what is actually on disk.
fn consumer_script(index_url: &str) -> String {
// The asset list is the recipe's (every bundle group + the loader + the routing
// table), rendered as a python list — not re-spelled here.
let names = crate::pip::required_assets()
.iter()
.map(|n| format!("\"{n}\""))
.collect::<Vec<_>>()
.join(", ");
let wasm = &crate::pip::required_assets()[0];
let project = facett_build_thing::PYPI_PROJECT;
let pkg = crate::pip::IMPORT_PACKAGE;
// `--no-cache-dir` so a previous run's wheel cannot satisfy this one — the
// download must really happen against the index under test.
format!(
r#"set -eu
python3 -m venv /tmp/v
/tmp/v/bin/pip install --no-cache-dir --disable-pip-version-check \
--index-url {index_url} --trusted-host 127.0.0.1 {project}
/tmp/v/bin/python - <<'PY'
import json, hashlib, importlib.util
import {pkg} as f
try:
import {pkg}.streamlit
glue = "IMPORTED WITHOUT STREAMLIT"
except ImportError as e:
glue = str(e)
a = f.assets_dir()
names = [{names}]
out = {{
"version": f.__version__,
"assets_dir": str(a),
"assets": [[n, (a / n).stat().st_size] for n in names],
"wasm_magic": (a / "{wasm}").read_bytes()[:4].hex(),
"components": len(f.manifest()),
"streamlit_installed": importlib.util.find_spec("streamlit") is not None,
"glue_refusal": glue,
}}
print("{CHECK_MARKER}" + json.dumps(out))
PY
"#
)
}
/// Pull one field out of the check line. A hand-rolled reader rather than a serde
/// derive: the payload is six flat fields the script beside it writes, and adding a
/// serde model here would put a second definition of that shape in the tree.
fn field<'a>(json: &'a str, key: &str) -> Option<&'a str> {
let at = json.find(&format!("\"{key}\":"))?;
let rest = &json[at + key.len() + 3..];
let rest = rest.trim_start();
if let Some(r) = rest.strip_prefix('"') {
let end = r.find('"')?;
Some(&r[..end])
} else {
let end = rest.find([',', '}']).unwrap_or(rest.len());
Some(rest[..end].trim())
}
}
/// Build the consumer image.
pub fn build_consumer_image(root: &Path) -> Result<(String, Option<String>), String> {
let ctx = root.join("target").join("pip-build").join("consumer-context");
std::fs::create_dir_all(&ctx).map_err(|e| format!("create {}: {e}", ctx.display()))?;
let cf = "pip-consumer.Containerfile";
std::fs::write(ctx.join(cf), PIP_CONSUMER_CONTAINERFILE)
.map_err(|e| format!("write {cf}: {e}"))?;
println!("ops: building the clean consumer image `{PIP_CONSUMER_IMAGE}`…");
draupnir::container::ContainerBoot::new()
.build_image_reproducible_pinned(&ctx, cf, PIP_CONSUMER_IMAGE, crate::pip::TOOLCHAIN_EPOCH)
.map_err(|e| format!("build image `{PIP_CONSUMER_IMAGE}`: {e}"))
}
/// **`cargo ops verify-install`** — pip-install the published wheel in a clean
/// container and read the installed bytes back.
///
/// This is the step that turns "the archive is correct" into "a client resolved it".
/// Everything before it is our own code checking our own output; this is a real
/// `pip`, against a real PEP 503 index, in an image that has never seen this repo
/// and has no source tree mounted.
///
/// `NetMode::Host` because holger listens on loopback and the container has to
/// reach it. That is the only network this container gets, and holger stays bound
/// to 127.0.0.1 rather than being exposed to reach a bridge.
pub fn verify_install(root: &Path, index_url: &str) -> Result<InstallProof, String> {
let (image_ref, image_id) = build_consumer_image(root)?;
match &image_id {
Some(id) => println!("ops: consumer image {image_ref} = {id}"),
None => println!("ops: consumer image {image_ref} (UNPINNED)"),
}
let script = consumer_script(index_url);
let spec = draupnir::BootSpec::container("facett-pip-install", &image_ref)
.with_cmd(["/bin/sh", "-eu", "-c", script.as_str()])
.with_net(draupnir::NetMode::Host)
.with_mem_limit_mb(2048);
spec.validate().map_err(|e| format!("install spec: {e}"))?;
let opts = draupnir::container::RunOptions::bounded(
std::time::Duration::from_secs(600),
std::time::Duration::from_secs(1),
);
println!("ops: pip install --index-url {index_url} {}, in a clean container…", facett_build_thing::PYPI_PROJECT);
// No binds. Nothing from this repo is inside that container.
let out = draupnir::container::ContainerBoot::new()
.run_to_completion_with_binds(&spec, &opts, &[])
.map_err(|e| format!("consumer container: {e}"))?;
if out.exit_code != Some(0) {
for l in out.stdout.iter().rev().take(20).collect::<Vec<_>>().into_iter().rev() {
eprintln!("[pip-install] {}", l.trim_end());
}
for l in out.stderr.iter().rev().take(30).collect::<Vec<_>>().into_iter().rev() {
eprintln!("[pip-install] err: {}", l.trim_end());
}
return Err(format!(
"pip install from {index_url} exited {:?} — is holger serving? \
(`holger-server start --config pkg-facet-wrapped/holger-facett-pip.ron`)",
out.exit_code
));
}
let line = out
.stdout
.iter()
.chain(out.stderr.iter())
.find_map(|l| l.find(CHECK_MARKER).map(|i| l[i + CHECK_MARKER.len()..].trim().to_string()))
.ok_or_else(|| {
// Exit 0 with no marker means the install "succeeded" and the import
// check never ran — the exact shape of a green that proves nothing.
// SHOW WHAT IT DID PRINT. An error that says only "no check line" tells
// the reader nothing they can act on — they have to re-run the whole thing
// by hand to find out what happened, which is what happened the first time
// this fired.
let all: Vec<&String> = out.stdout.iter().chain(out.stderr.iter()).collect();
let tail: Vec<String> = all
.iter()
.skip(all.len().saturating_sub(25))
.map(|l| format!(" {}", l.trim_end()))
.collect();
format!(
"the container exited 0 but printed no `{CHECK_MARKER}` line — the install \
ran and the package was never imported. Last {} line(s) it did print:\n{}",
tail.len(),
if tail.is_empty() { " (nothing at all)".to_string() } else { tail.join("\n") }
)
})?;
let mut assets = Vec::new();
for name in &crate::pip::required_assets() {
// The assets array is `[["name", 1234], …]`; find this name and take the
// number after it.
let at = line
.find(&format!("\"{name}\", "))
.ok_or_else(|| format!("`{name}` is not in the installed package's report"))?;
let rest = &line[at + name.len() + 4..];
let end = rest.find(']').ok_or_else(|| format!("malformed report near `{name}`"))?;
let n: u64 = rest[..end]
.trim()
.parse()
.map_err(|e| format!("`{name}` size in the report is not a number: {e}"))?;
assets.push((name.to_string(), n));
}
Ok(InstallProof {
image_ref,
image_id,
version: field(&line, "version").unwrap_or_default().to_string(),
assets_dir: field(&line, "assets_dir").unwrap_or_default().to_string(),
assets,
wasm_magic: field(&line, "wasm_magic").unwrap_or_default().to_string(),
components: field(&line, "components").unwrap_or("0").parse().unwrap_or(0),
streamlit_installed: field(&line, "streamlit_installed") != Some("false"),
glue_refusal: field(&line, "glue_refusal").unwrap_or_default().to_string(),
})
}
/// **Judge the install, in Rust.**
///
/// Kept separate from [`verify_install`] so the verdict is not the container's exit
/// code. A python script asserting on itself and exiting 0 is the container saying
/// it is fine; this is us checking what it reported against what we BUILT.
pub fn judge_install(
proof: &InstallProof,
expect_version: &str,
built: &[(String, u64)],
) -> Result<(), String> {
if proof.version != expect_version {
return Err(format!(
"the installed package reports version `{}`, we published `{expect_version}` — \
either pip resolved a different distribution or the metadata is stale",
proof.version
));
}
if !proof.assets_dir.contains("site-packages") {
return Err(format!(
"assets_dir is `{}`, which is not a site-packages path — the check read a \
source tree, not the installed wheel",
proof.assets_dir
));
}
if proof.wasm_magic != "0061736d" {
return Err(format!(
"the INSTALLED wasm starts with {} , not the \\0asm magic (0061736d) — it \
survived the build and did not survive the trip",
proof.wasm_magic
));
}
if proof.components == 0 {
return Err("the installed manifest declares zero components".to_string());
}
if proof.streamlit_installed {
return Err(
"installing `facett` ALONE put Streamlit in the environment — it must be the \
`[streamlit]` extra, never a hard dependency"
.to_string(),
);
}
if !proof.glue_refusal.contains("facett[streamlit]") {
return Err(format!(
"`import facett.streamlit` without Streamlit said `{}` — it must name the extra \
(`pip install 'facett[streamlit]'`)",
proof.glue_refusal
));
}
// The lengths must match what we BUILT, byte for byte. This is the claim that
// makes the whole chain worth something: the bytes on the consumer's disk are
// the bytes wrap-wasm produced, having gone through a wheel, a znippy archive,
// an HTTP index and a pip resolve.
for (name, want) in built {
let got = proof
.assets
.iter()
.find(|(n, _)| n == name)
.map(|(_, n)| *n)
.ok_or_else(|| format!("`{name}` is missing from the installed package"))?;
if got != *want {
return Err(format!(
"`{name}` is {got} bytes as installed, {want} as built — the artifact changed \
somewhere between wrap-wasm and pip"
));
}
}
Ok(())
}
// ─────────────────────────────────────────────────────────────────────────────
// ROW 2 — the demo RUNS. Streamlit, in a container, on the INSTALLED wheels.
// ─────────────────────────────────────────────────────────────────────────────
/// The image the Streamlit demo runs in — the recipe's `[[build.container]]` name.
pub use facett_build_thing::STREAMLIT_IMAGE;
/// Its recipe, compiled in.
pub const STREAMLIT_CONTAINERFILE: &str = include_str!("containerfiles/streamlit.Containerfile");
/// The port the demo is served on.
pub const STREAMLIT_PORT: u16 = 8501;
/// Build the Streamlit runtime image.
pub fn build_streamlit_image(root: &Path) -> Result<(String, Option<String>), String> {
let ctx = root.join("target").join("pip-build").join("streamlit-context");
std::fs::create_dir_all(&ctx).map_err(|e| format!("create {}: {e}", ctx.display()))?;
let cf = "streamlit.Containerfile";
std::fs::write(ctx.join(cf), STREAMLIT_CONTAINERFILE)
.map_err(|e| format!("write {cf}: {e}"))?;
println!("ops: building the streamlit image `{STREAMLIT_IMAGE}` (reaches PyPI once, for the framework)…");
draupnir::container::ContainerBoot::new()
.build_image_reproducible_pinned(&ctx, cf, STREAMLIT_IMAGE, crate::pip::TOOLCHAIN_EPOCH)
.map_err(|e| format!("build image `{STREAMLIT_IMAGE}`: {e}"))
}
/// **Where the containers get the two wheels from.** Since 0.2.0 the demo is a WHEEL
/// too (`facett-demo`), so nothing runs from a source tree any more: both packages
/// are `pip install`ed, the way a user gets them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WheelSource {
/// The built wheelhouse: both releases' `out_dir`s, bind-mounted READ-ONLY at
/// [`WHEELHOUSE`]`/<project>`, installed with `--no-index --find-links`. The
/// default: it proves the wheels `publish-pip` just built, with no index running.
Local,
/// A PEP 503 index (holger's `http://127.0.0.1:8455/pip/simple/`), proving the
/// publish road as well.
Index(String),
}
/// Where [`WheelSource::Local`] mounts the wheelhouse inside a container.
pub const WHEELHOUSE: &str = "/wheels";
impl WheelSource {
/// `[--index URL | URL]` → the source. A bare `http…` argument is the pre-0.2.0
/// positional index form, kept so an old command line still means what it meant.
pub fn from_args(args: &[String]) -> Result<Self, String> {
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--index" => return Ok(WheelSource::Index(it.next().ok_or("--index needs a URL")?.clone())),
s if s.starts_with("http://") || s.starts_with("https://") => return Ok(WheelSource::Index(s.to_string())),
_ => {}
}
}
Ok(WheelSource::Local)
}
/// The `pip install` options that select the source (`requirement` is appended).
#[must_use]
pub fn pip_install(&self, requirement: &str) -> String {
match self {
WheelSource::Local => format!(
"pip install --no-cache-dir --disable-pip-version-check --no-index \\\n\
\x20 --find-links {WHEELHOUSE}/{} --find-links {WHEELHOUSE}/{} '{requirement}'",
facett_build_thing::PYPI_PROJECT,
facett_build_thing::DEMO_PYPI_PROJECT,
),
WheelSource::Index(url) => format!(
"pip install --no-cache-dir --disable-pip-version-check \\\n\
\x20 --index-url {url} --trusted-host 127.0.0.1 '{requirement}'"
),
}
}
/// The read-only binds the source needs (the wheelhouse, or nothing).
pub fn binds(&self, root: &Path) -> Result<Vec<String>, String> {
match self {
WheelSource::Index(_) => Ok(Vec::new()),
WheelSource::Local => crate::pip::Release::ALL
.iter()
.map(|r| {
let d = r.dist_dir(root)?;
let has_wheel = std::fs::read_dir(&d)
.map(|rd| rd.filter_map(|e| e.ok()).any(|e| e.file_name().to_string_lossy().ends_with(".whl")))
.unwrap_or(false);
if !has_wheel {
return Err(format!("no {} wheel in {} — run `cargo ops publish-pip` first", r.project(), d.display()));
}
Ok(format!("{}:{WHEELHOUSE}/{}:ro", d.display(), r.project()))
})
.collect(),
}
}
}
/// What the demo container installs: the app WITH its korp wire — the image carries
/// `grpcio` + `protobuf` (pinned from `py/requirements.txt`), so the `[korp]` extra
/// resolves with no network and the live tabs work when a korp-server is reachable.
fn demo_requirement() -> String {
format!("{}[korp]", facett_build_thing::DEMO_PYPI_PROJECT)
}
/// The launch command, exposed so `tests/streamlit_image_matches_requirements.rs` can
/// assert on the bind address without starting a container. The test needs the REAL
/// string, not a copy of it — a copy is how a guard ends up watching something
/// nobody runs.
#[must_use]
pub fn streamlit_launch_command() -> String {
streamlit_script(&WheelSource::Local, "127.0.0.1")
}
/// The import the pre-flight in [`streamlit_script`] performs before the app is
/// started, and the reason it is there.
///
/// # The defect it was written for (MEASURED on oden, 2026-09-06)
///
/// `cargo xtask streamlit` printed `✔ streamlit demo running`, exited 0, and the
/// container answered `curl` with **HTTP 200**. Every one of those 200s was a
/// traceback page (`ModuleNotFoundError` — `streamlit run /app/<pkg>/app.py` puts the
/// SCRIPT's directory on `sys.path`, never the package's parent, so the source-tree
/// app could not import itself). HTTP 200 is not a check: Streamlit renders its
/// exception INTO the page.
///
/// Since 0.2.0 the app is an INSTALLED package started by its own console script, so
/// that path problem is gone by construction — but the pre-flight stays: with `set
/// -eu`, a broken install makes the container EXIT LOUDLY instead of serving a
/// traceback page. It reaches through `panes.gallery_pane` so the whole import chain
/// the app performs is covered, and it stops short of `app` itself, which runs
/// `main()` at import time.
pub const STREAMLIT_PREFLIGHT_IMPORT: &str = "facett_demo.panes.gallery_pane";
/// The command [`test_demo`] runs, from the local wheelhouse — the readable half of
/// the pair, so a guard can hold the two launchers of this ONE app against each other.
#[must_use]
pub fn test_demo_command() -> String {
test_demo_script(&WheelSource::Local)
}
/// Where the two sibling `.proto` files are mounted, and the env vars that name
/// them to the suite.
///
/// The wire guards (`tests/test_korp_wire.py`, `tests/test_saga.py`) read the
/// ONE copy of each wire — korp's `korp.proto` and edda's `nornir.proto` — out of
/// the sibling checkouts. The container has no siblings, so they are bound in
/// read-only and named by env var.
pub const KORP_PROTO_IN_IMAGE: &str = "/wire/korp.proto";
/// See [`KORP_PROTO_IN_IMAGE`].
pub const NORNIR_PROTO_IN_IMAGE: &str = "/wire/nornir.proto";
/// The suite's own read-only mounts: the TESTS (not the package — that is installed
/// from the wheel), plus the two wire files.
fn test_demo_binds(root: &Path) -> Result<Vec<String>, String> {
// Named rather than discovered. A missing sibling is refused BY NAME here, with
// the path that was looked for — the alternative is a container that starts,
// collects two fewer files and reports a smaller green suite.
let korp = root
.parent()
.ok_or("facett has no parent directory")?
.join("korp/proto/korp.proto");
let nornir = root
.parent()
.ok_or("facett has no parent directory")?
.join("edda/crates/nornir-wire/proto/nornir.proto");
for (what, p) in [("korp", &korp), ("edda", &nornir)] {
if !p.is_file() {
return Err(format!(
"the {what} checkout's proto is missing ({}) — the demo's wire guards \
read the ONE copy of that wire from the sibling checkout, and a suite \
that silently skipped them would be green about a wire it never saw",
p.display()
));
}
}
Ok(vec![
format!("{}:/suite:ro", root.join(facett_build_thing::DEMO_SOURCE).join("tests").display()),
format!("{}:{KORP_PROTO_IN_IMAGE}:ro", korp.display()),
format!("{}:{NORNIR_PROTO_IN_IMAGE}:ro", nornir.display()),
])
}
/// The suite runs from `/tmp` with NO `PYTHONPATH`: every `import facett_demo` /
/// `import facett` resolves to site-packages, i.e. to the INSTALLED wheels — the tests
/// locate the app through `facett_demo.__file__`, so they drive what a user installs.
fn test_demo_script(source: &WheelSource) -> String {
format!(
"set -eu\n\
{install}\n\
cd /tmp\n\
export KORP_PROTO={KORP_PROTO_IN_IMAGE}\n\
export NORNIR_PROTO={NORNIR_PROTO_IN_IMAGE}\n\
python3 -c 'import facett_demo, facett; print(\"under test:\", facett_demo.__file__, facett.__file__)'\n\
python3 -m pytest /suite -q --no-header -p no:cacheprovider --rootdir /tmp\n",
install = source.pip_install(&demo_requirement()),
)
}
fn streamlit_script(source: &WheelSource, address: &str) -> String {
format!(
"set -eu\n\
{install}\n\
python3 -c 'import facett as f; print(\"facett\", f.__version__, f.build_info()[\"components\"], \"components\")'\n\
cd /tmp\n\
python3 -c 'import {STREAMLIT_PREFLIGHT_IMPORT} as g; print(\"app imports OK:\", g.__name__)'\n\
{cmd} --version\n\
exec {cmd} run --server.port {STREAMLIT_PORT} --server.address {address}\n",
install = source.pip_install(&demo_requirement()),
cmd = facett_build_thing::DEMO_ENTRY_POINT.0,
)
}
/// Start the Streamlit demo in a container and leave it running — the `facett-demo`
/// console script of the INSTALLED wheel, nothing from a source tree.
///
/// Returns the container's name. `NetMode::Host` so an index on loopback (holger)
/// is reachable and Streamlit's port lands straight on the host — bound to
/// 127.0.0.1 unless `FACETT_STREAMLIT_ADDRESS` says otherwise.
pub fn start_streamlit(root: &Path, source: &WheelSource) -> Result<String, String> {
let (image, id) = build_streamlit_image(root)?;
match &id {
Some(i) => println!("ops: streamlit image {image} = {i}"),
None => println!("ops: streamlit image {image} (UNPINNED)"),
}
// Where Streamlit listens: loopback by default; `FACETT_STREAMLIT_ADDRESS=0.0.0.0`
// to reach it from another machine (tailscale). WebGPU needs a secure context, so a
// remote browser on plain http must allow the origin itself (Chrome:
// `--unsafely-treat-insecure-origin-as-secure=http://<host>:8501`).
let address = std::env::var("FACETT_STREAMLIT_ADDRESS").unwrap_or_else(|_| "127.0.0.1".to_string());
let script = streamlit_script(source, &address);
let mut spec = draupnir::BootSpec::container("facet-wrapped-demo", &image)
.with_cmd(["/bin/sh", "-eu", "-c", script.as_str()])
.with_net(draupnir::NetMode::Host)
.with_mem_limit_mb(4096);
// Dev mode (the navgraph numbers on every canvas): `FACETT_DEVID=1` on the host is
// handed to the app, whose sidebar toggle then starts ON.
if let Ok(v) = std::env::var("FACETT_DEVID") {
spec = spec.with_env("FACETT_DEVID", v);
}
// The live panes' korp-server (`host:port`, the app's `$KORP_SERVER`), when it is
// not on this machine.
if let Ok(v) = std::env::var("KORP_SERVER") {
spec = spec.with_env("KORP_SERVER", v);
}
spec.validate().map_err(|e| format!("streamlit spec: {e}"))?;
// READ-ONLY, and only the WHEELS: the container installs the release and runs it.
let binds = source.binds(root)?;
println!("ops: starting streamlit on :{STREAMLIT_PORT} (the installed facett-demo wheel)…");
let machine = draupnir::container::ContainerBoot::new()
.boot_with_binds(&spec, &binds)
.map_err(|e| format!("start streamlit: {e}"))?;
Ok(machine.id)
}
/// Run the demo's own python suite **inside the container**, against the INSTALLED
/// wheels.
///
/// "No python on the host" is not only about the build — a `pytest` run on the host
/// would be the same rule broken at a different hour.
///
/// The verdict is the exit code plus the summary line, both surfaced to the caller;
/// a suite whose output is swallowed is a suite nobody can act on.
pub fn test_demo(root: &Path, source: &WheelSource) -> Result<Vec<String>, String> {
let (image, _) = build_streamlit_image(root)?;
let script = test_demo_script(source);
let spec = draupnir::BootSpec::container("facett-demo-tests", &image)
.with_cmd(["/bin/sh", "-eu", "-c", script.as_str()])
.with_net(draupnir::NetMode::Host)
.with_mem_limit_mb(4096);
spec.validate().map_err(|e| format!("test spec: {e}"))?;
let opts = draupnir::container::RunOptions::bounded(
std::time::Duration::from_secs(900),
std::time::Duration::from_secs(1),
);
let mut binds = test_demo_binds(root)?;
binds.extend(source.binds(root)?);
println!("ops: running the demo's python suite inside {image}, on the installed wheels…");
let out = draupnir::container::ContainerBoot::new()
.run_to_completion_with_binds(&spec, &opts, &binds)
.map_err(|e| format!("demo test container: {e}"))?;
let mut lines: Vec<String> = out.stdout.iter().chain(out.stderr.iter()).cloned().collect();
if out.exit_code != Some(0) {
for l in lines.iter().rev().take(40).collect::<Vec<_>>().into_iter().rev() {
eprintln!("[demo-tests] {}", l.trim_end());
}
return Err(format!("the demo's python suite failed (exit {:?})", out.exit_code));
}
lines.retain(|l| !l.trim().is_empty());
Ok(lines)
}
// ─────────────────────────────────────────────────────────────────────────────
// ROW 3 (0.2.0) — the CLEAN install a PyPI user gets, both wheels, no source.
// ─────────────────────────────────────────────────────────────────────────────
/// The marker lines the clean-install script prints.
pub const ALONE_MARKER: &str = "NORDISK_FACETT_ALONE ";
/// See [`ALONE_MARKER`].
pub const DEMO_MARKER: &str = "NORDISK_FACETT_DEMO ";
/// The port the clean-install check starts the demo on — NOT 8501, which the running
/// demo holds.
pub const CLEAN_INSTALL_PORT: u16 = 8599;
/// The script `verify-demo-install` runs in the clean consumer image (`python3`,
/// `pip`, `venv` — nothing else, no source, no index).
///
/// 1. venv A: `pip install facett-<v>-py3-none-any.whl` ALONE — `import facett`, the
/// manifest, the alias, and Streamlit ABSENT (`facett.streamlit` must refuse by
/// naming the extra).
/// 2. venv B: `pip install facett-….whl facett_demo-….whl` — what a PyPI user types,
/// with Streamlit and its tree from **PyPI over the network** (no `[korp]`, so no
/// grpc): `facett-demo --version`, then `facett-demo run` in the background, its
/// `/_stcore/health`, and Streamlit's own `AppTest` over the INSTALLED app
/// (offline mode: the korp tabs on gallery data).
#[must_use]
pub fn clean_install_script(facett_whl: &str, demo_whl: &str) -> String {
let w = WHEELHOUSE;
let p = facett_build_thing::PYPI_PROJECT;
let d = facett_build_thing::DEMO_PYPI_PROJECT;
let cmd = facett_build_thing::DEMO_ENTRY_POINT.0;
let port = CLEAN_INSTALL_PORT;
format!(
r#"set -eu
python3 -m venv /tmp/a
/tmp/a/bin/pip install --no-cache-dir --disable-pip-version-check {w}/{p}/{facett_whl}
/tmp/a/bin/python - <<'PY'
import importlib.util, json, warnings
import facett
try:
import facett.streamlit
glue = "IMPORTED WITHOUT STREAMLIT"
except ImportError as e:
glue = str(e)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
import facet_wrapped_web
alias_warns = any(issubclass(c.category, DeprecationWarning) for c in caught)
out = {{
"version": facett.__version__,
"components": len(facett.manifest()),
"bundles": [g["name"] for g in facett.bundles().get("groups", [])],
"wasm_bytes": facett.build_info()["wasm_bytes"],
"assets_dir": str(facett.assets_dir()),
"streamlit_installed": importlib.util.find_spec("streamlit") is not None,
"glue_refusal": glue,
"alias_same_assets": facet_wrapped_web.assets_dir() == facett.assets_dir(),
"alias_warns": alias_warns,
}}
print("{ALONE_MARKER}" + json.dumps(out))
PY
python3 -m venv /tmp/b
/tmp/b/bin/pip install --no-cache-dir --disable-pip-version-check {w}/{p}/{facett_whl} {w}/{d}/{demo_whl}
/tmp/b/bin/pip list --disable-pip-version-check 2>/dev/null | grep -iE '^(streamlit|facett|facett-demo|grpcio|protobuf) ' || true
VERSION_LINE="$(/tmp/b/bin/{cmd} --version)"
cd /tmp
/tmp/b/bin/{cmd} run --server.port {port} > /tmp/demo.log 2>&1 &
DEMO=$!
export VERSION_LINE DEMO
/tmp/b/bin/python - <<'PY'
import json, os, time, urllib.request, importlib.util
health = "never answered"
for _ in range(180):
try:
with urllib.request.urlopen("http://127.0.0.1:{port}/_stcore/health", timeout=2) as r:
health = r.read().decode().strip()
break
except Exception as e:
health = f"no answer: {{e}}"
time.sleep(0.5)
page = urllib.request.urlopen("http://127.0.0.1:{port}/", timeout=10).status
import facett_demo, facett_demo.client as client
from streamlit.testing.v1 import AppTest
app = os.path.join(os.path.dirname(facett_demo.__file__), "app.py")
at = AppTest.from_file(app, default_timeout=180).run()
out = {{
"version_line": os.environ["VERSION_LINE"],
"health": health,
"page_status": page,
"app_file": app,
"korp_available": client.korp_available(),
"grpc_installed": importlib.util.find_spec("grpc") is not None,
"exceptions": [str(e.value) for e in at.exception],
"titles": [t.value for t in at.title],
"offline_banners": sum(1 for i in at.info if "NOT live data" in i.value),
"tabs": len(at.tabs),
}}
print("{DEMO_MARKER}" + json.dumps(out))
PY
kill "$DEMO" || true
tail -n 5 /tmp/demo.log || true
"#
)
}
/// What the clean-install check reported, parsed off its two marker lines.
#[derive(Debug, Clone)]
pub struct CleanInstall {
/// venv A (`facett` alone).
pub alone: serde_json::Value,
/// venv B (`facett` + `facett-demo`, Streamlit from PyPI).
pub demo: serde_json::Value,
}
/// **The verdict, in Rust**, against what was BUILT (`expect_components` is counted
/// out of the wheel's own manifests, not written down here).
pub fn judge_clean_install(c: &CleanInstall, expect_version: &str, expect_components: u64) -> Result<(), String> {
let a = &c.alone;
let d = &c.demo;
let s = |v: &serde_json::Value, k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
let b = |v: &serde_json::Value, k: &str| v.get(k).and_then(serde_json::Value::as_bool);
let n = |v: &serde_json::Value, k: &str| v.get(k).and_then(serde_json::Value::as_u64).unwrap_or(0);
if s(a, "version") != expect_version {
return Err(format!("facett installed as version `{}`, built {expect_version}", s(a, "version")));
}
if !s(a, "assets_dir").contains("site-packages") {
return Err(format!("facett's assets_dir is `{}` — not the installed wheel", s(a, "assets_dir")));
}
if n(a, "components") != expect_components {
return Err(format!(
"the installed facett lists {} components, the wheel's manifests hold {expect_components}",
n(a, "components")
));
}
if b(a, "streamlit_installed") != Some(false) {
return Err("`pip install facett` alone put Streamlit in the environment — it must be an extra".into());
}
if !s(a, "glue_refusal").contains("facett[streamlit]") {
return Err(format!("`import facett.streamlit` without Streamlit said `{}`", s(a, "glue_refusal")));
}
if b(a, "alias_same_assets") != Some(true) || b(a, "alias_warns") != Some(true) {
return Err("the `facet_wrapped_web` alias must re-export facett's assets AND warn".into());
}
let want_line = format!("{} {expect_version} (", facett_build_thing::DEMO_PYPI_PROJECT);
if !s(d, "version_line").starts_with(&want_line) || s(d, "version_line").contains("(unknown)") {
return Err(format!(
"`facett-demo --version` said `{}` — want `{want_line}<git hash>)` from the baked stamp",
s(d, "version_line")
));
}
if s(d, "health") != "ok" {
return Err(format!("`facett-demo run` never became healthy: /_stcore/health → {}", s(d, "health")));
}
if n(d, "page_status") != 200 {
return Err(format!("the demo's page answered {}", n(d, "page_status")));
}
if !s(d, "app_file").contains("site-packages") {
return Err(format!("the app ran from `{}` — not the installed wheel", s(d, "app_file")));
}
if b(d, "grpc_installed") != Some(false) || b(d, "korp_available") != Some(false) {
return Err("grpc arrived without the [korp] extra — korp must be OPTIONAL".into());
}
let ex = d.get("exceptions").and_then(|x| x.as_array()).map(Vec::len).unwrap_or(1);
if ex != 0 {
return Err(format!("the installed app raised: {}", d.get("exceptions").map(ToString::to_string).unwrap_or_default()));
}
if n(d, "offline_banners") == 0 {
return Err("without [korp] the korp tabs must say they show gallery data, NOT live data".into());
}
Ok(())
}
/// The components a built facett wheel ships, counted from its OWN manifests (every
/// bundle group's `manifest.json`, as the routing table lists them).
pub fn components_in_wheel(wheel: &Path) -> Result<u64, String> {
use std::io::Read as _;
let f = std::fs::File::open(wheel).map_err(|e| format!("{}: {e}", wheel.display()))?;
let mut z = zip::ZipArchive::new(f).map_err(|e| format!("{}: {e}", wheel.display()))?;
let mut read = |name: &str| -> Result<String, String> {
let mut s = String::new();
z.by_name(name).map_err(|e| format!("{name}: {e}"))?.read_to_string(&mut s).map_err(|e| format!("{name}: {e}"))?;
Ok(s)
};
let pkg = crate::pip::IMPORT_PACKAGE;
let routing: serde_json::Value = serde_json::from_str(&read(&format!("{pkg}/assets/{}", facett_build_thing::ROUTING_FILE))?)
.map_err(|e| format!("routing table: {e}"))?;
let mut total = 0u64;
for g in routing.get("groups").and_then(|g| g.as_array()).ok_or("routing table has no groups")? {
let mf = g.get("manifest").and_then(|m| m.as_str()).unwrap_or("manifest.json");
let list: serde_json::Value = serde_json::from_str(&read(&format!("{pkg}/assets/{mf}"))?)
.map_err(|e| format!("{mf}: {e}"))?;
total += list.as_array().map(|a| a.len() as u64).ok_or_else(|| format!("{mf} is not a list"))?;
}
Ok(total)
}
/// **`cargo ops verify-demo-install`** — install the two built wheels in a clean
/// container the way a PyPI user would, start the demo, and judge it in Rust.
///
/// Network: `NetMode::Host`, because venv B fetches Streamlit and its dependency tree
/// from PyPI (exactly what `pip install facett-demo` does for a user). The wheels
/// under test come from the read-only wheelhouse bind, never from an index.
pub fn verify_demo_install(root: &Path) -> Result<(CleanInstall, Vec<String>), String> {
let pick = |r: crate::pip::Release| -> Result<PathBuf, String> {
let d = r.dist_dir(root)?;
std::fs::read_dir(&d)
.map_err(|e| format!("{}: {e} — run `cargo ops publish-pip` first", d.display()))?
.filter_map(|e| e.ok().map(|e| e.path()))
.find(|p| p.to_string_lossy().ends_with(".whl"))
.ok_or_else(|| format!("no {} wheel in {}", r.project(), d.display()))
};
let facett_whl = pick(crate::pip::Release::Components)?;
let demo_whl = pick(crate::pip::Release::Demo)?;
crate::pip::verify_wheel(&facett_whl).map_err(|f| format!("{}: {f}", facett_whl.display()))?;
crate::pip::verify_demo_wheel(&demo_whl).map_err(|f| format!("{}: {f}", demo_whl.display()))?;
let name = |p: &Path| p.file_name().unwrap().to_string_lossy().into_owned();
let (image_ref, image_id) = build_consumer_image(root)?;
println!("ops: consumer image {image_ref}{}", image_id.map(|i| format!(" = {i}")).unwrap_or_default());
let script = clean_install_script(&name(&facett_whl), &name(&demo_whl));
let spec = draupnir::BootSpec::container("facett-clean-install", &image_ref)
.with_cmd(["/bin/sh", "-eu", "-c", script.as_str()])
.with_net(draupnir::NetMode::Host)
.with_mem_limit_mb(4096);
spec.validate().map_err(|e| format!("clean-install spec: {e}"))?;
let opts = draupnir::container::RunOptions::bounded(
std::time::Duration::from_secs(1200),
std::time::Duration::from_secs(1),
);
let binds = WheelSource::Local.binds(root)?;
println!("ops: pip install {} {} in a clean container (Streamlit from PyPI)…", name(&facett_whl), name(&demo_whl));
let out = draupnir::container::ContainerBoot::new()
.run_to_completion_with_binds(&spec, &opts, &binds)
.map_err(|e| format!("clean-install container: {e}"))?;
let lines: Vec<String> = out.stdout.iter().chain(out.stderr.iter()).cloned().collect();
let marker = |m: &str| -> Option<serde_json::Value> {
lines.iter().find_map(|l| l.find(m).and_then(|i| serde_json::from_str(l[i + m.len()..].trim()).ok()))
};
let (Some(alone), Some(demo)) = (marker(ALONE_MARKER), marker(DEMO_MARKER)) else {
let tail: Vec<&String> = lines.iter().rev().take(40).collect::<Vec<_>>().into_iter().rev().collect();
return Err(format!(
"the clean-install container exited {:?} without both report lines. Last lines:\n{}",
out.exit_code,
tail.iter().map(|l| format!(" {}", l.trim_end())).collect::<Vec<_>>().join("\n")
));
};
let c = CleanInstall { alone, demo };
judge_clean_install(&c, facett_build_thing::PYPI_VERSION, components_in_wheel(&facett_whl)?)?;
Ok((c, lines))
}
/// **Render the holger pip-index config for THIS checkout.**
///
/// `pkg-facet-wrapped/holger-facett-pip.ron` used to hard-code an absolute
/// `$HOME/git/facett/dist/holger-pip/…`, which is wrong in two ways at once: it is
/// one person's home directory, and it names the SHARED checkout (the template now
/// carries the variable `${FACETT_CHECKOUT}`, replaced here). A lane
/// working from a worktree that ran `publish-holger` would pack its archive under its
/// own `dist/` and then serve the shared tree's stale one — or, worse, be tempted to
/// write into a checkout another lane is building from. Three lanes lost time to
/// exactly that class of thing on 2026-09-05.
///
/// So the config is GENERATED from the repo root the xtask is actually running in,
/// and the checked-in `.ron` becomes the template. The rendered file goes under
/// `target/`, which is per-worktree by construction.
pub fn render_index_config(root: &Path) -> Result<PathBuf, String> {
let template = root.join("pkg-facet-wrapped/holger-facett-pip.ron");
let text = std::fs::read_to_string(&template)
.map_err(|e| format!("read {}: {e}", template.display()))?;
// The one path in the template, replaced with this checkout's. Substring
// replacement rather than a RON parse: the template is ours, the token is exact,
// and a mis-replacement is caught immediately below by re-reading the result.
const BAKED: &str = "${FACETT_CHECKOUT}/dist/holger-pip";
let mine = root.join("dist/holger-pip");
let rendered = text.replace(BAKED, &mine.display().to_string());
// The check is that the replacement HAPPENED — i.e. the rendered text names THIS
// checkout — not merely that the old path is absent. Those differ exactly when the
// template stops carrying the baked token: then nothing is replaced, the old path
// is trivially absent, and an "absence" check passes while the config points
// wherever the edited template happened to point. That is the bug this function
// exists to prevent, so it must not be the way this function fails.
if !rendered.contains(&mine.display().to_string()) {
return Err(format!(
"{} does not name this checkout after rendering for {} — the template no \
longer carries `{BAKED}`, so nothing was rewritten and the index would \
serve whatever path the template names instead",
template.display(),
root.display()
));
}
let out = root.join("target").join("pip-build").join("holger-facett-pip.ron");
std::fs::create_dir_all(out.parent().unwrap())
.map_err(|e| format!("create {}: {e}", out.parent().unwrap().display()))?;
std::fs::write(&out, rendered).map_err(|e| format!("write {}: {e}", out.display()))?;
Ok(out)
}