# Configuration

---

# provium.toml

_Provium / Configuration_

> Every field in provium.toml — the [provium] section (roots, cache_dir, cache_max_size) and the [profiles.<name>] blocks (kernel, initrd, cmdline, guest_os).

`provium.toml` is the only config file Provium reads. The default location is `./provium.toml`; override with `--config <path>`.

The schema has two top-level keys: `[provium]` for runner settings, and `[profiles.<name>]` for per-VM-type tuples. Both are optional individually — an empty `provium.toml` parses cleanly but won't be useful for anything (no profiles means `provium:vm(name, profile)` won't find any profile to look up).

## Minimum useful config

```toml
[provium]
roots = ["tests"]

[profiles.peios]
kernel  = "/path/to/bzImage"
initrd  = "/path/to/initrd.cpio.gz"
cmdline = "console=ttyS0 quiet"
```

This declares one test root and one profile. `provium tests/` will scan `tests/` for `*.test.lua`, and tests can call `provium:vm("name", "peios")`.

## `[provium]` section

### `roots`

```toml
[provium]
roots = ["tests", "vendor/upstream-tests"]
```

| Type | Default | Description |
|---|---|---|
| array of strings | `[]` (empty) | Directories scanned for `*.test.lua` and `*.fixture.lua` files. Also prepended to `package.path` so `require("helper.module")` resolves under any root. |

When `roots` is empty (or unset), `provium <paths>` scans whichever paths you pass on the CLI; if you also omit those, it scans the current directory.

For most projects, set `roots = ["tests"]` (or whichever directory contains your tests) so that:

- `provium` (no arg) walks the test tree.
- `provium fixture build foo` finds `tests/foo.fixture.lua`.
- Tests can `require("helpers.assert_pingable")` and Provium resolves it as `tests/helpers/assert_pingable.lua`.

### `cache_dir`

```toml
[provium]
cache_dir = "/var/cache/provium/fixtures"
```

| Type | Default | Description |
|---|---|---|
| path string | `~/.cache/provium/fixtures` | Where fixture snapshots are stored. |

Useful for:

- Per-machine caches on shared hosts (default is per-user, set to a system-wide path for shared CI runners).
- Fast scratch storage (point at an SSD or tmpfs for build performance).
- Large dedicated cache (point at a partition with more headroom than `~/.cache`).

The directory is created on first build. If the path is unreadable / unwritable at startup, eviction silently skips and the next build attempt errors with the underlying I/O error.

### `cache_max_size`

```toml
[provium]
cache_max_size = "100G"
```

| Type | Default | Description |
|---|---|---|
| string with `K`/`M`/`G`/`T` suffix, or bare bytes | `"20G"` | LRU eviction target. The cache is allowed to grow beyond this between runs; eviction trims at the next `provium` startup. |

When the cache exceeds the cap, Provium sorts entries by access time and deletes oldest until total size is under the cap. Each successful restore bumps the entry's atime so popular fixtures stay hot.

## `[profiles.<name>]` blocks

Each `[profiles.<name>]` block declares one (kernel, initrd, cmdline, guest_os) tuple. Test code looks them up by name: `provium:vm("v", "<name>")`.

You can have any number of profiles; tests pick whichever they need.

### `kernel`

```toml
[profiles.peios]
kernel = "/build/peios/bzImage"
```

| Type | Required | Description |
|---|---|---|
| path string | yes | Path to a bzImage-format kernel image. Booted via QEMU's `-kernel` option. |

Path validation (does the file actually exist?) happens at VM-boot time, not config-load time. This lets a single `provium.toml` be portable across machines that have different kernel layouts.

### `initrd`

```toml
[profiles.peios]
initrd = "/build/peios/initrd.cpio.gz"
```

| Type | Required | Description |
|---|---|---|
| path string | yes | Path to an initramfs. Booted via QEMU's `-initrd`. |

By default, Provium injects the `provium-agent` binary at `/sbin/provium-agent` by concatenating a small overlay cpio onto your initrd at launch (the kernel unpacks concatenated gzip cpios into a single rootfs). The agent boots as PID 1 and forks immediately: the child becomes the vsock listener, the parent execs your initrd's `/init`, which therefore takes over PID 1. In this chained layout the agent deliberately mounts nothing first — your init owns the mount and pivot sequence exactly as it would alone. (Only on an agent-only initrd, with no user `/init`, does the agent perform the pseudo-FS mounts itself.) Userspace runs as it would have on its own; the agent runs alongside as PID 2.

Three consequences:

- **Your initrd doesn't need to know about Provium.** A vanilla distro initramfs, a buildroot image, or a from-scratch cpio with just `/init` will all work.
- **Your `/init` runs as PID 1.** If your tests need PID-1 init duties (signals, child reaping), they happen in your init as before.
- **The merged initrd is content-hash cached** under `<scratch_root>/agent-overlay-cache/<sha256>.cpio.gz`. Subsequent boots of the same `(initrd, overlay)` pair pay nothing.

To opt out (e.g. when your initrd already bundles an agent at the path Provium would set), set `inject_agent = false` on the profile.

### `inject_agent`

```toml
[profiles.peios]
inject_agent = false
```

| Type | Default | Description |
|---|---|---|
| bool | `true` | When `true`, Provium concatenates the agent overlay onto `initrd` at launch and appends `rdinit=/sbin/provium-agent` to the kernel cmdline. Set `false` to use the initrd as-is. |

If your cmdline already pins a different `rdinit=PATH`, the launch errors with a clear conflict message rather than silently overriding. Either remove the conflicting `rdinit=`, or set `inject_agent = false`.

### `agent_overlay_path`

```toml
[profiles.peios]
agent_overlay_path = "/usr/local/share/provium/agent-overlay.cpio.gz"
```

| Type | Default | Description |
|---|---|---|
| path string | unset | Override the path to the agent overlay cpio. Useful for distribution-installed Provium where the overlay isn't co-located with the binary. |

When unset, Provium tries (in order): the `PROVIUM_OVERLAY` env var, then `<provium-binary-dir>/../share/provium/agent-overlay.cpio.gz`, then walks up the binary's directory tree looking for `dist/agent-overlay.cpio.gz` (covers in-development runs from `target/`). Set this field — or the env var — when none of those apply.

### `cmdline`

```toml
[profiles.peios]
cmdline = "console=ttyS0 quiet"
```

| Type | Required | Description |
|---|---|---|
| string | one of `cmdline` / `cmdline_file` | Inline kernel command line. May be empty or omitted **when `cmdline_file` is set** — the two compose. A profile with neither a non-empty `cmdline` nor a `cmdline_file` is rejected with `profile \`<name>\` has empty cmdline and no \`cmdline_file\`; set at least one`. |

`vm:boot({kernel_cmdline = "..."})` overrides the whole command line for one boot.

Provium **always appends `console=ttyS0`** if absent. The kernel happily uses multiple `console=` directives, so any user-set values are preserved alongside. The reason: the QEMU command wires the serial port into Provium's console capture, so the kernel's printk needs to land there for failure diagnostics to be visible. Without `console=ttyS0`, PID 1's `/dev/console` may resolve to a device QEMU doesn't capture, and writes to stderr can fail and crash userspace.

Common additions:

- `loglevel=7` — verbose kernel logging for debugging.
- `nokaslr` — for reproducible kernel addresses in panic dumps.
- `panic=1` — panic immediately rather than hanging on unrecoverable errors.

### `cmdline_file`

```toml
[profiles.peios]
cmdline_file = "../peiso/out/root/boot/cmdline"
cmdline      = "loglevel=7"   # optional; appended after the file
```

| Type | Required | Description |
|---|---|---|
| path string | one of `cmdline` / `cmdline_file` | Path to a file whose contents are the **base** command line — typically an image builder's generated `cmdline`. Read at VM-boot time; resolved relative to the current directory, like `kernel`/`initrd`. |

All whitespace in the file — including newlines — is collapsed to single spaces, then the inline `cmdline` (if any) is appended **after**. Because the kernel applies last-wins semantics to most repeated parameters (`init=`, `loglevel=`, …), an inline token overrides the file's value for those.

The point is to stop the command line from drifting. If a builder bakes `init=/usr/bin/protoinit` into the image's cmdline and you hand-copy that into `cmdline`, the two silently diverge the next time the builder changes. Pointing `cmdline_file` at the builder's output means Provium reads the authoritative value every boot. See [Dynamic profiles](/provium/configuration/dynamic-profiles.md#cmdline-from-the-builder).

### `guest_os`

```toml
[profiles.peios]
guest_os = "peios"
```

| Type | Default | Description |
|---|---|---|
| string | `"peios"` | Guest OS identifier. v1 only supports `"peios"`. |

Validated at config load time. A profile with `guest_os = "linux"` (or any other value) errors with `profile \`<name>\` has guest_os = \`linux\`, only \`peios\` is supported in v1`.

The field exists to future-proof the schema for other ports — when there's a Windows agent, this would be how a profile selects it.

### `build`

```toml
[profiles.peios]
build        = "peiso build manifests/peios.toml --out {out}"
kernel       = "{out}/root/usr/lib/modules/<release>/vmlinuz-<release>"
initrd       = "{out}/initrd.img"
cmdline_file = "{out}/root/boot/cmdline"
```

| Type | Default | Description |
|---|---|---|
| string | unset | Shell command (run with `sh -c`) that produces this profile's boot artifacts. Runs **once before any VM boots**. A non-zero exit aborts the run. |

The literal token `{out}` — in `build` and in the path fields — expands to this profile's build-output directory (see `build_out`), so the command's `--out` and the `kernel`/`initrd`/`cmdline_file` Provium later reads are the same path and cannot drift.

Provium tracks no staleness: the command runs every invocation. Making rebuilds cheap when nothing changed is the builder's job, not Provium's. Skip the hook with `--no-build`, or run it without booting via `provium prepare`. Full treatment on [Dynamic profiles](/provium/configuration/dynamic-profiles.md).

### `build_out`

```toml
[profiles.peios]
build_out = "/tmp/provium-builds/peios"
```

| Type | Default | Description |
|---|---|---|
| path string | `$XDG_CACHE_HOME/provium/builds/<profile>/` | The directory `{out}` expands to. |

When unset, the default base is resolved like the fixture cache — `$PROVIUM_BUILD_DIR`, then `$XDG_CACHE_HOME/provium/builds`, then `~/.cache/provium/builds`, then `/tmp/provium-builds` — with the profile name appended. Provium creates the directory before running `build` but **never wipes it**: the `build` command owns its contents (so it can keep its own incremental-build caches there).

## Multiple profiles

You can declare any number of `[profiles.<name>]` blocks; tests pick one by name per VM. Patterns for multi-profile setups (debug builds, cross-version testing, feature-flag gating) are on [Profiles](/provium/configuration/profiles.md).

**Important:** the fixture cache key folds in EVERY profile's kernel + initrd identifier. Adding a new profile invalidates the entire fixture cache. This is intentional: a new kernel could behave differently, so existing fixtures are no longer trustworthy. See [Profiles — What happens to the cache when profiles change](/provium/configuration/profiles.md#what-happens-to-the-cache-when-profiles-change).

## Configuration loading

| Step | What happens |
|---|---|
| 1. Read | `provium.toml` is read from `--config <path>` (default `./provium.toml`). |
| 2. Parse | TOML is parsed. Parse errors include the file path. |
| 3. Validate | Each profile is validated (a non-empty `cmdline` **or** a `cmdline_file`; `guest_os = "peios"`). |
| 4. Expand | The `{out}` token in each profile's `build` command and path fields is replaced with that profile's resolved build-output directory. |
| 5. Use | The `Config` struct is wrapped in an `Arc` and passed to every file runner. |

Errors:

| Error | Cause |
|---|---|
| `read \`<path>\`: <io error>` | File missing or unreadable. |
| `parse \`<path>\`: <toml error>` | Malformed TOML. |
| `invalid config in \`<path>\`: <message>` | Validation failed (empty `cmdline`, unsupported `guest_os`, etc.). |

All three abort the run with exit code `2`.

## Worked example

```toml
[provium]
roots          = ["tests", "internal-tests"]
cache_dir      = "/srv/provium-cache"
cache_max_size = "200G"

[profiles.peios]
kernel   = "/srv/peios-builds/latest/bzImage"
initrd   = "/srv/peios-builds/latest/initrd.cpio.gz"
cmdline  = "console=ttyS0 quiet panic=1"
guest_os = "peios"

[profiles.peios-mainline]
kernel   = "/srv/peios-builds/mainline/bzImage"
initrd   = "/srv/peios-builds/mainline/initrd.cpio.gz"
cmdline  = "console=ttyS0 quiet panic=1"
guest_os = "peios"

[profiles.peios-debug]
kernel   = "/srv/peios-builds/debug/bzImage"
initrd   = "/srv/peios-builds/debug/initrd.cpio.gz"
cmdline  = "console=ttyS0 debug loglevel=7 nokaslr panic=1"
guest_os = "peios"
```

This declares three profiles (production, mainline, debug), centralises the cache on a 200 GiB dedicated mount, and scans both `tests/` and `internal-tests/`.

## See also

- [Project structure](/provium/getting-started/project-structure.md) — the broader project layout.
- [VM reference](/provium/reference/vm.md) — `boot_opts.kernel_cmdline` overrides.
- [Profiles](/provium/configuration/profiles.md) — patterns for using multiple profiles.

---

# Profiles

_Provium / Configuration_

> Patterns for multiple profiles — kernel-version testing, debug builds, feature-flag gating, and what happens to the fixture cache when profiles change.

A profile is one named `(kernel, initrd, cmdline, guest_os)` tuple in `provium.toml`. Tests pick a profile by name when they create a VM. This page covers the practical patterns.

The configuration field reference is on [provium.toml](/provium/configuration/provium-toml.md). A profile can also *build* its own artifacts before booting rather than pointing at files already on disk — see [Dynamic profiles](/provium/configuration/dynamic-profiles.md).

## When you need more than one profile

For most projects, one profile is enough — `peios`, pointing at the latest build. You'd add more when:

- You're testing across kernel versions (`peios-stable`, `peios-mainline`).
- You want a debug build available for diagnosing failures (`peios-debug`).
- You're gating tests on a feature flag in the kernel (`peios-prefeatx`, `peios-postfeatx`).
- You're testing a port to a new architecture (`peios-arm64`).

## Declaring multiple profiles

```toml
[profiles.peios]
kernel   = "/build/peios/bzImage"
initrd   = "/build/peios/initrd.cpio.gz"
cmdline  = "console=ttyS0 quiet"

[profiles.peios-debug]
kernel   = "/build/peios-debug/bzImage"
initrd   = "/build/peios-debug/initrd.cpio.gz"
cmdline  = "console=ttyS0 debug loglevel=7 nokaslr"

[profiles.peios-stable]
kernel   = "/build/peios-stable/bzImage"
initrd   = "/build/peios-stable/initrd.cpio.gz"
cmdline  = "console=ttyS0 quiet"
```

Tests pick:

```lua
provium:vm("v",        "peios")
provium:vm("debug-v",  "peios-debug")
provium:vm("stable-v", "peios-stable")
```

A test can mix profiles in the same file — useful for compatibility testing:

```lua
test("stable client can talk to mainline server", function(t)
    local lan    = provium:bridge("lan")
    local server = provium:vm("server", "peios"):boot()
    local client = provium:vm("client", "peios-stable"):boot()
    lan:attach({server, client})
    server:run("ip addr add 10.0.0.1/24 dev eth0 && ip link set eth0 up")
    client:run("ip addr add 10.0.0.2/24 dev eth0 && ip link set eth0 up")

    -- Run a real protocol against the mismatched pair.
    server:run_async("python3", {args = {"-m", "http.server", "8080"}})
    wait_until(function()
        return server:run("ss -ltn | grep :8080"):ok()
    end, {timeout = "5s"})
    client:run("curl -s http://10.0.0.1:8080/"):assert_ok()
end)
```

## Profile choice patterns

### Default vs debug

The most common split: a fast `peios` build for the default loop, and a `peios-debug` build with `loglevel=7`, `nokaslr`, and (probably) `KASAN`/`UBSAN` enabled. Use the debug profile when reproducing a kernel bug:

```lua
test("debug build reveals the leak", {tags = {"slow", "debug"}}, function(t)
    local vm = provium:vm("v", "peios-debug"):boot()
    vm:run("…workload that leaks…")
    local r = vm:run("dmesg | grep -i 'BUG:'")
    -- KASAN / UBSAN reports show up here in the debug build.
end)
```

### Cross-version compatibility

Two profiles, one per release line. Ship a small handful of cross-version tests:

```lua
test("rolling upgrade: stable client + mainline server", function(t)
    -- … as above …
end)

test("rolling upgrade: mainline client + stable server", function(t)
    -- … inverse …
end)
```

Tag these `cross-version` and gate via `--no-tag cross-version` in the dev loop. CI runs them on a nightly cadence with `--include-slow`.

### Feature-flag gating

Two profiles built from the same source with one feature toggled:

```toml
[profiles.peios-prefeatx]
kernel  = "/build/peios-prefeatx/bzImage"   # FEATX disabled
# …

[profiles.peios-postfeatx]
kernel  = "/build/peios-postfeatx/bzImage"  # FEATX enabled
# …
```

Tests that exercise FEATX-specific behaviour pick `peios-postfeatx`; regression tests run against both.

```lua
test("featx changes API behaviour", function(t)
    local before = provium:vm("v1", "peios-prefeatx"):boot()
    local after  = provium:vm("v2", "peios-postfeatx"):boot()

    local r1 = before:run("…")
    local r2 = after:run("…")
    t:assert_neq(r1.stdout, r2.stdout)
end)
```

## Per-VM cmdline overrides

The profile's `cmdline` is the default; `kernel_cmdline` in `vm:boot(opts)` overrides it for one VM:

```lua
local vm = provium:vm("v", "peios")
vm:boot({kernel_cmdline = "console=ttyS0 quiet maxcpus=1 isolcpus=0"})
```

This is useful for one-off testing of cmdline-sensitive features. For cmdline configurations you use repeatedly, declare a dedicated profile.

## Default profile selection

A few CLI subcommands take an optional profile arg — when omitted, they fall back to "the first profile in `provium.toml` sorted by name":

| Subcommand | Behaviour |
|---|---|
| `provium repl --fixture <path>` (no profile) | First profile by sorted name. |
| `provium fixture build <path>` | Uses the first profile's kernel/initrd as the cache-key kernel inputs. |

> [!WARNING]
> With more than one profile configured, `provium fixture build` folds only that first profile into the key, while the test runner folds every profile — so a pre-warmed entry may not be the one the runner looks up. Pre-warming is reliable on single-profile configs.

To make the default predictable, name your most common profile so it sorts first alphabetically. `aaa-default` is ugly but it works; `peios` is fine when it's your only profile.

## What happens to the cache when profiles change

The fixture cache key folds in the kernel and initrd identifiers of **every profile** in `provium.toml`, sorted by name for determinism (the full key model is on [fixtures and dependencies](/provium/running-tests/fixtures-and-dependencies.md#what-ends-up-in-the-cache-key)). Practical consequences for profile changes specifically:

| Action | Cache effect |
|---|---|
| Edit a kernel image (any profile) | Every fixture invalidates. |
| Edit an initrd image (any profile) | Every fixture invalidates. |
| Add a new profile | Every fixture invalidates (the new profile's kernel/initrd are folded in). |
| Remove a profile | Every fixture invalidates. |
| Rename a profile | Usually invalidates everything (the fold order follows profile names). |
| Edit `cmdline` on a profile | Cache is unaffected (cmdline isn't in the key). |
| Change `cache_dir` | Cache is unaffected — the new dir is just empty. |

This is deliberately conservative. A new profile means a new kernel could behave differently, so the existing fixtures might be subtly stale. Rather than guess, the harness rebuilds.

If you need to add a profile without invalidating the cache, you can't. Either accept the rebuild or use a sibling `provium.toml` with `--config alt-config.toml`.

## Multi-arch profiles (preview)

In v1, the QemuVmm backend invokes `qemu-system-x86_64` exclusively — there's no per-profile arch field yet. To run on aarch64, you'd have to swap the binary at the harness level (not currently exposed).

When multi-arch lands, the profile is the natural place for an `arch = "x86_64"` / `arch = "aarch64"` field.

## See also

- [provium.toml reference](/provium/configuration/provium-toml.md) — every field.
- [VMs and profiles](/provium/writing-tests/vms-and-profiles.md) — `provium:vm(name, profile, opts?)`.
- [Fixtures and dependencies](/provium/running-tests/fixtures-and-dependencies.md) — what triggers a fixture rebuild.

---

# Dynamic profiles

_Provium / Configuration_

> Make a profile build its own image before the VM boots — the build command, the {out} token, cmdline from the builder, and prepare / --no-build.

A static profile points at artifacts that already exist on disk:

```toml
[profiles.peios]
kernel  = "/build/peios/bzImage"
initrd  = "/build/peios/initrd.cpio.gz"
cmdline = "console=ttyS0 quiet"
```

That leaves a gap. You build the image with one tool, then run Provium with another, and nothing connects the two — so it's easy to test last week's image because you forgot to rebuild. A *dynamic* profile closes the gap: the profile owns a `build` command, Provium runs it, and the VM boots the fresh result.

This is the same idea as [`cmdline_file`](/provium/configuration/provium-toml.md#cmdline-file) — let the test consume the authoritative build instead of a hand-copied snapshot — applied to the whole image. This page covers both.

## Cmdline from the builder

Start with the smaller case. Image builders usually emit a kernel command line of their own — `peiso`, for instance, writes one to `out/root/boot/cmdline`. If you copy that string into your profile's `cmdline`, the two drift the moment the builder changes it (a new `init=`, a new console). Point `cmdline_file` at the builder's file instead:

```toml
[profiles.peios]
kernel       = "../peiso/out/root/usr/lib/modules/<release>/vmlinuz-<release>"
initrd       = "../peiso/out/initrd.img"
cmdline_file = "../peiso/out/root/boot/cmdline"
```

Provium reads the file at boot, collapses its whitespace to single spaces, and uses it as the command line. You can still add tokens inline — they're appended *after* the file, so they win for last-wins kernel parameters:

```toml
cmdline_file = "../peiso/out/root/boot/cmdline"
cmdline      = "loglevel=7"     # verbose, on top of whatever the builder set
```

No manual copy means nothing to forget to update.

## Building the whole image

The `build` field takes a shell command. Provium runs it — once, before any VM boots — and only then reads the profile's `kernel` / `initrd` / `cmdline_file`:

```toml
[profiles.peios-full]
build        = "peiso build manifests/peios-full.toml --out {out}"
kernel       = "{out}/root/usr/lib/modules/<release>/vmlinuz-<release>"
initrd       = "{out}/initrd.img"
cmdline_file = "{out}/root/boot/cmdline"
```

Now `provium` builds `peios-full` before the suite runs, and tests boot exactly what was just built:

```lua
local vm = provium:vm("v", "peios-full"):boot()
```

The command runs with `sh -c`, from Provium's working directory, inheriting your environment and streaming its output straight to the terminal. Reference inputs (the manifest above) relative to the suite so the suite stays portable across checkouts — there is deliberately no "build working directory" knob to tie it to one machine's layout.

## `{out}`: one directory, no drift

Look again at the example: `{out}` appears in the `build` command's `--out` *and* in every path field. That's the point. `{out}` expands — once, when the config loads — to this profile's build-output directory, so the place the build *writes* and the places Provium *reads* are guaranteed to be the same directory. You can't update one and forget the other, because there's only one.

Where does `{out}` point? By default, a per-profile directory under the Provium cache:

```
$XDG_CACHE_HOME/provium/builds/<profile>/     # e.g. ~/.cache/provium/builds/peios-full/
```

Set [`build_out`](/provium/configuration/provium-toml.md#build-out) to pin it somewhere specific — but you still only write the path once:

```toml
[profiles.peios-full]
build_out    = "/tmp/provium-builds/peios-full"
build        = "peiso build manifests/peios-full.toml --out {out}"
kernel       = "{out}/root/usr/lib/modules/<release>/vmlinuz-<release>"
# …
```

Provium creates the directory before running the build, but never wipes it — the build command owns its contents, so it's free to keep its own incremental-build caches there.

## When the build runs

| You run | What gets built |
|---|---|
| `provium` (the test suite) | Every profile that declares a `build`, up front, before the scheduler starts. |
| `provium console <profile>` | Just that profile. |
| `provium repl <profile>` | Just that profile. |
| `provium prepare [profile]` | That profile, or — with no argument — every profile with a `build`. No VM boots. |

The test runner builds *all* dynamic profiles rather than only the ones a run will touch, because tests choose their profiles at runtime from Lua (`provium:vm(name, profile)`) — Provium can't know in advance which a given run needs. If you keep several dynamic profiles and want to build only one, use `provium prepare <profile>` followed by `provium --no-build`.

Two controls shape this:

- **`provium prepare`** builds without booting. It even skips the pre-flight checks (`/dev/kvm`, networking, `CAP_NET_ADMIN`), so you can build an image on a machine that isn't set up to run VMs.
- **`provium --no-build`** skips the hook entirely for one run — for when you know the artifacts are already current and just want to boot.

## No staleness — that's the builder's job

Provium does not try to decide whether your image is up to date. It runs the `build` command **every** invocation and trusts the builder to be cheap when nothing has changed. Tracking inputs, hashing sources, and skipping unnecessary work is what build tools are for; reimplementing that inside a test harness would only get it subtly wrong.

The practical consequence: if your builder does a full rebuild every time, `provium` pays that cost every time. That's usually fine — Provium tends to be the *last* gate you run, where a clean build is what you want anyway — but if it bites, make the builder incremental (or use `--no-build` between source changes). Don't expect Provium to shortcut it.

## Fixtures rebuild when the image changes

Provium's [fixture cache](/provium/running-tests/fixtures-and-dependencies.md) keys each snapshot partly on the kernel and initrd's path, size, and modification time. A dynamic profile rebuilds its image every run, which usually changes those timestamps — so fixtures captured against the old image are treated as stale and rebuilt. That's correct (a freshly built kernel can behave differently), but it does mean a dynamic profile plus a large fixture tree pays a fixture rebuild each run on top of the image build. If that's too slow for the inner loop, `--no-build` keeps the image — and therefore its fixtures — warm between source changes.

## Failures abort — never fall through

A `build` command that exits non-zero stops the run:

```
provium: building profile `peios-full` → /home/you/.cache/provium/builds/peios-full
… builder output …
provium: profile `peios-full`: build command failed (exit status: 1)
```

This matters because a half-finished build often leaves the *previous* run's artifacts in place — they'd pass an existence check and boot cleanly, and you'd be testing a stale image while believing the build succeeded. Provium refuses to do that: a failed build is a failed run (exit code `2`), full stop.

## Worked example

A self-contained suite that builds its own image. The manifest lives in the suite, so a fresh clone can `provium` with nothing else set up:

```toml
# test-suite/provium.toml
[provium]
roots = ["tests"]

[profiles.peios-full]
build        = "peiso build manifests/peios-full.toml --out {out}"
kernel       = "{out}/root/usr/lib/modules/<release>/vmlinuz-<release>"
initrd       = "{out}/initrd.img"
cmdline_file = "{out}/root/boot/cmdline"
```

```
test-suite/
├── provium.toml
├── manifests/
│   └── peios-full.toml        # the image recipe, versioned with the tests
└── tests/
    └── smoke.test.lua
```

Day-to-day:

```bash
provium                       # build the image, then run the whole suite
provium --no-build            # skip the build; boot the last-built image
provium prepare               # build the image, don't run anything
provium console peios-full    # build, then drop to the guest's console
```

## See also

- [provium.toml reference](/provium/configuration/provium-toml.md) — the `build`, `build_out`, and `cmdline_file` fields.
- [Profiles](/provium/configuration/profiles.md) — patterns for static and multi-profile setups.
- [CLI reference](/provium/reference/cli.md) — `provium prepare` and `--no-build`.
