use std::{env, fs, path::PathBuf};
const FEATURES: [(&str, &str); 7] = [
("external", "DIFFSOL_EXTERNAL"),
("diffsl-external-f64", "DIFFSOL_DIFFSL_EXTERNAL_F64"),
("diffsl-external-f32", "DIFFSOL_DIFFSL_EXTERNAL_F32"),
("diffsl-external-dynamic", "DIFFSOL_DIFFSL_EXTERNAL_DYNAMIC"),
("diffsl-cranelift", "DIFFSOL_DIFFSL_CRANELIFT"),
("diffsl-llvm", "DIFFSOL_DIFFSL_LLVM"),
("suitesparse", "DIFFSOL_SUITESPARSE"),
];
const OPTION_ACCESSOR_FILES: [&str; 2] =
["src/ode_options_c.rs", "src/initial_condition_options_c.rs"];
fn main() {
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=cbindgen.toml");
println!("cargo:rerun-if-changed=build.rs");
set_soname();
let mut config = cbindgen::Config::from_file(crate_dir.join("cbindgen.toml"))
.expect("cannot read cbindgen.toml");
let defines: String = FEATURES
.iter()
.filter(|(feature, _)| feature_enabled(feature))
.map(|(_, define)| format!("#define {define} 1\n"))
.collect();
config.after_includes = Some(format!(
"\n/* The features of the library that this header was made with. */\n{defines}"
));
let bindings = cbindgen::Builder::new()
.with_src(crate_dir.join("src/lib.rs"))
.with_config(config)
.generate()
.expect("cannot generate the C header");
let mut header = Vec::new();
bindings.write(&mut header);
let header = String::from_utf8(header).expect("the C header is not UTF-8");
let header = insert_option_accessors(header, &crate_dir);
let path = include_dir().join("diffsol.h");
write_if_changed(&path, &header);
println!("cargo:rerun-if-changed={}", path.display());
}
fn write_if_changed(path: &std::path::Path, contents: &str) {
if fs::read_to_string(path).ok().as_deref() != Some(contents) {
fs::create_dir_all(path.parent().unwrap()).expect("cannot make the include directory");
fs::write(path, contents).expect("cannot write the C header");
}
}
fn set_soname() {
println!("cargo:rerun-if-env-changed=DIFFSOL_SONAME");
let Ok(soname) = env::var("DIFFSOL_SONAME") else {
return;
};
if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") {
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,{soname}");
}
}
fn feature_enabled(feature: &str) -> bool {
let name = feature.to_uppercase().replace('-', "_");
env::var_os(format!("CARGO_FEATURE_{name}")).is_some()
}
fn include_dir() -> PathBuf {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
out_dir
.ancestors()
.find(|dir| dir.file_name().is_some_and(|name| name == "build"))
.and_then(|build| build.parent())
.expect("unexpected OUT_DIR")
.join("include")
}
fn insert_option_accessors(header: String, crate_dir: &std::path::Path) -> String {
let mut decls = String::from("\n/* The option accessors. */\n");
for file in OPTION_ACCESSOR_FILES {
let source = fs::read_to_string(crate_dir.join(file)).expect("cannot read source");
for (kind, args) in macro_calls(&source) {
let [prefix, options, value, field] = args;
let c_value = c_type(&value);
if kind == "c_getter_simple" {
decls.push_str(&format!(
"\n/* Get the option `{field}`. Return DIFFSOL_OK on success. */\n\
int32_t {prefix}_get_{field}(const {options} *options, {c_value} *out_value);\n"
));
} else {
decls.push_str(&format!(
"\n/* Set the option `{field}`. Return DIFFSOL_OK on success. */\n\
int32_t {prefix}_set_{field}({options} *options, {c_value} value);\n"
));
}
}
}
let end = header
.rfind("#ifdef __cplusplus")
.expect("the header has no end of the extern \"C\" block");
format!("{}{}\n{}", &header[..end], decls, &header[end..])
}
fn macro_calls(source: &str) -> Vec<(&'static str, [String; 4])> {
let mut calls = Vec::new();
for kind in ["c_getter_simple", "c_setter_simple"] {
let start = format!("{kind}!(");
let mut rest = source;
while let Some(i) = rest.find(&start) {
rest = &rest[i + start.len()..];
let end = rest.find(')').expect("unterminated macro call");
let args: Vec<String> = rest[..end]
.split(',')
.map(|a| a.trim().to_string())
.filter(|a| !a.is_empty())
.collect();
let args: [String; 4] = args.try_into().expect("expected four macro arguments");
calls.push((kind, args));
rest = &rest[end..];
}
}
calls.sort_by(|a, b| (&a.1[0], &a.1[3], a.0).cmp(&(&b.1[0], &b.1[3], b.0)));
calls
}
fn c_type(rust_type: &str) -> &'static str {
match rust_type {
"usize" => "size_t",
"i32" => "int32_t",
"f64" => "double",
other => panic!("no C type for the option type `{other}`"),
}
}