Skip to main content

zenrc_msgen/
lib.rs

1mod msg_gen;
2
3use std::{env, fs};
4use std::path::{Path, PathBuf};
5use std::process::Command;
6
7pub use crate::msg_gen::generate_rust_wrappers;
8
9/// 递归收集 dir 下所有 .c 文件
10fn collect_c_files(dir: &Path) -> Vec<PathBuf> {
11    let mut result = Vec::new();
12    if let Ok(entries) = fs::read_dir(dir) {
13        for entry in entries.flatten() {
14            let path = entry.path();
15            if path.is_dir() {
16                result.extend(collect_c_files(&path));
17            } else if path.extension().map_or(false, |e| e == "c") {
18                result.push(path);
19            }
20        }
21    }
22    result
23}
24
25/// 将 idlc 生成的 .c 文件编译并打包为静态库
26pub fn compile_idl_libs(lib_name :&str, dds_include_paths: &Vec<PathBuf>, idl_out_dir: &Path) {
27    compile_idl_files(&dds_include_paths, idl_out_dir);
28    let c_files = collect_c_files(idl_out_dir);
29    if c_files.is_empty() {
30        return;
31    }
32
33    let mut build = cc::Build::new();
34    for inc in dds_include_paths {
35        build.include(inc);
36    }
37    build.include(idl_out_dir);
38    for f in &c_files {
39        build.file(f);
40    }
41    build.out_dir(idl_out_dir);
42    build.compile(lib_name);
43}
44
45/// 递归收集 dir 下所有 .h 文件
46fn collect_h_files(dir: &Path) -> Vec<PathBuf> {
47    let mut result = Vec::new();
48    if let Ok(entries) = fs::read_dir(dir) {
49        for entry in entries.flatten() {
50            let path = entry.path();
51            if path.is_dir() {
52                result.extend(collect_h_files(&path));
53            } else if path.extension().map_or(false, |e| e == "h") {
54                result.push(path);
55            }
56        }
57    }
58    result
59}
60
61/// 为 idlc 生成的 .h 文件生成 Rust binding,写入 msg_bindings.rs
62pub fn generate_msg_bindings(idl_out_dir: &Path, out_dir: &Path) {
63    let h_files = collect_h_files(idl_out_dir);
64    if h_files.is_empty() {
65        // 无 IDL 头文件时写入空文件,保证 include! 宏可以正常编译
66        let out_path = out_dir.join("msg_bindings.rs");
67        if let Err(e) = fs::write(&out_path, "") {
68            println!("cargo:warning=Failed to write empty msg_bindings.rs: {e}");
69        }
70        return;
71    }
72
73    let wrapper_path = out_dir.join("msg_bindings_wrapper.h");
74    let includes: String = h_files
75        .iter()
76        .filter_map(|p| p.to_str())
77        .map(|s| format!("#include \"{s}\"\n"))
78        .collect();
79    if let Err(e) = fs::write(&wrapper_path, &includes) {
80        println!("cargo:warning=Failed to write wrapper header: {e}");
81        return;
82    }
83
84    let idl_dir_pattern: String = idl_out_dir
85        .to_str()
86        .unwrap_or("")
87        .chars()
88        .flat_map(|c| {
89            if r"\^$.|?*+()[]{}".contains(c) {
90                vec!['\\', c]
91            } else {
92                vec![c]
93            }
94        })
95        .collect();
96
97    let builder = bindgen::Builder::default()
98        .header(wrapper_path.to_str().unwrap())
99        .clang_arg(format!("-I{}", idl_out_dir.display()))
100        .allowlist_file(format!("{idl_dir_pattern}/.*"))
101        .blocklist_type("dds_key_.*")
102        .blocklist_type("dds_topic_.*")
103        .blocklist_type("dds_type_.*")
104        .size_t_is_usize(true)
105        .merge_extern_blocks(true)
106        .derive_partialeq(true)
107        .generate_comments(true);
108
109    match builder.generate() {
110        Ok(b) => {
111            let out_path = out_dir.join("msg_bindings.rs");
112            if let Err(e) = b.write_to_file(&out_path) {
113                println!("cargo:warning=Failed to write {}: {e}", out_path.display());
114            }
115        }
116        Err(e) => println!("cargo:warning=bindgen failed for msg_bindings: {e}"),
117    }
118}
119
120/// 优先从 PATH 查找 idlc,其次从 dds link_paths 推导 bin 目录
121fn find_idlc() -> Option<PathBuf> {
122    let which_cmd = if cfg!(target_os = "windows") {
123        "where"
124    } else {
125        "which"
126    };
127    if let Ok(output) = Command::new(which_cmd).arg("idlc").output() {
128        if output.status.success() {
129            if let Ok(s) = std::str::from_utf8(&output.stdout) {
130                let path = PathBuf::from(s.lines().next().unwrap_or("").trim());
131                if path.exists() {
132                    return Some(path);
133                }
134            }
135        }
136    }
137
138    None
139}
140
141/// IDL 文件条目,携带路径和 idlc `-b` 基目录
142struct IdlEntry {
143    path: PathBuf,
144    base: PathBuf,
145}
146
147/// 收集所有待编译的 IDL 文件(ROS2 系统包 + `DDS_IDL_PATH` 自定义)
148fn collect_idl_files() -> Vec<IdlEntry> {
149    let split_char = if cfg!(target_os = "windows") {
150        ';'
151    } else {
152        ':'
153    };
154    let mut result = Vec::new();
155
156    if let Ok(val) = env::var("DDS_IDL_PATH") {
157        for entry in val.split(split_char) {
158            let entry = entry.trim();
159            if entry.is_empty() {
160                continue;
161            }
162            let path = PathBuf::from(entry);
163            if path.is_file() && path.extension().map_or(false, |e| e == "idl") {
164                let base = path.parent().unwrap_or(Path::new(".")).to_path_buf();
165                result.push(IdlEntry {
166                    path,
167                    base,
168                });
169            } else if path.is_dir() {
170                let mut stack = vec![path.clone()];
171                while let Some(dir) = stack.pop() {
172                    let Ok(rd) = fs::read_dir(&dir) else { continue };
173                    let mut entries: Vec<_> = rd.flatten().collect();
174                    entries.sort_by_key(|e| e.file_name());
175                    for e in entries {
176                        let p = e.path();
177                        if p.is_dir() {
178                            stack.push(p);
179                        } else if p.extension().map_or(false, |ext| ext == "idl") {
180                            result.push(IdlEntry {
181                                path: p,
182                                base: path.clone(),
183                            });
184                        }
185                    }
186                }
187            }
188        }
189    }
190
191    fs::write(
192        PathBuf::from(env::var("OUT_DIR").unwrap()).join("msg_list.txt"),
193        result
194            .iter()
195            .map(|e| e.path.display().to_string())
196            .collect::<Vec<_>>()
197            .join("\n"),
198    )
199    .expect("Failed to write msg_list.txt");
200    result
201}
202
203/// 编译所有 IDL 文件,产物(`.c`/`.h`)写入 `out_dir`
204fn compile_idl_files(dds_include_paths: &Vec<PathBuf>, out_dir: &Path) {
205    let idl_files = collect_idl_files();
206    if idl_files.is_empty() {
207        return;
208    }
209    let Some(idlc) = find_idlc() else {
210        println!("cargo:warning=idlc not found, skipping IDL compilation");
211        return;
212    };
213
214    let mut include_dirs: Vec<PathBuf> = dds_include_paths.clone();
215    for base in idl_files.iter().map(|e| &e.base) {
216        if !include_dirs.contains(base) {
217            include_dirs.push(base.clone());
218        }
219    }
220
221    for entry in idl_files {
222        let mut cmd = Command::new(&idlc);
223        cmd.arg("-f").arg("case-sensitive");
224        cmd.arg("-b").arg(&entry.base);
225        cmd.arg("-o").arg(out_dir);
226        for inc in &include_dirs {
227            cmd.arg("-I").arg(inc);
228        }
229        cmd.arg(&entry.path);
230
231        match cmd.status() {
232            Ok(_) => {}
233            Err(e) => println!(
234                "cargo:warning=Failed to run idlc for {}: {e}",
235                entry.path.display()
236            )
237        }
238    }
239}