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 publish |
Publish a signed .mpk to the app registry. |
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 --no-icon # 4. build all services, write manifest.json, sign, tar dist/<package>-<version>.mpkmeroctl app install --path dist/<package>-<version>.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>-<appVersion>.mpk. Pass a signing method:--dev(local) or--key <file>(production), and an icon:icon = "<path>"in[package.metadata.calimero], or--no-icon.$ cargo mero bundle --dev --no-icon• staging bundle files in ...• writing manifest.json============================================================WARNING: signing with the DEVELOPMENT keyThe dev key is well-known and public: fine locally, REFUSEDby the registry. For a publishable bundle, use --key.============================================================• signing manifest.json with the DEV key• packaging dist/com.example.my-app-0.1.0.mpk✓ bundle dist/com.example.my-app-0.1.0.mpk (12.3 KiB)Name My AppPackage com.example.my-appVersion 0.1.0Application 7E6eADgrrxr98AUpFUaisPHAvwZofq41xqDD9357ctU9Signer did:key:z6MknF3p5L5FDHJQ7FREUapuX4Wmp4MtF6WrHYaXS2B3eZQdThe Application line is the
ApplicationIda context binds to; see Signing for howpackageandsignerIdderive it. Both are printed in full so you can paste them into a command or compare them against a previous release. -
Install.
meroctl app install --path dist/<package>-<appVersion>.mpkinstalls the bundle on a runningmerodnode. The node derives theApplicationIdfrom the bundle’spackageand signer (see Signing).
Cargo features
Section titled “Cargo features”build and bundle take cargo’s feature flags, spelled the way cargo spells them - comma or space separated, repeatable:
cargo mero build --features schema_v2cargo mero bundle --dev --no-icon --features "schema_v2 telemetry" --no-default-featuresThey reach cargo build and the cargo metadata call the ABI is emitted against, so the embedded calimero_abi_v1 section always describes the schema the bytecode was compiled with.
That pairing is the point: a wasm and an ABI that disagree do not fail the build, they produce a wrong migration plan at upgrade time, because the node resolves upgrades from the embedded section alone.
Features gate top-level items, which is how one crate carries two schema versions:
#[cfg(not(feature = "schema_v2"))]#[app::state(version = 1)]pub struct State { /* ... */ }
#[cfg(feature = "schema_v2")]#[app::state(version = 2)]pub struct StateV2 { /* ... */ }A #[cfg] on a method inside an #[app::logic] block, or on a variant of an #[app::event] enum, is not applied to the ABI - an attribute macro sees the item before cfg is - so express a variant as whole gated items rather than gated members.
See Describing your ABI for the rule in full.
In a multi-service workspace all services compile in one cargo build, so a feature only one service declares is fine: it applies to that service and is ignored by the rest.
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 |
icon |
metadata.icon |
required unless --no-icon |
license |
metadata.license |
omitted |
tags |
metadata.tags |
empty |
min-runtime-version |
minRuntimeVersion |
0.1.0 |
slug |
handlers.slug |
package |
frontend |
links.frontend |
omitted |
github |
links.github |
omitted |
docs |
links.docs |
omitted |
services |
services[] |
empty (see Multi-service workspaces) |
icon is a filesystem path (resolved against the directory of the table that declares it), not the manifest value itself: cargo mero bundle reads the PNG and writes a base64-encoded data:image/png;base64,... URI into metadata.icon.
slug names the deep-link handler (https://links.calimero.network/<slug>/...) and defaults to package when unset.
author is not just displayed: the registry checks it against your authenticated registry username (or _ownerEmail against your account email) on publish, and refuses a new version of the package when it doesn’t match.
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> or --bump <patch|minor|major> (the next version published to the registry).
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"icon = "assets/icon.png"license = "MIT"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.
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"icon = "assets/icon.png"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"A crate that is a member of a larger workspace (and so cannot own the workspace-root table) may instead declare services under its own [package.metadata.calimero].
A crate entry may name the declaring package itself; two entries naming the same crate build its wasm once and stage it under both service names:
[package.metadata.calimero]package = "com.example.my-suite"min-runtime-version = "0.7.0"
[[package.metadata.calimero.services]]name = "store-a"crate = "my-suite"
[[package.metadata.calimero.services]]name = "store-b"crate = "my-suite"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, both bundle and sign read 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.
Publishing to the registry
Section titled “Publishing to the registry”cargo mero publish <mpk> uploads a signed .mpk to the Calimero App Registry.
It requires the CALIMERO_API_KEY environment variable; the registry base URL defaults to https://apps.calimero.network and is overridable with CALIMERO_REGISTRY_URL.
cargo mero bundle --key my-key.json --bump patch # fetch and use the next patch versionexport CALIMERO_API_KEY=...cargo mero publish dist/com.example.my-app-1.2.4.mpkThe registry refuses bundles signed with the development key, so publishing always needs a production key.
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.