pub struct OciImageBuilder { /* private fields */ }Expand description
Every setter that affects the workload’s behavior (env, cmd,
memory, guest_port) is part of the bake’s input fingerprint:
changing it forces a re-bake and produces a different snapshot.
Use distinct with_name values if you want side-by-side
snapshots for the same image ref with different configs.
Implementations§
Source§impl OciImageBuilder
impl OciImageBuilder
Sourcepub fn new(image_ref: impl Into<String>) -> Self
pub fn new(image_ref: impl Into<String>) -> Self
Start a new builder for image_ref (e.g. "nginx:1.27-alpine",
"ghcr.io/owner/image@sha256:...").
Sourcepub fn with_platform(self, platform: impl Into<String>) -> Self
pub fn with_platform(self, platform: impl Into<String>) -> Self
Set the target platform for the image pull, in Docker
--platform form. Two values are supported:
"linux/arm64"(default) — native arm64 path, full perf."linux/amd64"— pull the amd64 variant from a multi- arch index; auto-mount Apple’s Rosetta runtime share at/run/rosetta; register binfmt_misc so amd64 ELFs run via Rosetta translation. Requires Rosetta to be installed on the host (softwareupdate --install-rosetta).
let img = Image::builder("mcr.microsoft.com/playwright:v1.59.1-jammy")
.with_platform("linux/amd64")
.with_memory_mib(4096)
.build()?;Sourcepub fn with_extra_file(
self,
host_path: impl Into<PathBuf>,
guest_path: impl Into<String>,
) -> Self
pub fn with_extra_file( self, host_path: impl Into<PathBuf>, guest_path: impl Into<String>, ) -> Self
Stage host_path into the snapshot’s delta layer at
guest_path. The file appears at guest_path inside the
guest’s root filesystem after restore. Folded into the
bake’s content hash so changing the host file invalidates
the cached snapshot.
Used (e.g.) to ship supermachine-smpark.ko so init-oci
can finit_module it on boot for multi-vCPU snapshot
support.
Sourcepub fn with_oci_archive(self, archive_path: impl Into<PathBuf>) -> Self
pub fn with_oci_archive(self, archive_path: impl Into<PathBuf>) -> Self
Override where the image bytes come from. Default (when not
called) treats the constructor’s image_ref as a registry
reference and pulls from Docker Hub or the registry encoded
in the ref.
Internally the bake driver also accepts image_ref directly
in the prefixed forms oci-archive:/path and oci-layout:/path;
this method is the structured convenience: the user keeps
image_ref as a logical identifier (used to derive the
snapshot dir name) and points at a local source separately.
let img = Image::builder("shopify-test-sm:latest")
.with_oci_archive("/tmp/shopify.tar")
.build()?;Sourcepub fn with_oci_layout(self, layout_dir: impl Into<PathBuf>) -> Self
pub fn with_oci_layout(self, layout_dir: impl Into<PathBuf>) -> Self
Like with_oci_archive but points at an OCI layout DIRECTORY
(the un-tar’d form, with index.json + oci-layout +
blobs/sha256/... at the top level).
Sourcepub fn with_mount(
self,
host_path: impl Into<PathBuf>,
guest_tag: impl Into<String>,
guest_path: impl Into<String>,
) -> Self
pub fn with_mount( self, host_path: impl Into<PathBuf>, guest_tag: impl Into<String>, guest_path: impl Into<String>, ) -> Self
Expose a host directory to the guest via virtio-fs (with DAX).
The guest mounts it inside init-oci as
mount -t virtiofs <tag> <target> — by convention init-oci
mounts each declared tag at /mnt/<tag> and bind-mounts into
the workload’s filesystem if a per-image policy says so.
Reads from the guest land in the host’s page cache (DAX-mapped
via hv_vm_map; validated by spike 22 to be zero-copy + shared
across VMs that mount the same host path). The mount is added
to the bake’s input hash so changing host_path invalidates the
cached snapshot.
let img = Image::builder("node:22-alpine")
.with_mount("/Users/me/myapp", "myapp", "/workspace")
.with_cmd(["node", "/workspace/index.js"])
.build()?;Sourcepub fn with_mount_symlinks(
self,
host_path: impl Into<PathBuf>,
guest_tag: impl Into<String>,
guest_path: impl Into<String>,
symlinks: SymlinkPolicy,
) -> Self
pub fn with_mount_symlinks( self, host_path: impl Into<PathBuf>, guest_tag: impl Into<String>, guest_path: impl Into<String>, symlinks: SymlinkPolicy, ) -> Self
Like Self::with_mount but lets the caller pick a
[SymlinkPolicy] other than the default Opaque.
Sourcepub fn with_volume(self, spec: VolumeSpec) -> Self
pub fn with_volume(self, spec: VolumeSpec) -> Self
Attach a writable virtio-blk volume backing host_path
(created sparse if missing, sized to spec.size_bytes) at
spec.guest_path inside the guest. Use for dependency caches
and other write-heavy paths where you’d otherwise pay the
per-file FUSE round-trip cost of with_mount.
§Durability contract (both backends)
Volume writes become durable at snapshot capture: the bake
(boot + warmup) writes the volume’s canonical content, and every
capture records the volume’s at-capture bytes alongside the
snapshot (macOS: a clonefile “pristine”; KVM: a materialized
volumes/<i>.img) so restores always see content that matches
the captured ext4-in-RAM state.
Writes made during a pool cycle are ephemeral by design:
each acquired VM works on an isolated copy-on-write view
(macOS: per-worker temp clone; KVM: MAP_PRIVATE pages reset in
O(1) on release), so the next acquire sees capture-time bytes
again — never another cycle’s half-written state. To persist a
cycle’s volume writes, snapshot the VM (vm.snapshot(...))
before releasing it.
let img = Image::builder("node:22-alpine")
.with_volume(VolumeSpec::new("/var/cache/sm/node_modules.img",
"/work/node_modules"))
.build()?;Sourcepub fn with_extra_kernel_arg(self, arg: impl Into<String>) -> Self
pub fn with_extra_kernel_arg(self, arg: impl Into<String>) -> Self
Append one extra token to the kernel cmdline. Tokens go
after supermachine’s own defaults (earlycon, console,
tsi_hijack, etc.) but before the per-bake tsi_token.
Power-user / testing escape hatch — e.g. pass
"init=/nope" in a test to deliberately trigger an
early-init kernel panic, or "panic=1" to make the
kernel panic on any warning. Folded into the bake’s input
hash so changing this re-bakes.
Sourcepub fn with_vcpus(self, vcpus: u32) -> Self
pub fn with_vcpus(self, vcpus: u32) -> Self
Override the number of vCPUs the snapshot is baked with.
Default 1. Multi-vCPU is opt-in: it lifts sustained
HTTP-serving throughput (single-vCPU is the c=32+
bottleneck) at the cost of slightly higher cold boot and
some snapshot/restore caveats. See
docs/design/concurrency-floor-2026-05-04.md.
Sourcepub fn with_name(self, name: impl Into<String>) -> Self
pub fn with_name(self, name: impl Into<String>) -> Self
Snapshot name. Default: derived from the image ref via
bake::snapshot_name_for_image. Use this when you want
nginx:1.27-alpine baked twice with different configs.
Sourcepub fn with_pull_policy(self, policy: PullPolicy) -> Self
pub fn with_pull_policy(self, policy: PullPolicy) -> Self
Cache + registry policy. See PullPolicy.
Sourcepub fn with_memory_mib(self, mib: u32) -> Self
pub fn with_memory_mib(self, mib: u32) -> Self
Override the bake-time memory budget (MiB). The runtime
memory is set on VmConfig; this is the size the
snapshot is captured at.
Sourcepub fn with_guest_port(self, port: u16) -> Self
pub fn with_guest_port(self, port: u16) -> Self
Override the guest service port the bake waits for as the
readiness signal. Default 80.
Sourcepub fn with_cmd<I, S>(self, cmd: I) -> Self
pub fn with_cmd<I, S>(self, cmd: I) -> Self
Override the image’s CMD. Pass an argv array, same shape
as Docker’s --entrypoint + arguments combined.
let img = Image::builder("python:3.12-alpine")
.with_cmd(["python", "-m", "http.server", "8080"])
.with_guest_port(8080)
.build()?;Sourcepub fn with_env(self, key: impl Into<String>, value: impl Into<String>) -> Self
pub fn with_env(self, key: impl Into<String>, value: impl Into<String>) -> Self
Add an environment variable for the workload. Repeatable.
Mirrors docker run -e KEY=VAL.
Sourcepub fn with_snapshots_dir(self, dir: impl Into<PathBuf>) -> Self
pub fn with_snapshots_dir(self, dir: impl Into<PathBuf>) -> Self
Override the directory snapshots are stored in. Default
is ~/.local/supermachine-snapshots. Use this to keep
per-project snapshot stores isolated from each other.
Sourcepub fn with_warmup<F>(self, warmup: F) -> Self
pub fn with_warmup<F>(self, warmup: F) -> Self
Run a warmup closure once after the bake, then re-snapshot the post-warmup state. Future restores from this image land at the warm state — guest page cache for the workload already populated, so e.g. compiling a small Rust program drops from ~370 ms to ~50–100 ms.
Cached: if the warm snapshot already exists with a
matching warmup tag (see Self::with_warmup_tag), the
warmup is skipped on subsequent builds. Without an
explicit tag, the warmup re-runs whenever the snapshot
is invalidated by other inputs (image_ref, memory, etc.)
— set a tag if you change the closure body and want the
cache to invalidate.
let image = Image::ensure_baked("rust_warm", "rust:1-slim", |b| b
.with_memory_mib(2048)
.with_warmup(|vm| {
vm.write_file("/tmp/probe.rs", b"fn main(){}")?;
vm.exec_builder()
.argv(["sh", "-c", "rustc -O /tmp/probe.rs -o /tmp/probe && /tmp/probe"])
.timeout(Duration::from_secs(60))
.output()?;
Ok(())
})
.with_warmup_tag("v1")
)?;Sourcepub fn with_warmup_tag(self, tag: impl Into<String>) -> Self
pub fn with_warmup_tag(self, tag: impl Into<String>) -> Self
Stable tag for the warmup closure (see Self::with_warmup).
Bump when you change the warmup body and want the previously
cached warm snapshot invalidated.
Sourcepub fn with_listener_required(self) -> Self
pub fn with_listener_required(self) -> Self
For the no-warmup .build() path, wait for the workload’s
in-guest listener to come up before capturing the snapshot
(the v0.4.22 behavior). Without this, v0.4.23+ defaults to
the pre-exec trigger which captures BEFORE the workload
runs — fast bake, but each restore re-execs the workload
fresh (workload’s own startup time is paid per acquire).
When to call this:
- You’re baking a service image (nginx, redis, postgres,
anything that binds a listener and stays up) AND you want
the listener pre-bound at restore-time so first acquire’s
port-traffic works immediately, but you don’t want to pay
the cost of routing through
with_warmup(which adds a warm-snapshot round-trip).
When NOT to call this:
-
You’re using
vm.exec(...)for arbitrary commands and don’t care about the workload’s listener. Default (pre-exec trigger) is faster and gives you the same agent behavior. -
The workload doesn’t bind a listener and doesn’t exit quickly (e.g. a long-running daemon that never serves a port). With
require_listener=trueyou’d time out at--snapshot-after-ms(~7 s default). The pre-exec trigger handles this case in ~150 ms regardless.
No effect when with_warmup is also set — the warmup path
always uses listener-ready (or the warmup callback would run
against a not-yet-ready guest).
Sourcepub fn build(self) -> Result<Image, Error>
pub fn build(self) -> Result<Image, Error>
Run the bake (or reuse a cached snapshot per
with_pull_policy) and return the resulting Image.
Dispatches by backend: KVM (Linux/x86_64) bakes through the self-contained KVM pipeline; HVF (macOS/aarch64) through the worker pipeline. The cache / pull-policy / version-skew handling is shared.