Skip to content

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.

cargo mero is a Cargo subcommand: install the cargo-mero binary and Cargo picks it up as cargo mero.

Terminal window
# 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-mero

Prebuilt 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:

Terminal window
rustup target add wasm32-unknown-unknown

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

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 five steps below match cargo mero guide (the canonical source; run it any time).

Terminal window
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>.mpk
meroctl app install --path dist/<package>-<version>.mpk ... # 5. install on a node (merod)
  1. Scaffold. cargo mero new my-app writes a crate with Cargo.toml (SDK pins, the [package.metadata.calimero] app id, and the app-release / app-profiling profiles), src/lib.rs (state, events, logic, and a #[cfg(test)] TestHost test), and tests/converge.rs. There is no build script - cargo mero build emits the ABI itself.

    $ cargo mero new my-app
    Scaffolded `my-app` at ./my-app
    Next steps:
    cd my-app
    cargo mero build # compile -> wasm-opt -> embed ABI
    cargo mero test # TestHost + convergence tests (no node needed)
  2. Build. Compiles to wasm32-unknown-unknown, copies the wasm into res/, size-optimizes it with wasm-opt -Oz (release only), and embeds the canonicalized full ABI as the wasm calimero_abi_v1 custom 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.wasm

    Artifacts: res/<name>.wasm (the built, ABI-embedded wasm) plus res/abi.json and res/state-schema.json, both emitted from the crate’s src/*.rs. The app needs no build.rs.

  3. Test. Runs the native test suite - the in-crate TestHost unit tests plus the tests/converge.rs convergence test. No node or network is needed.

    $ cargo mero test
    running 2 tests
    test tests::set_and_get ... ok
    test converge::two_nodes_converge ... ok
    test result: ok. 2 passed; 0 failed
  4. Bundle. Builds every service, stages the wasm/abi files, writes manifest.json, signs it, and packages everything into a tar.gz .mpk at dist/<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 key
    The dev key is well-known and public: fine locally, REFUSED
    by 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 App
    Package com.example.my-app
    Version 0.1.0
    Application 7E6eADgrrxr98AUpFUaisPHAvwZofq41xqDD9357ctU9
    Signer did:key:z6MknF3p5L5FDHJQ7FREUapuX4Wmp4MtF6WrHYaXS2B3eZQd

    The Application line is the ApplicationId a context binds to; see Signing for how package and signerId derive it. Both are printed in full so you can paste them into a command or compare them against a previous release.

  5. Install. meroctl app install --path dist/<package>-<appVersion>.mpk installs the bundle on a running merod node. The node derives the ApplicationId from the bundle’s package and signer (see Signing).

build and bundle take cargo’s feature flags, spelled the way cargo spells them - comma or space separated, repeatable:

Terminal window
cargo mero build --features schema_v2
cargo mero bundle --dev --no-icon --features "schema_v2 telemetry" --no-default-features

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

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"

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"

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 --dev bundles and cargo mero bundle --dev prints 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 with cargo 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.

Terminal window
export MERO_SIGN_KEY="$RUNNER_TEMP/mero-key.json"
echo "$MERO_SIGN_KEY_JSON" > "$MERO_SIGN_KEY" # from a CI secret
cargo mero bundle

Full details - key generation, the did:key derivation, and how package + signer become the ApplicationId - are in the toolchain’s SIGNING.md.

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.

Terminal window
cargo mero bundle --key my-key.json --bump patch # fetch and use the next patch version
export CALIMERO_API_KEY=...
cargo mero publish dist/com.example.my-app-1.2.4.mpk

The registry refuses bundles signed with the development key, so publishing always needs a production key.

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.