r2r/r2r_common/src/lib.rs

415 lines
14 KiB
Rust

use itertools::Itertools;
use os_str_bytes::RawOsString;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::env;
use std::fs::{self, File};
use std::io::Read;
use std::path::Path;
#[cfg(not(feature = "doc-only"))]
const SUPPORTED_ROS_DISTROS: &[&str] = &["foxy", "galactic", "humble", "rolling"];
const WATCHED_ENV_VARS: &[&str] = &[
"AMENT_PREFIX_PATH",
"CMAKE_INCLUDE_DIRS",
"CMAKE_LIBRARIES",
"CMAKE_IDL_PACKAGES",
"IDL_PACKAGE_FILTER",
"ROS_DISTRO",
];
pub fn get_env_hash() -> String {
let mut hasher = Sha256::new();
for var in WATCHED_ENV_VARS {
hasher.update(var.as_bytes());
hasher.update("=");
if let Ok(value) = env::var(var) {
hasher.update(value);
}
hasher.update("\n");
}
let hash = hasher.finalize();
format!("{:x}", hash)
}
pub fn print_cargo_watches() {
for var in WATCHED_ENV_VARS {
println!("cargo:rerun-if-env-changed={}", var);
}
}
pub fn setup_bindgen_builder() -> bindgen::Builder {
let mut builder = bindgen::Builder::default()
.derive_copy(false)
.size_t_is_usize(true)
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
});
if let Ok(cmake_includes) = env::var("CMAKE_INCLUDE_DIRS") {
// we are running from cmake, do special thing.
let mut includes = cmake_includes.split(':').collect::<Vec<_>>();
includes.sort_unstable();
includes.dedup();
for x in &includes {
let clang_arg = format!("-I{}", x);
println!("adding clang arg: {}", clang_arg);
builder = builder.clang_arg(clang_arg);
}
} else if !cfg!(feature = "doc-only") {
let ament_prefix_var_name = "AMENT_PREFIX_PATH";
let ament_prefix_var =
RawOsString::new(env::var_os(ament_prefix_var_name).expect("Source your ROS!"));
for p in ament_prefix_var.split(":") {
let path = Path::new(&p.to_os_str()).join("include");
let entries = std::fs::read_dir(path.clone());
if let Ok(e) = entries {
let dirs = e
.filter_map(|a| {
let path = a.unwrap().path();
if path.is_dir() {
Some(path)
} else {
None
}
})
.collect::<Vec<_>>();
builder = dirs.iter().fold(builder, |builder, d| {
// Hack to build rolling after https://github.com/ros2/rcl/pull/959 was merged.
//
// The problem is that now we need to use CMAKE to properly find the
// include paths. But we don't want to do that so we hope that the ros
// developers use the same convention everytime they move the include
// files to a subdirectory.
//
// The convention is to put include files in include/${PROJECT_NAME}
//
// So we check if there is a double directory on the form
// include/${PROJECT_NAME}/${PROJECT_NAME}, and if so append it only once.
//
// Should work mostly, and shouldn't really change often, so manual
// intervention could be applied. But yes it is hacky.
if let Some(leaf) = d.file_name() {
let double_include_path = Path::new(d).join(leaf);
if double_include_path.is_dir() {
let temp = d.to_str().unwrap();
builder.clang_arg(format!("-I{}", temp))
} else {
// pre humble case, where we did not have include/package/package
let temp = d.parent().unwrap().to_str().unwrap();
builder.clang_arg(format!("-I{}", temp))
}
} else {
builder
}
});
}
}
}
builder
}
#[cfg(feature = "doc-only")]
pub fn print_cargo_ros_distro() {}
#[cfg(not(feature = "doc-only"))]
pub fn print_cargo_ros_distro() {
if cfg!(feature = "doc-only") {
return;
}
let ros_distro =
env::var("ROS_DISTRO").unwrap_or_else(|_| panic!("ROS_DISTRO not set: Source your ROS!"));
if SUPPORTED_ROS_DISTROS.contains(&ros_distro.as_str()) {
println!("cargo:rustc-cfg=r2r__ros__distro__{ros_distro}");
} else {
panic!("ROS_DISTRO not supported: {ros_distro}");
}
}
pub fn print_cargo_link_search() {
if env::var_os("CMAKE_INCLUDE_DIRS").is_some() {
if let Some(paths) = env::var_os("CMAKE_LIBRARIES") {
let paths = RawOsString::new(paths);
paths
.split(":")
.filter(|s| s.contains(".so") || s.contains(".dylib"))
.filter_map(|l| {
let l = l.to_os_str();
let parent = Path::new(&l).parent()?;
let parent = parent.to_str()?;
Some(parent.to_string())
})
.unique()
.for_each(|pp| println!("cargo:rustc-link-search=native={}", pp));
}
} else {
let ament_prefix_var_name = "AMENT_PREFIX_PATH";
if let Some(paths) = env::var_os(ament_prefix_var_name) {
let paths = RawOsString::new(paths);
for path in paths.split(":") {
let lib_path = Path::new(&path.to_os_str()).join("lib");
if let Some(s) = lib_path.to_str() {
println!("cargo:rustc-link-search=native={}", s)
}
}
}
}
}
pub fn get_wanted_messages() -> Vec<RosMsg> {
let msgs = if let Ok(cmake_package_dirs) = env::var("CMAKE_IDL_PACKAGES") {
// CMAKE_PACKAGE_DIRS should be a (cmake) list of "cmake" dirs
// e.g. For each dir install/r2r_minimal_node_msgs/share/r2r_minimal_node_msgs/cmake
// we can traverse back and then look for .msg files in msg/ srv/ action/
let dirs = cmake_package_dirs
.split(':')
.flat_map(|i| Path::new(i).parent())
.collect::<Vec<_>>();
get_ros_msgs_files(&dirs)
} else {
// Else we look for all msgs we can find using the ament prefix path.
if let Ok(ament_prefix_var) = env::var("AMENT_PREFIX_PATH") {
let paths = ament_prefix_var
.split(':')
.map(Path::new)
.collect::<Vec<_>>();
get_ros_msgs(&paths)
} else {
vec![]
}
};
let msgs = parse_msgs(&msgs);
// When working on large workspaces without colcon, build times
// can be a pain. This code adds a the possibility to define an
// additional filter to make building a little bit quicker.
//
// The environment variable IDL_PACKAGE_FILTER should be a semicolon
// separated list of package names (e.g. std_msgs;my_msgs), so it
// is required to be correct for packages to be used. This means
// dependencies need to be manually specified.
//
// Suitable to customize with .cargo/config.toml [env] from consumers
// of the r2r package.
let needed_msg_pkgs = &[
"rcl_interfaces",
"builtin_interfaces",
"unique_identifier_msgs",
"action_msgs",
];
if let Ok(idl_filter) = env::var("IDL_PACKAGE_FILTER") {
let mut idl_packages = idl_filter.split(';').collect::<Vec<&str>>();
for needed in needed_msg_pkgs {
if !idl_packages.contains(needed) {
idl_packages.push(needed);
}
}
msgs.into_iter()
.filter(|msg| idl_packages.contains(&msg.module.as_str()))
.collect()
} else {
msgs
}
}
#[derive(Debug)]
pub struct RosMsg {
pub module: String, // e.g. std_msgs
pub prefix: String, // e.g. "msg" or "srv"
pub name: String, // e.g. "String"
}
fn get_msgs_from_package(package: &Path) -> Vec<String> {
let resource_index_subfolder = "share/ament_index/resource_index";
let resource_type = "rosidl_interfaces";
let path = package.to_owned();
let path = path.join(resource_index_subfolder);
let path = path.join(resource_type);
let mut msgs = vec![];
if let Ok(paths) = fs::read_dir(path) {
for path in paths {
let path = path.unwrap().path();
let path2 = path.clone();
let file_name = path2.file_name().unwrap();
if let Ok(mut file) = File::open(path) {
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
let lines = s.lines();
lines.for_each(|l| {
if l.starts_with("msg/") && (l.ends_with(".idl") || l.ends_with(".msg")) {
if let Some(file_name_str) = file_name.to_str() {
let substr = &l[4..l.len() - 4];
let msg_name = format!("{}/msg/{}", file_name_str, substr);
msgs.push(msg_name);
}
}
if l.starts_with("srv/") && (l.ends_with(".idl") || l.ends_with(".srv")) {
if let Some(file_name_str) = file_name.to_str() {
let substr = &l[4..l.len() - 4];
let srv_name = format!("{}/srv/{}", file_name_str, substr);
msgs.push(srv_name);
}
}
if l.starts_with("action/") && (l.ends_with(".idl") || l.ends_with(".action")) {
if let Some(file_name_str) = file_name.to_str() {
let substr = if l.ends_with(".action") {
&l[7..l.len() - 7]
} else {
&l[7..l.len() - 4] // .idl
};
let action_name = format!("{}/action/{}", file_name_str, substr);
println!("found action: {}", action_name);
msgs.push(action_name);
}
}
});
}
}
}
msgs.sort();
msgs.dedup();
msgs
}
pub fn get_ros_msgs(paths: &[&Path]) -> Vec<String> {
let mut msgs: Vec<String> = Vec::new();
for p in paths {
let package_msgs = get_msgs_from_package(p);
msgs.extend(package_msgs)
}
msgs.sort();
msgs.dedup();
msgs
}
fn get_msgs_in_dir(base: &Path, subdir: &str, package: &str) -> Vec<String> {
let path = base.to_owned();
let path = path.join(subdir);
let mut msgs = vec![];
if let Ok(paths) = fs::read_dir(path) {
for path in paths {
let path = path.unwrap().path();
let filename = path.file_name().unwrap().to_str().unwrap();
// message name.idl or name.msg
if !filename.ends_with(".idl") {
continue;
}
let substr = &filename[0..filename.len() - 4];
msgs.push(format!("{}/{}/{}", package, subdir, substr));
}
}
msgs
}
pub fn get_ros_msgs_files(paths: &[&Path]) -> Vec<String> {
let mut msgs: Vec<String> = Vec::new();
for p in paths {
if let Some(package_name) = p.file_name() {
let package_name = package_name.to_str().unwrap();
msgs.extend(get_msgs_in_dir(p, "msg", package_name));
msgs.extend(get_msgs_in_dir(p, "srv", package_name));
msgs.extend(get_msgs_in_dir(p, "action", package_name));
}
}
msgs.sort();
msgs.dedup();
msgs
}
pub fn parse_msgs(msgs: &[String]) -> Vec<RosMsg> {
let v: Vec<Vec<&str>> = msgs
.iter()
.map(|l| l.split('/').into_iter().take(3).collect())
.collect();
// hack because I don't have time to find out the root cause of this at the moment.
// for some reason the library files generated to this are called
// liblibstatistics_collector_test_msgs__..., but I don't know where test_msgs come from.
// (this seems to be a useless package anyway)
// also affects message generation below.
v.iter()
.filter(|v| v.len() == 3)
.map(|v| RosMsg {
module: v[0].into(),
prefix: v[1].into(),
name: v[2].into(),
})
.filter(|v| v.module != "libstatistics_collector")
.collect()
}
pub fn as_map(included_msgs: &[RosMsg]) -> HashMap<&str, HashMap<&str, Vec<&str>>> {
let mut msgs = HashMap::new();
for msg in included_msgs {
msgs.entry(msg.module.as_str())
.or_insert_with(HashMap::new)
.entry(msg.prefix.as_str())
.or_insert_with(Vec::new)
.push(msg.name.as_str());
}
msgs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_msgs() {
let msgs = "
std_msgs/msg/Bool
x/y
std_msgs/msg/String
";
let msgs = msgs.lines().map(|l| l.to_string()).collect::<Vec<_>>();
let parsed = parse_msgs(&msgs);
assert_eq!(parsed[0].module, "std_msgs");
assert_eq!(parsed[0].prefix, "msg");
assert_eq!(parsed[0].name, "Bool");
assert_eq!(parsed[1].module, "std_msgs");
assert_eq!(parsed[1].prefix, "msg");
assert_eq!(parsed[1].name, "String");
}
#[test]
fn test_as_map() {
let msgs = "
std_msgs/msg/Bool
x/y
std_msgs/msg/String
";
let msgs: Vec<String> = msgs.lines().map(|l| l.to_string()).collect();
let parsed = parse_msgs(&msgs);
let map = as_map(&parsed);
assert_eq!(map.get("std_msgs").unwrap().get("msg").unwrap()[0], "Bool");
assert_eq!(map.get("std_msgs").unwrap().get("msg").unwrap()[1], "String");
}
}