Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Workflow by Platform and Language

You have a target and a language. This page is the sequence of commands that pair implies, and the one step whose absence is the most common first failure.

The rest of the book is organized by where you are going — a starter page per platform. This page is organized by what you type, because the commands vary along a different axis than the pages do: the builder follows from your language, and the toolchain follows from your platform. Neither table below is a summary of the other.

Where the target comes from

Every leaf, in every language, states what it deploys to in one file: system.toml, beside its Cargo.toml or CMakeLists.txt (RFC-0098). [system] carries the RMW and the domain, [[component]] carries what runs, and one [image.<id>] per target carries its board and, on an embedded image, its network identity:

[system]
rmw       = "zenoh"
domain_id = 0

[image.mps2]
board   = "qemu-mps2-an385"
locator = "tcp/10.0.2.2:10500"
net     = { eth0 = { network = "qemu", host = 10 } }

Everything that choice implies — the target triple, the linker flags, the QEMU runner, the cross compiler for the cc crate, the pool sizes derived from the components, the [patch.crates-io] table that resolves nano-ros’s registry-style names into your checkout — is generated, never written by hand. Retargeting is editing the board line: in a workspace that is the whole edit, and in a single-package Rust leaf too once it depends on its generated <pkg>_nros_selection package instead of a board crate, as every in-tree single-package example on a supported board does (issue 1305).

The step that is easy to miss

Rust leaves need nros sync before their first build. C and C++ leaves do not.

That split is a property of the two languages, not an inconsistency. CMake generates a C/C++ leaf’s message bindings during configure, so there is nothing to prepare first. Cargo has no such stage: the message crates a Rust leaf path-depends on, and the settings file that carries its board facts, both have to exist before cargo parses the manifest. nros sync produces them:

<leaf>/generated/<pkg>/                  # message crates
<leaf>/build/<image>/nros-cargo.toml     # every cargo setting the board implies

Skipping it fails in three ways, and they read very differently. nros build says so in one line and names the remedy:

Error: <leaf> has not been synced — missing the resolved model under
`build/nros/models/`, the generated message crate `std_msgs`
(generated/std_msgs).
  Run `nros sync` in <leaf> (RFC-0098 D2), then build again.

A plain cargo build gets as far as the manifest and stops on the missing message crate, without mentioning nano-ros at all:

error: failed to load manifest for dependency `std_msgs`
Caused by:
  failed to read `<leaf>/generated/std_msgs/Cargo.toml`
Caused by:
  No such file or directory (os error 2)

And a cargo build --config <leaf>/build/<image>/nros-cargo.toml whose settings file does not exist yet produces the one that does not look like a missing file at all — cargo falls back to reading the argument as an inline setting:

error: failed to parse value from --config argument
`<leaf>/build/<image>/nros-cargo.toml` as a dotted key expression
Caused by:
  TOML parse error at line 1, column 39
  key with no value, expected `=`

If you see any of the three, run nros sync.

Contributors (in-tree checkout): the just recipes — just <module> build-fixtures and friends — run nros sync for you, so they work from a fresh clone. It is the hand-run build in a leaf that needs you to run it yourself.

You need it once per checkout location, not once per build. Re-run it after editing a .msg, .srv, or .action file, after changing system.toml, and after moving the checkout.

Which builder your cell uses

Each cell is the builder that nano-ros’s own CI uses for that pair, read from examples/fixtures.toml — the manifest the fixture builds and the staleness probe both consume. A dash means the pair has no in-tree coverage today, not that it is forbidden.

The row names are the manifest’s, which are shorter than the ones you type: freertos here is the platform whose board is mps2-an385-freertos and whose examples live in examples/mps2-an385-freertos/. The per-platform table further down maps all three spellings.

platformrustccppmixed
linuxcargocmakecmakecmake
freertoscargocmakecmakecmake
nuttxcargocmakecmake—
nuttx-riscvcargocmakecmake—
threadx-linuxcargocmakecmakecmake
threadx-riscv64cargo, cmakecmakecmake—
zephyrcargo, westcmake, westcmake, westcargo
zephyr-cortex-mwestwestwest—
esp32cargo———
baremetalcargo———

Where a row lists two builders, both are real and they build different things: on Zephyr, west builds the single-node examples under examples/zephyr/<lang>/, and cargo/cmake build the workspace examples under examples/workspaces/.

The three command shapes

Every cell above is one of these three, plus platform-specific flags that live on the platform’s own starter page.

cargo — Rust, every platform

./scripts/bootstrap.sh          # builds the in-tree nros CLI
source ./activate.sh            # OR: direnv allow / source ./activate.fish
nros setup <board> --rmw zenoh  # toolchain + SDK for the target

cd <leaf>
nros sync                       # ← the step above; once per checkout
nros build                      # every [image.*]; or: nros build <image-id>

The artifact lands under build/, keyed on the image: <leaf>/build/<image-id>/target/[<triple>/]<profile>/<bin>. No --target on the command line, on any platform — the board’s triple is in the generated settings file, which is also where its linker flags and its QEMU runner live.

If you would rather drive cargo yourself — an IDE, a CI step, --release — run nros sync and then point cargo at that file. Run it from the directory above the package, so the package’s own .cargo/ is not read a second time; phase-445 W6 deletes that directory, and then the working directory stops mattering:

cd <leaf-parent>
cargo build --manifest-path <leaf>/Cargo.toml \
            --config <leaf>/build/<image-id>/nros-cargo.toml

cargo run through the same file works for a QEMU board — the runner is in it.

cmake — C and C++

./scripts/bootstrap.sh
source ./activate.sh
nros setup <board> --rmw zenoh

cd <leaf>
cmake -B build -Dnano_ros_ROOT="$(nros sdk-root)" \
    -DCMAKE_TOOLCHAIN_FILE=<toolchain> -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel

No nros sync, and no -DNROS_BOARD / -DNROS_RMW either: find_package(nano_ros) reads the leaf’s system.toml and derives the platform from the board it names. The toolchain file is still per platform — see the starter page. -D_NANO_ROS_CODEGEN_TOOL= is not needed when nros is on PATH; CMake resolves it.

nros build drives exactly this cmake pair for you, in a single-package leaf (into build/<image>/cmake, with the board’s toolchain file — issue 1296) as in a C/C++ workspace.

west — Zephyr single-node examples

Zephyr owns the build. See Zephyr (west module) for the module wiring; the Rust leaves under examples/zephyr/rust/ still need nros sync first, because west drives cargo and cargo needs the generated message crates either way.

In the checkout, or copied out?

The commands above are written as cd <leaf> inside the nano-ros checkout, which is the fastest way to see something run. For anything beyond that, copy the example directory out — examples are standalone copy-out projects with no workspace walk-up, so a copied one builds on its own.

Either way the build output goes to the same place — build/<image>/, under the leaf, because the generated settings file names that target-dir — so an in-tree build leaves no target/ beside the sources for the repo’s own gate to reject (check-example-leaf-target-dirs). A copied-out example carries no path into the checkout at all: nros ties it to the SDK the binary belongs to, and --nano-ros-path names another on the command line rather than in a file.

So: exploring in the checkout, cd <leaf> && nros sync && nros build. Building your own thing, copy the example out and run the same two commands there.

Per platform

platformgrid rownros setup <board>examples underrecipes (contributors)starter page
Linux hostlinuxlinuxexamples/linux/just linux …Native host build
FreeRTOS (QEMU MPS2-AN385)freertosmps2-an385-freertosexamples/mps2-an385-freertos/just freertos …FreeRTOS
NuttX (Arm)nuttxqemu-armv7a-nuttxexamples/qemu-armv7a-nuttx/just nuttx …NuttX
NuttX (RISC-V)nuttx-riscvrv-virt-nuttxexamples/rv-virt-nuttx/just nuttx …NuttX
ThreadX (Linux sim)threadx-linuxthreadx-linuxexamples/threadx-linux/just threadx_linux …ThreadX
ThreadX (QEMU RISC-V 64)threadx-riscv64rv-virt-threadxexamples/rv-virt-threadx/just threadx_riscv64 …ThreadX
Zephyrzephyr, zephyr-cortex-mzephyrexamples/zephyr/just zephyr …Zephyr
ESP32 (dormant, issue 1525)esp32esp32-c3-baremetalexamples/esp32-c3-baremetal/— (module not mounted)ESP32
Bare-metal Cortex-M3baremetalmps2-an385-baremetalexamples/mps2-an385-baremetal/just qemu …Bare-metal
Arm FVP (Cortex-A SMP)—zephyr + the Arm FVP (nros setup --tool arm-fvp, x86_64 only)——ARM FVP

Multi-node workspace examples do not follow that directory rule: they all live under examples/workspaces/, selected by fixture row rather than by directory. So ESP32’s single-node and workspace examples share one grid row, esp32: a grid row names a platform FAMILY, never a board (RFC-0093 R6), and esp32 carries both. Zephyr is the case that does split — zephyr and zephyr-cortex-m build the SAME examples for different boards.

Contributors (in-tree checkout): each module’s recipes are discoverable rather than memorized:

just --list freertos          # every FreeRTOS recipe, grouped
just --list zephyr

More than one node

The sequence above builds one leaf. A project with several nodes adds a Bringup package carrying the same system.toml — and nothing else: no root build file, and no entry package to write, because the entry is generated per [image.*]. The commands stay nros sync and nros build, which then drive one of the three shapes above for you. See Project layout.

If it does not work