mavlink_bindgen/
lib.rs

1pub use crate::error::BindGenError;
2use std::fs::{read_dir, File};
3use std::io::{self, BufWriter};
4use std::ops::Deref;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8pub mod binder;
9pub mod error;
10pub mod parser;
11mod util;
12
13#[derive(Debug)]
14pub struct GeneratedBinding {
15    pub module_name: String,
16    pub mavlink_xml: PathBuf,
17    pub rust_module: PathBuf,
18}
19
20#[derive(Debug)]
21pub struct GeneratedBindings {
22    pub bindings: Vec<GeneratedBinding>,
23    pub mod_rs: PathBuf,
24}
25
26/// Specifies the source(s) of MAVLink XML definition files used for generating
27/// Rust MAVLink dialect bindings.
28pub enum XmlDefinitions<T: AsRef<Path>> {
29    /// A collection of individual MAVLink XML definition files.
30    Files(Vec<T>),
31    /// A directory containing one or more MAVLink XML definition files.
32    Directory(T),
33}
34
35/// Generate Rust MAVLink dialect binding for dialects present in the given `xml_definitions`
36/// into `destination_dir`.
37///
38/// If successful returns paths of generated bindings linked to their dialect definitions files.
39pub fn generate<P1: AsRef<Path>, P2: AsRef<Path>>(
40    xml_definitions: XmlDefinitions<P1>,
41    destination_dir: P2,
42) -> Result<GeneratedBindings, BindGenError> {
43    let destination_dir = destination_dir.as_ref();
44
45    let mut bindings = vec![];
46
47    match xml_definitions {
48        XmlDefinitions::Files(files) => {
49            if files.is_empty() {
50                return Err(
51                    BindGenError::CouldNotReadDirectoryEntryInDefinitionsDirectory {
52                        source: io::Error::new(
53                            io::ErrorKind::InvalidInput,
54                            "At least one file must be given.",
55                        ),
56                        path: PathBuf::default(),
57                    },
58                );
59            }
60
61            for file in files {
62                let file = file.as_ref();
63
64                bindings.push(generate_single_file(file, destination_dir)?);
65            }
66        }
67        XmlDefinitions::Directory(definitions_dir) => {
68            let definitions_dir = definitions_dir.as_ref();
69
70            if !definitions_dir.is_dir() {
71                return Err(
72                    BindGenError::CouldNotReadDirectoryEntryInDefinitionsDirectory {
73                        source: io::Error::new(
74                            io::ErrorKind::InvalidInput,
75                            format!("{} is not a directory.", definitions_dir.display()),
76                        ),
77                        path: definitions_dir.to_owned(),
78                    },
79                );
80            }
81
82            for entry_maybe in read_dir(definitions_dir).map_err(|source| {
83                BindGenError::CouldNotReadDefinitionsDirectory {
84                    source,
85                    path: definitions_dir.to_path_buf(),
86                }
87            })? {
88                let entry = entry_maybe.map_err(|source| {
89                    BindGenError::CouldNotReadDirectoryEntryInDefinitionsDirectory {
90                        source,
91                        path: definitions_dir.to_path_buf(),
92                    }
93                })?;
94
95                let definition_filename = PathBuf::from(entry.file_name());
96                // Skip non-XML files
97                if !definition_filename.extension().is_some_and(|e| e == "xml") {
98                    continue;
99                }
100
101                bindings.push(generate_single_file(entry.path(), destination_dir)?);
102            }
103        }
104    }
105
106    // Creating `mod.rs`
107    let dest_path = destination_dir.join("mod.rs");
108    let mut outf = File::create(&dest_path).map_err(|source| {
109        BindGenError::CouldNotCreateRustBindingsFile {
110            source,
111            dest_path: dest_path.clone(),
112        }
113    })?;
114
115    // generate code
116    binder::generate(
117        bindings
118            .iter()
119            .map(|binding| binding.module_name.deref())
120            .collect(),
121        &mut outf,
122    );
123
124    Ok(GeneratedBindings {
125        bindings,
126        mod_rs: dest_path,
127    })
128}
129
130/// Generate a Rust MAVLink dialect binding for the given `source_file` dialect into `destination_dir`.
131///
132/// If successful returns path of the generated binding linked to their dialect definition file.
133fn generate_single_file<P1: AsRef<Path>, P2: AsRef<Path>>(
134    source_file: P1,
135    destination_dir: P2,
136) -> Result<GeneratedBinding, BindGenError> {
137    let source_file = source_file.as_ref();
138    let destination_dir = destination_dir.as_ref();
139
140    let definitions_dir = source_file.parent().unwrap_or(Path::new(""));
141
142    if !source_file.exists() {
143        return Err(
144            BindGenError::CouldNotReadDirectoryEntryInDefinitionsDirectory {
145                source: io::Error::new(io::ErrorKind::NotFound, "File not found."),
146                path: definitions_dir.to_owned(),
147            },
148        );
149    }
150
151    if !source_file.extension().is_some_and(|e| e == "xml") {
152        return Err(
153            BindGenError::CouldNotReadDirectoryEntryInDefinitionsDirectory {
154                source: io::Error::new(
155                    io::ErrorKind::InvalidInput,
156                    "Non-XML files are not supported.",
157                ),
158                path: definitions_dir.to_owned(),
159            },
160        );
161    }
162
163    let definition_filename = PathBuf::from(source_file.file_name().unwrap());
164    let module_name = util::to_module_name(&definition_filename);
165    let definition_rs = PathBuf::from(&module_name).with_extension("rs");
166
167    let dest_path = destination_dir.join(definition_rs);
168    let mut outf = BufWriter::new(File::create(&dest_path).map_err(|source| {
169        BindGenError::CouldNotCreateRustBindingsFile {
170            source,
171            dest_path: dest_path.clone(),
172        }
173    })?);
174
175    // codegen
176    parser::generate(definitions_dir, &definition_filename, &mut outf)?;
177
178    Ok(GeneratedBinding {
179        module_name,
180        mavlink_xml: source_file.to_owned(),
181        rust_module: dest_path,
182    })
183}
184
185/// Formats generated code using `rustfmt`.
186pub fn format_generated_code(result: &GeneratedBindings) {
187    if let Err(error) = Command::new("rustfmt")
188        .args(
189            result
190                .bindings
191                .iter()
192                .map(|binding| binding.rust_module.clone()),
193        )
194        .arg(result.mod_rs.clone())
195        .status()
196    {
197        if std::env::args()
198            .next()
199            .unwrap_or_default()
200            .contains("build-script-")
201        {
202            println!("cargo:warning=Failed to run rustfmt: {error}");
203        } else {
204            eprintln!("Failed to run rustfmt: {error}");
205        }
206    }
207}
208
209/// Prints definitions for cargo that describe which files the generated code depends on, indicating when it has to be regenerated.
210pub fn emit_cargo_build_messages(result: &GeneratedBindings) {
211    for binding in &result.bindings {
212        // Re-run build if definition file changes
213        println!(
214            "cargo:rerun-if-changed={}",
215            binding.mavlink_xml.to_string_lossy()
216        );
217    }
218}