Gunnar

nordisk/Skidbladnir

.nornir/msi-oci-routing.md

<!-- nornir:class: reasoning -->
# MSI build → draupnir's ONE OCI engine (killing a podman `-v` twin)

**dup-audit P1.** Skidbladnir shelled `Command::new("podman") -v …` in two places; the
constellation law is ONE container engine (draupnir's `backend-oci`, pure-Rust bollard over the
podman/Docker REST API), zero shell. draupnir landed `run_to_completion_with_binds` (host `-v`
binds) for exactly this — so the *run-an-image* twin now routes through it.

## What changed
`src/msi.rs` `WixBuilder::build` — the WiX v4 / Wine MSI build is a **`podman run --rm -v
work:/work <image> wine wix.exe build`**, i.e. run-an-existing-image-to-completion with one bind.
That maps 1:1 onto draupnir:

```
WXS render + harvest (staged workdir)          ── unchanged, always real
        │
        ▼
#[cfg(feature = "msi-oci")]                     ── zero-shell path
  BootSpec::container("skidbladnir-wix-msi", image)
     .with_cmd([wine, wix.exe, build, /work/Product.wxs, -o, /work/out.msi])
  ContainerBoot::run_to_completion_with_binds(spec, RunOptions::default(),
                                              ["<workdir>:/work"])   ← draupnir, ONE engine
     └─ exit_code != Some(0) → honest bail (never a fake MSI)
        │
#[cfg(not(feature = "msi-oci"))]                ── lean fallback (kept, honest)
  podman_available() ? Command::new("podman") run : bail("needs podman …")
        │
        ▼
/work/out.msi  ──(bind)──▶  work/out.msi  ──▶  copy to `out`
```

- `msi-oci` is **default-off** (the lean core never pulls draupnir's bollard/tokio); the podman
  subprocess remains only as the feature-off fallback, with an honest message pointing at
  `--features msi-oci`.
- Absolute `/work/...` paths in `cmd` remove the need for podman's `-w`.
- Build green both ways; `build_pipeline_is_real_and_honest_without_podman` is now cfg-aware
  (both paths error honestly + keep the "WXS rendered" stage real).

## Also routed now: `src/pack.rs` (`pack-oci`)
`PodmanBuild::build_into` does `podman **build**` (image build from a rendered Containerfile) +
`podman create` + `podman cp` (extract `/out`). draupnir since grew the missing surface —
`ContainerBoot::build_image` (tars the context → daemon `/build`) and `ContainerBoot::extract_path`
(the `create`+`cp` twin) — so the *second half* of this twin now routes too, behind the
**`pack-oci`** feature (default-off, same lean posture as `msi-oci`).

```
render Containerfile → ctx  +  tag                 ── unchanged, shared
        │
        ▼
#[cfg(feature = "pack-oci")]                        ── zero-shell path
  ContainerBoot::build_image(ctx, "Containerfile", tag)      ← draupnir, ONE engine
  ContainerBoot::extract_path(tag, "/out", tmp)              ← create+cp twin
     └─ extract roots the tar at the basename → tmp/out/…
     └─ copy_tree_contents(tmp/out, into)  ── flatten to the `cp {cid}:/out/.` layout
        (both engines leave `into` BYTE-IDENTICAL → same `walk` manifest)
        │
#[cfg(not(feature = "pack-oci"))]                   ── lean fallback (kept, honest)
  runtime_available() ? podman build/create/cp : bail("needs podman … or --features pack-oci")
        │
        ▼
walk(into) → sorted relative paths (engine-agnostic manifest)
```

- The one subtlety: draupnir's `extract_path("/out", …)` roots its tar at the path **basename**
  (`out/…`), whereas `podman cp {cid}:/out/.` drops the *contents* of `/out` straight into `into`.
  `copy_tree_contents` flattens `out/`'s contents into `into` so the two engines are byte-identical
  — proven by `copy_tree_contents_flattens_extracted_out_dir_into_staging` (pure, no daemon).
- Convenience umbrella `oci = ["msi-oci", "pack-oci"]` routes **both** builds through the one engine.

## Reuse (Law #1)
Zero new container code here — the whole engine is draupnir's; this only swaps the call. Second
engine? none. Shell? none (under the feature).