-
Notifications
You must be signed in to change notification settings - Fork 182
Re-Export Generated Messages in rclrs #556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
709ead8
3114d96
442abcc
d0d22d7
7cc282c
448c44e
8b55304
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| use std::{env, path::Path}; | ||
| use std::{env, fs, path::Path}; | ||
| use std::path::PathBuf; | ||
|
|
||
| use cargo_toml::Manifest; | ||
|
|
||
| const AMENT_PREFIX_PATH: &str = "AMENT_PREFIX_PATH"; | ||
| const ROS_DISTRO: &str = "ROS_DISTRO"; | ||
|
|
||
|
|
@@ -13,6 +17,19 @@ fn get_env_var_or_abort(env_var: &'static str) -> String { | |
| } | ||
| } | ||
|
|
||
| fn marked_reexport(cargo_toml: String) -> bool { | ||
| cargo_toml.contains("[package.metadata.rclrs]") | ||
| && cargo_toml.contains("reexport = true") | ||
| } | ||
|
|
||
| fn star_deps_to_use(manifest: &Manifest) -> String { | ||
| manifest.dependencies | ||
| .iter() | ||
| .filter(|(_, version)| version.req() == "*") | ||
| .map(|(name, _)| format!("use crate::{name};\n")) | ||
| .collect::<String>() | ||
| } | ||
|
|
||
| fn main() { | ||
| println!( | ||
| "cargo:rustc-check-cfg=cfg(ros_distro, values(\"{}\"))", | ||
|
|
@@ -54,6 +71,91 @@ fn main() { | |
| println!("cargo:rustc-link-search=native={}", library_path.display()); | ||
| } | ||
|
|
||
| // Re-export any generated interface crates that we find | ||
| let ament_prefix_paths = env!("AMENT_PREFIX_PATH", "AMENT_PREFIX_PATH environment variable not set - please source ROS 2 installation first."); | ||
| let export_crate_tomls = ament_prefix_paths | ||
| .split(':') | ||
| .map(PathBuf::from) | ||
| .flat_map(|base_path| { | ||
| // 1. Try to read share/ directory | ||
| fs::read_dir(base_path.join("share")).into_iter().flatten() | ||
| }) | ||
| .filter_map(|entry| entry.ok()) | ||
| .filter(|entry| entry.path().is_dir()) | ||
| .flat_map(|package_dir| { | ||
| // 2. Try to read <package>/rust/ directory | ||
| fs::read_dir(package_dir.path().join("rust")) | ||
| .into_iter() | ||
| .flatten() | ||
| }) | ||
| .filter_map(|entry| entry.ok()) | ||
| .map(|entry| entry.path()) | ||
| .filter(|path| path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml"))) | ||
| .filter(|path| { | ||
| fs::read_to_string(path) | ||
| .map(marked_reexport) | ||
| .unwrap_or(false) | ||
| }); | ||
|
|
||
| let content: String = export_crate_tomls | ||
| .filter_map(|path| path.parent().map(|p| p.to_path_buf())) | ||
| .map(|package_dir| { | ||
| let package = package_dir | ||
| .parent() | ||
| .unwrap() | ||
| .file_name() | ||
| .unwrap() | ||
| .to_str() | ||
| .unwrap(); | ||
|
|
||
| // Find all dependencies for this crate that have a `*` version requirement. | ||
| // We will assume that these are other exported dependencies that need symbols | ||
| // exposed in their module. | ||
| let dependencies: String = Manifest::from_path(package_dir.clone().join("Cargo.toml")) | ||
| .iter() | ||
| .map(star_deps_to_use) | ||
| .collect(); | ||
|
|
||
| let internal_mods: String = fs::read_dir(package_dir.join("src")) | ||
| .into_iter() | ||
| .flatten() | ||
| .filter_map(|entry| entry.ok()) | ||
| .filter(|entry| entry.path().is_file()) | ||
| // Ignore lib.rs and any rmw.rs. lib.rs is only used if the crate is consumed | ||
| // independently, and rmw.rs files need their top level module | ||
| // (i.e. msg, srv, action) to exist to be re-exported. | ||
| .filter(|entry| { | ||
| let name = entry.file_name(); | ||
| name != "lib.rs" && name != "rmw.rs" | ||
| }) | ||
| // Wrap the inclusion of each file in a module matching the file stem | ||
| // so that the generated code can be imported like `rclrs::std_msgs::msgs::Bool` | ||
| .filter_map(|e| { | ||
| e.path() | ||
| .file_stem() | ||
| .and_then(|stem| stem.to_str()) | ||
| .map(|stem| { | ||
| let idiomatic_path = e.path().display().to_string(); | ||
| let sep = std::path::MAIN_SEPARATOR; | ||
| let rmw_path = idiomatic_path | ||
| .rsplit_once(std::path::MAIN_SEPARATOR) | ||
| .map(|(dir, _)| format!("{dir}{sep}{stem}{sep}rmw.rs")) | ||
| .unwrap_or_else(|| "rmw.rs".to_string()); | ||
| format!("pub mod {stem} {{ {dependencies} include!(\"{idiomatic_path}\"); pub mod rmw {{ {dependencies} include!(\"{rmw_path}\");}} }}") | ||
| }) | ||
| }) | ||
| .collect(); | ||
|
|
||
| format!("pub mod {package} {{ {internal_mods} }}") | ||
| }) | ||
| .collect(); | ||
|
|
||
| let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set "); | ||
| let dest_path = PathBuf::from(out_dir).join("interfaces.rs"); | ||
|
|
||
| // TODO I would like to run rustfmt on this generated code, similar to how bindgen does it | ||
| fs::write(dest_path, content.clone()).expect("Failed to write interfaces.rs"); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interface.rs ends up looking like this pub mod builtin_interfaces { ... };
pub mod std_msgs {
pub mod msg {
use crate::builtin_interfaces;
// use crate::...; etc
include!("path/to/generated/code");
pub mod rmw {
use crate::builtin_interfaces;
// use crate::...; etc
include!("path/to/generated/rmw/code");
}
}
}Where all generated interface code is placed into a module based on the paths found in the Interestingly enough, I also learned that the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to change the location of the |
||
|
|
||
| println!("cargo:rustc-link-lib=dylib=rcl"); | ||
| println!("cargo:rustc-link-lib=dylib=rcl_action"); | ||
| println!("cargo:rustc-link-lib=dylib=rcl_yaml_param_parser"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,7 +32,7 @@ | |
| //! | ||
| //! ```no_run | ||
| //! use rclrs::*; | ||
| //! # use crate::rclrs::vendor::example_interfaces; | ||
| //! # use crate::rclrs::example_interfaces; | ||
| //! | ||
| //! let context = Context::default_from_env()?; | ||
| //! let mut executor = context.create_basic_executor(); | ||
|
|
@@ -59,7 +59,7 @@ | |
| //! # let context = Context::default_from_env()?; | ||
| //! # let mut executor = context.create_basic_executor(); | ||
| //! # let node = executor.create_node("example_node")?; | ||
| //! # use crate::rclrs::vendor::example_interfaces; | ||
| //! # use crate::rclrs::example_interfaces; | ||
| //! # | ||
| //! // This worker will manage the data for us. | ||
| //! // The worker's data is called its payload. | ||
|
|
@@ -99,7 +99,7 @@ | |
| //! The following is a simple example of using a mandatory parameter: | ||
| //! ```no_run | ||
| //! use rclrs::*; | ||
| //! # use crate::rclrs::vendor::example_interfaces; | ||
| //! # use crate::rclrs::example_interfaces; | ||
| //! use std::sync::Arc; | ||
| //! | ||
| //! let mut executor = Context::default_from_env()?.create_basic_executor(); | ||
|
|
@@ -129,7 +129,7 @@ | |
| //! | ||
| //! ```no_run | ||
| //! use rclrs::*; | ||
| //! # use crate::rclrs::vendor::example_interfaces; | ||
| //! # use crate::rclrs::example_interfaces; | ||
| //! use std::time::Duration; | ||
| //! | ||
| //! let mut executor = Context::default_from_env()?.create_basic_executor(); | ||
|
|
@@ -196,7 +196,6 @@ mod subscription; | |
| mod time; | ||
| mod time_source; | ||
| mod timer; | ||
| pub mod vendor; | ||
| mod wait_set; | ||
| mod worker; | ||
|
|
||
|
|
@@ -205,6 +204,8 @@ mod test_helpers; | |
|
|
||
| mod rcl_bindings; | ||
|
|
||
| include!(concat!(env!("OUT_DIR"), "/interfaces.rs")); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This generates a lot of warnings right now. Will need to look at how to hygienically do that (or move to another crate and suppress there)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what I see, the vast majority of the warnings are about missing docstrings. While I do agree that we should suppress warnings I feel this is a symptom that we can still improve our message generation pipeline here. Which maps to GoalInfo: Now And the rust code looks like this: So we are already doing this in part. |
||
|
|
||
| #[cfg(feature = "dyn_msg")] | ||
| pub mod dynamic_message; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like the main hairy part to me, does this mean that we will require message packages to always have a
*version requirement? Would specifying message versions like we do in our examples not work?On the other hand, how do we make sure we are only re-exporting message packages? What happens if a user has a Rust library installed instead? Is there a risk that if I have a
foolibrary that is:colcon-ros-cargopackage.*version).Then
rclrswill end up bundling it and exportingrclrs::foo?