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-ociis 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 incmdremove the need for podman's-w. - Build green both ways;
build_pipeline_is_real_and_honest_without_podmanis 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/…), whereaspodman cp {cid}:/out/.drops the contents of/outstraight intointo.copy_tree_contentsflattensout/'s contents intointoso the two engines are byte-identical — proven bycopy_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).