Skip to main content

nros/
init.rs

1//! Phase 212.L.5 — top-level init API.
2//!
3//! Three patterns are supported (per the Phase 212.L canonical pkg shape):
4//!
5//! 1. **Node pkg** — register via the [`nros::node!`](crate::node!)
6//!    macro (Phase 172 W.3); the generated runtime owns the spin loop.
7//! 2. **Application pkg + launch-aware** — call [`init_with_launch_auto`] (or
8//!    [`init_with_launch`] for an explicit path). The returned [`Context`]
9//!    carries launch-resolved fields (domain id, locator, RMW choice). User
10//!    code drives its own spin via `Executor::open` +
11//!    `Executor::spin_blocking`.
12//! 3. **Application pkg + custom spin** — call [`init()`] (or [`init_with_args`]
13//!    for argv-style overrides). Launch file is ignored; env vars +
14//!    `ExecutorConfig::from_env()` semantics still apply.
15//!
16//! The [`Context`] struct is a thin holder of the resolved init knobs. To
17//! actually open a session, materialise an [`crate::ExecutorConfig`] via
18//! [`Context::config`] and pass it to `Executor::open`.
19//!
20//! ## Launch overlay (current limitation)
21//!
22//! `init_with_launch_auto` / `init_with_launch` currently consume the
23//! launch-resolved knobs the parent `nros launch` process exports via env
24//! vars (`ROS_DOMAIN_ID`, `NROS_LOCATOR`, `NROS_SESSION_MODE`,
25//! `RMW_IMPLEMENTATION`, plus the placeholder `NROS_RUNTIME_OVERLAY` for
26//! the future structured overlay path). The launch XML is NOT parsed
27//! in-process; the runtime trusts the launcher to project the relevant
28//! params / remaps / env into the child environment. A follow-up wave wires
29//! the structured overlay (Option A — `nros launch --emit-runtime-overlay`
30//! → JSON sidecar consumed here). See Phase 212.L.5 notes.
31
32#[cfg(feature = "env")]
33// phase-359 W10 — kept, and it is not a spelling that can be unwound.
34// `init_with_launch` verifies a launch file EXISTS, which is a filesystem
35// question; `AsRef<Path>` is also what a Rust caller expects to pass a
36// `PathBuf`, a `&str` or a `Path` to. Narrowing the signature to `&str` would
37// trade a real ergonomic for one census point, which is moving the number
38// rather than the build. The whole module is behind `env`, which requires
39// `std`, so nothing here is reachable without one.
40use std::path::Path;
41
42use nros_node::ExecutorConfig;
43use nros_rmw::SessionMode;
44
45/// Errors returned by the init API.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum InitError {
48    /// `init_with_launch(path)` was passed a path that does not exist or
49    /// could not be read.
50    LaunchFileNotFound,
51    /// The launch file existed but could not be parsed.
52    ///
53    /// Phase 212.L.5 ships a stub — actual XML parsing arrives with the
54    /// runtime-overlay wave. Until then this variant is unused.
55    LaunchParseFailed,
56    /// A launch-derived env var (`ROS_DOMAIN_ID`, etc.) failed to parse.
57    EnvParseFailed,
58}
59
60impl core::fmt::Display for InitError {
61    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
62        match self {
63            InitError::LaunchFileNotFound => f.write_str("launch file not found"),
64            InitError::LaunchParseFailed => f.write_str("launch file parse failed"),
65            InitError::EnvParseFailed => f.write_str("env var parse failed"),
66        }
67    }
68}
69
70// `core::error::Error` — `std::error::Error` is a re-export of it since Rust
71// 1.81, so this is the same trait. The `cfg` stays: this whole module reads the
72// environment (`std::env::var`), which has no no_std equivalent.
73#[cfg(feature = "env")]
74impl core::error::Error for InitError {}
75
76/// Phase 212.L.5 — resolved init context.
77///
78/// Returned by every `init*` entry point. Carries the fields the user
79/// needs to construct an [`ExecutorConfig`] and open a session.
80///
81/// Fields are owned (`String` on hosted builds) so the `Context` can
82/// outlive transient parents (env caches, parsed launch files).
83#[cfg(feature = "env")]
84#[derive(Debug, Clone)]
85pub struct Context {
86    /// ROS 2 domain ID (`ROS_DOMAIN_ID`, default 0).
87    pub domain_id: u32,
88    /// Middleware locator (`NROS_LOCATOR` / legacy `ZENOH_LOCATOR`).
89    pub locator: alloc::string::String,
90    /// Session mode (`NROS_SESSION_MODE` / legacy `ZENOH_MODE`, default `Client`).
91    pub mode: SessionMode,
92    /// RMW implementation hint (`RMW_IMPLEMENTATION` /  `NROS_RMW`).
93    ///
94    /// Empty when neither var is set. The runtime uses this to pick a
95    /// primary backend when multiple are linked; see
96    /// `crate::internals::open_session`.
97    pub rmw: alloc::string::String,
98    /// Source of this context — useful for diagnostics + tests.
99    pub source: ContextSource,
100}
101
102/// Where the [`Context`] came from. Diagnostics only.
103#[cfg(feature = "env")]
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum ContextSource {
106    /// Built from env vars by [`init`] / [`init_with_args`].
107    Env,
108    /// Built from a launch file (path supplied to [`init_with_launch`]) or
109    /// auto-discovered via [`init_with_launch_auto`]. The launch XML itself
110    /// is NOT yet parsed (see module docs); the launcher's projected env
111    /// is the source of truth for now.
112    Launch,
113}
114
115#[cfg(feature = "env")]
116impl Context {
117    /// Materialise an [`ExecutorConfig`] for a node with the given name.
118    ///
119    /// The returned config borrows from `self`, so callers usually do:
120    ///
121    /// ```ignore
122    /// let ctx = nros::init()?;
123    /// let cfg = ctx.config("talker");
124    /// let mut executor = nros::Executor::open(&cfg)?;
125    /// ```
126    pub fn config<'a>(&'a self, node_name: &'a str) -> ExecutorConfig<'a> {
127        ExecutorConfig::new(self.locator.as_str())
128            .node_name(node_name)
129            .domain_id(self.domain_id)
130            .mode(self.mode)
131    }
132}
133
134#[cfg(feature = "env")]
135fn read_env_context(source: ContextSource) -> Result<Context, InitError> {
136    // issue 0687 — through the ONE resolver, not a third parse of the same four
137    // variables. This function had its own copy, and the copies had drifted:
138    // it did not warn on the legacy `$ZENOH_LOCATOR`/`$ZENOH_MODE` spellings
139    // the other reader deprecates, and it range-checked nothing, so
140    // `ROS_DOMAIN_ID=300` reached a backend that could only fail later.
141    //
142    // Issue 0330 — no backend default for the locator: `nros` is RMW-agnostic,
143    // so unset env leaves it EMPTY (= absent) and the linked backend
144    // substitutes its own (zenoh: `nros_rmw_zenoh::DEFAULT_LOCATOR`; xrce: its
145    // agent default; cyclonedds ignores the locator entirely). That is what
146    // `resolve_hosted` does with an empty `BootConfig` too.
147    let resolved = crate::env::try_resolve_hosted(nros_node::BootConfig::default())
148        .map_err(|_| InitError::EnvParseFailed)?;
149    let locator = alloc::string::String::from(resolved.locator);
150    let domain_id = resolved.domain_id;
151    let mode = resolved.mode;
152    // issue 0687 — the `$NROS_RMW` half comes from the shared selector; the
153    // `RMW_IMPLEMENTATION` fallback stays HERE and only here. `Context.rmw` is
154    // a ROS-vocabulary HINT (`rmw_cyclonedds_cpp`), not the cffi registry
155    // selector (`cyclonedds`) — folding the two together would hand a ROS name
156    // to `resolve_backend`, which answers `Unknown` and fails the open.
157    let rmw = crate::rmw_selector()
158        .map(|s| alloc::string::String::from(s.as_str()))
159        .or_else(|| std::env::var("RMW_IMPLEMENTATION").ok())
160        .unwrap_or_default();
161    Ok(Context {
162        domain_id,
163        locator,
164        mode,
165        rmw,
166        source,
167    })
168}
169
170/// Pattern 3 — raw init, launch file ignored.
171///
172/// Reads env vars (`ROS_DOMAIN_ID`, `NROS_LOCATOR`, `NROS_SESSION_MODE`,
173/// `NROS_RMW` / `RMW_IMPLEMENTATION`) and returns a [`Context`]. The
174/// caller owns the spin loop — typically `Executor::open(&ctx.config(name))`
175/// followed by `spin_blocking` or a hand-rolled `spin_once` loop.
176#[cfg(feature = "env")]
177pub fn init() -> Result<Context, InitError> {
178    read_env_context(ContextSource::Env)
179}
180
181/// Pattern 3 — like [`init`] but accepts a `[--arg=value, ...]`-style argv
182/// iterator. Currently a thin wrapper over [`init`] that ignores the args;
183/// the structured argv parse (`--ros-args -p foo:=42`, etc.) lands with the
184/// runtime-overlay wave.
185#[cfg(feature = "env")]
186pub fn init_with_args<I, S>(_args: I) -> Result<Context, InitError>
187where
188    I: IntoIterator<Item = S>,
189    S: AsRef<str>,
190{
191    // TODO (Phase 212.L.5 follow-up): parse `--ros-args` style flags.
192    init()
193}
194
195/// Pattern 2 — launch-aware init.
196///
197/// Resolves the launch file via:
198///
199/// 1. `$NROS_RUNTIME_OVERLAY` — when set, the path points at a JSON sidecar
200///    written by `nros launch --emit-runtime-overlay`. (NOT yet consumed;
201///    placeholder for the follow-up wave.)
202/// 2. `<CARGO_MANIFEST_DIR>/launch/<pkg>.launch.xml` or
203///    `<CARGO_MANIFEST_DIR>/launch/system.launch.xml`. (NOT yet parsed;
204///    placeholder.)
205/// 3. The env vars described in [`init`] — the launcher projects launch
206///    params into the child env before `exec()`, so the env path is the
207///    de-facto launch overlay today.
208///
209/// Returns a [`Context`] whose `source = ContextSource::Launch` so callers
210/// can introspect whether the run is launch-driven.
211#[cfg(feature = "env")]
212pub fn init_with_launch_auto() -> Result<Context, InitError> {
213    // TODO (Phase 212.L.5 follow-up):
214    //   1. If $NROS_RUNTIME_OVERLAY is set, read the JSON sidecar and fold
215    //      its params/remaps/env into the Context.
216    //   2. Else walk <CARGO_MANIFEST_DIR>/launch/* and parse the XML
217    //      in-process (Option B — only if Option A overhead is rejected).
218    // For now the env path is the only overlay channel.
219    read_env_context(ContextSource::Launch)
220}
221
222/// Pattern 2 — explicit-path variant of [`init_with_launch_auto`].
223///
224/// Verifies the file exists (so misspelled paths fail fast at init time)
225/// but does NOT yet parse the XML; the launcher's projected env is the
226/// active overlay. See the module-level notes for the follow-up plan.
227#[cfg(feature = "env")]
228pub fn init_with_launch(path: impl AsRef<Path>) -> Result<Context, InitError> {
229    let p = path.as_ref();
230    if !p.exists() {
231        return Err(InitError::LaunchFileNotFound);
232    }
233    // TODO (Phase 212.L.5 follow-up): parse the launch XML and fold params
234    // / remaps / env into the returned Context. Today we only verify the
235    // file exists and fall through to the env overlay path.
236    read_env_context(ContextSource::Launch)
237}