use std::fmt::Write as _;
const EXAMPLE: &str = "../../examples/c/features/slots/panel.rs";
fn main() {
println!("cargo::rerun-if-changed=build.rs");
println!("cargo::rerun-if-changed={EXAMPLE}");
println!("cargo::rerun-if-changed=src");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
let entries = entry_points(&manifest_dir);
write_abi_rows(&manifest_dir, &entries);
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
install_name(&target_os);
if !std::path::Path::new(&manifest_dir).join(EXAMPLE).is_file() {
return;
}
let flag = match target_os.as_str() {
"macos" | "ios" => "-Wl,-export_dynamic".to_string(),
"windows" => match export_def(&entries) {
Some(def) => format!("/DEF:{def}"),
None => return,
},
_ => "-Wl,--export-dynamic".to_string(),
};
println!("cargo::rustc-link-arg-examples={flag}");
}
fn install_name(target_os: &str) {
let arg = match target_os {
"macos" | "ios" => "-Wl,-install_name,@rpath/libkui_ffi.dylib",
"windows" => return,
_ => "-Wl,-soname,libkui_ffi.so",
};
println!("cargo::rustc-cdylib-link-arg={arg}");
}
struct Entry {
file: String,
name: String,
params: Vec<String>,
ret: Option<String>,
}
fn entry_points(manifest_dir: &str) -> Vec<Entry> {
let src = std::path::Path::new(manifest_dir).join("src");
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(&src)
.map(|d| {
d.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
.collect()
})
.unwrap_or_default();
files.sort();
let mut out = Vec::new();
for path in files {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let file = path.file_name().unwrap().to_string_lossy().to_string();
let text: String = text
.lines()
.map(|l| l.split("//").next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n");
for marker in ["pub extern \"C\" fn ", "pub unsafe extern \"C\" fn "] {
for rest in text.split(marker).skip(1) {
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
if !name.starts_with("kui_") {
continue;
}
let after = rest[name.len()..].trim_start();
let Some(after) = after.strip_prefix('(') else {
continue;
};
let mut depth = 1usize;
let mut end = 0;
for (i, c) in after.char_indices() {
match c {
'(' | '<' | '[' => depth += 1,
')' | '>' | ']' => {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
_ => {}
}
}
let list = &after[..end];
let tail = after[end + 1..].split('{').next().unwrap_or("");
let ret = tail
.trim()
.strip_prefix("->")
.map(|r| r.split_whitespace().collect::<Vec<_>>().join(" "));
let params = split_top(list)
.into_iter()
.filter_map(|p| {
let bytes = p.as_bytes();
let colon = (0..bytes.len()).find(|&i| {
bytes[i] == b':'
&& bytes.get(i + 1) != Some(&b':')
&& (i == 0 || bytes[i - 1] != b':')
})?;
Some(
p[colon + 1..]
.split_whitespace()
.collect::<Vec<_>>()
.join(" "),
)
})
.collect();
out.push(Entry {
file: file.clone(),
name,
params,
ret,
});
}
}
}
out
}
fn split_top(list: &str) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut cur = String::new();
for c in list.chars() {
match c {
'<' | '(' | '[' => depth += 1,
'>' | ')' | ']' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
out.push(std::mem::take(&mut cur));
continue;
}
_ => {}
}
cur.push(c);
}
if !cur.trim().is_empty() {
out.push(cur);
}
out.into_iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn module_cfgs(manifest_dir: &str) -> Vec<(String, String)> {
let lib = std::path::Path::new(manifest_dir).join("src/lib.rs");
let text = std::fs::read_to_string(lib).unwrap_or_default();
let mut out = Vec::new();
let mut pending: Option<String> = None;
for line in text.lines() {
let line = line.trim();
if let Some(cfg) = line
.strip_prefix("#[cfg(")
.and_then(|l| l.strip_suffix(")]"))
{
pending = Some(cfg.to_string());
continue;
}
let decl = line.split_once("mod ").filter(|(vis, _)| {
let vis = vis.trim_end();
vis.is_empty() || vis == "pub" || (vis.starts_with("pub(") && vis.ends_with(')'))
});
if let Some((_, rest)) = decl
&& let Some(name) = rest.strip_suffix(';')
&& let Some(cfg) = pending.take()
{
out.push((format!("{name}.rs"), cfg));
continue;
}
pending = None;
}
out
}
fn write_abi_rows(manifest_dir: &str, entries: &[Entry]) {
let Ok(out_dir) = std::env::var("OUT_DIR") else {
return;
};
let cfgs = module_cfgs(manifest_dir);
let mut rows = String::from(
"// Generated by build.rs from the `pub extern \"C\" fn kui_*` signatures in\n\
// src/ (backlog AR2); `src/abi_parity.rs` includes it. Not committed.\n{\n",
);
for e in entries {
if let Some((_, cfg)) = cfgs.iter().find(|(f, _)| *f == e.file) {
let _ = writeln!(rows, " #[cfg({cfg})]");
}
let ret = e.ret.as_ref().map_or(String::new(), |r| format!(" -> {r}"));
let _ = writeln!(
rows,
" abi_fn!(o, n, {}({}){});",
e.name,
e.params.join(", "),
ret
);
}
rows.push_str("}\n");
let path = std::path::Path::new(&out_dir).join("abi_rows.rs");
let _ = std::fs::write(path, rows);
}
fn export_def(entries: &[Entry]) -> Option<String> {
let mut names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
if names.is_empty() {
return None;
}
names.sort_unstable();
names.dedup();
let mut def = String::from(
"; Generated by build.rs - the host's kui_* exports, so that\n\
; link.exe writes the import library a C plugin links against.\n\
EXPORTS\n",
);
for name in &names {
let _ = writeln!(def, " {name}");
}
let out = std::path::Path::new(&std::env::var("OUT_DIR").ok()?).join("kui-host-exports.def");
std::fs::write(&out, def).ok()?;
Some(
out.to_string_lossy()
.replace(std::path::MAIN_SEPARATOR, "/"),
)
}