use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
const GMSH_VERSION: &str = "4.15.2";
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let project_root = env::current_dir().unwrap();
let sdk_root = match target_os.as_str() {
"linux" => project_root.join("external/linux/gmsh"),
"windows" => project_root.join("external/windows/gmsh"),
"macos" => {
if target_arch == "aarch64" {
project_root.join("external/macos_arm/gmsh")
} else {
project_root.join("external/macos/gmsh")
}
}
_ => panic!("Unsupported OS: {}", target_os),
};
let lib_path = sdk_root.join("lib");
let include_path = sdk_root.join("include");
fs::create_dir_all(&sdk_root).unwrap();
if !lib_path.exists() || !include_path.exists() {
download_and_extract(&sdk_root, &target_os, &target_arch);
}
#[cfg(target_os = "windows")]
{
let original_dll = find_dll(&lib_path).expect("No gmsh-*.dll found in SDK lib");
println!("cargo:warning=Found original DLL: {}", original_dll.display());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let target_dir = out_dir.parent().unwrap().parent().unwrap().parent().unwrap();
let output_dir = target_dir.to_path_buf();
println!("cargo:warning=Output directory (deduced): {}", output_dir.display());
fs::create_dir_all(&output_dir).unwrap();
let dest_original = output_dir.join("gmsh-4.15.dll");
if !dest_original.exists() || dest_original.metadata().unwrap().len() != original_dll.metadata().unwrap().len() {
fs::copy(&original_dll, &dest_original).expect("copy original DLL");
println!("cargo:warning=Copied {} to {}", original_dll.display(), dest_original.display());
} else {
println!("cargo:warning=Original DLL already exists in output directory");
}
}
#[cfg(target_os = "linux")]
{
let real = lib_path.join(format!("libgmsh.so.{}", GMSH_VERSION));
let link = lib_path.join("libgmsh.so");
if real.exists() && !link.exists() {
std::os::unix::fs::symlink(&real, &link).unwrap();
}
}
#[cfg(target_os = "macos")]
{
let real = lib_path.join("libgmsh.dylib");
let link = lib_path.join(format!("libgmsh.{}.dylib", GMSH_VERSION));
if real.exists() && !link.exists() {
std::os::unix::fs::symlink(&real, &link).unwrap();
}
}
let abs_lib = lib_path.canonicalize().unwrap();
println!("cargo:rustc-link-search=native={}", abs_lib.display());
#[cfg(windows)]
println!("cargo:rustc-link-lib=dylib=gmsh.dll");
#[cfg(unix)]
{
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", abs_lib.display());
println!("cargo:rustc-link-lib=dylib=gmsh");
}
}
fn download_and_extract(dest: &PathBuf, os: &str, arch: &str) {
let (url, archive_name, is_zip) = match os {
"linux" => (
format!("https://gmsh.info/bin/Linux/gmsh-{}-Linux64-sdk.tgz", GMSH_VERSION),
format!("gmsh-{}-Linux64-sdk.tgz", GMSH_VERSION),
false,
),
"windows" => (
format!("https://gmsh.info/bin/Windows/gmsh-{}-Windows64-sdk.zip", GMSH_VERSION),
format!("gmsh-{}-Windows64-sdk.zip", GMSH_VERSION),
true,
),
"macos" => {
let suffix = if arch == "aarch64" { "MacOSARM" } else { "MacOSX" };
(
format!("https://gmsh.info/bin/MacOSX/gmsh-{}-{}-sdk.tgz", GMSH_VERSION, suffix),
format!("gmsh-{}-{}-sdk.tgz", GMSH_VERSION, suffix),
false,
)
}
_ => panic!("Unsupported OS: {}", os),
};
let archive_path = dest.parent().unwrap().join(&archive_name);
let min_size = 5_000_000;
if archive_path.exists() && fs::metadata(&archive_path).unwrap().len() >= min_size {
println!("cargo:warning=Using cached archive: {}", archive_path.display());
} else {
if archive_path.exists() {
fs::remove_file(&archive_path).unwrap();
}
println!("cargo:warning=Downloading Gmsh SDK from {}...", url);
let status = Command::new("curl")
.args(["-L", "-f", "-A", "Mozilla/5.0", &url, "-o", archive_path.to_str().unwrap()])
.status()
.expect("curl failed");
if !status.success() {
panic!("Download failed");
}
if fs::metadata(&archive_path).unwrap().len() < min_size {
panic!("Downloaded file too small, likely corrupted");
}
}
if dest.join("lib").exists() {
fs::remove_dir_all(dest.join("lib")).unwrap();
}
if dest.join("include").exists() {
fs::remove_dir_all(dest.join("include")).unwrap();
}
if is_zip {
let temp_dir = dest.join("temp_extract");
if temp_dir.exists() {
fs::remove_dir_all(&temp_dir).unwrap();
}
fs::create_dir_all(&temp_dir).unwrap();
let status = Command::new("unzip")
.args(["-q", archive_path.to_str().unwrap(), "-d", temp_dir.to_str().unwrap()])
.status()
.unwrap();
if !status.success() {
panic!("unzip failed");
}
let extracted_root = fs::read_dir(&temp_dir)
.unwrap()
.filter_map(|e| e.ok())
.find(|e| e.file_name().to_string_lossy().starts_with("gmsh-"))
.expect("No gmsh-* directory found in zip")
.path();
for entry in fs::read_dir(&extracted_root).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name();
let target = dest.join(&name);
if target.exists() {
if target.is_dir() {
fs::remove_dir_all(&target).unwrap();
} else {
fs::remove_file(&target).unwrap();
}
}
fs::rename(entry.path(), &target).unwrap();
}
fs::remove_dir_all(temp_dir).unwrap();
} else {
let status = Command::new("tar")
.args([
"-xzf",
archive_path.to_str().unwrap(),
"-C",
dest.to_str().unwrap(),
"--strip-components=1",
])
.status()
.unwrap();
if !status.success() {
panic!("tar failed");
}
}
println!("cargo:warning=SDK extracted to {}", dest.display());
}
#[cfg(target_os = "windows")]
fn find_dll(lib_path: &PathBuf) -> Option<PathBuf> {
fs::read_dir(lib_path)
.ok()?
.filter_map(|e| e.ok())
.find(|e| {
let fname = e.file_name();
let name = fname.to_string_lossy();
name.starts_with("gmsh") && name.ends_with(".dll")
})
.map(|e| e.path())
}