r2r_common/
lib.rs
1use os_str_bytes::RawOsString;
2use regex::*;
3use sha2::{Digest, Sha256};
4use std::{
5 collections::HashMap,
6 env,
7 fs::{self, File},
8 io::Read,
9 path::Path,
10};
11
12#[cfg(not(feature = "doc-only"))]
13const SUPPORTED_ROS_DISTROS: &[&str] = &["foxy", "galactic", "humble", "iron", "jazzy", "rolling"];
14
15const WATCHED_ENV_VARS: &[&str] = &[
16 "AMENT_PREFIX_PATH",
17 "CMAKE_PREFIX_PATH",
18 "CMAKE_IDL_PACKAGES",
19 "IDL_PACKAGE_FILTER",
20 "ROS_DISTRO",
21];
22
23pub fn get_env_hash() -> String {
24 let mut hasher = Sha256::new();
25 for var in WATCHED_ENV_VARS {
26 hasher.update(var.as_bytes());
27 hasher.update("=");
28
29 if let Ok(value) = env::var(var) {
30 hasher.update(value);
31 }
32
33 hasher.update("\n");
34 }
35 let hash = hasher.finalize();
36 format!("{:x}", hash)
37}
38
39pub fn print_cargo_watches() {
40 for var in WATCHED_ENV_VARS {
41 println!("cargo:rerun-if-env-changed={}", var);
42 }
43}
44
45pub fn setup_bindgen_builder() -> bindgen::Builder {
46 let mut builder = bindgen::Builder::default()
47 .layout_tests(false)
48 .derive_copy(false)
49 .size_t_is_usize(true)
50 .default_enum_style(bindgen::EnumVariation::Rust {
51 non_exhaustive: false,
52 });
53 if !cfg!(feature = "doc-only") {
54 if let Ok(cmake_includes) = env::var("CMAKE_INCLUDE_DIRS") {
55 let mut includes = cmake_includes.split(':').collect::<Vec<_>>();
58 includes.sort_unstable();
59 includes.dedup();
60
61 for x in &includes {
62 let clang_arg = format!("-I{}", x);
63 println!("adding clang arg: {}", clang_arg);
64 builder = builder.clang_arg(clang_arg);
65 }
66 }
67
68 let ament_prefix_var_name = "AMENT_PREFIX_PATH";
69 let split_char = if cfg!(target_os = "windows") {
70 ';'
71 } else {
72 ':'
73 };
74 let ament_prefix_var = {
75 let mut ament_str = env::var_os(ament_prefix_var_name).expect("Source your ROS!");
76 if let Some(cmake_prefix_var) = env::var_os("CMAKE_PREFIX_PATH") {
77 ament_str.push(&split_char.to_string());
78 ament_str.push(cmake_prefix_var);
79 }
80 RawOsString::new(ament_str)
81 };
82 for p in ament_prefix_var.split(split_char) {
83 let path = Path::new(&p.to_os_str()).join("include");
84
85 let entries = std::fs::read_dir(path.clone());
86 if let Ok(e) = entries {
87 let dirs = e
88 .filter_map(|a| {
89 let path = a.unwrap().path();
90 if path.is_dir() {
91 Some(path)
92 } else {
93 None
94 }
95 })
96 .collect::<Vec<_>>();
97
98 builder = dirs.iter().fold(builder, |builder, d| {
99 if let Some(leaf) = d.file_name() {
114 let double_include_path = Path::new(d).join(leaf);
115 if double_include_path.is_dir() {
116 let temp = d.to_str().unwrap();
117 builder.clang_arg(format!("-I{}", temp))
118 } else {
119 let temp = d.parent().unwrap().to_str().unwrap();
121 builder.clang_arg(format!("-I{}", temp))
122 }
123 } else {
124 builder
125 }
126 });
127 }
128 }
129 }
130
131 builder
132}
133
134#[cfg(feature = "doc-only")]
135pub fn print_cargo_ros_distro() {}
136
137#[cfg(not(feature = "doc-only"))]
138pub fn print_cargo_ros_distro() {
139 if cfg!(feature = "doc-only") {
140 return;
141 }
142
143 let ros_distro =
144 env::var("ROS_DISTRO").unwrap_or_else(|_| panic!("ROS_DISTRO not set: Source your ROS!"));
145
146 if SUPPORTED_ROS_DISTROS.contains(&ros_distro.as_str()) {
147 println!("cargo:rustc-cfg=r2r__ros__distro__{ros_distro}");
148 } else {
149 panic!("ROS_DISTRO not supported: {ros_distro}");
150 }
151}
152
153#[cfg(feature = "doc-only")]
154pub fn print_cargo_used_cfgs(_message_cfgs: &[&str]) {}
155
156#[cfg(not(feature = "doc-only"))]
157pub fn print_cargo_used_cfgs(message_cfgs: &[&str]) {
158 for d in SUPPORTED_ROS_DISTROS {
160 println!("cargo:rustc-check-cfg=cfg(r2r__ros__distro__{d})");
161 }
162
163 for c in message_cfgs {
166 println!("cargo:rustc-check-cfg=cfg({c})");
167 }
168}
169
170pub fn print_cargo_link_search() {
171 let ament_prefix_var_name = "AMENT_PREFIX_PATH";
172 if let Some(paths) = env::var_os(ament_prefix_var_name) {
173 let split_char = if cfg!(target_os = "windows") {
174 ';'
175 } else {
176 ':'
177 };
178 let paths = if let Some(cmake_prefix_var) = env::var_os("CMAKE_PREFIX_PATH") {
179 let mut cmake_paths = paths;
180 cmake_paths.push(split_char.to_string());
181 cmake_paths.push(cmake_prefix_var);
182 RawOsString::new(cmake_paths)
183 } else {
184 RawOsString::new(paths)
185 };
186 for path in paths.split(split_char) {
187 if cfg!(target_os = "windows") {
188 let lib_path = Path::new(&path.to_os_str()).join("Lib");
189 if !lib_path.exists() {
190 continue;
191 }
192 if let Some(s) = lib_path.to_str() {
193 println!("cargo:rustc-link-search={}", s);
194 }
195 } else {
196 let lib_path = Path::new(&path.to_os_str()).join("lib");
197 if let Some(s) = lib_path.to_str() {
198 println!("cargo:rustc-link-search=native={}", s)
199 }
200 }
201 }
202 }
203}
204
205pub fn get_wanted_messages() -> Vec<RosMsg> {
206 let msgs = if let Ok(cmake_package_dirs) = env::var("CMAKE_IDL_PACKAGES") {
207 let dirs = cmake_package_dirs
211 .split(':')
212 .flat_map(|i| Path::new(i).parent())
213 .collect::<Vec<_>>();
214
215 get_ros_msgs_files(&dirs)
216 } else {
217 let split_char = if cfg!(target_os = "windows") {
219 ';'
220 } else {
221 ':'
222 };
223 match (env::var("AMENT_PREFIX_PATH"), env::var("CMAKE_PREFIX_PATH")) {
224 (Ok(ament_prefix_var), Ok(cmake_prefix_var)) => {
225 let mut paths = ament_prefix_var
226 .split(split_char)
227 .map(Path::new)
228 .collect::<Vec<_>>();
229 paths.extend(cmake_prefix_var.split(split_char).map(Path::new));
230 get_ros_msgs(&paths)
231 }
232 (Ok(ament_prefix_var), _) => {
233 let paths = ament_prefix_var
234 .split(split_char)
235 .map(Path::new)
236 .collect::<Vec<_>>();
237 get_ros_msgs(&paths)
238 }
239 (_, Ok(cmake_prefix_var)) => {
240 let paths = cmake_prefix_var
241 .split(split_char)
242 .map(Path::new)
243 .collect::<Vec<_>>();
244 get_ros_msgs(&paths)
245 }
246 _ => vec![],
247 }
248 };
249
250 let msgs = parse_msgs(&msgs);
251
252 let needed_msg_pkgs = &[
264 "rcl_interfaces",
265 "builtin_interfaces",
266 "unique_identifier_msgs",
267 "action_msgs",
268 ];
269 if let Ok(idl_filter) = env::var("IDL_PACKAGE_FILTER") {
270 let mut idl_packages = idl_filter.split(';').collect::<Vec<&str>>();
271 for needed in needed_msg_pkgs {
272 if !idl_packages.contains(needed) {
273 idl_packages.push(needed);
274 }
275 }
276 msgs.into_iter()
277 .filter(|msg| idl_packages.contains(&msg.module.as_str()))
278 .collect()
279 } else {
280 msgs
281 }
282}
283
284#[derive(Debug)]
285pub struct RosMsg {
286 pub module: String, pub prefix: String, pub name: String, }
290
291fn get_msgs_from_package(package: &Path) -> Vec<String> {
292 let resource_index_subfolder = "share/ament_index/resource_index";
293 let resource_type = "rosidl_interfaces";
294
295 let path = package.to_owned();
296 let path = path.join(resource_index_subfolder);
297 let path = path.join(resource_type);
298
299 let mut msgs = vec![];
300
301 if let Ok(paths) = fs::read_dir(path) {
302 for path in paths {
303 let path = path.unwrap().path();
304 let path2 = path.clone();
305 let file_name = path2.file_name().unwrap();
306
307 if let Ok(mut file) = File::open(path) {
308 let mut s = String::new();
309 file.read_to_string(&mut s).unwrap();
310 let lines = s.lines();
311
312 lines.for_each(|l| {
313 if l.starts_with("msg/") && (l.ends_with(".idl") || l.ends_with(".msg")) {
314 if let Some(file_name_str) = file_name.to_str() {
315 let substr = &l[4..l.len() - 4];
316 let msg_name = format!("{}/msg/{}", file_name_str, substr);
317 msgs.push(msg_name);
318 }
319 }
320 if l.starts_with("srv/") && (l.ends_with(".idl") || l.ends_with(".srv")) {
321 if let Some(file_name_str) = file_name.to_str() {
322 let substr = &l[4..l.len() - 4];
323 let srv_name = format!("{}/srv/{}", file_name_str, substr);
324 msgs.push(srv_name);
325 }
326 }
327 if l.starts_with("action/") && (l.ends_with(".idl") || l.ends_with(".action")) {
328 if let Some(file_name_str) = file_name.to_str() {
329 let substr = if l.ends_with(".action") {
330 &l[7..l.len() - 7]
331 } else {
332 &l[7..l.len() - 4] };
334 let action_name = format!("{}/action/{}", file_name_str, substr);
335 msgs.push(action_name);
336 }
337 }
338 });
339 }
340 }
341 }
342 msgs.sort();
343 msgs.dedup();
344 msgs
345}
346
347pub fn get_ros_msgs(paths: &[&Path]) -> Vec<String> {
348 let mut msgs: Vec<String> = Vec::new();
349
350 for p in paths {
351 let package_msgs = get_msgs_from_package(p);
352 msgs.extend(package_msgs)
353 }
354 msgs.sort();
355 msgs.dedup();
356 msgs
357}
358
359fn get_msgs_in_dir(base: &Path, subdir: &str, package: &str) -> Vec<String> {
360 let path = base.to_owned();
361 let path = path.join(subdir);
362
363 let mut msgs = vec![];
364
365 if let Ok(paths) = fs::read_dir(path) {
366 for path in paths {
367 let path = path.unwrap().path();
368 let filename = path.file_name().unwrap().to_str().unwrap();
369
370 if !filename.ends_with(".idl") {
372 continue;
373 }
374
375 let substr = &filename[0..filename.len() - 4];
376
377 msgs.push(format!("{}/{}/{}", package, subdir, substr));
378 }
379 }
380 msgs
381}
382
383pub fn get_ros_msgs_files(paths: &[&Path]) -> Vec<String> {
384 let mut msgs: Vec<String> = Vec::new();
385
386 for p in paths {
387 if let Some(package_name) = p.file_name() {
388 let package_name = package_name.to_str().unwrap();
389 msgs.extend(get_msgs_in_dir(p, "msg", package_name));
390 msgs.extend(get_msgs_in_dir(p, "srv", package_name));
391 msgs.extend(get_msgs_in_dir(p, "action", package_name));
392 }
393 }
394 msgs.sort();
395 msgs.dedup();
396 msgs
397}
398
399pub fn parse_msgs(msgs: &[String]) -> Vec<RosMsg> {
400 let v: Vec<Vec<&str>> = msgs
401 .iter()
402 .map(|l| l.split('/').take(3).collect())
403 .collect();
404
405 v.iter()
411 .filter(|v| v.len() == 3)
412 .map(|v| RosMsg {
413 module: v[0].into(),
414 prefix: v[1].into(),
415 name: v[2].into(),
416 })
417 .filter(|v| v.module != "libstatistics_collector")
418 .collect()
419}
420
421pub fn as_map(included_msgs: &[RosMsg]) -> HashMap<&str, HashMap<&str, Vec<&str>>> {
422 let mut msgs = HashMap::new();
423 for msg in included_msgs {
424 msgs.entry(msg.module.as_str())
425 .or_insert_with(HashMap::new)
426 .entry(msg.prefix.as_str())
427 .or_insert_with(Vec::new)
428 .push(msg.name.as_str());
429 }
430 msgs
431}
432
433thread_local! {
434 static UPPERCASE_BEFORE: Regex = Regex::new(r"(.)([A-Z][a-z]+)").unwrap();
435 static UPPERCASE_AFTER: Regex = Regex::new(r"([a-z0-9])([A-Z])").unwrap();
436}
437
438pub fn camel_to_snake(s: &str) -> String {
442 let s = UPPERCASE_BEFORE.with(|ub| ub.replace_all(s, "${1}_${2}"));
443 let s = UPPERCASE_AFTER.with(|ua| ua.replace_all(&s, "${1}_${2}"));
444 s.to_lowercase()
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn test_parse_msgs() {
453 let msgs = "
454std_msgs/msg/Bool
455x/y
456std_msgs/msg/String
457";
458 let msgs = msgs.lines().map(|l| l.to_string()).collect::<Vec<_>>();
459 let parsed = parse_msgs(&msgs);
460 assert_eq!(parsed[0].module, "std_msgs");
461 assert_eq!(parsed[0].prefix, "msg");
462 assert_eq!(parsed[0].name, "Bool");
463 assert_eq!(parsed[1].module, "std_msgs");
464 assert_eq!(parsed[1].prefix, "msg");
465 assert_eq!(parsed[1].name, "String");
466 }
467
468 #[test]
469 fn test_as_map() {
470 let msgs = "
471std_msgs/msg/Bool
472x/y
473std_msgs/msg/String
474";
475 let msgs: Vec<String> = msgs.lines().map(|l| l.to_string()).collect();
476 let parsed = parse_msgs(&msgs);
477 let map = as_map(&parsed);
478
479 assert_eq!(map.get("std_msgs").unwrap().get("msg").unwrap()[0], "Bool");
480 assert_eq!(map.get("std_msgs").unwrap().get("msg").unwrap()[1], "String");
481 }
482
483 #[test]
484 fn test_camel_to_snake_case() {
485 assert_eq!(camel_to_snake("AB01CD02"), "ab01_cd02");
486 assert_eq!(camel_to_snake("UnboundedSequences"), "unbounded_sequences");
487 assert_eq!(
488 camel_to_snake("BoundedPlainUnboundedSequences"),
489 "bounded_plain_unbounded_sequences"
490 );
491 assert_eq!(camel_to_snake("WStrings"), "w_strings");
492 }
493}