gmsh_ffi 0.1.0

Safe Rust FFI bindings to the Gmsh SDK
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();

    // SDK 存放路径:项目根目录/external/gmsh
    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();

    // 下载并解压 SDK(如果不存在)
    if !lib_path.exists() || !include_path.exists() {
        download_and_extract(&sdk_root, &target_os, &target_arch);
    }

    // Windows 平台处理:将原始 DLL 复制到宿主项目的输出目录
    #[cfg(target_os = "windows")]
    {
        // 1. 查找原始 DLL
        let original_dll = find_dll(&lib_path).expect("No gmsh-*.dll found in SDK lib");
        println!("cargo:warning=Found original DLL: {}", original_dll.display());

        // 2. 确定宿主项目的输出目录
        // 方法:使用 OUT_DIR 向上两级得到 target/debug 或 target/release
        let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
        let target_dir = out_dir.parent().unwrap().parent().unwrap().parent().unwrap();
        // 或者直接使用 CARGO_TARGET_DIR + PROFILE,但需考虑相对路径问题,此处采用 OUT_DIR 推导
        let output_dir = target_dir.to_path_buf();
        println!("cargo:warning=Output directory (deduced): {}", output_dir.display());

        fs::create_dir_all(&output_dir).unwrap();

        // 3. 复制原始 DLL(保留原名)
        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");
        }
    }

    // Linux/macOS 符号链接
    #[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)]
    // Windows的导入库 gmsh.dll.lib
    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");
    }

    // 绑定文件需通过 bindgen 命令行生成,详见 README.md
}

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; // 5 MB

    // 下载或使用缓存
    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");
        }

        // 找到顶层目录(通常名为 gmsh-*-sdk)
        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();

        // 移动内容到 dest
        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())
}