scip-sys 0.1.27

Bindings for the C SCIP solver.
Documentation
mod bundled;
mod callback;
mod from_source;

#[cfg(any(feature = "bundled", feature = "from-source"))]
mod download;

extern crate bindgen;

use glob::glob;
use std::env;
use std::error::Error;
use std::path::PathBuf;

use crate::from_source::{compile_scip, download_scip_source, is_from_source_feature_enabled};
use bundled::*;
use callback::DeriveCastedConstant;

#[cfg(not(feature = "bundled"))]
pub fn is_bundled_feature_enabled() -> bool {
    false
}

fn _build_from_scip_dir(path: &str) -> bindgen::Builder {
    let lib_dir = PathBuf::from(&path).join("lib");
    let lib_dir_path = lib_dir.to_str().unwrap();

    if lib_dir.exists() {
        println!("cargo:warning=Using SCIP from {}", lib_dir_path);
        println!("cargo:rustc-link-search={}", lib_dir_path);
        println!("cargo:libdir={}", lib_dir_path);

        #[cfg(windows)]
        let lib_dir_path = PathBuf::from(&path).join("bin");
        #[cfg(windows)]
        println!("cargo:rustc-link-search={}", lib_dir_path.to_str().unwrap());
    } else {
        panic!(
            "{}",
            format!(
                "{}/lib does not exist, please check your SCIP installation",
                path
            )
        );
    }

    println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir_path);

    let include_dir = PathBuf::from(&path).join("include");
    let include_dir_path = include_dir.to_str().unwrap();
    let scip_header_file = PathBuf::from(&path)
        .join("include")
        .join("scip")
        .join("scip.h")
        .to_str()
        .unwrap()
        .to_owned();
    let scipdefplugins_header_file = PathBuf::from(&path)
        .join("include")
        .join("scip")
        .join("scipdefplugins.h")
        .to_str()
        .unwrap()
        .to_owned();
    let scipdef_file = PathBuf::from(&path)
        .join("include")
        .join("scip")
        .join("def.h")
        .to_str()
        .unwrap()
        .to_owned();

    bindgen::Builder::default()
        .header(scip_header_file)
        .header(scipdefplugins_header_file)
        .header(scipdef_file)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .clang_arg(format!("-I{}", include_dir_path))
}

fn lib_scip_in_dir(path: &str) -> bool {
    glob(&format!("{}/lib/libscip*", path)).unwrap().count() > 0
}

fn look_in_scipoptdir_and_conda_env() -> Option<bindgen::Builder> {
    let env_vars = vec!["SCIPOPTDIR", "CONDA_PREFIX"];

    for env_var_name in env_vars {
        println!("cargo:rerun-if-env-changed={}", env_var_name);
        let env_var = env::var(env_var_name);
        if let Ok(scip_dir) = env_var {
            println!("cargo:warning=Looking for SCIP in {}", scip_dir);
            if lib_scip_in_dir(&scip_dir) {
                return Some(_build_from_scip_dir(&scip_dir));
            } else {
                println!("cargo:warning=SCIP was not found in {}", scip_dir);
            }
        } else {
            println!("cargo:warning={} is not set", env_var_name);
        }
    }

    None
}

fn try_system_include_paths() -> Option<bindgen::Builder> {
    println!("cargo:warning=Searching for SCIP in standard system directories");

    // Common system include paths
    let search_paths = vec![
        "/usr/include",
        "/usr/local/include",
        "/opt/local/include",          // MacPorts
        "/opt/homebrew/include",       // Homebrew ARM Mac
        "/usr/local/opt/scip/include", // Homebrew Intel Mac
    ];

    for base_path in search_paths {
        let base = PathBuf::from(base_path);
        let scip_h = base.join("scip").join("scip.h");
        let scipdefplugins_h = base.join("scip").join("scipdefplugins.h");
        let def_h = base.join("scip").join("def.h");

        if scip_h.exists() && scipdefplugins_h.exists() && def_h.exists() {
            println!("cargo:warning=Found SCIP headers in {}", base_path);

            return Some(
                bindgen::Builder::default()
                    .header(scip_h.to_str().unwrap())
                    .header(scipdefplugins_h.to_str().unwrap())
                    .header(def_h.to_str().unwrap())
                    .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
                    .clang_arg(format!("-I{}", base_path)),
            );
        }
    }

    println!("cargo:warning=Could not find SCIP headers in standard system directories");
    None
}

fn main() -> Result<(), Box<dyn Error>> {
    // Detect docs.rs build environment (no network access)
    if env::var("DOCS_RS").is_ok() {
        println!("cargo:warning=Building on docs.rs, using pre-generated bindings");
        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
        std::fs::copy("src/bindings_pregenerated.rs", out_path.join("bindings.rs"))?;
        return Ok(());
    }

    let builder = if is_bundled_feature_enabled() {
        download_scip();
        let path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scip_install");
        _build_from_scip_dir(path.to_str().unwrap())
    } else if is_from_source_feature_enabled() {
        let source_path = download_scip_source();
        let build_path = compile_scip(source_path);
        _build_from_scip_dir(build_path.to_str().unwrap())
    } else {
        let builder = look_in_scipoptdir_and_conda_env();
        if builder.is_some() {
            builder.unwrap()
        } else {
            println!("cargo:warning=SCIP was not found in SCIPOPTDIR or in Conda environment");
            println!("cargo:warning=Looking for SCIP in system libraries");

            // Try common system include paths
            try_system_include_paths().unwrap_or_else(|| {
                panic!(
                    "Could not find SCIP installation.\n\
                    Please either:\n\
                    - Set SCIPOPTDIR environment variable to point to your SCIP installation\n\
                    - Install SCIP system-wide (headers in /usr/include or /usr/local/include)\n\
                    - Use --features bundled to download and use a bundled version\n\
                    - Use --features from-source to build SCIP from source"
                )
            })
        }
    };

    #[cfg(windows)]
    {
        println!("cargo:rustc-link-lib=libscip");
    }
    #[cfg(not(windows))]
    {
        println!("cargo:rustc-link-lib=scip");
    }

    #[cfg(feature = "from-source")]
    {
        let target = env::var("TARGET").unwrap();
        let apple = target.contains("apple");
        let linux = target.contains("linux");
        let mingw = target.contains("pc-windows-gnu");
        if apple {
            println!("cargo:rustc-link-lib=dylib=c++");
        } else if linux || mingw {
            println!("cargo:rustc-link-lib=dylib=stdc++");
        }

        #[cfg(windows)]
        {
            println!("cargo:rustc-link-lib=libsoplex");
        }
        #[cfg(not(windows))]
        {
            println!("cargo:rustc-link-lib=soplex");
        }
    }
    // Setup the DeriveCastedConstant callback to target SCIP_INVALID
    let derive_casted_constant = DeriveCastedConstant::new().target("SCIP_INVALID");

    let builder = builder
        // SCIP 10 annotates the deprecated `SCIP_VARTYPE_IMPLINT` enumerator with
        // `SCIP_DEPRECATED`. On Windows that expands to `__declspec(deprecated)`,
        // which clang cannot parse inside an enum; neutralize the macro for bindgen.
        .clang_arg("-DSCIP_DEPRECATED=")
        .blocklist_item("FP_NAN")
        .blocklist_item("FP_INFINITE")
        .blocklist_item("FP_ZERO")
        .blocklist_item("FP_SUBNORMAL")
        .blocklist_item("FP_NORMAL")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .parse_callbacks(Box::new(derive_casted_constant));

    let bindings = builder.generate()?;
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings.write_to_file(out_path.join("bindings.rs"))?;

    Ok(())
}