The cargo-mero toolchain
cargo mero is the supported build path for Calimero applications.
It scaffolds an app, compiles it to wasm32-unknown-unknown with the ABI embedded, runs the node-free test suite, and packages a signed .mpk bundle ready for meroctl app install.
One tool covers the whole path from cargo mero new to an installable bundle, replacing the hand-written build.sh / build-bundle.sh scripts each app used to carry.
Install
Section titled “Install”cargo mero is a Cargo subcommand: install the cargo-mero binary and Cargo picks it up as cargo mero.
# From crates.io:cargo install cargo-mero
# From a core checkout (e.g. for unreleased changes):cargo install --path tools/cargo-mero
# Or straight from the repository:cargo install --git https://github.com/calimero-network/core cargo-meroPrebuilt binaries also ship with core’s
GitHub releases and the
Homebrew tap, alongside merod and meroctl.
The build step also needs the wasm32-unknown-unknown target.
cargo mero build auto-installs it when rustup is available, or add it yourself:
rustup target add wasm32-unknown-unknownSize-optimization with wasm-opt -Oz runs automatically on every release build.
The optimizer is compiled into cargo-mero, so there is nothing to install on PATH and the output is reproducible across machines.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
cargo mero new <name> |
Scaffold a new app (Cargo.toml, lib.rs with a TestHost test). |
cargo mero build |
Emit the ABI, compile to wasm32, optimize, and embed the ABI into res/<name>.wasm. |
cargo mero test |
Run the node-free native tests (TestHost unit tests + convergence tests). |
cargo mero bundle |
Build all services and package a signed .mpk bundle. |
cargo mero abi |
ABI utilities: extract, state, types, inspect, embed, diff. |
cargo mero key |
Signing-key utilities: generate, derive-signer-id. |
cargo mero sign |
Sign a bundle manifest.json in place. |
cargo mero guide |
Print the end-to-end workflow guide. |
The workflow
Section titled “The workflow”The five steps below match cargo mero guide (the canonical source; run it any time).
cargo mero new my-app # 1. scaffold an app (state, events, logic, tests)cargo mero build # 2. compile -> wasm-opt -> embed ABI (res/my_app.wasm)cargo mero test # 3. run TestHost + convergence tests (no node needed)cargo mero bundle --dev # 4. build all services, write manifest.json, sign, tar dist/<package>.mpkmeroctl app install --path dist/<package>.mpk ... # 5. install on a node (merod)-
Scaffold.
cargo mero new my-appwrites a crate withCargo.toml(SDK pins, the[package.metadata.calimero]app id, and theapp-release/app-profilingprofiles),src/lib.rs(state, events, logic, and a#[cfg(test)]TestHost test), andtests/converge.rs. There is no build script -cargo mero buildemits the ABI itself.$ cargo mero new my-appScaffolded `my-app` at ./my-appNext steps:cd my-appcargo mero build # compile -> wasm-opt -> embed ABIcargo mero test # TestHost + convergence tests (no node needed) -
Build. Compiles to
wasm32-unknown-unknown, copies the wasm intores/, size-optimizes it withwasm-opt -Oz(release only), and embeds the canonicalized full ABI as the wasmcalimero_abi_v1custom section.$ cargo mero build• target wasm32-unknown-unknown present• building my-app (--profile app-release)• emitted res/abi.json• emitted res/state-schema.json• copied wasm to res/my_app.wasm• optimizing res/my_app.wasm (wasm-opt -Oz)• embedding res/abi.json into res/my_app.wasmArtifacts:
res/<name>.wasm(the built, ABI-embedded wasm) plusres/abi.jsonandres/state-schema.json, both emitted from the crate’ssrc/*.rs. The app needs nobuild.rs. -
Test. Runs the native test suite - the in-crate TestHost unit tests plus the
tests/converge.rsconvergence test. No node or network is needed.$ cargo mero testrunning 2 teststest tests::set_and_get ... oktest converge::two_nodes_converge ... oktest result: ok. 2 passed; 0 failed -
Bundle. Builds every service, stages the wasm/abi files, writes
manifest.json, signs it, and packages everything into a tar.gz.mpkatdist/<package>.mpk. Pass a signing method:--dev(local) or--key <file>(production).$ cargo mero bundle --dev• staging bundle files in ...• writing manifest.json• signing manifest.json with the DEV key============================================================WARNING: signing with the DEVELOPMENT keyThe dev key is well-known and public: fine locally, REFUSEDby the registry. For a publishable bundle, use --key.============================================================• packaging dist/com.example.my-app.mpkbundle: dist/com.example.my-app.mpkpackage: com.example.my-appappVersion: 0.1.0signerId: did:key:z6MknF3p5L5FDHJQ7FREUapuX4Wmp4MtF6WrHYaXS2B3eZQd -
Install.
meroctl app install --path dist/<package>.mpkinstalls the bundle on a runningmerodnode. The node derives theApplicationIdfrom the bundle’spackageand signer (see Signing).
Metadata reference
Section titled “Metadata reference”Bundle metadata comes from a [package.metadata.calimero] table in the app’s Cargo.toml.
Keys are kebab-case.
Cargo.toml key |
manifest.json field |
Default |
|---|---|---|
package |
package |
required (no default) |
name |
metadata.name |
the crate name |
description |
metadata.description |
omitted |
author |
metadata.author |
omitted |
min-runtime-version |
minRuntimeVersion |
0.1.0 |
frontend |
links.frontend |
omitted |
services |
services[] |
empty (workspace table only) |
The app version (manifest.json appVersion) is not a metadata key.
It defaults to the crate’s [package] version and is overridable with cargo mero bundle --app-version <v>.
The manifest.json version field is the manifest schema version and is always 1.0.
[package.metadata.calimero]package = "com.example.my-app"name = "My App"description = "A collaborative example app"min-runtime-version = "0.7.0"frontend = "https://my-app.example.com"Multi-service workspaces
Section titled “Multi-service workspaces”A workspace that ships several wasm services declares them under [workspace.metadata.calimero], which wins over a [package.metadata.calimero] table when both are present.
Each services entry maps a bundle service name to the crate that builds it; services under a [package.metadata.calimero] table is rejected.
The bundle then emits services/<name>.wasm + services/<name>-abi.json per service instead of a top-level app.wasm / abi.json.
# workspace root Cargo.toml[workspace.metadata.calimero]package = "network.calimero.mero-drive"name = "Mero Drive"description = "Collaborative file storage"min-runtime-version = "0.7.0"frontend = "https://drive.calimero.network"
[[workspace.metadata.calimero.services]]name = "drive"crate = "mero-drive-service"
[[workspace.metadata.calimero.services]]name = "index"crate = "mero-index-service"Signing
Section titled “Signing”Every .mpk carries an Ed25519 signature over its manifest.json, and cargo mero bundle signs as part of packaging.
There are two kinds of key:
- Development key (
--dev). A single well-known key baked into the tool - no key file, fine for local installs and CI. Because the key is public it proves nothing about provenance, so the registry refuses--devbundles andcargo mero bundle --devprints a warning saying so. - Production key (
--key <file>). A private Ed25519 key that only you hold, for anything you publish or install against a registry. Generate one withcargo mero key generate -o my-key.json.
In CI, inject the key rather than checking it in: when neither --key nor --dev is passed, bundle reads MERO_SIGN_KEY as the path to a key file.
export MERO_SIGN_KEY="$RUNNER_TEMP/mero-key.json"echo "$MERO_SIGN_KEY_JSON" > "$MERO_SIGN_KEY" # from a CI secretcargo mero bundleFull details - key generation, the did:key derivation, and how package + signer become the ApplicationId - are in the toolchain’s SIGNING.md.
What gets embedded
Section titled “What gets embedded”A built app carries its ABI in two places that hold the same full manifest but differ in ordering.
The wasm’s embedded calimero_abi_v1 custom section holds the canonicalized full ABI: every method (with its per-method metadata such as xcall_callable) and event, plus the state schema.
Its methods and events arrays are sorted by name, because the node’s validate_manifest requires them name-sorted and silently discards a section that fails validation.
The bundle’s abi.json sidecar is the same full ABI as emitted by the SDK, in source-declaration order.
This is also what cargo mero abi diff compares between versions to flag breaking or unsafe identity downgrades.