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.5.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 ontoPATHand exportsnano_ros_ROOT. CI can skip this and pass paths explicitly (-Dnano_ros_ROOT=…, absolute CLI path). - 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.) - Message codegen.
NROS_REPO_DIR=<checkout> nros syncfrom your workspace root — see the next section for why this is not optional.
The Rust side: nros sync is mandatory, and your config is inline
Two things in every Rust leaf are gitignored and generated: the
generated/ message crates and the cargo patch machinery that
resolves registry-style names (nros = { version = "*" }) to the
vendored sources. A fresh clone cannot build a Rust package until
nros sync has run — the failure otherwise is cargo dying during
manifest parse with an error that never names sync.
For an out-of-tree consumer (your workspace, outside the nano-ros
checkout), nros sync writes the [patch.crates-io] table inline
into your leaf’s .cargo/config.toml, with absolute paths, and no
include line. This is deliberate:
- The in-repo examples use a relative
include = ["…/nros-patch.toml"]pointing at a central gitignored file. That shape has three fragile preconditions (cargo ≥ 1.93, a correct relative path, the central file existing) and its failure modes are confusing. Outside the checkout you get the whole table inline, so the only failure mode is loud. - Never copy an in-repo example’s
.cargo/config.tomlinto your tree. You would be copying the relativeincludeline, which cannot resolve from your directory. Copy the package and runnros sync— it writes the correct config for that location. - A moved checkout invalidates the absolute paths — re-run
nros syncafter relocating either the workspace or the vendored nano-ros.
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 vianano_ros_ROOT, whichactivate.shexports; CI passes-Dnano_ros_ROOT=<checkout>explicitly.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
(NANO_ROS_PLATFORM, NANO_ROS_RMW) pair needs, and may FetchContent
Corrosion from the network if none is installed. That is correct on a
developer machine and wrong in hermetic CI — see the next section.
Air-gapped / mirrored CI
The offline contract, all of it:
-DNANO_ROS_SKIP_BOOTSTRAP=ON. Disables the configure-time submodule fetch and any network FetchContent. 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 corrosionstages 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.5.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.6.0 nros-v0.5.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 passes
-Dnano_ros_ROOT=/NROS_REPO_DIR=explicitly (no reliance on an activated shell) -
nros syncruns in CI before any Rust build; no.cargo/config.tomlcopied from in-repo examples - Air-gapped:
NANO_ROS_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