rssat 0.1.0

rssat is a Rust library that provides Rust bindings for multiple popular SAT solvers
use std::path::PathBuf;
use std::process::Command;
use std::{env, fs};

use flate2::read::GzDecoder;
use tar::Archive;
const MINISAT_URL: &str = "https://github.com/niklasso/minisat/archive/refs/heads/master.tar.gz";
const CADICAL_URL: &str =
    "https://github.com/arminbiere/cadical/archive/refs/tags/rel-2.0.0.tar.gz";
const GLUCOSE_URL: &str = "https://github.com/audemard/glucose/archive/refs/tags/4.2.1.tar.gz";

fn get_extract_dir(out_dir: &PathBuf) -> PathBuf {
    fs::read_dir(&out_dir)
        .expect("Failed to read extracted directory")
        .filter_map(Result::ok)
        .find(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
        .map(|entry| entry.path())
        .expect("Failed to find extracted solver directory")
}

fn download_and_extract(url: &str, name: &str) -> PathBuf {
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    let out_dir = out_path.join("third_parts").join(name);
    let out_dir_ref = &out_dir;
    let complete_file = out_dir_ref.join(".complete");
    if complete_file.exists() {
        return get_extract_dir(out_dir_ref);
    }
    let response = reqwest::blocking::get(url).expect("Failed to download solver");
    let tar = GzDecoder::new(response);
    let mut archive = Archive::new(tar);
    fs::create_dir_all(out_dir_ref).expect("Failed to create directory");
    archive
        .unpack(out_dir_ref)
        .expect("Failed to extract solver");
    // Find the extracted directory (it might have a version suffix)
    let d = get_extract_dir(out_dir_ref);
    apply_patch(&d, name);
    fs::write(&complete_file, b"").expect("Failed to create .complete file");
    d
}

fn binding_cadical() {
    let cadical_dir = download_and_extract(CADICAL_URL, "cadical");

    let status = Command::new("sh")
        .current_dir(&cadical_dir)
        .arg("configure")
        .status()
        .expect("Failed to execute configure");

    if !status.success() {
        panic!("Failed to configure CaDiCaL");
    }

    let status = Command::new("make")
        .current_dir(&cadical_dir)
        .status()
        .expect("Failed to execute make");

    if !status.success() {
        panic!("Failed to build CaDiCaL");
    }
    println!(
        "cargo:rustc-link-search=native={}/build",
        cadical_dir.display()
    );
    println!("cargo:rustc-link-lib=static=cadical");
    println!("cargo:rustc-link-lib=stdc++");

    println!(
        "cargo:rerun-if-changed={}/src/cadical.hpp",
        cadical_dir.display()
    );

    let bindings = bindgen::Builder::default()
        .header(format!("{}/src/cadical.hpp", cadical_dir.display()))
        .allowlist_type("CaDiCaL::Solver")
        .allowlist_function("CaDiCaL::.*")
        .opaque_type("std::.*")
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("cadical_bindings.rs"))
        .expect("Couldn't write bindings!");
}
fn binding_minisat() {
    let minisat_dir = download_and_extract(MINISAT_URL, "minisat");
    let dst = cmake::build(&minisat_dir);
    println!("cargo:rustc-link-search=native={}/lib", dst.display());
    println!("cargo:rustc-link-lib=static=minisat");
    println!("cargo:rustc-link-lib=stdc++");
    println!("cargo:rerun-if-changed={}", minisat_dir.display());

    let bindings = bindgen::Builder::default()
        .headers([format!(
            "{}/include/minisat/simp/StdSimpSolver.hpp",
            dst.display()
        )])
        .allowlist_function("Minisat::.*")
        .opaque_type("std::.*")
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("minisat_bindings.rs"))
        .expect("Couldn't write bindings!");
}

fn binding_glucose() {
    let glucose_dir = download_and_extract(GLUCOSE_URL, "glucose");
    let dst = cmake::build(&glucose_dir);
    println!("cargo:rustc-link-search=native={}/lib", dst.display());
    println!("cargo:rustc-link-lib=static=glucose");
    println!("cargo:rustc-link-lib=stdc++");
    println!("cargo:rerun-if-changed={}", glucose_dir.display());

    let bindings = bindgen::Builder::default()
        .headers([format!(
            "{}/include/glucose/simp/StdSimpSolver.hpp",
            dst.display()
        )])
        .allowlist_function("Glucose::.*")
        .opaque_type("std::.*")
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("glucose_bindings.rs"))
        .expect("Couldn't write bindings!");
}

fn apply_patch(dir: &PathBuf, patch_name: &str) {
    let patch_path = PathBuf::from("patches").join(format!("{}.patch", patch_name));
    if patch_path.exists() {
        let abs_path = fs::canonicalize(&patch_path).unwrap();
        let status = Command::new("patch")
            .arg("-f")
            .arg("-r")
            .arg(".ignore")
            .arg("--ignore-whitespace")
            .arg("--directory")
            .arg(dir)
            .arg("-p1")
            .arg("-i")
            .arg(abs_path)
            .status()
            .expect("Failed to execute git apply");
        if !status.success() {
            println!("cargo::warning=Failed to apply patch: {}", patch_name);
        }
    }
}

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    if cfg!(feature = "cadical") {
        binding_cadical();
    }
    if cfg!(feature = "minisat") {
        binding_minisat();
    }
    if cfg!(feature = "glucose") {
        binding_glucose();
    }
}