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}