inchi-sys 0.1.2

Low-level FFI bindings to the vendored IUPAC InChI 1.07 reference C library
Documentation
//! Build script for `inchi-sys`.
//!
//! Compiles the vendored IUPAC InChI 1.07 reference C library into a static
//! archive and (optionally) regenerates the Rust FFI bindings with `bindgen`.
//!
//! The set of translation units and the preprocessor defines mirror the
//! upstream `INCHI_API/libinchi/gcc/makefile` so the result is byte-for-byte
//! the same library the InChI project ships, just statically linked.

use std::path::{Path, PathBuf};

/// Object files compiled from `INCHI_BASE/src` (the core algorithm + the
/// classic API). Taken verbatim from `INCHI_LIB_OBJS` in the upstream makefile.
const BASE_SRC: &[&str] = &[
    "ichican2",
    "ichicano",
    "ichi_io",
    "ichierr",
    "ichicans",
    "ichiisot",
    "ichimak2",
    "ichimake",
    "ichimap1",
    "ichimap2",
    "ichimap4",
    "ichinorm",
    "ichiparm",
    "ichiprt1",
    "ichiprt2",
    "ichiprt3",
    "ichiqueu",
    "ichiring",
    "ichisort",
    "ichister",
    "ichitaut",
    "ichi_bns",
    "ichiread",
    "ichirvr1",
    "ichirvr2",
    "ichirvr3",
    "ichirvr4",
    "ichirvr5",
    "ichirvr6",
    "ichirvr7",
    "ikey_dll",
    "ikey_base26",
    "mol_fmt1",
    "mol_fmt2",
    "mol_fmt3",
    "mol_fmt4",
    "mol2atom",
    "readinch",
    "runichi",
    "runichi2",
    "runichi3",
    "runichi4",
    "sha2",
    "strutil",
    "util",
    "bcf_s",
];

/// Object files compiled from `INCHI_API/libinchi/src` (the DLL/API layer:
/// `GetINCHI`, `FreeINCHI`, `MakeINCHIFromMolfileText`, ...).
const LIBINCHI_SRC: &[&str] = &[
    "ichilnct",
    "inchi_dll",
    "inchi_dll_main",
    "inchi_dll_a",
    "inchi_dll_a2",
    "inchi_dll_b",
];

/// Object files compiled from `INCHI_API/libinchi/src/ixa` (the InChI
/// eXtensible API).
const IXA_SRC: &[&str] = &[
    "ixa_inchikey_builder",
    "ixa_read_mol",
    "ixa_status",
    "ixa_builder",
    "ixa_mol",
    "ixa_read_inchi",
];

fn main() {
    let vendor = PathBuf::from(env("CARGO_MANIFEST_DIR")).join("vendor/inchi");
    let base = vendor.join("INCHI_BASE/src");
    let libinchi = vendor.join("INCHI_API/libinchi/src");
    let ixa = libinchi.join("ixa");

    // Re-run only when inputs that affect the build change.
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed={}", base.display());

    compile_native(&base, &libinchi, &ixa);

    #[cfg(feature = "regenerate-bindings")]
    regenerate_bindings(&base);
}

fn compile_native(base: &Path, libinchi: &Path, ixa: &Path) {
    let mut build = cc::Build::new();
    build.include(base);

    for name in BASE_SRC {
        build.file(base.join(format!("{name}.c")));
    }
    for name in LIBINCHI_SRC {
        build.file(libinchi.join(format!("{name}.c")));
    }
    for name in IXA_SRC {
        build.file(ixa.join(format!("{name}.c")));
    }

    let target = env("TARGET");
    let msvc = target.contains("msvc");

    // Preprocessor defines for a statically linked build of the InChI API.
    //
    // `TARGET_API_LIB` selects the library API surface. `COMPILE_ANSI_ONLY`
    // drops the non-ANSI console/display/timer code we never call, keeping the
    // build portable across gcc, clang, and MSVC.
    //
    // We deliberately do NOT define `BUILD_LINK_AS_DLL`: that would make the
    // headers mark the API `__declspec(dllimport)` on MSVC, which is illegal
    // when *compiling* (defining) those functions into a static archive
    // (error C2491). Leaving it undefined selects the headers' static-linkage
    // path (empty `INCHI_API`) on every platform.
    build.define("TARGET_API_LIB", None);
    build.define("COMPILE_ANSI_ONLY", None);

    // The InChI sources rely on type punning that strict aliasing would break.
    build.flag_if_supported("-fno-strict-aliasing");

    // The reference C is warning-heavy by design; do not let that fail or spam
    // the build. `cc` already omits `-Werror` unless asked.
    build.warnings(false);
    if !msvc {
        for flag in [
            "-Wno-unused-but-set-variable",
            "-Wno-unused-variable",
            "-Wno-unused-function",
            "-Wno-implicit-fallthrough",
            "-Wno-format",
            "-Wno-deprecated-declarations",
        ] {
            build.flag_if_supported(flag);
        }
    }

    build.compile("inchi");
    // `cc` emits the `cargo:rustc-link-lib`/`-search` directives for us.
}

#[cfg(feature = "regenerate-bindings")]
fn regenerate_bindings(base: &Path) {
    let header = base.join("inchi_api.h");
    println!("cargo:rerun-if-changed={}", header.display());

    let bindings = bindgen::Builder::default()
        .header(header.to_string_lossy())
        .clang_arg(format!("-I{}", base.display()))
        .clang_arg("-DTARGET_API_LIB")
        .clang_arg("-DCOMPILE_ANSI_ONLY")
        // Public classic-API surface.
        .allowlist_function("Get(Std)?INCHI(Ex)?")
        .allowlist_function("GetStructFrom(Std)?INCHI(Ex)?")
        .allowlist_function("Free(Std)?INCHI")
        .allowlist_function("FreeStructFrom(Std)?INCHI(Ex)?")
        .allowlist_function("Get(Std)?INCHIKeyFrom(Std)?INCHI")
        .allowlist_function("GetINCHIfromINCHI")
        .allowlist_function("MakeINCHIFromMolfileText")
        .allowlist_function("CheckINCHI(Key)?")
        .allowlist_function("GetStringLength")
        .allowlist_function("Free_(std_)?inchi_Input")
        .allowlist_function("Get_(std_)?inchi_Input_FromAuxInfo")
        .allowlist_type("inchi_.*")
        .allowlist_type("RetVal.*")
        .allowlist_type("INCHI_.*")
        .allowlist_var("INCHI_.*")
        .allowlist_var(
            "(MAXVAL|NO_ATOM|ATOM_EL_LEN|NUM_H_ISOTOPES|ISOTOPIC_SHIFT_FLAG|ISOTOPIC_SHIFT_MAX)",
        )
        // Plain integer constants: sound for values the C library returns
        // (an out-of-range return code can never be UB), and the constants are
        // accessible bare (e.g. `inchi_Ret_OKAY`).
        .default_enum_style(bindgen::EnumVariation::Consts)
        // The C enum names are already namespaced (`inchi_Ret_OKAY`,
        // `INCHI_BOND_TYPE_SINGLE`); don't double-prefix with the tag name.
        .prepend_enum_name(false)
        .derive_default(true)
        .derive_debug(true)
        // Layout tests bake in host-specific struct offsets/sizes, which are
        // wrong on other ABIs (e.g. Windows MSVC, where `long` is 4 bytes, so
        // `unsigned long` fields shift every following field). The committed
        // bindings must compile on every target, so we omit them; the field
        // types are already resolved per-target by Rust.
        .layout_tests(false)
        .generate_comments(false)
        .generate()
        .expect("failed to generate InChI bindings");

    let out = PathBuf::from(env("OUT_DIR")).join("bindings.rs");
    bindings
        .write_to_file(&out)
        .expect("failed to write generated bindings");
}

fn env(key: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| panic!("environment variable `{key}` not set"))
}