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 project | installing a release — scripts/install.sh, below |
| developing nano-ros itself | building 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.shand 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 getsnros.install.shsays 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.shis the after-install step, NOT a prereq. Sourcingactivate.sh(oractivate.fish, ordirenv allow) only wires the workspace env into a new shell — it presumes thenrosbinary already exists atpackages/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 cycloneddsworks 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 installros-<distro>-rmw-zenoh-cpp, or pointNROS_RMW_ZENOHDat armw_zenohdyou 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:
PATHis not searched. Azenohdthere — 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 setROS_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.
| Task | Needs a ROS 2 install? |
|---|---|
nros setup, nros sync, message codegen | No — interface sources are vendored |
| Building any node (Rust / C / C++), any RMW | No |
| Embedded targets (Zephyr, FreeRTOS, NuttX, ThreadX) | No |
| Cyclone DDS backend, build and run | No — idlc comes from the index dist, and there is no daemon |
| Running a multi-process zenoh example | Yes — the router is ROS’s rmw_zenohd (or set NROS_RMW_ZENOHD) |
Verifying with ros2 topic echo / ros2 node list | Yes |
Interop tests against rmw_zenoh_cpp / a real ROS 2 graph | Yes |
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.shon a host withoutjustprints 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 throughnros sync+nros build, which drives cargo, cmake, west oridf.pyfor you.justis a contributor tool: only the in-treejustrecipes (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.
| Command | Provisions |
|---|---|
nros setup linux | host build; zenoh-pico + mbedtls sources (no router — see above) |
nros setup linux --rmw xrce | host build; the Micro-XRCE-DDS agent |
nros setup linux --rmw cyclonedds | host build; Cyclone DDS runtime + idlc |
nros setup mps2-an385-freertos | arm-none-eabi-gcc, patched qemu-system-arm, FreeRTOS + lwIP sources |
nros setup qemu-armv7a-nuttx | arm-none-eabi-gcc, qemu, NuttX sources |
nros setup rv-virt-threadx | riscv64-*-gcc, qemu, ThreadX/NetX sources |
nros setup threadx-linux | ThreadX POSIX-sim sources |
nros setup esp32-c3-baremetal | the ESP32-C3 QEMU + bare-metal (esp-hal) toolchain bits |
nros setup zephyr | the Zephyr west workspace + Zephyr SDK bits |
nros setup mps2-an385-baremetal / stm32f4 | bare-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 xrceinstalls 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’srmw_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_zenohdIt is not configured on the command line —
rmw_zenohdignores argv and readsZENOH_CONFIG_OVERRIDE, as above. To pick a different endpoint, change the port in thelisten/endpointsentry.Without a router the talker blocks forever on
Executor::openwith no output. Default ports:tcp/127.0.0.1:7447on POSIX,tcp/10.0.2.2:7447on 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 letnros syncresolve the names into the SDK root (the checkout the project sits in, else thenrosbinary’s own;--nano-ros-pathnames one). The[patch.crates-io]block it writes goes into the generated settings file underbuild/, not into your package —nros buildhands that file to cargo with--config, and a cargo command you drive yourself passes the same path. Copy the project anywhere; re-runnros syncto 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
- First Project — the Quick Start continues here: scaffold a two-node workspace and run it (no ROS 2 needed)
- First Node — Rust — build + run a Rust talker on Linux
- First Node — C — build + run a C talker on Linux
- First Node — C++ — build + run a C++ talker on Linux
- Build as Subdirectory — CMake consumption walkthrough
- C API — API entry points and CMake integration
examples/templates/multi-package-workspace/— full mixed C / C++ / Rust example