Integrating nano-ros into a Vendored Tree
This page is for the engineer whose company has its own source tree — a BSP, possibly a forked RTOS, an existing build system — and needs nano-ros to live inside it: pinned, reproducible, buildable offline, upgradable on their schedule. It collects the contracts the other chapters mention in passing into one place.
nano-ros is source-only: nothing is on crates.io, there is no install prefix, and no binary distribution. Vendoring the repository is not a workaround — it is the supported consumption model.
Pinning the checkout
Vendor the repo as a git submodule (or subtree) and pin a release tag:
git submodule add https://github.com/NEWSLabNTU/nano-ros.git third_party/nano-ros
git -C third_party/nano-ros checkout nros-v0.6.0
git add third_party/nano-ros
Rules that keep the pin trustworthy:
- Pin tags (
nros-v<X.Y.Z>), notmain. A moving branch unships the pairing you tested. - Move the pin forward only. A rewind silently drops whatever the skipped commits fixed, and two hex strings in a diff cannot be ordered by eye.
- nano-ros’s own submodules are decisions, not lags. The Cyclone DDS fork tracks the Cyclone that ROS ships; zenoh-pico is pinned at a wire-stable version. Never “update to latest” inside the vendored checkout — take what the tag pinned.
Initialize nano-ros’s submodules non-recursively — the launch toolchain’s third-level runtime submodules are never built by nano-ros:
git -C third_party/nano-ros submodule update --init packages/cli/third-party/play_launch
For build-related submodules you normally do nothing: the configure
step populates the ones your (platform, RMW) pair needs (see
Bootstrap below), or you pre-seed them explicitly for offline builds.
What a fresh vendored checkout needs, in order
Four things, and the order matters:
- The
nrosCLI../scripts/bootstrap.shinside the checkout buildspackages/cli/target/release/nros. Nothing else works without it. - An activated environment for interactive use:
source third_party/nano-ros/activate.shwiresnrosand the SDK-store tools ontoPATH. It exports no root:nrosknows the checkout it was built in. CI can skip this and use an absolute CLI path (a rawcmakepasses-Dnano_ros_ROOT=…). - Provisioned toolchains.
nros setup <board> --rmw <rmw>fetches the cross-compiler, emulator, and SDK sources for exactly that pair into the shared store at${NROS_HOME:-~/.nros}/sdk.--dry-runprints the plan first. (The zenoh router is deliberately not provisioned — it comes from a ROS 2 install,ros2 run rmw_zenoh_cpp rmw_zenohd.) nros sync.nros syncfrom your workspace root (the vendorednrosanswers with its own checkout;--nano-ros-path <checkout>names it explicitly). It produces the message bindings and the build settings each[image.*]implies — see the next section for why this is not optional.
The Rust side: nros sync is mandatory, and nothing it writes is in your source tree
Two things a Rust package needs are generated, not authored: the
generated/ message crates, and every build setting the target implies
— the triple, the runner, the link flags, the resolved [env], and the
[patch.crates-io] table that resolves registry-style names
(nros = { version = "*" }) to the vendored sources. You state the
target once, as a board name in the package’s system.toml, and
nros sync derives the rest (RFC-0098):
[system]
name = "my_app"
rmw = "cyclonedds"
[image.linux]
board = "linux"
nros sync writes those settings to build/<image-id>/nros-cargo.toml
— one file per [image.*] row, under build/, never beside your
package — and nros build hands it to cargo:
nros sync
nros build # or: nros build <image-id> for one image
A CI job that drives cargo itself passes the same file:
cargo build --manifest-path <pkg>/Cargo.toml \
--config <pkg>/build/<image-id>/nros-cargo.toml
(If the package still carries a .cargo/ directory of its own — the
in-tree examples do, until that directory is retired — run cargo from
the directory above it, or cargo reads both and joins two sets of
link flags. A package of yours has no such directory, so the working
directory does not matter.)
This is what makes a vendored tree cheap to move: there is no leaf
.cargo/config.toml to copy from an in-repo example, no include
line whose relative path cannot resolve from your directory, and no
table of absolute paths under version control to invalidate. After
relocating either the workspace or the vendored checkout, re-run
nros sync; it rewrites what is under build/ and touches nothing
you track. Switching a package to another board is editing its one
board = line and syncing again.
A build that runs before sync fails at the first preflight with a line
naming nros sync, rather than deep inside cargo.
Message dependencies stay path deps pinned 0.0.0
(std_msgs = { path = "generated/std_msgs" } after sync); never
registry-name a message crate — a bare std_msgs = "*" can resolve
against the public crates.io.
The CMake side
Two entry points, both source-backed:
find_package(nano_ros REQUIRED)— the ament-shaped entry (nano_rosConfig.cmakeat the checkout root). Located via-Dnano_ros_ROOT=<checkout>, whichnros buildand the CMakePresetsnros initwrites pass; a rawcmakenames it (no shell exports it).add_subdirectory(third_party/nano-ros)— the lower-level shape. See Build as a CMake subdirectory for the cache-variable table.
Either way, at configure time nano-ros’s bootstrap.cmake runs
tools/setup.sh to populate the source submodules your
(NROS_PLATFORM, NROS_RMW) pair needs. That is correct on a
developer machine and wrong in hermetic CI — see the next section.
Corrosion is never fetched at configure time: without it in the SDK store
the configure stops and names nros setup --tool corrosion. nros setup <board> installs it too, for every board with a CMake road.
Air-gapped / mirrored CI
The offline contract, all of it:
-DNROS_SKIP_BOOTSTRAP=ON. Disables the configure-time submodule fetch. You now own pre-seeding.- Pre-seed the submodules your platform/RMW pair needs in your
mirror (e.g.
zpico-sys/zenoh-pico, the FreeRTOS kernel + lwIP, the Cyclone fork —tools/setup.shderives the per-target list fromnros-sdk-index.toml, which is the single manifest of what comes from where). - Pre-install Corrosion:
nros setup --tool corrosion(or thenros setup <board>you already run) stages it into the SDK store, where the configure finds it without the network. Verify the configure line printsnano-ros: Corrosion <ver> via <origin>— never infer the version from having run an installer; a stale store entry can shadow a fresh one. - Cache the SDK store. Everything
nros setupfetches lands under${NROS_HOME:-~/.nros}/sdk; pointNROS_HOMEat a shared or cached location and the store is reusable across jobs and machines of the same host triple. - Mirror the asset sources.
nros-sdk-index.tomlnames every asset’s upstream ([tool.*],[source.*]sections). For a full mirror, rehost those URLs and patch the index in your vendored checkout — that patch is part of the carried diff below, and the index is versioned with the tree so the patch rebases cleanly.
The patch set you carry
Be clear-eyed: some extensions live inside the vendored checkout. There is no out-of-tree escape hatch for them today, so a vendored tree carries a small local diff and rebases it across tag bumps:
| You add… | Files inside the checkout |
|---|---|
| A platform (your RTOS) | packages/platform/nros-platform/Cargo.toml (feature + optional dep), nros-platform/src/resolve.rs (type alias), cmake/platform/nano-ros-<name>.cmake |
| A board the tooling can see | none needed since NROS_EXTRA_BOARD_PATH: point it (env, or CMake cache var for nano_ros_use_board()) at a directory of board crates in YOUR tree — same layout as packages/boards/. Board keys stay global (a name under two roots errors). Copying into packages/boards/ remains the fallback |
| A mirrored SDK index | nros-sdk-index.toml URL edits |
An application, a message package, a board overlay crate, and a custom transport all live entirely in your tree — no diff.
Workflow for the diff: keep a branch in your nano-ros fork
(company/nros-v0.6.0-patches), and on upgrade rebase it onto the new
tag rather than merging. The registration edits are a few lines each;
conflicts are rare and loud.
Upgrading the pin
git -C third_party/nano-ros fetch origin --tags
git -C third_party/nano-ros rebase --onto nros-v0.7.0 nros-v0.6.0 company/patches
# build + test, then:
git add third_party/nano-ros
After every pin move, in order: rebuild the nros CLI
(./scripts/bootstrap.sh), re-run nros sync in each consuming
workspace (the sync output is keyed to the CLI), then rebuild. Stale
sync output after an upgrade produces build errors that look like
missing packages, not like staleness.
Checklist
- Submodule pinned to an
nros-vtag, forward-only -
play_launchsubmodule initialized non-recursively - CI drives
nros build(or passes-Dnano_ros_ROOT=to a rawcmake) — no reliance on an activated shell -
nros syncruns in CI before any Rust build — beforenros build, and before any cargo line that passes--config build/<image-id>/nros-cargo.toml; no build setting hand-written into a package - Air-gapped:
NROS_SKIP_BOOTSTRAP=ON, submodules pre-seeded, Corrosion staged,NROS_HOMEcached - Local patch set (platform/board/index edits) lives on a fork branch, rebased per tag
- Router comes from the ROS 2 install on the host that runs it — nothing in the vendored tree provides one
Related
- Build as a CMake subdirectory — cache variables, link targets
- How Integration Works — per-RTOS build hosting
- Vendor Overlay Board Crate — board glue that lives in your tree
- Worked Example — STM32F4 Out of Tree
- Install — the interactive first-time flow this page assumes you are automating