orbit2_buildtools/
lib.rs

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}
24// Requested for Result<_, Box<dyn Error>> trait objects
25// Nothing to implement as Display and Debug are implemented already.
26impl 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    /// Note this can panic! as it uses [`cc::Build`]
88    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            //.cargo_debug(true)
99            .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        // Time to do some bindgen stuff.
105        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        // output the bindgen at the right place.
126        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 is fine as this has orbit2-sys as a dependency
173        .expect("Cannot find ORBit-2.0 with pkg_config")
174        .include_paths
175    //.iter()
176    //.map(|p| format!("-I{}", p.display()))
177    //.collect::<Vec<_>>()
178}
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    // see https://doc.rust-lang.org/reference/conditional-compilation.html
209    //#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))]
210    #[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        // Need to set OUT_DIR to tmp_path
219        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")); // No cross compilation
222        env::set_var("OPT_LEVEL", "0");
223
224        //env::set_var("TARGET", "x86_64-unknown-linux-gnu");
225        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        //assert!( cfiles.iter().zip())
247        let filenames = ["echo-common.c", "echo-skels.c", "echo-stubs.c", "echo.h"];
248        cfiles.sort();
249        //assert_eq!(cfiles, ["ba", "ba"].map(|s| PathBuf::from_str(s).unwrap()));
250        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}