Skip to main content

OciImageBuilder

Struct OciImageBuilder 

Source
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

Source

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:...").

Source

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()?;
Source

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.

Source

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()?;
Source

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).

Source

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()?;

Like Self::with_mount but lets the caller pick a [SymlinkPolicy] other than the default Opaque.

Source

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()?;
Source

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.

Source

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.

Source

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.

Source

pub fn with_pull_policy(self, policy: PullPolicy) -> Self

Cache + registry policy. See PullPolicy.

Source

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.

Source

pub fn with_guest_port(self, port: u16) -> Self

Override the guest service port the bake waits for as the readiness signal. Default 80.

Source

pub fn with_cmd<I, S>(self, cmd: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

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()?;
Source

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.

Source

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.

Source

pub fn with_warmup<F>(self, warmup: F) -> Self
where F: FnOnce(&Vm) -> Result<(), Error> + Send + 'static,

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")
)?;
Source

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.

Source

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=true you’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).

Source

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.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more