1use anyhow::{Context, Result};
23use colored::*;
24use robonix_cli::{Config, SourcePathKey};
25use std::path::{Path, PathBuf};
26use std::process::Command;
27
28fn run_cmd(label: &str, cmd: &mut Command) -> Result<()> {
29 log::debug!("[codegen] {}: {:?}", label, cmd);
30 let status = cmd
31 .status()
32 .with_context(|| format!("failed to execute `{label}`"))?;
33 if !status.success() {
34 anyhow::bail!("{label} failed with {status}");
35 }
36 Ok(())
37}
38
39fn resolve_pkg_root(package: &Path) -> Result<PathBuf> {
40 let abs = if package.is_absolute() {
41 package.to_path_buf()
42 } else {
43 let base = std::env::var("RBNX_INVOCATION_CWD")
44 .map(PathBuf::from)
45 .unwrap_or(std::env::current_dir()?);
46 base.join(package)
47 };
48 let abs = abs
49 .canonicalize()
50 .with_context(|| format!("package path not found: {}", abs.display()))?;
51 if !abs.join("package_manifest.yaml").exists()
52 && !abs.join("robonix_manifest.yaml").exists()
53 && !abs.join("rbnx_manifest.yaml").exists()
54 {
55 eprintln!(
56 "{}: {} has no package_manifest.yaml (continuing anyway)",
57 "warn".yellow().bold(),
58 abs.display()
59 );
60 }
61 Ok(abs)
62}
63
64pub async fn execute(
65 config: Config,
66 package: Option<PathBuf>,
67 mcp: bool,
68 ros2: bool,
69 clean: bool,
70 out_dir: Option<PathBuf>,
71) -> Result<()> {
72 let pkg_root = match package {
73 Some(p) => resolve_pkg_root(&p)?,
74 None => super::run_package::find_package_from_cwd()?,
75 };
76 let rust_root = config.resolve_source_path(SourcePathKey::RustRoot)?;
77 let capabilities_dir = config.resolve_source_path(SourcePathKey::Capabilities)?;
79 let interfaces_lib = config.resolve_source_path(SourcePathKey::InterfacesLib)?;
83 let runtime_proto = config.resolve_source_path(SourcePathKey::RuntimeProto)?;
84 let pkg_caps = pkg_root.join("capabilities");
90 let pkg_caps_lib: Option<PathBuf> = {
91 let p = pkg_caps.join("lib");
92 p.is_dir().then_some(p)
93 };
94 let pkg_caps_root: Option<PathBuf> = pkg_caps.is_dir().then_some(pkg_caps);
98
99 let rbnx_build = pkg_root.join("rbnx-build");
106 let out_root = match out_dir {
107 Some(d) if d.is_absolute() => d,
108 Some(d) => pkg_root.join(d),
109 None => rbnx_build.join("codegen"),
110 };
111 let proto_gen = out_root.join("proto_gen");
112 let mcp_types = out_root.join("robonix_mcp_types");
113 let ros2_idl = out_root.join("ros2_idl");
117 let proto_staging = rbnx_build.join("proto-staging");
120
121 if clean {
122 for p in [&proto_gen, &mcp_types, &rbnx_build] {
123 if p.exists() {
124 std::fs::remove_dir_all(p).ok();
125 }
126 }
127 }
128 std::fs::create_dir_all(&proto_staging)?;
129
130 let direct_codegen = locate_codegen_bin(&rust_root);
138 let cargo_bin = if direct_codegen.is_none() {
139 if Path::new("/usr/bin/cargo").exists() {
140 Some("/usr/bin/cargo".to_string())
141 } else {
142 Some(std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()))
143 }
144 } else {
145 None
146 };
147
148 println!("{} package: {}", "[codegen]".bold(), pkg_root.display());
149 println!(
150 "{} robonix source: {}",
151 "[codegen]".bold(),
152 rust_root.display()
153 );
154
155 println!("{} robonix-codegen --lang proto ...", "[codegen]".bold());
160 let mut proto_cmd =
161 build_codegen_cmd(direct_codegen.as_ref(), cargo_bin.as_deref(), &rust_root);
162 proto_cmd
163 .args(["--lang", "proto", "-I"])
164 .arg(&interfaces_lib);
165 if let Some(p) = pkg_caps_lib.as_ref() {
166 proto_cmd.arg("-I").arg(p);
167 }
168 proto_cmd.arg("--contracts").arg(&capabilities_dir);
169 if let Some(p) = pkg_caps_root.as_ref() {
170 proto_cmd.arg("--contracts").arg(p);
171 }
172 proto_cmd.arg("-o").arg(&proto_staging);
173 run_cmd("robonix-codegen proto", &mut proto_cmd)?;
174
175 if mcp {
177 println!("{} robonix-codegen --lang mcp ...", "[codegen]".bold());
178 std::fs::create_dir_all(&mcp_types).ok();
179 let mut mcp_cmd =
180 build_codegen_cmd(direct_codegen.as_ref(), cargo_bin.as_deref(), &rust_root);
181 mcp_cmd.args(["--lang", "mcp", "-I"]).arg(&interfaces_lib);
182 if let Some(p) = pkg_caps_lib.as_ref() {
187 mcp_cmd.arg("-I").arg(p);
188 }
189 mcp_cmd.arg("-o").arg(&mcp_types);
190 run_cmd("robonix-codegen mcp", &mut mcp_cmd)?;
191 }
192
193 probe_python_grpc_tools()?;
203
204 println!(
205 "{} grpc_tools.protoc → {}",
206 "[codegen]".bold(),
207 proto_gen.display()
208 );
209 std::fs::create_dir_all(&proto_gen)?;
210 let proto_files: Vec<PathBuf> = std::fs::read_dir(&runtime_proto)?
211 .chain(std::fs::read_dir(&proto_staging)?)
212 .filter_map(|e| e.ok().map(|e| e.path()))
213 .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("proto"))
214 .collect();
215
216 let mut protoc = Command::new("python3");
217 protoc
218 .args(["-m", "grpc_tools.protoc", "-I"])
219 .arg(&runtime_proto)
220 .arg("-I")
221 .arg(&proto_staging)
222 .arg(format!("--python_out={}", proto_gen.display()))
223 .arg(format!("--grpc_python_out={}", proto_gen.display()));
224 for f in &proto_files {
225 protoc.arg(f);
226 }
227 let status = protoc
228 .status()
229 .with_context(|| "failed to spawn python3 -m grpc_tools.protoc")?;
230 if !status.success() {
231 anyhow::bail!(
232 "python3 -m grpc_tools.protoc failed with {status}. \
233 Re-run with -v / RUST_LOG=debug to see protoc output."
234 );
235 }
236
237 if ros2 {
243 println!("{} robonix-codegen --lang ros2 ...", "[codegen]".bold());
244 let mut ros2_cmd =
245 build_codegen_cmd(direct_codegen.as_ref(), cargo_bin.as_deref(), &rust_root);
246 ros2_cmd.args(["--lang", "ros2", "-I"]).arg(&interfaces_lib);
247 if let Some(p) = pkg_caps_lib.as_ref() {
248 ros2_cmd.arg("-I").arg(p);
249 }
250 ros2_cmd.arg("-o").arg(&ros2_idl);
251 run_cmd("robonix-codegen ros2", &mut ros2_cmd)?;
252 }
253
254 let ws_install = rbnx_build.join("ws").join("install");
256 std::fs::create_dir_all(&ws_install)?;
257 let mut py_parts: Vec<String> = vec![
258 pkg_root.display().to_string(),
259 proto_gen.display().to_string(),
260 ];
261 if mcp {
262 py_parts.push(mcp_types.display().to_string());
263 }
264 let joined = py_parts.join(":");
265 let setup_bash = ws_install.join("setup.bash");
266 std::fs::write(
267 &setup_bash,
268 format!("#!/usr/bin/env bash\nexport PYTHONPATH=\"{joined}:${{PYTHONPATH:-}}\"\n"),
269 )?;
270
271 println!(
272 "{} done — {}, {} setup.bash",
273 "[codegen]".green().bold(),
274 if mcp {
275 "proto+mcp+stubs"
276 } else {
277 "proto+stubs"
278 },
279 if mcp_types.exists() {
280 "mcp_types, "
281 } else {
282 ""
283 }
284 );
285 Ok(())
286}
287
288fn locate_codegen_bin(rust_root: &Path) -> Option<PathBuf> {
301 if let Ok(s) = std::env::var("ROBONIX_CODEGEN_BIN")
302 && !s.is_empty()
303 {
304 let p = PathBuf::from(s);
305 if p.is_file() {
306 return Some(p);
307 }
308 }
309 let cargo_home = std::env::var("CARGO_HOME")
310 .map(PathBuf::from)
311 .ok()
312 .or_else(|| dirs::home_dir().map(|h| h.join(".cargo")));
313 if let Some(home) = cargo_home {
314 let p = home.join("bin").join("robonix-codegen");
315 if p.is_file() {
316 return Some(p);
317 }
318 }
319 for profile in ["release", "debug"] {
320 let p = rust_root
321 .join("target")
322 .join(profile)
323 .join("robonix-codegen");
324 if p.is_file() {
325 return Some(p);
326 }
327 }
328 if let Ok(path_env) = std::env::var("PATH") {
329 for dir in std::env::split_paths(&path_env) {
330 let p = dir.join("robonix-codegen");
331 if p.is_file() {
332 return Some(p);
333 }
334 }
335 }
336 None
337}
338
339fn probe_python_grpc_tools() -> Result<()> {
344 let py = Command::new("python3").arg("--version").output();
345 if py.is_err() || !py.as_ref().unwrap().status.success() {
346 anyhow::bail!(
347 "python3 not found on PATH. `rbnx codegen` shells out to \
348 `python3 -m grpc_tools.protoc` to emit Python gRPC stubs. \
349 Install python3 (>=3.10) and re-run."
350 );
351 }
352 let mod_probe = Command::new("python3")
353 .args(["-c", "import grpc_tools.protoc"])
354 .output()
355 .with_context(|| "failed to spawn python3 for grpc_tools probe")?;
356 if !mod_probe.status.success() {
357 let py_path = Command::new("python3")
358 .args(["-c", "import sys; print(sys.executable)"])
359 .output()
360 .ok()
361 .and_then(|o| String::from_utf8(o.stdout).ok())
362 .map(|s| s.trim().to_string())
363 .unwrap_or_else(|| "python3".to_string());
364 anyhow::bail!(
365 "Python module 'grpc_tools' not importable from {py_path}.\n\
366 `rbnx codegen` needs grpcio-tools to emit Python `_pb2.py` + `_pb2_grpc.py`.\n\
367 Install into the python3 above:\n\
368 \n python3 -m pip install --user grpcio-tools\n"
369 );
370 }
371 Ok(())
372}
373
374fn build_codegen_cmd(direct: Option<&PathBuf>, cargo: Option<&str>, rust_root: &Path) -> Command {
379 if let Some(bin) = direct {
380 Command::new(bin)
381 } else {
382 let cargo = cargo.expect("either direct codegen bin or cargo bin must be set");
383 let mut cmd = Command::new(cargo);
384 cmd.args(["run", "-p", "robonix-codegen", "--manifest-path"])
385 .arg(rust_root.join("Cargo.toml"))
386 .arg("--");
387 cmd
388 }
389}