1use std::{
2 fs,
3 path::{Path, PathBuf},
4 process::Output,
5};
6
7use bindgen::BindgenError;
8
9#[derive(Debug)]
10pub enum Error {
11 Bingen(BindgenError),
12 Io(std::io::Error),
13 NoHeaderFound,
14 BadPathBuf(PathBuf),
15 CommandFailure(Output),
16 CompileFailure(cc::Error),
17}
18
19impl std::fmt::Display for Error {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(f, "{:?}", self)
22 }
23}
24impl std::error::Error for Error {}
27
28impl From<BindgenError> for Error {
29 fn from(value: BindgenError) -> Self {
30 Error::Bingen(value)
31 }
32}
33
34impl From<std::io::Error> for Error {
35 fn from(value: std::io::Error) -> Self {
36 Error::Io(value)
37 }
38}
39
40impl From<cc::Error> for Error {
41 fn from(value: cc::Error) -> Self {
42 Error::CompileFailure(value)
43 }
44}
45
46pub type Result<T> = core::result::Result<T, Error>;
47
48pub struct BindingCode {
49 pub binding_file: PathBuf,
50}
51
52#[derive(Debug, Default, Clone)]
53pub struct CommonBuilder {
54 service_name: String,
55 out_path: PathBuf,
56 idl_file: PathBuf,
57 orbit_idl: String,
58 includes: Vec<PathBuf>,
59}
60
61impl CommonBuilder {
62 pub fn new(service_name: &str) -> Self {
63 let includes = find_orbit2_includes();
64
65 CommonBuilder {
66 orbit_idl: "orbit-idl-2".to_owned(),
67 includes,
68 service_name: service_name.to_owned(),
69 ..Default::default()
70 }
71 }
72
73 pub fn idl_file(self, idl_file: &Path) -> Self {
74 CommonBuilder {
75 idl_file: idl_file.to_owned(),
76 ..self
77 }
78 }
79
80 pub fn out_path(self, out_path: &Path) -> Self {
81 CommonBuilder {
82 out_path: out_path.to_owned(),
83 ..self
84 }
85 }
86
87 pub fn generate(&self) -> Result<BindingCode> {
89 let cfiles = self.generate_common_ccode()?;
90
91 let mut cc = cc::Build::new();
92 let cc = cfiles
93 .iter()
94 .filter(|f| f.extension().unwrap_or_default().eq("c"))
95 .fold(&mut cc, |cc, f| cc.file(f));
96
97 cc.include(self.out_path.clone())
98 .includes(self.includes.clone())
100 .flag("-Wno-unused-const-variable")
101 .flag("-Wno-unused-parameter")
102 .try_compile(&format!("{}_common", self.service_name))?;
103
104 let the_header = cfiles
106 .iter()
107 .filter(|f| f.extension().unwrap_or_default().eq("h"))
108 .take(1)
109 .next()
110 .ok_or(Error::NoHeaderFound)?;
111
112 let bindgen = bindgen::Builder::default()
113 .header(
114 the_header
115 .to_str()
116 .ok_or(Error::BadPathBuf(the_header.clone()))?
117 .to_string(),
118 )
119 .allowlist_recursively(false)
120 .clang_args(self.includes.iter().map(|p| format!("-I{}", p.display())))
121 .allowlist_function(format!("{}_.*", self.service_name))
122 .allowlist_type(format!("{}(?:_[[:alpha:]].*)?", self.service_name))
123 .generate()?;
124
125 let binding_file = self
127 .out_path
128 .join(format!("{}_binding.rs", self.service_name));
129 bindgen.write_to_file(&binding_file)?;
130
131 Ok(BindingCode { binding_file })
132 }
133
134 fn generate_common_ccode(&self) -> Result<Vec<PathBuf>> {
135 use std::process::Command;
136 let output = Command::new(self.orbit_idl.clone())
137 .arg(format!(
138 "--output-dir={}",
139 self.out_path.clone().to_str().expect("Not unicode dirname")
140 ))
141 .arg(self.idl_file.clone())
142 .output()?;
143
144 if !output.status.success() {
145 return Err(Error::CommandFailure(output));
146 }
147
148 let cfiles: Result<_> = fs::read_dir(self.out_path.clone())?
149 .map(|entry: std::result::Result<fs::DirEntry, std::io::Error>| entry.map(|d| d.path()))
150 .filter(|p| {
151 if p.is_ok() {
152 let p = p.as_ref().unwrap();
153 let ex = p.extension().unwrap_or_default();
154 ex.eq("c") || ex.eq("h")
155 } else {
156 false
157 }
158 })
159 .map(|r| r.map_err(Error::Io))
160 .collect::<Result<Vec<PathBuf>>>();
161
162 cfiles
163 }
164}
165
166fn find_orbit2_includes() -> Vec<PathBuf> {
167 pkg_config::Config::new()
168 .atleast_version("2.14.19")
169 .print_system_cflags(true)
170 .cargo_metadata(false)
171 .probe("ORBit-2.0")
172 .expect("Cannot find ORBit-2.0 with pkg_config")
174 .include_paths
175 }
179
180#[cfg(test)]
181mod tests {
182 use std::{env, fs};
183
184 use tempdir::TempDir;
185
186 use super::*;
187 #[test]
188 fn base() {
189 assert_eq!(CommonBuilder::new("").service_name, "");
190 assert_eq!(CommonBuilder::new("").orbit_idl, "orbit-idl-2");
191 }
192
193 fn test_fixture() -> (PathBuf, PathBuf) {
194 let tmp_path = TempDir::new("example")
195 .expect("Cannot create temp dir")
196 .into_path();
197 let idl_path = tmp_path.join("echo.idl");
198 fs::write(
199 idl_path.clone(),
200 "interface Echo {
201 void echoString(in string input);
202};",
203 )
204 .expect("can write example");
205 (tmp_path, idl_path)
206 }
207
208 #[test]
211 fn test_generate() {
212 let (tmp_path, idl_path) = test_fixture();
213
214 let builder = CommonBuilder::new("Echo")
215 .idl_file(&idl_path)
216 .out_path(&tmp_path);
217
218 env::set_var("OUT_DIR", tmp_path.as_os_str());
220 env::set_var("TARGET", env!("TEST_TARGET"));
221 env::set_var("HOST", env!("TEST_TARGET")); env::set_var("OPT_LEVEL", "0");
223
224 let binding_res = builder.generate();
226 assert!(binding_res.is_ok());
227
228 let binding_code = binding_res.unwrap();
229 assert_eq!(
230 binding_code.binding_file.as_os_str(),
231 tmp_path.join("Echo_binding.rs").as_os_str()
232 );
233 }
234
235 #[test]
236 fn generate_ccode() {
237 let (tmp_path, idl_path) = test_fixture();
238
239 let cfiles = CommonBuilder::new("Echo")
240 .idl_file(&idl_path)
241 .out_path(&tmp_path)
242 .generate_common_ccode();
243 assert!(cfiles.is_ok());
244 let mut cfiles = cfiles.unwrap();
245
246 let filenames = ["echo-common.c", "echo-skels.c", "echo-stubs.c", "echo.h"];
248 cfiles.sort();
249 assert!(cfiles
251 .iter()
252 .zip(filenames.iter())
253 .all(|(p, &s)| p.ends_with(s)));
254
255 assert_eq!(cfiles.len(), 4);
256 }
257
258 #[test]
259 fn find_includes_test() {
260 assert!(!find_orbit2_includes().is_empty());
261 }
262}