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), tests/converge.rs, and GUIDE.md (the app’s guide for agents, with the five sections the registry requires). 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 5c810ccd09a8016c282cb1c8d745937860560f4bb4645af6dfb5387f9cb4d92a
    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
guide metadata.guide omitted
license metadata.license omitted
tags metadata.tags empty
min-runtime-version minRuntimeVersion 0.1.0
slug handlers.slug package
frontend links.frontend omitted
frontend-aliases frontendAliases empty
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. guide is a path to the app’s Markdown guide for agents, resolved exactly like icon; cargo mero bundle reads the file as UTF-8 and writes its full text into metadata.guide. A missing or unreadable file fails the bundle and names the path it tried. cargo-mero does not check the guide’s content; the app registry defines its required sections when you publish. slug names the deep-link handler (https://links.calimero.network/<slug>/...) and defaults to package when unset. frontend-aliases lists other URLs the same frontend is served at, e.g. the old domain of an app that moved. Clients never open them: links.frontend stays where the app loads. A node’s sign-in page also trusts a login callback to any alias’s origin, so a node or launcher still opening the old address keeps signing in. Drop an alias once nothing opens it. Each entry must be an absolute http(s) URL, and cargo mero bundle warns about a localhost frontend or alias unless signing with --dev. 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"
(umask 077; 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.