fn main() {
println!("cargo:rerun-if-changed=build.rs");
#[cfg(feature = "opf-highs")]
highs::build();
#[cfg(feature = "opf-ipopt")]
ipopt::build();
#[cfg(feature = "smallsignal-arpack")]
arpack::build();
#[cfg(feature = "capi")]
capi::build();
}
#[cfg(feature = "capi")]
mod capi {
use std::env;
use std::path::PathBuf;
pub fn build() {
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let header = manifest.join("include/gridoxide.h");
let generated = match generate(&manifest).map(inject_abi_version) {
Some(text) => text,
None => {
println!(
"cargo:warning=cbindgen could not parse src/capi; \
include/gridoxide.h was left untouched"
);
return;
}
};
let current = std::fs::read_to_string(&header).unwrap_or_default();
if current == generated {
println!("cargo:rerun-if-changed=src/capi");
return;
}
if let Some(parent) = header.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::write(&header, &generated).is_err() {
println!(
"cargo:warning=couldn't write {} (read-only tree?); the committed header \
may be stale — run `cargo build --features capi` in a writable checkout",
header.display()
);
}
println!("cargo:rerun-if-changed=src/capi");
}
fn inject_abi_version(header: String) -> String {
let version = include_str!("src/capi/mod.rs")
.lines()
.find_map(|line| {
line.trim()
.strip_prefix("pub const ABI_VERSION: u32 = ")
.and_then(|rest| rest.trim_end_matches(';').parse::<u32>().ok())
})
.expect("src/capi/mod.rs must declare `pub const ABI_VERSION: u32 = <n>;`");
let define = format!(
r#"
/**
* The ABI this header describes. Compare it against `gridoxide_abi_version()`
* at run time when linking a prebuilt library, so a layout mismatch fails
* early instead of quietly corrupting a struct.
*/
#define GRIDOXIDE_ABI_VERSION {version}
"#
);
match header.find("#define GRIDOXIDE_H") {
Some(at) => {
let end = at + "#define GRIDOXIDE_H".len();
format!("{}{}{}", &header[..end], define, &header[end..])
}
None => header,
}
}
fn generate(manifest: &std::path::Path) -> Option<String> {
let config = cbindgen::Config::from_root_or_default(manifest);
cbindgen::Builder::new()
.with_src(manifest.join("src/capi/mod.rs"))
.with_config(config)
.generate()
.ok()
.map(|bindings| {
let mut buffer = Vec::new();
bindings.write(&mut buffer);
String::from_utf8_lossy(&buffer).into_owned()
})
}
}
#[cfg(feature = "smallsignal-arpack")]
mod arpack {
use std::env;
use std::path::PathBuf;
pub fn build() {
let root = PathBuf::from(env::var("ARPACK_ROOT").unwrap_or_else(|_| "/usr".to_string()));
let lib_dir = [
root.join("lib")
.join(env::var("CARGO_CFG_TARGET_ARCH").map_or_else(
|_| "x86_64-linux-gnu".to_string(),
|arch| format!("{arch}-linux-gnu"),
)),
root.join("lib"),
root.join("lib64"),
root.clone(),
]
.into_iter()
.find(|p| p.join("libarpack.so").is_file())
.unwrap_or_else(|| {
panic!(
"couldn't find `libarpack.so` under {} — the `smallsignal-arpack` \
feature needs a local ARPACK *with its development files* (on \
Debian/Ubuntu: `apt install libarpack2-dev`; the runtime package \
alone ships only `libarpack.so.2`, which the linker will not \
take). Set ARPACK_ROOT if it is installed elsewhere.",
root.display()
)
});
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=arpack");
println!("cargo:rerun-if-env-changed=ARPACK_ROOT");
}
}
#[cfg(feature = "opf-ipopt")]
mod ipopt {
use std::env;
use std::path::PathBuf;
pub fn build() {
let root = PathBuf::from(env::var("IPOPT_ROOT").unwrap_or_else(|_| "/usr".to_string()));
let include_dir = root.join("include/coin-or");
let header = include_dir.join("IpStdCInterface.h");
if !header.is_file() {
panic!(
"couldn't find {} — the `opf-ipopt` feature needs a local IPOPT \
*with its development files* (on Debian/Ubuntu: `apt install \
coinor-libipopt-dev`). Set IPOPT_ROOT if it is installed \
somewhere other than {}.",
header.display(),
root.display()
);
}
let lib_dir = [
root.join("lib")
.join(env::var("CARGO_CFG_TARGET_ARCH").map_or_else(
|_| "x86_64-linux-gnu".to_string(),
|arch| format!("{arch}-linux-gnu"),
)),
root.join("lib"),
root.join("lib64"),
]
.into_iter()
.find(|p| p.join("libipopt.so").is_file())
.unwrap_or_else(|| {
panic!(
"found IPOPT headers under {} but no `libipopt.so` beside them — \
the unversioned symlink comes from the development package",
include_dir.display()
)
});
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=ipopt");
println!("cargo:rustc-link-lib=dylib=stdc++");
generate_bindings(&include_dir, &header);
println!("cargo:rerun-if-env-changed=IPOPT_ROOT");
println!("cargo:rerun-if-changed={}", header.display());
}
fn generate_bindings(include_dir: &std::path::Path, header: &std::path::Path) {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let mut builder = bindgen::Builder::default()
.header(header.to_str().unwrap())
.clang_arg(format!("-I{}", include_dir.display()))
.clang_arg("-DHAVE_CSTDDEF")
.allowlist_function("(Create|Free|Add|Open|Close|Set|Get)?Ipopt.*")
.allowlist_type("Ipopt.*|Eval_.*|ip(index|number)|UserDataPtr|ApplicationReturnStatus")
.allowlist_var("IPOPT_.*");
if let Some(gcc_builtin_include) = find_gcc_builtin_include() {
builder = builder.clang_arg(format!("-I{}", gcc_builtin_include.display()));
}
let bindings = builder
.generate()
.expect("failed to generate IPOPT FFI bindings");
bindings
.write_to_file(out_dir.join("ipopt_bindings.rs"))
.expect("failed to write IPOPT FFI bindings");
}
fn find_gcc_builtin_include() -> Option<PathBuf> {
let cc = env::var("CC").unwrap_or_else(|_| "cc".to_string());
let output = std::process::Command::new(cc)
.arg("-print-file-name=include")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
path.is_dir().then_some(path)
}
}
#[cfg(feature = "opf-highs")]
mod highs {
use std::env;
use std::path::PathBuf;
pub fn build() {
let root = PathBuf::from(env::var("HIGHS_ROOT").unwrap_or_else(|_| "/usr".to_string()));
let include_dir = root.join("include/highs");
let header = include_dir.join("interfaces/highs_c_api.h");
if !header.is_file() {
panic!(
"couldn't find {} — the `opf-highs` feature needs a local HiGHS \
*with its development files* (on Debian/Ubuntu: `apt install \
libhighs-dev`; the runtime `libhighs1` package alone is not \
enough, as it ships neither the headers nor the `libhighs.so` \
link symlink). Set HIGHS_ROOT if it is installed somewhere \
other than {}.",
header.display(),
root.display()
);
}
let lib_dir = [
root.join("lib")
.join(env::var("CARGO_CFG_TARGET_ARCH").map_or_else(
|_| "x86_64-linux-gnu".to_string(),
|arch| format!("{arch}-linux-gnu"),
)),
root.join("lib"),
root.join("lib64"),
]
.into_iter()
.find(|p| p.join("libhighs.so").is_file())
.unwrap_or_else(|| {
panic!(
"found HiGHS headers under {} but no `libhighs.so` beside them — \
`libhighs.so.1` alone is not linkable, and the unversioned \
symlink comes from the development package (`libhighs-dev`)",
include_dir.display()
)
});
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=highs");
println!("cargo:rustc-link-lib=dylib=stdc++");
generate_bindings(&include_dir, &header);
println!("cargo:rerun-if-env-changed=HIGHS_ROOT");
println!("cargo:rerun-if-changed={}", header.display());
}
fn generate_bindings(include_dir: &std::path::Path, header: &std::path::Path) {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let mut builder = bindgen::Builder::default()
.header(header.to_str().unwrap())
.clang_arg(format!("-I{}", include_dir.display()))
.allowlist_function("Highs_.*")
.allowlist_type("Highs.*")
.allowlist_var("k?Highs.*");
if let Some(gcc_builtin_include) = find_gcc_builtin_include() {
builder = builder.clang_arg(format!("-I{}", gcc_builtin_include.display()));
}
let bindings = builder
.generate()
.expect("failed to generate HiGHS FFI bindings");
bindings
.write_to_file(out_dir.join("highs_bindings.rs"))
.expect("failed to write HiGHS FFI bindings");
}
fn find_gcc_builtin_include() -> Option<PathBuf> {
let cc = env::var("CC").unwrap_or_else(|_| "cc".to_string());
let output = std::process::Command::new(cc)
.arg("-print-file-name=include")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
path.is_dir().then_some(path)
}
}