roslibrust_codegen/
lib.rs

1//! A library for generating rust type definitions from ROS IDL files
2//! Supports both ROS1 and ROS2.
3//! Generated types implement roslibrust's MessageType and ServiceType traits making them compatible with all roslibrust backends.
4//!
5//! This library is a pure rust implementation from scratch and requires no ROS installation.
6//!
7//! See [example_package](https://github.com/RosLibRust/roslibrust/tree/master/example_package) for how best to integrate this crate with build.rs
8//!
9//! Directly depending on this crate is not recommended. Instead access it via roslibrust with the `codegen` feature enabled.
10
11use std::{
12    collections::{BTreeMap, BTreeSet, VecDeque},
13    fmt::{Debug, Display},
14    path::{Path, PathBuf},
15};
16
17use log::*;
18use proc_macro2::TokenStream;
19use quote::{quote, ToTokens};
20use simple_error::{bail, SimpleError as Error};
21use utils::Package;
22
23mod gen;
24pub use gen::CodegenOptions;
25use gen::*;
26mod parse;
27use parse::*;
28pub mod utils;
29use utils::RosVersion;
30mod ros2_hashing;
31use ros2_hashing::*;
32mod ros2_builtin_interfaces;
33
34pub mod integral_types;
35pub use integral_types::*;
36
37// Custom serde module for Vec<u8> that handles both base64 (rosbridge) and arrays (other formats)
38pub mod serde_rosmsg_bytes;
39
40// These pub use statements are here to be able to export the dependencies of the generated code
41// so that crates using this crate don't need to add these dependencies themselves.
42// Our generated code should find these exports.
43// Modeled from: https://users.rust-lang.org/t/proc-macros-using-third-party-crate/42465/4
44pub use ::serde;
45pub use serde::{de::DeserializeOwned, Deserialize, Serialize};
46pub use serde_big_array::BigArray; // Used in generated code for large fixed sized arrays
47pub use serde_bytes;
48pub use smart_default::SmartDefault; // Used in generated code for default values // Used in generated code for faster Vec<u8> serialization
49
50/// A unique hash per message type calculated via the RIHS01 Ros2 methodology
51#[derive(Clone, Debug, Default)]
52pub struct Ros2Hash([u8; 32]);
53
54impl Ros2Hash {
55    pub fn to_hash_string(&self) -> String {
56        format!("RIHS01_{}", hex::encode(self.0))
57    }
58
59    pub fn from_string(hash_str: &str) -> Self {
60        // Remove "RIHS01_" prefix if present
61        let hex_str = hash_str.trim_start_matches("RIHS01_");
62        let mut bytes = [0u8; 32];
63        hex::decode_to_slice(hex_str, &mut bytes).expect("Invalid hex string");
64        Ros2Hash(bytes)
65    }
66}
67
68// Conversion from Ros2Hash to TokenStream for use in generated code
69impl ToTokens for Ros2Hash {
70    fn to_tokens(&self, tokens: &mut TokenStream) {
71        let bytes = self.0;
72        let arr_tokens = bytes
73            .iter()
74            .map(|b| syn::LitInt::new(&format!("0x{:02x}", b), proc_macro2::Span::call_site()));
75
76        tokens.extend(quote! { [ #(#arr_tokens,)* ] });
77    }
78}
79
80#[derive(Clone, Debug)]
81pub struct MessageFile {
82    pub parsed: ParsedMessageFile,
83    pub md5sum: String,
84    // Type Hash following the ros2 RIHS01 standard stored as bytes
85    pub ros2_hash: Ros2Hash,
86    // This is the expanded definition of the message for use in message_definition field of
87    // a connection header.
88    // See how https://wiki.ros.org/ROS/TCPROS references gendeps --cat
89    // See https://wiki.ros.org/roslib/gentools for an example of the output
90    pub definition: String,
91    // If true this message has no dynamic sized members and fits in a fixed size in memory
92    pub is_fixed_encoding_length: bool,
93}
94
95impl MessageFile {
96    fn resolve(parsed: ParsedMessageFile, graph: &BTreeMap<String, MessageFile>) -> Option<Self> {
97        let md5sum = Self::compute_md5sum(&parsed, graph).or_else(|| {
98            log::error!("Failed to calculate md5sum for message: {parsed:#?}");
99            None
100        })?;
101        let ros2_hash = calculate_ros2_hash(&parsed, graph);
102        let definition = Self::compute_full_definition(&parsed, graph).or_else(|| {
103            log::error!("Failed to calculate full definition for message: {parsed:#?}");
104            None
105        })?;
106        let is_fixed_length = Self::determine_if_fixed_length(&parsed, graph).or_else(|| {
107            log::error!("Failed to determine if message is fixed length: {parsed:#?}");
108            None
109        })?;
110        Some(MessageFile {
111            parsed,
112            md5sum,
113            ros2_hash,
114            definition,
115            is_fixed_encoding_length: is_fixed_length,
116        })
117    }
118
119    pub fn get_package_name(&self) -> String {
120        self.parsed.package.clone()
121    }
122
123    pub fn get_short_name(&self) -> String {
124        self.parsed.name.clone()
125    }
126
127    pub fn get_full_name(&self) -> String {
128        format!("{}/{}", self.parsed.package, self.parsed.name)
129    }
130
131    pub fn get_md5sum(&self) -> &str {
132        self.md5sum.as_str()
133    }
134
135    pub fn get_fields(&self) -> &[FieldInfo] {
136        &self.parsed.fields
137    }
138
139    pub fn get_constants(&self) -> &[ConstantInfo] {
140        &self.parsed.constants
141    }
142
143    pub fn is_fixed_length(&self) -> bool {
144        self.is_fixed_encoding_length
145    }
146
147    pub fn get_definition(&self) -> &str {
148        &self.definition
149    }
150
151    fn compute_md5sum(
152        parsed: &ParsedMessageFile,
153        graph: &BTreeMap<String, MessageFile>,
154    ) -> Option<String> {
155        let md5sum_content = Self::_compute_md5sum(parsed, graph)?;
156        // Subtract the trailing newline
157        let md5sum = md5::compute(md5sum_content.trim_end().as_bytes());
158        log::trace!(
159            "Message type: {} calculated with md5sum: {md5sum:x}",
160            parsed.get_full_name()
161        );
162        Some(format!("{md5sum:x}"))
163    }
164
165    fn _compute_md5sum(
166        parsed: &ParsedMessageFile,
167        graph: &BTreeMap<String, MessageFile>,
168    ) -> Option<String> {
169        let mut md5sum_content = String::new();
170        for constant in &parsed.constants {
171            md5sum_content.push_str(&format!(
172                "{} {}={}\n",
173                constant.constant_type, constant.constant_name, constant.constant_value
174            ));
175        }
176        for field in &parsed.fields {
177            let field_type = field.field_type.field_type.as_str();
178            if is_intrinsic_type(parsed.version.unwrap_or(RosVersion::ROS1), field_type) {
179                md5sum_content.push_str(&format!("{} {}\n", field.field_type, field.field_name));
180            } else {
181                let field_package = field
182                    .field_type
183                    .package_name
184                    .as_ref()
185                    .unwrap_or_else(|| panic!("Expected package name for field {field:#?}"));
186                let field_full_name = format!("{field_package}/{field_type}");
187                let sub_message = graph.get(field_full_name.as_str())?;
188                let sub_md5sum = Self::compute_md5sum(&sub_message.parsed, graph)?;
189                md5sum_content.push_str(&format!("{} {}\n", sub_md5sum, field.field_name));
190            }
191        }
192
193        Some(md5sum_content)
194    }
195
196    /// Returns the set of all referenced non-intrinsic field types in this type or any of its dependencies
197    fn get_unique_field_types(
198        parsed: &ParsedMessageFile,
199        graph: &BTreeMap<String, MessageFile>,
200    ) -> Option<BTreeSet<String>> {
201        let mut unique_field_types = BTreeSet::new();
202        for field in &parsed.fields {
203            let field_type = field.field_type.field_type.as_str();
204            if is_intrinsic_type(parsed.version.unwrap_or(RosVersion::ROS1), field_type) {
205                continue;
206            }
207            let sub_message = graph.get(field.get_full_type_name().as_str())?;
208            // Note: need to add both the field that is referenced AND its sub-dependencies
209            unique_field_types.insert(field.get_full_type_name());
210            let mut sub_deps = Self::get_unique_field_types(&sub_message.parsed, graph)?;
211            unique_field_types.append(&mut sub_deps);
212        }
213        Some(unique_field_types)
214    }
215
216    /// Computes the full definition of the message, including all referenced custom types
217    /// For reference see: https://wiki.ros.org/roslib/gentools
218    /// Implementation in gentools: https://github.com/strawlab/ros/blob/c3a8785f9d9551cc05cd74000c6536e2244bb1b1/core/roslib/src/roslib/gentools.py#L245
219    fn compute_full_definition(
220        parsed: &ParsedMessageFile,
221        graph: &BTreeMap<String, MessageFile>,
222    ) -> Option<String> {
223        let mut definition_content = String::new();
224        definition_content.push_str(&format!("{}\n", parsed.source.trim()));
225        let sep: &str =
226            "================================================================================\n";
227        for field in Self::get_unique_field_types(parsed, graph)? {
228            let Some(sub_message) = graph.get(&field) else {
229                log::error!(
230                    "Unable to find message type: {field:?}, while computing full definition of {}",
231                    parsed.get_full_name()
232                );
233                return None;
234            };
235            definition_content.push_str(sep);
236            definition_content.push_str(&format!("MSG: {}\n", sub_message.get_full_name()));
237            definition_content.push_str(&format!("{}\n", sub_message.get_definition().trim()));
238        }
239        // Remove trailing \n added by concatenation logic
240        definition_content.pop();
241        Some(definition_content)
242    }
243
244    /// Reports if any field (recursively) referenced by the message contains a dynamic sized type
245    fn determine_if_fixed_length(
246        parsed: &ParsedMessageFile,
247        graph: &BTreeMap<String, MessageFile>,
248    ) -> Option<bool> {
249        for field in &parsed.fields {
250            // If the field has a bounded or unbounded vector type, the message is not fixed length
251            match field.field_type.array_info {
252                ArrayType::Unbounded | ArrayType::Bounded(_) => return Some(false),
253                _ => {}
254            }
255            if field.field_type.package_name.is_none() {
256                // If any field is a string, the message is not fixed length
257                if field.field_type.field_type == "string" {
258                    return Some(false);
259                }
260            } else {
261                let field_msg = graph.get(field.get_full_type_name().as_str())?;
262                let field_is_fixed_length =
263                    Self::determine_if_fixed_length(&field_msg.parsed, graph)?;
264                if !field_is_fixed_length {
265                    return Some(false);
266                }
267            }
268        }
269        Some(true)
270    }
271}
272
273#[derive(Clone, Debug)]
274pub struct ServiceFile {
275    pub(crate) parsed: ParsedServiceFile,
276    pub(crate) request: MessageFile,
277    pub(crate) response: MessageFile,
278    pub(crate) md5sum: String,
279    pub(crate) ros2_hash: Ros2Hash,
280}
281
282impl ServiceFile {
283    /// Attempts to convert a [ParsedServiceFile] into a fully resolved [ServiceFile]
284    /// This will only succeed if all dependencies are already resolved in the graph
285    fn resolve(parsed: ParsedServiceFile, graph: &BTreeMap<String, MessageFile>) -> Option<Self> {
286        if let (Some(request), Some(response)) = (
287            MessageFile::resolve(parsed.request_type.clone(), graph),
288            MessageFile::resolve(parsed.response_type.clone(), graph),
289        ) {
290            let md5sum = Self::compute_md5sum(&parsed, graph)?;
291            let ros2_hash = calculate_ros2_srv_hash(&parsed, graph);
292            Some(ServiceFile {
293                parsed,
294                request,
295                response,
296                md5sum,
297                ros2_hash,
298            })
299        } else {
300            log::error!("Unable to resolve dependencies in service: {parsed:#?}");
301            None
302        }
303    }
304
305    pub fn get_full_name(&self) -> String {
306        format!("{}/{}", self.parsed.package, self.parsed.name)
307    }
308
309    pub fn get_short_name(&self) -> String {
310        self.parsed.name.clone()
311    }
312
313    pub fn get_package_name(&self) -> String {
314        self.parsed.package.clone()
315    }
316
317    pub fn request(&self) -> &MessageFile {
318        &self.request
319    }
320
321    pub fn response(&self) -> &MessageFile {
322        &self.response
323    }
324
325    pub fn get_md5sum(&self) -> String {
326        self.md5sum.clone()
327    }
328
329    pub fn get_ros2_hash(&self) -> &Ros2Hash {
330        &self.ros2_hash
331    }
332
333    fn compute_md5sum(
334        parsed: &ParsedServiceFile,
335        graph: &BTreeMap<String, MessageFile>,
336    ) -> Option<String> {
337        let request_content = MessageFile::_compute_md5sum(&parsed.request_type, graph)?;
338        let response_content = MessageFile::_compute_md5sum(&parsed.response_type, graph)?;
339        let mut md5sum_context = md5::Context::new();
340        md5sum_context.consume(request_content.trim_end().as_bytes());
341        md5sum_context.consume(response_content.trim_end().as_bytes());
342
343        let md5sum = md5sum_context.compute();
344        log::trace!(
345            "Message type: {} calculated with md5sum: {md5sum:x}",
346            parsed.get_full_name()
347        );
348        Some(format!("{md5sum:x}"))
349    }
350}
351
352/// Resolved action file with type hashes for ROS 2 action service wrappers
353pub struct ActionWithHashes {
354    pub parsed: ParsedActionFile,
355    pub send_goal_hash: Ros2Hash,
356    pub get_result_hash: Ros2Hash,
357    pub feedback_message_hash: Ros2Hash,
358}
359
360impl ActionWithHashes {
361    /// Creates an ActionWithHashes with type hashes loaded from ROS 2 JSON metadata
362    pub fn from_json_metadata(parsed: ParsedActionFile, json_path: &Path) -> Option<Self> {
363        use std::fs;
364
365        let json_content = fs::read_to_string(json_path).ok()?;
366        let json: serde_json::Value = serde_json::from_str(&json_content).ok()?;
367
368        let type_hashes = json.get("type_hashes")?.as_array()?;
369
370        // Helper to find hash by suffix
371        let find_hash = |suffix: &str| -> Option<Ros2Hash> {
372            type_hashes.iter().find_map(|type_hash| {
373                let type_name = type_hash.get("type_name")?.as_str()?;
374                let hash_string = type_hash.get("hash_string")?.as_str()?;
375
376                type_name
377                    .ends_with(suffix)
378                    .then(|| Ros2Hash::from_string(hash_string))
379            })
380        };
381
382        Some(ActionWithHashes {
383            parsed,
384            send_goal_hash: find_hash("_SendGoal")?,
385            get_result_hash: find_hash("_GetResult")?,
386            feedback_message_hash: find_hash("_FeedbackMessage")?,
387        })
388    }
389
390    pub fn get_package_name(&self) -> String {
391        self.parsed.package.clone()
392    }
393
394    pub fn get_short_name(&self) -> String {
395        self.parsed.name.clone()
396    }
397}
398
399/// Stores the ROS string representation of a literal
400#[derive(Clone, Debug)]
401pub struct RosLiteral {
402    pub inner: String,
403}
404
405impl Display for RosLiteral {
406    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        std::fmt::Display::fmt(&self.inner, f)
408    }
409}
410
411impl From<String> for RosLiteral {
412    fn from(value: String) -> Self {
413        Self { inner: value }
414    }
415}
416
417/// Represents the different options for a field being an array
418#[derive(PartialEq, Eq, Hash, Debug, Clone)]
419pub enum ArrayType {
420    NotArray,
421    FixedLength(usize),
422    // Bounded is ROS2 only
423    Bounded(usize),
424    Unbounded,
425}
426
427/// Describes the type for an individual field in a message
428#[derive(PartialEq, Eq, Hash, Debug, Clone)]
429pub struct FieldType {
430    // Present when an externally referenced package is used
431    pub package_name: Option<String>,
432    // Redundantly store the name of the package the field is in
433    // This is so that when an external package_name is not present
434    // we can still construct the full name of the field "package/field_type"
435    pub source_package: String,
436    // Explicit text of type without array specifier, referenced package, or string capacity
437    // e.g. "string", "uint8", "Header", "MyCustomType"
438    // Not: "std_msgs/Header", "uint8[10]", "string<=10"
439    pub field_type: String,
440    // Indicates if the field is some type of list or "NotArray"
441    pub array_info: ArrayType,
442
443    // ROS2 specific feature, you can write "string<=10" to indicate a string with a maximum length
444    // When this happen we'll parse the capacity here, and convert the field_type to "string"
445    pub string_capacity: Option<usize>,
446}
447
448/// Serializes the field type exactly how it would be written in a .msg file
449impl std::fmt::Display for FieldType {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        match self.array_info {
452            ArrayType::FixedLength(n) => f.write_fmt(format_args!("{}[{}]", self.field_type, n)),
453            ArrayType::Unbounded => f.write_fmt(format_args!("{}[]", self.field_type)),
454            ArrayType::NotArray => f.write_fmt(format_args!("{}", self.field_type)),
455            ArrayType::Bounded(n) => f.write_fmt(format_args!("{}[<={}]", self.field_type, n)),
456        }
457    }
458}
459
460impl FieldType {
461    /// Returns true iff this field type is a primitive type in ROS1 & ROS2, and not referenced sub-type
462    /// Time, Duration, and Header are not considered primitive types
463    pub fn is_primitive(&self) -> bool {
464        crate::parse::ROS_PRIMITIVE_TYPE_LIST.contains(&self.field_type.as_str())
465    }
466}
467
468/// Describes all information for an individual field
469#[derive(Clone, Debug)]
470pub struct FieldInfo {
471    pub field_type: FieldType,
472    pub field_name: String,
473    // Exists if this is a ros2 message field with a default value
474    pub default: Option<RosLiteral>,
475}
476
477// Because TokenStream doesn't impl PartialEq we have to do it manually for FieldInfo
478impl PartialEq for FieldInfo {
479    fn eq(&self, other: &Self) -> bool {
480        self.field_type == other.field_type && self.field_name == other.field_name
481        // && self.default == other.default
482    }
483}
484
485impl FieldInfo {
486    // Returns the full name of the type of this field in ROS1 format, e.g. std_msgs/String or example_interfaces/Int32
487    pub fn get_full_type_name(&self) -> String {
488        let field_package = self
489            .field_type
490            .package_name
491            .as_ref()
492            .unwrap_or(&self.field_type.source_package);
493        format!("{field_package}/{}", self.field_type.field_type)
494    }
495
496    // Returns the full name of the type of this field in ROS2 format, e.g. std_msgs/msg/String or example_interfaces/msg/Int32
497    pub fn get_ros2_full_type_name(&self) -> String {
498        let field_package = self
499            .field_type
500            .package_name
501            .as_ref()
502            .unwrap_or(&self.field_type.source_package);
503        // Not sure this is a safe assumption, but I think we can safely shove msg here?
504        format!("{field_package}/msg/{}", self.field_type.field_type)
505    }
506}
507
508/// Describes all information for a constant within a message
509/// Note: Constants are not fully supported yet (waiting on codegen support)
510#[derive(Clone, Debug)]
511pub struct ConstantInfo {
512    pub constant_type: String,
513    pub constant_name: String,
514    pub constant_value: RosLiteral,
515}
516
517// Because TokenStream doesn't impl PartialEq we have to do it manually for ConstantInfo
518impl PartialEq for ConstantInfo {
519    fn eq(&self, other: &Self) -> bool {
520        self.constant_type == other.constant_type && self.constant_name == other.constant_name
521        // && self.constant_value == other.constant_value
522    }
523}
524
525/// Searches a list of paths for ROS packages and generates struct definitions
526/// and implementations for message files and service files in packages it finds.
527/// Returns a tuple of the generated source code and list of file system paths that if
528/// modified would trigger re-generation of the source. This function is designed to
529/// be used either in a build.rs file or via the roslibrust_codegen_macro crate.
530/// * `additional_search_paths` - A list of additional paths to search beyond those
531///   found in ROS_PACKAGE_PATH environment variable.
532pub fn find_and_generate_ros_messages(
533    additional_search_paths: Vec<PathBuf>,
534) -> Result<(TokenStream, Vec<PathBuf>), Error> {
535    let mut ros_package_paths = utils::get_search_paths();
536    ros_package_paths.extend(additional_search_paths);
537    find_and_generate_ros_messages_without_ros_package_path(ros_package_paths)
538}
539
540/// Searches a list of paths for ROS packages and generates struct definitions
541/// and implementations for message files and service files in packages it finds.
542/// Returns a tuple of the generated source code and list of file system paths that if
543/// modified would trigger re-generation of the source. This function is designed to
544/// be used either in a build.rs file or via the roslibrust_codegen_macro crate.
545///
546/// * `search_paths` - A list of paths to search for ROS packages.
547pub fn find_and_generate_ros_messages_without_ros_package_path(
548    search_paths: Vec<PathBuf>,
549) -> Result<(TokenStream, Vec<PathBuf>), Error> {
550    let (messages, services, actions) = find_and_parse_ros_messages(&search_paths)?;
551    if messages.is_empty() && services.is_empty() {
552        // I'm considering this an error for now, but I could see this one being debateable
553        // As it stands there is not good way for us to manually produce a warning, so I'd rather fail loud
554        bail!("Failed to find any services or messages while generating ROS message definitions, paths searched: {search_paths:?}");
555    }
556    tokenize_messages_and_services(messages, services, actions)
557}
558
559/// Generates source code and list of depnendent file system paths
560fn tokenize_messages_and_services(
561    messages: Vec<ParsedMessageFile>,
562    services: Vec<ParsedServiceFile>,
563    actions: Vec<ParsedActionFile>,
564) -> Result<(TokenStream, Vec<PathBuf>), Error> {
565    let (messages, services) = resolve_dependency_graph(messages, services)?;
566    let msg_iter = messages.iter().map(|m| m.parsed.path.clone());
567    let srv_iter = services.iter().map(|s| s.parsed.path.clone());
568    let action_iter = actions.iter().map(|a| a.path.clone());
569    let dependent_paths = msg_iter.chain(srv_iter).chain(action_iter).collect();
570    let source =
571        generate_rust_ros_message_definitions(messages, services, &CodegenOptions::default())?;
572    Ok((source, dependent_paths))
573}
574
575/// Generates struct definitions and implementations for message and service files
576/// in the given packages.
577pub fn generate_ros_messages_for_packages(
578    packages: Vec<Package>,
579) -> Result<(TokenStream, Vec<PathBuf>), Error> {
580    let msg_paths = packages
581        .iter()
582        .flat_map(|package| {
583            utils::get_message_files(package).map(|msgs| {
584                msgs.into_iter()
585                    .map(|msg| (package.clone(), msg))
586                    .collect::<Vec<_>>()
587            })
588        })
589        .flatten()
590        .collect();
591    let (messages, services, actions) = parse_ros_files(msg_paths)?;
592    if messages.is_empty() && services.is_empty() {
593        bail!("Failed to find any services or messages while generating ROS message definitions, packages searched: {packages:?}")
594    }
595    tokenize_messages_and_services(messages, services, actions)
596}
597
598/// Searches a list of paths for ROS packages to find their associated message
599/// and service files, parsing and performing dependency resolution on those
600/// it finds. Returns a map of PACKAGE_NAME/MESSAGE_NAME strings to message file
601/// data and vector of service file data.
602///
603/// * `search_paths` - A list of paths to search.
604///
605#[allow(clippy::type_complexity)]
606pub fn find_and_parse_ros_messages(
607    search_paths: &[PathBuf],
608) -> Result<
609    (
610        Vec<ParsedMessageFile>,
611        Vec<ParsedServiceFile>,
612        Vec<ParsedActionFile>,
613    ),
614    Error,
615> {
616    let search_paths  = search_paths
617        .iter()
618        .map(|path| {
619            path.canonicalize().map_err(
620            |e| {
621                    Error::with(format!("Codegen was instructed to search a path that could not be canonicalized relative to {:?}: {path:?}", std::env::current_dir().unwrap()).as_str(), e)
622        })
623        })
624        .collect::<Result<Vec<_>, Error>>()?;
625    debug!(
626        "Codegen is looking in following paths for files: {:?}",
627        &search_paths
628    );
629    let packages = utils::crawl(&search_paths);
630    // Check for duplicate package names
631    let packages = utils::deduplicate_packages(packages);
632    if packages.is_empty() {
633        bail!(
634            "No ROS packages found while searching in: {search_paths:?}, relative to {:?}",
635            std::env::current_dir().unwrap()
636        );
637    }
638    debug!("After deduplication {:?} packages remain.", packages.len());
639
640    let message_files = packages
641        .iter()
642        .flat_map(|pkg| {
643            let files = utils::get_message_files(pkg).map_err(|err| {
644                Error::with(
645                    format!("Unable to get paths to message files for {pkg:?}:").as_str(),
646                    err,
647                )
648            });
649            // See https://stackoverflow.com/questions/59852161/how-to-handle-result-in-flat-map
650            match files {
651                Ok(files) => {
652                    debug!(
653                        "Found {:?} interface files in package: {:?}",
654                        files.len(),
655                        pkg.name
656                    );
657                    files
658                        .into_iter()
659                        .map(|path| Ok((pkg.clone(), path)))
660                        .collect()
661                }
662                Err(e) => vec![Err(e)],
663            }
664        })
665        .collect::<Result<Vec<(Package, PathBuf)>, Error>>()?;
666
667    parse_ros_files(message_files)
668}
669
670/// Takes in collections of ROS message and ROS service data and generates Rust
671/// source code corresponding to the definitions.
672///
673/// This function assumes that the provided messages make up a completely resolved
674/// tree of dependent messages.
675///
676/// * `messages` - Collection of ROS message definition data.
677/// * `services` - Collection of ROS service definition data.
678/// * `options` - Code generation options.
679pub fn generate_rust_ros_message_definitions(
680    messages: Vec<MessageFile>,
681    services: Vec<ServiceFile>,
682    options: &CodegenOptions,
683) -> Result<TokenStream, Error> {
684    let mut modules_to_struct_definitions: BTreeMap<String, Vec<TokenStream>> = BTreeMap::new();
685
686    // Convert messages files into rust token streams and insert them into BTree organized by package
687    messages.into_iter().try_for_each(|message| {
688        let pkg_name = message.parsed.package.clone();
689        let definition = generate_struct(message, Some(options))?;
690        if let Some(entry) = modules_to_struct_definitions.get_mut(&pkg_name) {
691            entry.push(definition);
692        } else {
693            modules_to_struct_definitions.insert(pkg_name, vec![definition]);
694        }
695        Ok::<(), Error>(())
696    })?;
697    // Do the same for services
698    services.into_iter().try_for_each(|service| {
699        let pkg_name = service.parsed.package.clone();
700        let definition = generate_service(service, Some(options))?;
701        if let Some(entry) = modules_to_struct_definitions.get_mut(&pkg_name) {
702            entry.push(definition);
703        } else {
704            modules_to_struct_definitions.insert(pkg_name, vec![definition]);
705        }
706        Ok::<(), Error>(())
707    })?;
708    // Now generate modules to wrap all of the TokenStreams in a module for each package
709    let all_pkgs = modules_to_struct_definitions
710        .keys()
711        .cloned()
712        .collect::<Vec<String>>();
713    let module_definitions = modules_to_struct_definitions
714        .into_iter()
715        .map(|(pkg, struct_defs)| generate_mod(pkg, struct_defs, &all_pkgs[..]))
716        .collect::<Vec<TokenStream>>();
717
718    Ok(quote! {
719        #(#module_definitions)*
720
721    })
722}
723
724struct MessageMetadata {
725    msg: ParsedMessageFile,
726    seen_count: u32,
727}
728
729pub fn resolve_dependency_graph(
730    messages: Vec<ParsedMessageFile>,
731    services: Vec<ParsedServiceFile>,
732) -> Result<(Vec<MessageFile>, Vec<ServiceFile>), Error> {
733    const MAX_PARSE_ITER_LIMIT: u32 = 2048;
734    let mut unresolved_messages = messages
735        .into_iter()
736        .map(|msg| MessageMetadata { msg, seen_count: 0 })
737        .collect::<VecDeque<_>>();
738
739    // We seed the initial map with some hard codeded definitions containing types we consider "standard"
740    let mut resolved_messages = ros2_builtin_interfaces::get_builtin_interfaces();
741
742    // First resolve the message dependencies
743    while let Some(MessageMetadata { msg, seen_count }) = unresolved_messages.pop_front() {
744        // Check our resolved messages for each of the fields
745        let fully_resolved = msg.fields.iter().all(|field| {
746            let is_primitive = field.field_type.is_primitive();
747            if !is_primitive {
748                let is_resolved =
749                    resolved_messages.contains_key(field.get_full_type_name().as_str());
750                is_resolved
751            } else {
752                true
753            }
754        });
755
756        if fully_resolved {
757            let debug_name = msg.get_full_name();
758            let msg_file = MessageFile::resolve(msg, &resolved_messages).ok_or(
759                Error::new(format!("Failed to correctly resolve message {debug_name:?}, either md5sum could not be calculated, or fixed length was indeterminate"))
760            )?;
761            resolved_messages.insert(msg_file.get_full_name(), msg_file);
762        } else {
763            unresolved_messages.push_back(MessageMetadata {
764                seen_count: seen_count + 1,
765                msg,
766            });
767        }
768
769        if seen_count > MAX_PARSE_ITER_LIMIT {
770            let msg_names = unresolved_messages
771                .iter()
772                .map(|item| format!("{}/{}", item.msg.package, item.msg.name))
773                .collect::<Vec<_>>();
774
775            // Determine which fields are still unresolved, that don't reference other messages that aren't resolved
776            let mut unresolved_fields = unresolved_messages
777                .iter()
778                .flat_map(|item| {
779                    item.msg
780                        .fields
781                        .iter()
782                        .filter_map(|field| {
783                            if !field.field_type.is_primitive() {
784                                if resolved_messages
785                                    .contains_key(field.get_full_type_name().as_str())
786                                {
787                                    None
788                                } else {
789                                    // Field is unresolved!
790                                    Some(field.get_full_type_name())
791                                }
792                            } else {
793                                None
794                            }
795                        })
796                        .collect::<Vec<_>>()
797                })
798                .collect::<Vec<_>>();
799            unresolved_fields.sort();
800            unresolved_fields.dedup();
801            let unresolved_fields = unresolved_fields
802                .into_iter()
803                .filter(|f| !msg_names.contains(f))
804                .collect::<Vec<_>>();
805
806            bail!(
807                "Unable to resolve ROS message dependencies after reaching search limit.\n\
808                 The following types are still unresolved:\n{unresolved_fields:#?}\n
809                 This is preventing full resolution for the following messages:\n{msg_names:#?}"
810            );
811        }
812    }
813
814    // Now that all messages are parsed, we can parse and resolve services
815    let mut resolved_services: Vec<_> = services
816        .into_iter()
817        .map(|srv| {
818            let name = srv.path.clone();
819            ServiceFile::resolve(srv, &resolved_messages).ok_or(Error::new(format!(
820                "Failed to correctly resolve service: {:?}",
821                &name
822            )))
823        })
824        .collect::<Result<Vec<_>, Error>>()?;
825    resolved_services.sort_by(|a: &ServiceFile, b: &ServiceFile| a.parsed.name.cmp(&b.parsed.name));
826
827    Ok((resolved_messages.into_values().collect(), resolved_services))
828}
829
830/// Parses all ROS file types and returns a final expanded set
831/// Currently supports service files, message files, and action files
832/// The returned collection will contain all messages files including those buried with the
833/// service or action files, and will have fully expanded and resolved referenced types in other packages.
834/// * `msg_paths` -- List of tuple (Package, Path to File) for each file to parse
835#[allow(clippy::type_complexity)]
836pub(crate) fn parse_ros_files(
837    msg_paths: Vec<(Package, PathBuf)>,
838) -> Result<
839    (
840        Vec<ParsedMessageFile>,
841        Vec<ParsedServiceFile>,
842        Vec<ParsedActionFile>,
843    ),
844    Error,
845> {
846    let mut parsed_messages = Vec::new();
847    let mut parsed_services = Vec::new();
848    let mut parsed_actions = Vec::new();
849    for (pkg, path) in msg_paths {
850        let contents = std::fs::read_to_string(&path).map_err(|e| {
851            Error::with(
852                format!("Codgen failed while attempting to read file {path:?} from disk:").as_str(),
853                e,
854            )
855        })?;
856        // Probably being overly aggressive with error shit here, but I'm on a kick
857        let name = path
858            .file_stem()
859            .ok_or(Error::new(format!(
860                "Failed to extract valid file stem for file at {path:?}"
861            )))?
862            .to_str()
863            .ok_or(Error::new(format!(
864                "File stem for file at path {path:?} was not valid unicode?"
865            )))?;
866        match path.extension().unwrap().to_str().unwrap() {
867            "srv" => {
868                let srv_file = parse_ros_service_file(&contents, name, &pkg, &path)?;
869                parsed_services.push(srv_file);
870            }
871            "msg" => {
872                let msg = parse_ros_message_file(&contents, name, &pkg, &path)?;
873                parsed_messages.push(msg);
874            }
875            "action" => {
876                let action = parse_ros_action_file(&contents, name, &pkg, &path)?;
877                parsed_actions.push(action.clone());
878                parsed_messages.push(action.action_type);
879                parsed_messages.push(action.action_goal_type);
880                parsed_messages.push(action.goal_type);
881                parsed_messages.push(action.action_result_type);
882                parsed_messages.push(action.result_type);
883                parsed_messages.push(action.action_feedback_type);
884                parsed_messages.push(action.feedback_type);
885            }
886            _ => {
887                log::error!("File extension not recognized as a ROS file: {path:?}");
888            }
889        }
890    }
891    Ok((parsed_messages, parsed_services, parsed_actions))
892}
893
894/// Resolves parsed actions into ActionWithHashes with type hashes from JSON metadata
895pub fn resolve_action_hashes(parsed_actions: Vec<ParsedActionFile>) -> Vec<ActionWithHashes> {
896    parsed_actions
897        .into_iter()
898        .filter_map(|parsed_action| {
899            // The JSON file should be in the same directory as the .action file
900            let json_path = parsed_action.path.with_extension("json");
901
902            ActionWithHashes::from_json_metadata(parsed_action.clone(), &json_path).or_else(|| {
903                log::warn!(
904                    "Failed to resolve action hashes for {}/{}",
905                    parsed_action.package,
906                    parsed_action.name
907                );
908                None
909            })
910        })
911        .collect()
912}
913
914#[cfg(test)]
915mod test {
916    use crate::find_and_generate_ros_messages;
917
918    /// Confirms we don't panic on ros1 parsing
919    #[test_log::test]
920    fn generate_ok_on_ros1() {
921        let assets_path = concat!(
922            env!("CARGO_MANIFEST_DIR"),
923            "/../assets/ros1_common_interfaces"
924        );
925
926        let (source, paths) = find_and_generate_ros_messages(vec![assets_path.into()]).unwrap();
927        // Make sure something actually got generated
928        assert!(!source.is_empty());
929        // Make sure we have some paths
930        assert!(!paths.is_empty());
931    }
932
933    /// Confirms we don't panic on ros2 parsing
934    #[test_log::test]
935    fn generate_ok_on_ros2() {
936        let assets_path = concat!(
937            env!("CARGO_MANIFEST_DIR"),
938            "/../assets/ros2_common_interfaces"
939        );
940
941        let required_path = concat!(
942            env!("CARGO_MANIFEST_DIR"),
943            "/../assets/ros2_required_msgs/rcl_interfaces/builtin_interfaces"
944        );
945
946        let (source, paths) =
947            find_and_generate_ros_messages(vec![assets_path.into(), required_path.into()]).unwrap();
948        // Make sure something actually got generated
949        assert!(!source.is_empty());
950        // Make sure we have some paths
951        assert!(!paths.is_empty());
952    }
953
954    /// Confirms we don't panic on ros1_test_msgs parsing
955    #[test_log::test]
956    fn generate_ok_on_ros1_test_msgs() {
957        // Note: because our test msgs depend on std_message this test will fail unless ROS_PACKAGE_PATH includes std_msgs
958        // To avoid that we add std_messsages to the extra paths.
959        let assets_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/ros1_test_msgs");
960        let std_msgs = concat!(
961            env!("CARGO_MANIFEST_DIR"),
962            "/../assets/ros1_common_interfaces/std_msgs"
963        );
964        let (source, paths) =
965            find_and_generate_ros_messages(vec![assets_path.into(), std_msgs.into()]).unwrap();
966        assert!(!source.is_empty());
967        // Make sure we have some paths
968        assert!(!paths.is_empty());
969    }
970
971    /// Confirms we don't panic on ros2_test_msgs parsing
972    #[test_log::test]
973    fn generate_ok_on_ros2_test_msgs() {
974        let assets_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/ros2_test_msgs");
975        let required_path = concat!(
976            env!("CARGO_MANIFEST_DIR"),
977            "/../assets/ros2_required_msgs/rcl_interfaces/builtin_interfaces"
978        );
979
980        let (source, paths) =
981            find_and_generate_ros_messages(vec![assets_path.into(), required_path.into()]).unwrap();
982        assert!(!source.is_empty());
983        assert!(!paths.is_empty());
984    }
985}