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

Installation

The library is distributed as source, vendored into the consumer’s project tree (git submodule, west manifest, ESP-IDF component, etc.) and built in-tree via add_subdirectory(nano-ros). There is no binary tarball, no system-wide install step and no find_package(NanoRos).

The nros CLI is a separate question with a separate answer, and which one you want depends on who you are:

you are…you get nros by…
using nano-ros to build your own projectinstalling a release — scripts/install.sh, below
developing nano-ros itselfbuilding it from the checkout — ./scripts/bootstrap.sh

That is not a preference. Inside a nano-ros checkout the tree’s own build is the only correct binary: a released nros run against a checkout is refused outright, because it emits its own generated code and this tree’s runtime is the one that has to compile it (RFC-0090).

Host prerequisites

Everything toolchain-shaped — rustup, cross-compilers, emulators, the zenoh router — is provisioned by the steps below. The host itself only needs the basics: git, curl, a C toolchain, pkg-config, python3.

Debian / Ubuntu:

sudo apt-get update
sudo apt-get install -y git curl ca-certificates build-essential \
    cmake pkg-config python3 python3-dev zstd \
    python3-catkin-pkg python3-empy python3-lark python3-yaml

Fedora / RHEL:

sudo dnf install -y git curl ca-certificates gcc gcc-c++ make \
    cmake pkgconf-pkg-config python3 python3-devel python3-pip zstd
# Fedora ships no rosidl packages — install the codegen Python deps via pip:
pip install --user catkin_pkg 'empy==3.3.4' lark PyYAML

Arch:

sudo pacman -S --needed git curl base-devel cmake python python-pip zstd
# Arch ships no rosidl packages — install the codegen Python deps
# (use a venv or pipx if your Python is externally managed):
pip install --user catkin_pkg 'empy==3.3.4' lark PyYAML

(base-devel is a package group, and it covers pkg-config; ca-certificates ships with the base system.)

(cmake builds the C/C++ quick start and every workspace root; zstd unpacks the prebuilt SDK assets nros setup fetches; python3-dev is needed once, while bootstrap.sh builds the launch resolver — it embeds CPython to parse ROS launch files. Each tool’s absence produces an error naming it. The Python packages feed rosidl_adapter, the ROS message-to-IDL step the CycloneDDS backend uses — on Fedora/Arch install catkin_pkg, empy==3.3.4, lark and PyYAML via pip or your package manager; empy must stay 3.x.)

These few packages are the only ones you install by hand. Per-board OS dependencies later on are declared in the index and printed for your package manager — apt, dnf or pacman — by:

nros setup --system          # what is missing + the exact install command

That command needs the nros CLI, which is why the basics above come first: they are what it takes to clone the repo and build the CLI.

The whole flow, end to end

Four steps take a bare machine to a building project; each is detailed in a section below.

Why step 2 builds the CLI here. These four steps are the contributor flow — they start by cloning the repository, so step 2 is the checkout’s own build, which is the only binary that may be used against it.

Prebuilt binaries were shipped early and withdrawn, because a released binary emitted code that had drifted from the runtime — and drifted generated code compiles, so the failure was silent. Restoring the download needed one question to have a checkable answer: can this binary’s output work with this runtime? RFC-0090 answers it with a codegen version that every generated artifact carries and every runtime asserts, and every arm of it refuses at compile time.

No release has been cut yet. The machinery is in place (scripts/install.sh and a manual release workflow), and cutting one is a deliberate act rather than a consequence of a version bump — so until the first one exists, building from the checkout is how everyone gets nros. install.sh says so itself if you run it early, and points you back here.

# 1. Pull the source at a pinned version (or `main` for latest):
git clone --branch nros-v0.6.0 https://github.com/NEWSLabNTU/nano-ros.git
cd nano-ros

# 2. Build the `nros` CLI from source (installs rustup if needed):
./scripts/bootstrap.sh

# 3. Wire the workspace env into this (and every future) shell:
source ./activate.sh          # or: direnv allow / source ./activate.fish

# 4. Provision the toolchains + RMW daemon for your target:
nros setup <board> --rmw <zenoh|xrce|cyclonedds>    # e.g. `native`

Then scaffold a project and build it: nros new, then nros sync and nros build — First Project walks it end to end. Your project says what it deploys to with a board name in its system.toml; it does not name a path, a triple or a backend crate in any build file. Consuming nano-ros below has the shape, and Pinning a version how the tag relates to nros version.

activate.sh is the after-install step, NOT a prereq. Sourcing activate.sh (or activate.fish, or direnv allow) only wires the workspace env into a new shell — it presumes the nros binary already exists at packages/cli/target/release/nros. The bootstrap front door below is what produces that binary; the activate file is what makes it findable in every subsequent shell.

build-as-subdirectory.md is the lower-level CMake escape hatch, for a C/C++ tree that is not a nano-ros workspace. This page walks the surrounding workspace + per-target setup choices.

Do I need ROS 2 installed?

Building, no. Running two zenoh nodes, yes — or use Cyclone. nros sync generates the message bindings from the interface sources vendored in packages/cli/interfaces/, so nothing about setup, codegen or compilation needs ROS 2.

Running is where it splits, and the reason is the router. zenoh-pico connects in client mode, so any two-process example needs a zenoh router — and since RFC-0075 that router is rmw_zenoh_cpp/rmw_zenohd, which ships with ROS 2. nano-ros no longer vendors one. So on a host with no ROS 2:

  • --rmw cyclonedds works standalone — Cyclone is in-process, there is no daemon to start. This is the ROS-less path.
  • --rmw zenoh (the default) has no router. Either install ros-<distro>-rmw-zenoh-cpp, or point NROS_RMW_ZENOHD at a rmw_zenohd you obtained another way.

If you do have ROS 2, source its setup first — that is what makes the router findable:

source /opt/ros/<distro>/setup.bash

nano-ros reads AMENT_PREFIX_PATH from that, so a ROS installed anywhere works, including one you built from source or a colcon overlay — it is not assumed to be under /opt/ros.

Do not expect command -v rmw_zenohd to print anything: rmw_zenohd installs into <prefix>/lib/rmw_zenoh_cpp/, and setup.bash puts only bin/ on PATH. ROS’s own way to reach it is ros2 run rmw_zenoh_cpp rmw_zenohd. So a silent command -v is normal and not a broken install — the ros2 run rmw_zenoh_cpp rmw_zenohd spelling finds it regardless.

Only what you name is used. nano-ros needs the router that ships with your rmw_zenoh_cpp: the two share a zenoh build, and a mismatch shows up as “nothing was delivered” rather than as an error. So the search is NROS_RMW_ZENOHD, then the prefixes you sourced, then $ROS_DISTRO — and nothing else. In particular:

  • PATH is not searched. A zenohd there — installed from zenoh’s own instructions, or left behind by an older nano-ros — is usually a different and older zenoh. The machine this was written on had two, at v1.4.0 and 1.7.2, against ROS’s 1.8.0.
  • /opt/ros/* is not globbed. If you have humble and jazzy installed, guessing between them is not better than saying nothing. Source the one you want, or set ROS_DISTRO.

If you do want a specific binary, name it with NROS_RMW_ZENOHD; it is used as given, with a warning if it is not an rmw_zenoh_cpp router.

If you once used an older nano-ros, run nros doctor: it reports a retired SDK store entry that is still installed, zenohd included, with the command to remove it.

TaskNeeds a ROS 2 install?
nros setup, nros sync, message codegenNo — interface sources are vendored
Building any node (Rust / C / C++), any RMWNo
Embedded targets (Zephyr, FreeRTOS, NuttX, ThreadX)No
Cyclone DDS backend, build and runNo — idlc comes from the index dist, and there is no daemon
Running a multi-process zenoh exampleYes — the router is ROS’s rmw_zenohd (or set NROS_RMW_ZENOHD)
Verifying with ros2 topic echo / ros2 node listYes
Interop tests against rmw_zenoh_cpp / a real ROS 2 graphYes
Contributor lanes that bridge to ROS 2 (just test-all interop cells)Yes

When you source activate.sh without ROS 2 present it says so:

activate.sh: /opt/ros/humble/setup.bash not found — ROS-dependent recipes
  will fail (interop tests, `ros2` CLI verification). The setup, codegen
  and first-node flows do not need it.

That line is informational on a getting-started host. If you do want the ROS 2 side and your distro has no Humble packages (Arch, for example), a container is the usual answer — nano-ros itself stays on the host.

Consuming nano-ros

Pattern A: your project is a nano-ros workspace

This is the supported shape, in any language. A workspace is a directory of packages — node packages plus one bringup package — and it has no root build file at all, neither CMakeLists.txt nor Cargo.toml (RFC-0098 D9):

~/my_robot/
├── .colcon_workspace          # marks the root
└── src/
    ├── pkg_a/                 # your node package(s)
    ├── pkg_b/
    └── my_robot_bringup/      # system.toml + launch files, no code

The bringup’s system.toml names the board once per deploy target:

[system]
name = "my_robot"
rmw  = "cyclonedds"

[image.linux]
board = "linux"

nros sync resolves the launch files and generates what the board choice implies; nros build generates the entry and the build root for each [image.*] under build/, then hands off to CMake, cargo, west or idf.py:

nros sync
nros build                     # or: nros build linux

nros new my_robot --workspace scaffolds exactly that layout — see First Project. A POSIX C++ workspace that already uses colcon can still be built by colcon build; the packages keep their package.xml and their own CMakeLists.txt.

A complete working example lives at examples/templates/multi-package-workspace/.

Pattern B: nano-ros as a third-party subdirectory

For a C/C++ project that is not a nano-ros workspace — an existing CMake tree with its own layout, that wants nano-ros as a subdirectory and nothing else:

~/my_project/
├── CMakeLists.txt
├── src/main.c
└── third_party/
    └── nano-ros/             # git submodule

There the platform and backend are cache variables set before add_subdirectory(), because there is no system.toml for the build to read them from. That is the lower-level shape and it has its own page, with the full variable table: Build as a CMake subdirectory. Reach for it when Pattern A does not fit your tree, not before.

Provision your toolchain with nros setup

nros setup is the single command that prepares a machine to build nano-ros for a given board. The cross-compiler, emulator, RMW host daemon, and any SDK sources a board needs are resolved from a pinned index and placed in a shared store (~/.nros/sdk). You do not install cross-toolchains by hand, and you do not need ROS 2 on the machine.

Packages are prebuilt where the index has a binary for your host, and built from source otherwise. Source builds are normal, not an error — but they cost real time and disk, so nros setup announces them before it starts:

nros setup: 1 package(s) have no prebuilt for linux-x86_64 — BUILDING FROM SOURCE: <name>
  Expect minutes (tens of minutes for a large recipe) and hundreds of MB under …

Source recipes build with the workspace’s pinned Rust channel, so a recipe carrying its own pin does not make rustup fetch a second toolchain behind your back. nros setup <board> --dry-run prints the whole plan, prebuilt vs source, without fetching anything — on the native board it is two source packages (zenoh-pico and mbedtls, both vendored submodules) and no daemon at all.

1. Get the nros CLI onto PATH

If you only want to use nano-ros, install a release — no checkout, no cargo, no just:

curl -fsSL https://raw.githubusercontent.com/NEWSLabNTU/nano-ros/main/scripts/install.sh | sh
export PATH="$HOME/.nros/bin:$PATH"   # install.sh prints this line too

It verifies the download’s sha256 (and refuses an asset it cannot verify), installs into ~/.nros/sdk/nros/<version>/ and points ~/.nros/bin/nros at the newest version installed — so however many versions accumulate, nros means one command. Put ~/.nros/bin on your PATH. Until the first release is cut it will tell you so and send you to the source build below.

If you are working on nano-ros itself, build the CLI from the checkout — that binary is the only one this tree accepts. One front door from a fresh checkout (just is NOT a prereq; rustup is installed on demand):

Sourcing activate.sh on a host without just prints one line about RTOS SDK path defaults (FREERTOS_DIR, NUTTX_DIR, THREADX_DIR, …) not being loaded. Harmless — nothing in the user flows reads them, and embedded builds go through nros sync + nros build, which drives cargo, cmake, west or idf.py for you. just is a contributor tool: only the in-tree just recipes (test lanes, fixture builds) need it (cargo install just).

git clone https://github.com/NEWSLabNTU/nano-ros.git
cd nano-ros
./scripts/bootstrap.sh

Builds the in-tree nros CLI from source at packages/cli/target/release/nros and leaves it on PATH for this shell.

Already have cargo? The equivalent by hand (same build, same binary):

git submodule update --init packages/cli/third-party/play_launch
cargo build --release --manifest-path packages/cli/Cargo.toml --bin nros
export PATH="$PWD/packages/cli/target/release:$PATH"

Both produce the per-checkout binary at packages/cli/target/release/nros. One checkout = one CLI version = one runtime ABI — no global install, no ~/.nros/bin PATH skew across worktrees.

2. Activate the workspace (every subsequent shell)

The bootstrap path you ran in §1 left nros on PATH for that shell only. Every new shell needs the workspace env wired in. Pick whichever fits your shell — all three wire the same env exports + PATH entries from the Phase 218.C SSoT activate.sh:

direnv allow                  # auto-activates on `cd nano-ros` (recommended)
source ./activate.sh          # bash / zsh, one-shot per shell
source ./activate.fish        # fish, one-shot per shell

The activate file also sources /opt/ros/humble/setup.bash if present (used by the rmw_zenoh interop tests, and it lets codegen resolve message packages from the ament index; building with any RMW — Cyclone included — does NOT require ROS 2, only the pip-installable rosidl_adapter Python packages from the prerequisites). It exports no checkout root: the in-tree nros knows the checkout it was built in.

3. Provision a board (+ RMW)

nros setup <board> --rmw <zenoh|xrce|cyclonedds>

nros setup resolves the board’s package set union the RMW’s host packages and fetches them all — prebuilt cross-toolchain + emulator + RMW daemon + board SDK sources. --rmw defaults to zenoh. For every board with a CMake build road (all of them except Zephyr, which builds with west) it also installs the pinned Corrosion, the CMake-to-cargo bridge a C/C++ configure needs: a configure never fetches it, and without it in the SDK store the configure stops and names this command.

CommandProvisions
nros setup linuxhost build; zenoh-pico + mbedtls sources (no router — see above)
nros setup linux --rmw xrcehost build; the Micro-XRCE-DDS agent
nros setup linux --rmw cycloneddshost build; Cyclone DDS runtime + idlc
nros setup mps2-an385-freertosarm-none-eabi-gcc, patched qemu-system-arm, FreeRTOS + lwIP sources
nros setup qemu-armv7a-nuttxarm-none-eabi-gcc, qemu, NuttX sources
nros setup rv-virt-threadxriscv64-*-gcc, qemu, ThreadX/NetX sources
nros setup threadx-linuxThreadX POSIX-sim sources
nros setup esp32-c3-baremetalthe ESP32-C3 QEMU + bare-metal (esp-hal) toolchain bits
nros setup zephyrthe Zephyr west workspace + Zephyr SDK bits
nros setup mps2-an385-baremetal / stm32f4bare-metal arm-none-eabi-gcc + qemu

Every board also needs a Rust toolchain, whatever language you write: the runtime your project links is compiled from source when you first configure. If the host has neither rustup nor rustc + cargo, nros setup installs rustup (a pinned, checksum-verified installer) into ~/.cargo with the stable toolchain as the default — where cmake finds it without any PATH change. A host that already has Rust is left as it is. For cargo in a shell afterwards: . "$HOME/.cargo/env".

Useful flags:

nros setup --list            # every package in the index + its version
nros setup <board> --dry-run # resolve + print the plan, fetch nothing
nros setup --licenses        # license-gated packages + how to install them
nros setup --tool qemu       # one tool by name
nros setup --source freertos-kernel   # one source package by name

Each board’s exact package set lives in the index; run nros setup <board> --dry-run to see precisely what a board pulls. See Supported Boards for the full board list and nros CLI for every subcommand.

Heads-up before your first example. Every nano-ros example (Linux talker, FreeRTOS talker, …) connects to its RMW host daemon at startup — a zenoh router for zenoh, the Micro-XRCE-DDS agent for xrce. Cyclone DDS is in-process — no separate daemon — so none of this applies if you ran nros setup … --rmw cyclonedds.

For xrce, nros setup … --rmw xrce installs the agent into the nros store (~/.nros/sdk/<tool>/<version>/bin/), and you run it in a dedicated terminal before launching any example.

For zenoh, the router is not provisioned by nros setup: it is ROS 2’s rmw_zenoh_cpp/rmw_zenohd, so that nano-ros is tested against the router a ROS 2 deployment actually runs (RFC-0075). Start it with your ROS environment sourced (leave it running for the whole session):

ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false' \
    ros2 run rmw_zenoh_cpp rmw_zenohd

It is not configured on the command line — rmw_zenohd ignores argv and reads ZENOH_CONFIG_OVERRIDE, as above. To pick a different endpoint, change the port in the listen/endpoints entry.

Without a router the talker blocks forever on Executor::open with no output. Default ports: tcp/127.0.0.1:7447 on POSIX, tcp/10.0.2.2:7447 on QEMU FreeRTOS (Slirp forwards to host). Mismatch = silent hang; see Troubleshooting — First 10 Minutes.

After provisioning, follow the per-platform starter page (FreeRTOS, Zephyr, NuttX, …) for the board’s build + run steps.

Rust-only consumers

nano-ros is source-only — nothing is published to crates.io (decision 2026-05-14). The full nros crate can’t be published because it depends transitively on C/C++ submodules (zenoh-pico, mbedtls); nros-core isn’t carved out for crates.io either, to avoid a hybrid distribution model with version drift between the crates.io snapshot and in-repo HEAD.

Rust packages consume nano-ros one of two ways:

  • Registry-style names + patch redirection (what every shipped example uses): declare nros = { version = "*" } (+ msg crates) and let nros sync resolve the names into the SDK root (the checkout the project sits in, else the nros binary’s own; --nano-ros-path names one). The [patch.crates-io] block it writes goes into the generated settings file under build/, not into your package — nros build hands that file to cargo with --config, and a cargo command you drive yourself passes the same path. Copy the project anywhere; re-run nros sync to re-point.
  • Explicit path dependency on the in-workspace checkout:
[dependencies]
nros = { path = "src/nano-ros/packages/api/nros",
         default-features = false,
         features = ["std", "rmw-cffi", "platform-posix", "ros-humble"] }

A Rust package carries no build settings of its own — no target triple, no linker flag, no patch table. It states its board in system.toml and nros sync derives the rest (RFC-0098); see examples/templates/multi-package-workspace/src/pkg_rust_publisher/ for a package in that shape, and Integrating into a Vendored Tree for the CI spelling.

Pinning a version

Consumers pin the source tree, not a package: clone (or submodule) at a nros-v<X.Y.Z> git tag —

git clone --branch nros-v0.6.0 https://github.com/NEWSLabNTU/nano-ros.git
# or, as a submodule in your project:
git submodule add -b main https://github.com/NEWSLabNTU/nano-ros third_party/nano-ros
git -C third_party/nano-ros checkout nros-v0.6.0

The tag names the bundle version: the nros CLI built from that tree prints the same number (nros version → nros 0.5.0; the scripts/check-version-lockstep.sh gate + bootstrap.sh shell-doctor enforce binary↔tree agreement), and the tree’s committed nros-sdk-index.toml pins the exact toolchain/SDK versions nros setup will provision for it. One tag therefore fixes the whole surface: runtime source, CLI behavior, and provisioned toolchains. Upgrading = checking out a newer tag and re-running ./scripts/bootstrap.sh (rebuilds the CLI to match) + nros setup (reconciles the SDK store against the new index; already-current packages are no-ops). See versioning for how the bundle number is chosen.

Contributor setup (working on nano-ros itself)

Contributors clone the repo and drive everything through just. The just setup recipes are thin wrappers over the same nros setup index — just setup <module> calls nros setup <board> under the hood, so the toolchains a contributor gets are identical to a user’s.

git clone https://github.com/NEWSLabNTU/nano-ros.git
cd nano-ros
./scripts/bootstrap.sh base   # path A from §1 above; gets rustup + just + nros
source ./activate.sh          # OR: direnv allow / source ./activate.fish
just setup all                # provision every supported board's SDK/toolchain

Diagnose missing tools (read-only):

just doctor tier=all

Provision one module:

just setup freertos           # → nros setup mps2-an385-freertos
just setup nuttx              # → nros setup qemu-armv7a-nuttx
just setup threadx_linux      # → nros setup threadx-linux

Docker environment (contributors)

For a containerized environment with QEMU 7.2+ (in-tree checkout):

just docker build
just docker shell      # Interactive shell with all tools
just docker test-qemu  # Run QEMU tests in container

Migrating from a pre-140 checkout

If you were on a nano-ros version that still had the (contributor) just install-local recipe, see migration-install-local-removal.md for the one-page rewrite.

Next Steps