primer3-sys 0.1.0

Raw FFI bindings to the primer3 C library
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

/// Rewrites a vendored C source file into `OUT_DIR` after applying a set of
/// exact-string replacements, and returns the path to the patched copy.
///
/// Used to carry targeted thread-safety patches against upstream primer3
/// without mutating the vendored submodule tree. Each replacement is applied
/// exactly once and asserts the target string is present, so a stale patch
/// fails the build loudly as soon as upstream fixes the underlying issue.
#[cfg(feature = "vendored")]
fn patch_source(src: &Path, out_dir: &Path, replacements: &[(&str, &str)], label: &str) -> PathBuf {
    let filename = src.file_name().expect("source path has no file name");
    let dst = out_dir.join(filename);
    let mut contents =
        fs::read_to_string(src).unwrap_or_else(|e| panic!("failed to read {}: {e}", src.display()));
    for (needle, replacement) in replacements {
        assert!(
            contents.contains(needle),
            "{label}: patch target not found in {} — upstream may have fixed this; \
             remove the corresponding patch from build.rs.",
            src.display()
        );
        contents = contents.replacen(needle, replacement, 1);
    }
    fs::write(&dst, contents).unwrap_or_else(|e| panic!("failed to write {}: {e}", dst.display()));
    println!("cargo:rerun-if-changed={}", src.display());
    dst
}

#[allow(clippy::too_many_lines)]
fn main() {
    let src_dir = PathBuf::from("vendor/primer3/src");
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    // Thread-safety patch for oligotm.c: the Owczarzy salt correction branch
    // declares its polynomial coefficients as function-local `static`
    // variables, racing under concurrent oligotm() calls. Dropping `static`
    // is a pure win (values are overwritten on every call anyway). Pending
    // upstream at primer3-org/primer3#96.
    #[cfg(feature = "vendored")]
    let oligotm_src = patch_source(
        &src_dir.join("oligotm.c"),
        &out_dir,
        &[(
            "    static double a = 0,b = 0,c = 0,d = 0,e = 0,f = 0,g = 0;",
            "    double a = 0,b = 0,c = 0,d = 0,e = 0,f = 0,g = 0;",
        )],
        "oligotm.c thread-safety",
    );

    // NOTE: dpal.c's `_dpal_generic()` declares its ~40 MB scoring and
    // traceback matrices as function-local `static` arrays, which is a
    // concurrency bug. We tried promoting them to `static _Thread_local`
    // here, but on Linux with glibc the resulting 40 MB per-thread static
    // TLS block overflows Rust's default 2 MB test thread stack, so the fix
    // has to live at the Rust boundary instead — see the ALIGN_MUTEX in
    // primer3/src/alignment.rs. A proper upstream fix would heap-allocate
    // these matrices lazily via thread-local pointers.

    #[cfg(feature = "vendored")]
    {
        // C sources (compiled as C)
        // MAX_PRIMER_LENGTH defaults to 36 in the C source but can be up to
        // THAL_MAX_ALIGN (60). Set it to 60 so oligotm/seqtm can handle
        // sequences up to 60 bp with the nearest-neighbor model.
        cc::Build::new()
            .files([
                src_dir.join("thal.c"),
                oligotm_src.clone(),
                src_dir.join("dpal.c"),
                src_dir.join("p3_seq_lib.c"),
                src_dir.join("masker.c"),
                src_dir.join("read_boulder.c"),
                src_dir.join("print_boulder.c"),
                src_dir.join("format_output.c"),
            ])
            .include(&src_dir)
            .define("MAX_PRIMER_LENGTH", "60")
            .opt_level(2)
            .warnings(false)
            // thal.c needs -ffloat-store for reproducible floating point
            .flag_if_supported("-ffloat-store")
            .compile("primer3_c");

        // C++ source (libprimer3.cc requires C++11)
        cc::Build::new()
            .file(src_dir.join("libprimer3.cc"))
            .include(&src_dir)
            .define("MAX_PRIMER_LENGTH", "60")
            .cpp(true)
            .std("c++11")
            .opt_level(2)
            .warnings(false)
            .compile("primer3_cc");

        // Link the C++ standard library
        let target = env::var("TARGET").unwrap();
        if target.contains("apple") || target.contains("freebsd") {
            println!("cargo:rustc-link-lib=c++");
        } else {
            println!("cargo:rustc-link-lib=stdc++");
        }
    }

    #[cfg(feature = "system")]
    {
        println!("cargo:rustc-link-lib=primer3");
    }

    // Generate bindings
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .clang_arg(format!("-I{}", src_dir.display()))
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .allowlist_function("thal")
        .allowlist_function("set_thal_default_args")
        .allowlist_function("set_thal_oligo_default_args")
        .allowlist_function("thal_set_null_parameters")
        .allowlist_function("thal_load_parameters")
        .allowlist_function("thal_free_parameters")
        .allowlist_function("get_thermodynamic_values")
        .allowlist_function("destroy_thal_structures")
        .allowlist_function("oligotm")
        .allowlist_function("seqtm")
        .allowlist_function("long_seq_tm")
        .allowlist_function("oligodg")
        .allowlist_function("end_oligodg")
        .allowlist_function("symmetry")
        .allowlist_function("divalent_to_monovalent")
        .allowlist_function("p3_create_global_settings")
        .allowlist_function("p3_destroy_global_settings")
        .allowlist_function("create_seq_arg")
        .allowlist_function("destroy_seq_args")
        .allowlist_function("choose_primers")
        .allowlist_function("destroy_p3retval")
        .allowlist_function("p3_set_gs_.*")
        .allowlist_function("p3_set_sa_.*")
        .allowlist_function("p3_sa_add_to_.*")
        .allowlist_function("read_and_create_seq_lib")
        .allowlist_function("destroy_seq_lib")
        .allowlist_function("seq_lib_num_seq")
        .allowlist_function("create_empty_seq_lib")
        .allowlist_function("add_seq_and_rev_comp_to_seq_lib")
        .allowlist_type("thal_args")
        .allowlist_type("thal_results")
        .allowlist_type("thal_parameters")
        .allowlist_type("thal_alignment_type")
        .allowlist_type("thal_mode")
        .allowlist_type("tm_ret")
        .allowlist_type("tm_method_type")
        .allowlist_type("salt_correction_type")
        .allowlist_type("p3_global_settings")
        .allowlist_type("seq_args")
        .allowlist_type("p3retval")
        .allowlist_type("primer_rec")
        .allowlist_type("primer_pair")
        .allowlist_type("oligo_array")
        .allowlist_type("oligo_stats")
        .allowlist_type("pair_stats")
        .allowlist_type("seq_lib")
        .allowlist_type("args_for_one_oligo_or_primer")
        .allowlist_type("oligo_weights")
        .allowlist_type("pair_weights")
        .allowlist_type("task")
        // masker types
        .allowlist_type("masker_parameters")
        .allowlist_type("formula_parameters")
        .allowlist_type("input_sequence")
        .allowlist_type("output_sequence")
        .allowlist_type("masking_direction")
        .allowlist_var("THAL_MAX_ALIGN")
        .allowlist_var("THAL_MAX_SEQ")
        .allowlist_var("_INFINITY")
        .allowlist_var("ABSOLUTE_ZERO")
        .allowlist_var("MAX_LOOP")
        .allowlist_var("MIN_LOOP")
        .generate()
        .expect("Unable to generate bindings");

    bindings.write_to_file(out_dir.join("bindings.rs")).expect("Couldn't write bindings!");
}