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            // note, this is a colon on both windows and linux, it is set
56            // in r2r_cargo.cmake
57            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                    // Hack to build rolling after https://github.com/ros2/rcl/pull/959 was merged.
100                    //
101                    // The problem is that now we need to use CMAKE to properly find the
102                    // include paths. But we don't want to do that so we hope that the ros
103                    // developers use the same convention everytime they move the include
104                    // files to a subdirectory.
105                    //
106                    // The convention is to put include files in include/${PROJECT_NAME}
107                    //
108                    // So we check if there is a double directory on the form
109                    // include/${PROJECT_NAME}/${PROJECT_NAME}, and if so append it only once.
110                    //
111                    // Should work mostly, and shouldn't really change often, so manual
112                    // intervention could be applied. But yes it is hacky.
113                    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                            // pre humble case, where we did not have include/package/package
120                            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    // Declare all supported ros distros as cfg directives for cargo
159    for d in SUPPORTED_ROS_DISTROS {
160        println!("cargo:rustc-check-cfg=cfg(r2r__ros__distro__{d})");
161    }
162
163    // additionally we have conditional tests and features based on some
164    // optional ros message packages.
165    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        // CMAKE_PACKAGE_DIRS should be a (cmake) list of "cmake" dirs
208        // e.g. For each dir install/r2r_minimal_node_msgs/share/r2r_minimal_node_msgs/cmake
209        // we can traverse back and then look for .msg files in msg/ srv/ action/
210        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        // Else we look for all msgs we can find using the ament prefix path.
218        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    // When working on large workspaces without colcon, build times
253    // can be a pain. This code adds a the possibility to define an
254    // additional filter to make building a little bit quicker.
255    //
256    // The environment variable IDL_PACKAGE_FILTER should be a semicolon
257    // separated list of package names (e.g. std_msgs;my_msgs), so it
258    // is required to be correct for packages to be used. This means
259    // dependencies need to be manually specified.
260    //
261    // Suitable to customize with .cargo/config.toml [env] from consumers
262    // of the r2r package.
263    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, // e.g. std_msgs
287    pub prefix: String, // e.g. "msg" or "srv"
288    pub name: String,   // e.g. "String"
289}
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] // .idl
333                            };
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            // message name.idl or name.msg
371            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    // hack because I don't have time to find out the root cause of this at the moment.
406    // for some reason the library files generated to this are called
407    // liblibstatistics_collector_test_msgs__..., but I don't know where test_msgs come from.
408    // (this seems to be a useless package anyway)
409    // also affects message generation below.
410    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
438/// camel case to to snake case adapted from from ros_idl_cmake. This
439/// is not a general "to snake case" converter, it only handles the
440/// specific case of CamelCase to snake_case that we need.
441pub 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}