zanbil_build/
lib.rs

1use std::{
2    collections::HashMap,
3    fmt::Write,
4    path::{Path, PathBuf},
5};
6
7use base64::{Engine, prelude::BASE64_URL_SAFE};
8use build_rs::input::out_dir;
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use toml::Value;
12
13fn dep_includes() -> Vec<(String, String)> {
14    let mut includes = Vec::new();
15
16    for (dep, val) in std::env::vars() {
17        if let Some(dep) = dep.strip_prefix("DEP_") {
18            if let Some(dep) = dep.strip_suffix("_ZANBIL_INCLUDE") {
19                includes.push((dep.to_string(), val));
20            }
21        }
22    }
23
24    includes
25}
26
27#[derive(Debug, Deserialize)]
28#[serde(tag = "kind", rename_all = "snake_case")]
29enum FileRuleKind {
30    Source,
31    Test,
32    PrivateHeader,
33    PublicHeader { destination: String },
34}
35
36#[derive(Debug, Deserialize)]
37pub struct FileRule {
38    path: String,
39    #[serde(flatten)]
40    kind: FileRuleKind,
41}
42
43#[derive(Debug, Deserialize)]
44pub struct FileRules(Vec<FileRule>);
45
46impl Default for FileRules {
47    fn default() -> Self {
48        Self(vec![
49            FileRule {
50                path: r#"^src/.*\.cpp$"#.to_owned(),
51                kind: FileRuleKind::Source,
52            },
53            FileRule {
54                path: r#"^src/.*\.c$"#.to_owned(),
55                kind: FileRuleKind::Source,
56            },
57            FileRule {
58                path: r#"^src/(.*)\.h$"#.to_owned(),
59                kind: FileRuleKind::PublicHeader {
60                    destination: "$lib/$1.h".to_owned(),
61                },
62            },
63            FileRule {
64                path: r#"^test/.*\.cpp$"#.to_owned(),
65                kind: FileRuleKind::Test,
66            },
67            FileRule {
68                path: r#"^test/.*\.c$"#.to_owned(),
69                kind: FileRuleKind::Test,
70            },
71        ])
72    }
73}
74
75fn extract_first_directory(regex: &str) -> Option<&str> {
76    let r = regex.split_once("/")?.0.strip_prefix("^")?;
77    for c in r.chars() {
78        if !c.is_ascii_alphanumeric() && !c.is_whitespace() {
79            return None;
80        }
81    }
82    Some(r)
83}
84
85impl FileRules {
86    fn compile(self) -> FileRulesCompiled {
87        let mut rules: HashMap<String, Vec<FileRuleCompiled>> = HashMap::new();
88        for rule in self.0 {
89            let base_dir = extract_first_directory(&rule.path)
90                .expect("File rules should contain a base directory");
91            rules
92                .entry(base_dir.to_owned())
93                .or_default()
94                .push(FileRuleCompiled {
95                    path: Regex::new(&rule.path).unwrap(),
96                    kind: rule.kind,
97                });
98        }
99        FileRulesCompiled { rules }
100    }
101}
102
103#[derive(Debug, Default, Deserialize)]
104#[non_exhaustive]
105pub struct ZanbilConf {
106    pub cpp: Option<u8>,
107    #[serde(default)]
108    pub make_dependencies_public: bool,
109    #[serde(default)]
110    pub file_rules: FileRules,
111}
112
113struct FileRuleCompiled {
114    path: Regex,
115    kind: FileRuleKind,
116}
117
118struct FileRulesCompiled {
119    rules: HashMap<String, Vec<FileRuleCompiled>>,
120}
121
122impl FileRulesCompiled {
123    fn get_kind(
124        rule_set: &[FileRuleCompiled],
125        path: &str,
126        base: &str,
127        lib: &str,
128    ) -> Option<FileRuleKind> {
129        let path = path.strip_prefix("./").unwrap_or(path);
130        let (captures, kind) = rule_set
131            .iter()
132            .find_map(|x| Some((x.path.captures(path)?, &x.kind)))?;
133        Some(match kind {
134            FileRuleKind::Source => FileRuleKind::Source,
135            FileRuleKind::Test => FileRuleKind::Test,
136            FileRuleKind::PrivateHeader => FileRuleKind::PrivateHeader,
137            FileRuleKind::PublicHeader { destination } => FileRuleKind::PublicHeader {
138                destination: {
139                    let mut d = destination.replace("$base", base).replace("$lib", lib);
140                    for (i, c) in captures.iter().enumerate() {
141                        if let Some(c) = c {
142                            d = d.replace(&format!("${i}"), c.as_str());
143                        }
144                    }
145                    d
146                },
147            },
148        })
149    }
150}
151
152#[derive(Debug, Serialize, Deserialize)]
153pub struct Dependency {
154    pub include_dirs: Vec<PathBuf>,
155}
156
157#[derive(Debug)]
158#[non_exhaustive]
159pub struct ZanbilCrate {
160    pub name: String,
161    pub config: ZanbilConf,
162    pub include_dir: PathBuf,
163    pub aggregated_include_dirs: Vec<PathBuf>,
164    pub dependencies: Vec<Dependency>,
165    pub compiler: Option<String>,
166}
167
168pub fn init_zanbil_crate() -> ZanbilCrate {
169    let name = build_rs::input::cargo_manifest_links().expect("zanbil expects a link name");
170
171    let cargo_toml_path = build_rs::input::cargo_manifest_dir().join("Cargo.toml");
172    build_rs::output::rerun_if_changed(&cargo_toml_path);
173    let cargo_toml = std::fs::read_to_string(cargo_toml_path).unwrap();
174
175    let value: Value = toml::from_str(&cargo_toml).unwrap();
176
177    let config: ZanbilConf = value
178        .get("package")
179        .and_then(|x| {
180            x.get("metadata")?
181                .get("zanbil")?
182                .clone()
183                .try_into()
184                .expect("failed to validate zanbil schema")
185        })
186        .unwrap_or_default();
187
188    let include_dir = out_dir().join("include");
189    let mut dependencies: Vec<Dependency> = vec![];
190    std::fs::create_dir_all(&include_dir).unwrap();
191    std::fs::remove_dir_all(&include_dir).unwrap();
192    std::fs::create_dir_all(&include_dir).unwrap();
193
194    let mut main_rs_file = String::new();
195
196    for (dep, include) in dep_includes() {
197        writeln!(main_rs_file, "extern crate {};", dep.to_lowercase()).unwrap();
198        dependencies.push(toml::from_slice(&BASE64_URL_SAFE.decode(&include).unwrap()).unwrap());
199    }
200    writeln!(
201        main_rs_file,
202        r#"
203#[cfg(test)]
204unsafe extern "C" {{
205    fn zanbil_test_runner(argc: std::ffi::c_int, argv: *const *const std::ffi::c_char) -> std::ffi::c_int;
206}}
207
208#[test]
209fn zanbil_tests() {{
210    // 1. Collect command-line arguments from the Rust environment.
211    let args: Vec<String> = std::env::args().collect();
212
213    // 2. Convert Rust `String`s to C-compatible `CString`s.
214    let c_args: Vec<_> = args.into_iter()
215        .map(|arg| std::ffi::CString::new(arg).unwrap())
216        .collect();
217
218    // 3. Convert `CString`s to raw pointers (`*const c_char`).
219    let argv: Vec<*const std::ffi::c_char> = c_args.iter()
220        .map(|arg| arg.as_ptr())
221        .collect();
222    
223    // 4. Get the argument count (`argc`).
224    let argc = argv.len() as i32;
225    if unsafe {{ zanbil_test_runner(argc, argv.as_ptr()) }} != 0 {{
226        panic!("zanbil_test_runner exit code was not zero");
227    }}
228}}"#
229    )
230    .unwrap();
231
232    build_rs::output::rerun_if_env_changed("ZANBIL_CXX");
233    build_rs::output::rerun_if_env_changed("ZANBIL_CC");
234
235    let compiler = if config.cpp.is_some() {
236        if let Ok(cxx) = std::env::var("ZANBIL_CXX") {
237            Some(cxx)
238        } else {
239            Some("zanbil_c++".to_owned())
240        }
241    } else {
242        if let Ok(cc) = std::env::var("ZANBIL_CC") {
243            Some(cc)
244        } else {
245            Some("zanbil_cc".to_owned())
246        }
247    };
248
249    let mut aggregated_include_dirs: Vec<PathBuf> = dependencies
250        .iter()
251        .flat_map(|x| &x.include_dirs)
252        .chain([&include_dir])
253        .cloned()
254        .collect();
255
256    aggregated_include_dirs.sort();
257    aggregated_include_dirs.dedup();
258
259    std::fs::write(out_dir().join("generated_lib.rs"), main_rs_file).unwrap();
260
261    let me = Dependency {
262        include_dirs: if config.make_dependencies_public {
263            aggregated_include_dirs.clone()
264        } else {
265            vec![include_dir.clone()]
266        },
267    };
268
269    build_rs::output::metadata(
270        "ZANBIL_INCLUDE",
271        &BASE64_URL_SAFE.encode(toml::to_string(&me).unwrap()),
272    );
273
274    ZanbilCrate {
275        name,
276        config,
277        include_dir,
278        dependencies,
279        aggregated_include_dirs,
280        compiler,
281    }
282}
283
284pub fn build() {
285    let zc = init_zanbil_crate();
286
287    let mut cc = cc::Build::new();
288
289    cc.includes(&zc.aggregated_include_dirs);
290
291    let cpp = zc.config.cpp;
292
293    build_rs::output::rerun_if_env_changed("ZANBIL_CXX");
294    build_rs::output::rerun_if_env_changed("ZANBIL_CC");
295
296    if let Some(compiler) = &zc.compiler {
297        cc.compiler(compiler);
298    }
299
300    if let Some(cpp) = cpp {
301        cc.cpp(true);
302        cc.std(&format!("c++{cpp}"));
303    }
304
305    let is_binary = std::fs::exists("src/main.rs").unwrap();
306
307    let enable_test = build_rs::input::cargo_cfg_feature()
308        .iter()
309        .any(|x| x == "test");
310
311    if enable_test {
312        if is_binary {
313            cc.define("main", "no_main");
314        }
315        cc.define("ENABLE_TEST", None);
316    }
317
318    let rules = zc.config.file_rules.compile();
319
320    let include_base = zc.include_dir.to_string_lossy();
321    let include_lib = zc.include_dir.join(&zc.name);
322    let include_lib = include_lib.to_string_lossy();
323
324    for (base_dir, rule_set) in rules.rules {
325        if !std::fs::exists(&base_dir).unwrap() {
326            continue;
327        }
328        for entry in walkdir::WalkDir::new(&base_dir) {
329            let entry = entry.unwrap();
330            let path = entry.path().to_path_buf();
331            if let Some(kind) = FileRulesCompiled::get_kind(
332                &rule_set,
333                &path.to_string_lossy(),
334                &include_base,
335                &include_lib,
336            ) {
337                build_rs::output::rerun_if_changed(&path);
338                match kind {
339                    FileRuleKind::Source => {
340                        cc.file(&path);
341                    }
342                    FileRuleKind::Test if enable_test => {
343                        cc.file(&path);
344                    }
345                    FileRuleKind::Test => {}
346                    FileRuleKind::PrivateHeader => {}
347                    FileRuleKind::PublicHeader { destination } => {
348                        let dest = Path::new(&destination);
349                        std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
350                        std::fs::copy(path, dest).unwrap();
351                    }
352                }
353            }
354        }
355    }
356
357    cc.link_lib_modifier("+whole-archive").compile("main");
358}