Contributing
Contributions are welcome. Start with the contributor guidelines, which cover commit sign-off (DCO), employer contributions, and how reviews work.
Every PR needs a release-note block in its description (or NONE for changes
with no user-facing impact) — CI validates this automatically and comments on
the PR if the block is missing or malformed.
Adding a beta feature
When a new capability is not yet ready for all users, gate it behind a Cargo feature and a runtime flag. The full lifecycle:
private branch / draft PR
↓
Cargo feature: beta-<name> ← compiled in, invisible by default
↓
mur beta enable <name> ← user opts into the public beta
↓
graduate: remove is_enabled() check, remove #[cfg] guards
↓
(optional) remove Cargo feature if it is now core behaviour
Step 1 — Add a Cargo feature
In crates/murmur-cli/Cargo.toml:
[features]
default = []
beta = []
beta-blueprint = ["beta"] # ← add one line per new feature
Step 2 — Register in the feature list
In crates/murmur-cli/src/beta.rs, add a block to compiled_beta_features():
#[cfg(feature = "beta-blueprint")]
features.push(BetaFeature {
name: "blueprint",
description: "Blueprint file support in taskflow stage slots (preview)",
});
Step 3 — Gate the code
Wrap any new commands, handlers, or registrations in #[cfg(feature = "beta-blueprint")].
For runtime visibility, also check the enabled flag before registering subcommands in main.rs:
#[cfg(feature = "beta-blueprint")]
{
let beta_cfg = load_mur_config().map(|c| c.beta).unwrap_or_default();
if beta_cfg.is_enabled("blueprint") {
// register the subcommand
}
}
Step 4 — Graduate to stable
When the feature is ready for all users:
- Remove the
#[cfg(feature = "beta-blueprint")]guards frommain.rsand the command file. - Remove the
is_enabled("blueprint")check — always register the subcommand. - Remove the entry from
compiled_beta_features()inbeta.rs. - Optionally remove the
beta-blueprintCargo feature (keeping it as a no-op is harmless).
Running tests
Run the unit tests plus the integration suites for the area you touched before submitting:
cargo test --workspace --lib --bins # unit tests across all crates
cargo test -p murmur-cli --test build # one integration suite (see crates/murmur-cli/tests/)
cargo test --workspace --no-fail-fast # the full suite
Run the full suite with --no-fail-fast. Plain cargo test stops at the first test binary that
fails and never runs the binaries after it, and its output reports the failure without saying what
it skipped — murmur-cli alone has over a hundred integration-test binaries. The last lines of a
run tell the two apart:
| The run ends with | Meaning |
|---|---|
error: N targets failed: (error: 1 target failed: for one) and one `-p <crate> --test <name>` line per failed target |
Every test binary ran; that list is the whole set of failures |
error: test failed, to rerun pass … with no targets failed list |
The run stopped at that binary; the binaries after it never ran |
To count how many binaries a stopped run never reached, compare the binaries a full run executes
with the ones the stopped run started, using the same package selection as the stopped run. Cargo
prints its Running lines to standard error, so save a run with 2>&1, for example
cargo test --workspace 2>&1 | tee stopped-run.log:
cargo test --workspace --no-run 2>&1 | grep -c '^ *Executable' # binaries a full run executes
grep -c '^ *Running' stopped-run.log # binaries the stopped run started
The difference is the number of binaries the stopped run did not reach.
Every PR that changes behavior must include tests — see the testing section of the
contributor guidelines
for where tests go and how much coverage is expected. No test reads a default-artifacts
checkout. A contract that repository owns belongs in a test there, where the change that breaks it
is the change that reddens it.
CI runs the full workspace suite, including both beta CLI surfaces, on every push and pull
request. Tests that need a host able to isolate a capsule — a delegated cgroup v2 scope, or a
capsule network namespace — skip themselves with a [SKIP-HOST]-prefixed line instead of failing,
since a CI runner provides neither; the job's step summary reports how many tests were skipped for
that reason and points at
docs/content/reference/resource-limits-manual-verification.md, which covers them by hand.
Changing a WIT interface
The repository commits compiled WASM components (test fixtures and the escape-conformance
driver), each built against a WIT directory under crates/capsule-runtime/wit/. A component built
against an interface version the tree no longer declares stops loading, so a version bump has to
rebuild every component that uses the bumped package.
components.list
names each committed component, the WIT directory it builds against, and the crate it is built
from.
Run these from the repository root:
| Command | What it does |
|---|---|
scripts/rebuild-components.sh --check |
Writes nothing. Names every component whose murmur:* interface versions differ from the ones its WIT directory declares, with the command that rebuilds it, and exits 1 if any differ |
scripts/rebuild-components.sh <name> |
Rebuilds one component from source and copies it over its committed file |
scripts/rebuild-components.sh |
Rebuilds every component the list does not mark frozen: |
Rebuilding needs the wasm32-wasip2 target (rustup target add wasm32-wasip2). The check does
not need it. Commit the files a rebuild rewrites. A rebuild can change a component's bytes without
changing its interfaces, for example when you build with a different Rust toolchain, so to keep
the diff small, commit only the components --check named before the rebuild.
The same check runs in cargo test --workspace, and as the wit-components CI job on any pull
request that changes crates/capsule-runtime/wit/. Both also fail on a committed .wasm under
crates/ that no line in components.list names, so a new committed component needs a line
there. The columns are described at the top of the file. The full bump procedure, including the
default-artifacts rebuild, is in
wit/VERSIONING.md.
Optional allocator features
A default build of mur uses the system allocator. Two optional features on murmur-cli swap in
a different one, and a third builds the tool that compares them:
| Feature | Effect |
|---|---|
jemalloc |
mur allocates through jemalloc |
mimalloc |
mur allocates through mimalloc, unless jemalloc is also on, which wins |
alloc-bench |
Builds the mur-alloc-bench binary, which times component compilation and the runtime's per-session allocation churn |
Every build of mur already needs a C compiler, because some of its dependencies ship C and
assembly. jemalloc adds one more tool: it configures and builds jemalloc from source, so
--features jemalloc and any --all-features build also need make on PATH.
To compare the three allocators on your own machine:
scripts/alloc-bench.sh # 15 rounds, ~10 minutes with a warm target directory
scripts/alloc-bench.sh --cpu 3 --rounds 21 # different core, more rounds
The script builds all three configurations into separate target directories, runs them one round
each per pass pinned to a single core with taskset, and prints a table of median component
compile time and median per-session allocation time with the per-round spread. Raw per-round
figures are left at target/alloc-bench/rounds.tsv. The measurement resolves differences of a few
percent, so run it on an otherwise idle machine: when the system row's own min/max spans more than
about 10%, the run is measuring the scheduler rather than the allocator.
Formatting and lints
CI also runs a lint job on every push and pull request:
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
Run both locally before submitting a PR. --all-features includes the beta CLI surfaces
(topology_cmd, deploy_cmd) in the clippy pass, so a change gated behind a beta feature is
still checked; it also turns on the allocator features above. An #[allow(...)] is acceptable
when the lint's default judgment is wrong at that specific site, but it needs a comment saying why
— a bare #[allow(...)] with no justification, or a crate-level #![allow(...)], will not pass
review.