Skip to content

Commit

Permalink
Merge pull request #139 from Carter12s/codegen-dependent-files
Browse files Browse the repository at this point in the history
Abilty to correctly inform cargo of file dependnencies in build.rs
  • Loading branch information
Carter12s authored Oct 8, 2023
2 parents dffd3a0 + b4ddebf commit 7c248ea
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 38 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- The build.rs example in example_package now correctly informs cargo of filesystem dependencies

### Fixed

### Changed

- The function interface for top level generation functions in `roslibrust_codegen` have been changed to include the list of dependent
filesystem paths that should trigger re-running code generation. Note: new files added to the search paths will not be automatically detected.

## 0.8.0 - October 4th, 2023

### Added
Expand Down
14 changes: 10 additions & 4 deletions example_package/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Actually invoke code generation on our search paths.
// What we get back is a TokenStream the type normally returned by a proc_macro in rust.
// For a build.rs we typically want to serialize this data to a file for later import
let tokens = roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(p)?;
let (source, dependent_paths) =
roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(p)?;

// It is important for build scripts to only output files to OUT_DIR.
// This guidance can be ignored for end applications. However, crates published and downloaded with cargo
Expand All @@ -26,14 +27,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let dest_path = std::path::Path::new(&out_dir).join("messages.rs");
// Write the generate code to disk
// Note: it will not be nicely formatted at this point which can affect readability when debugging
std::fs::write(dest_path, tokens.to_string())?;
std::fs::write(dest_path, source.to_string())?;
// Optionally rustfmt could be invoked to format the file at this point
// Or https://github.com/dtolnay/prettyplease used on the TokenStream ahead of writing to disk

// If we stopped at this point, our code would still work, but Cargo would not know to rebuild
// our package when a message file changed.
// MAJOR TODO need to get codegen methods to return list of dependent files
// Also probably want to merge down function names to single function with more args / builder
for path in &dependent_paths {
let path_str = path
.as_os_str()
.to_str()
.expect(&format!("Failed to convert path into utf8: {path:?}"));
cargo_emit::rerun_if_changed!(path_str);
}

Ok(())
}
67 changes: 51 additions & 16 deletions roslibrust_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,33 +338,43 @@ impl PartialEq for ConstantInfo {

/// Searches a list of paths for ROS packages and generates struct definitions
/// and implementations for message files and service files in packages it finds.
///
/// Returns a tuple of the generated source code and list of file system paths that if
/// modified would trigger re-generation of the source. This function is designed to
/// be used either in a build.rs file or via the roslibrust_codegen_macro crate.
/// * `additional_search_paths` - A list of additional paths to search beyond those
/// found in ROS_PACKAGE_PATH environment variable.
pub fn find_and_generate_ros_messages(
additional_search_paths: Vec<PathBuf>,
) -> Result<TokenStream, Error> {
) -> Result<(TokenStream, Vec<PathBuf>), Error> {
let mut ros_package_paths = utils::get_search_paths();
ros_package_paths.extend(additional_search_paths);
find_and_generate_ros_messages_without_ros_package_path(ros_package_paths)
}

/// Searches a list of paths for ROS packages and generates struct definitions
/// and implementations for message files and service files in packages it finds.
/// Returns a tuple of the generated source code and list of file system paths that if
/// modified would trigger re-generation of the source. This function is designed to
/// be used either in a build.rs file or via the roslibrust_codegen_macro crate.
///
/// * `search_paths` - A list of paths to search for ROS packages.
pub fn find_and_generate_ros_messages_without_ros_package_path(
search_paths: Vec<PathBuf>,
) -> Result<TokenStream, Error> {
let (messages, services) = find_and_parse_ros_messages(&search_paths)?;
) -> Result<(TokenStream, Vec<PathBuf>), Error> {
let (messages, services, actions) = find_and_parse_ros_messages(&search_paths)?;

if messages.is_empty() && services.is_empty() {
// I'm considering this an error for now, but I could see this one being debateable
// As it stands there is not good way for us to manually produce a warning, so I'd rather fail loud
bail!("Failed to find any services or messages while generating ROS message definitions, paths searched: {search_paths:?}");
}
let (messages, services) = resolve_dependency_graph(messages, services)?;
generate_rust_ros_message_definitions(messages, services)
let msg_iter = messages.iter().map(|m| m.parsed.path.clone());
let srv_iter = services.iter().map(|s| s.parsed.path.clone());
let action_iter = actions.iter().map(|a| a.path.clone());
let dependent_paths = msg_iter.chain(srv_iter).chain(action_iter).collect();
let source = generate_rust_ros_message_definitions(messages, services)?;
Ok((source, dependent_paths))
}

/// Searches a list of paths for ROS packages to find their associated message
Expand All @@ -376,7 +386,14 @@ pub fn find_and_generate_ros_messages_without_ros_package_path(
///
pub fn find_and_parse_ros_messages(
search_paths: &Vec<PathBuf>,
) -> Result<(Vec<ParsedMessageFile>, Vec<ParsedServiceFile>), Error> {
) -> Result<
(
Vec<ParsedMessageFile>,
Vec<ParsedServiceFile>,
Vec<ParsedActionFile>,
),
Error,
> {
let search_paths = search_paths
.into_iter()
.map(|path| {
Expand Down Expand Up @@ -555,9 +572,17 @@ pub fn resolve_dependency_graph(
/// * `msg_paths` -- List of tuple (Package, Path to File) for each file to parse
fn parse_ros_files(
msg_paths: Vec<(Package, PathBuf)>,
) -> Result<(Vec<ParsedMessageFile>, Vec<ParsedServiceFile>), Error> {
) -> Result<
(
Vec<ParsedMessageFile>,
Vec<ParsedServiceFile>,
Vec<ParsedActionFile>,
),
Error,
> {
let mut parsed_messages = Vec::new();
let mut parsed_services = Vec::new();
let mut parsed_actions = Vec::new();
for (pkg, path) in msg_paths {
let contents = std::fs::read_to_string(&path).map_err(|e| {
Error::with(
Expand All @@ -579,13 +604,15 @@ fn parse_ros_files(
"srv" => {
let srv_file = parse_ros_service_file(&contents, name, &pkg, &path)?;
parsed_services.push(srv_file);
// TODO ask shane, shouldn't we be pushing request and response to messages here?
}
"msg" => {
let msg = parse_ros_message_file(&contents, name, &pkg, &path)?;
parsed_messages.push(msg);
}
"action" => {
let action = parse_ros_action_file(&contents, name, &pkg, &path)?;
parsed_actions.push(action.clone());
parsed_messages.push(action.action_type);
parsed_messages.push(action.action_goal_type);
parsed_messages.push(action.goal_type);
Expand All @@ -599,7 +626,7 @@ fn parse_ros_files(
}
}
}
Ok((parsed_messages, parsed_services))
Ok((parsed_messages, parsed_services, parsed_actions))
}

#[cfg(test)]
Expand All @@ -614,9 +641,11 @@ mod test {
"/../assets/ros1_common_interfaces"
);

let gen = find_and_generate_ros_messages(vec![assets_path.into()]);
let (source, paths) = find_and_generate_ros_messages(vec![assets_path.into()]).unwrap();
// Make sure something actually got generated
assert!(!gen.unwrap().is_empty())
assert!(!source.is_empty());
// Make sure we have some paths
assert!(!paths.is_empty());
}

/// Confirms we don't panic on ros2 parsing
Expand All @@ -627,9 +656,11 @@ mod test {
"/../assets/ros2_common_interfaces"
);

let gen = find_and_generate_ros_messages(vec![assets_path.into()]);
let (source, paths) = find_and_generate_ros_messages(vec![assets_path.into()]).unwrap();
// Make sure something actually got generated
assert!(!gen.unwrap().is_empty())
assert!(!source.is_empty());
// Make sure we have some paths
assert!(!paths.is_empty());
}

/// Confirms we don't panic on ros1_test_msgs parsing
Expand All @@ -643,8 +674,11 @@ mod test {
env!("CARGO_MANIFEST_DIR"),
"/../assets/ros1_common_interfaces/std_msgs"
);
let gen = find_and_generate_ros_messages(vec![assets_path.into(), std_msgs.into()]);
assert!(!gen.unwrap().is_empty());
let (source, paths) =
find_and_generate_ros_messages(vec![assets_path.into(), std_msgs.into()]).unwrap();
assert!(!source.is_empty());
// Make sure we have some paths
assert!(!paths.is_empty());
}

/// Confirms we don't panic on ros2_test_msgs parsing
Expand All @@ -653,7 +687,8 @@ mod test {
fn generate_ok_on_ros2_test_msgs() {
let assets_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/ros2_test_msgs");

let gen = find_and_generate_ros_messages(vec![assets_path.into()]);
assert!(!gen.unwrap().is_empty());
let (source, paths) = find_and_generate_ros_messages(vec![assets_path.into()]).unwrap();
assert!(!source.is_empty());
assert!(!paths.is_empty());
}
}
12 changes: 9 additions & 3 deletions roslibrust_codegen_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ impl Parse for RosLibRustMessagePaths {
/// variable ROS_PACKAGE_PATH.
#[proc_macro]
pub fn find_and_generate_ros_messages(input_stream: TokenStream) -> TokenStream {
// Note: there is not currently a way for proc_macros to indicate that they need to be re-generated
// We discard the "dependent_paths" part of the response here...
let RosLibRustMessagePaths { paths } =
parse_macro_input!(input_stream as RosLibRustMessagePaths);
match roslibrust_codegen::find_and_generate_ros_messages(paths) {
Ok(t) => t.into(),
Ok((source, _dependent_paths)) => source.into(),
Err(e) => {
let error_msg = e.to_string();
quote::quote!(compile_error!(#error_msg);).into()
Expand All @@ -58,7 +60,9 @@ pub fn find_and_generate_ros_messages_relative_to_manifest_dir(
}

match roslibrust_codegen::find_and_generate_ros_messages(paths) {
Ok(t) => t.into(),
// Note: there is not currently a way for proc_macros to indicate that they need to be re-generated
// We discard the "dependent_paths" part of the response here...
Ok((source, _dependent_paths)) => source.into(),
Err(e) => {
let error_msg = e.to_string();
quote::quote!(compile_error!(#error_msg);).into()
Expand All @@ -75,7 +79,9 @@ pub fn find_and_generate_ros_messages_without_ros_package_path(
let RosLibRustMessagePaths { paths } =
parse_macro_input!(input_stream as RosLibRustMessagePaths);
match roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(paths) {
Ok(t) => t.into(),
// Note: there is not currently a way for proc_macros to indicate that they need to be re-generated
// We discard the "dependent_paths" part of the response here...
Ok((source, _dependent_paths)) => source.into(),
Err(e) => {
let error_msg = e.to_string();
quote::quote!( compile_error!(#error_msg); ).into()
Expand Down
2 changes: 1 addition & 1 deletion roslibrust_genmsg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl<'a> CodeGeneratorBuilder<'a> {
pub fn build(self) -> std::io::Result<CodeGenerator<'a>> {
// Being lazy here and not infecting other error types to far
// Eventually I think we should move away from io::Result here and remove both of these unwraps
let (messages, services) =
let (messages, services, _actions) =
roslibrust_codegen::find_and_parse_ros_messages(&self.msg_paths).unwrap();
let (messages, services) =
roslibrust_codegen::resolve_dependency_graph(messages, services).unwrap();
Expand Down
33 changes: 19 additions & 14 deletions roslibrust_test/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,19 @@ lazy_static! {
/// This main function is used to generate the contents of ros1.rs, ros2.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let tokens = roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(
(*ROS_1_PATHS).clone(),
)?;
let source = format_rust_source(tokens.to_string().as_str()).to_string();
let (source, _paths) =
roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(
(*ROS_1_PATHS).clone(),
)?;
let source = format_rust_source(source.to_string().as_str()).to_string();
std::fs::write(concat!(env!("CARGO_MANIFEST_DIR"), "/src/ros1.rs"), source)?;

let tokens =
let (source, _paths) =
roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(vec![
ROS_2_PATH.into(),
ROS_2_TEST_PATH.into(),
])?;
let source = format_rust_source(tokens.to_string().as_str()).to_string();
let source = format_rust_source(source.to_string().as_str()).to_string();
std::fs::write(concat!(env!("CARGO_MANIFEST_DIR"), "/src/ros2.rs"), source)?;
Ok(())
}
Expand Down Expand Up @@ -73,10 +74,12 @@ mod test {
/// Confirms that codegen has been run and changes committed
#[test]
fn ros1_lib_is_up_to_date() {
let tokens = roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(
(*ROS_1_PATHS).clone(),
);
let source = format_rust_source(tokens.unwrap().to_string().as_str()).to_string();
let (source, _paths) =
roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(
(*ROS_1_PATHS).clone(),
)
.unwrap();
let source = format_rust_source(source.to_string().as_str()).to_string();
let lib_path = env!("CARGO_MANIFEST_DIR").to_string() + "/src/ros1.rs";
let lib_contents =
std::fs::read_to_string(lib_path).expect("Failed to load current ros1.rs contents");
Expand All @@ -93,10 +96,12 @@ mod test {
/// Confirms that codegen has been run and changes committed
#[test]
fn ros2_lib_is_up_to_date() {
let tokens = roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(
(*ROS_2_PATHS).clone(),
);
let source = format_rust_source(tokens.unwrap().to_string().as_str()).to_string();
let (source, _paths) =
roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(
(*ROS_2_PATHS).clone(),
)
.unwrap();
let source = format_rust_source(source.to_string().as_str()).to_string();
let lib_path = env!("CARGO_MANIFEST_DIR").to_string() + "/src/ros2.rs";
let lib_contents =
std::fs::read_to_string(lib_path).expect("Failed to load current ros2.rs contents");
Expand Down

0 comments on commit 7c248ea

Please sign in to comment.