flint3-sys 3.2.2-1

Rust bindings to the FLINT C library
use std::{path::PathBuf, process::Command};

use anyhow::{Context, Result};

// Don't generate bindings for the following headers
#[allow(dead_code)]
static SKIP_HEADERS: &[&str] = &[
    "NTL-interface.h",
    "config.h",
    "crt_helpers.h",
    "longlong_asm_clang.h",
    "longlong_asm_gcc.h",
    "longlong_div_gnu.h",
    "longlong_msc_arm64.h",
    "longlong_msc_x86.h",
    "mpfr_mat.h", // deprecated
    "mpfr_vec.h", // deprecated
    "gmpcompat.h",
    "fft_small.h", // seems to cause some issues, but not so
    // important, fft_small is still used by FLINT even if the header
    // is not exposed.
    "machine_vectors.h", // idem
    "mpn_extras.h",
    "gettimeofday.h",
];

struct Conf {
    out_dir: PathBuf,           // OUT_DIR from Cargo
    bindgen_extern_c: PathBuf, // $OUT_DIR/extern.c, generated by bindgen, see https://github.com/rust-lang/rust-bindgen/discussions/2405
    bindgen_flint_rs: PathBuf, // $OUT_DIR/flint.rs, generated by bindgen
    flint_include_dir: PathBuf, // $OUT_DIR/include, generated by `make install` from FLINT
    flint_lib_dir: PathBuf,    // $OUT_DIR/lib, idem
}

impl Conf {
    fn new() -> Self {
        let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap())
            .canonicalize()
            .unwrap();
        Conf {
            out_dir: out_dir.clone(),
            bindgen_extern_c: out_dir.join("extern.c"),
            bindgen_flint_rs: out_dir.join("flint.rs"),
            flint_include_dir: out_dir.join("include"),
            flint_lib_dir: out_dir.join("lib"),
        }
    }

    // Compile FLINT and install it with prefix `OUT_DIR`, so it populates
    // `$OUT_DIR/include` and `$OUT_DIR/lib`.
    fn build_flint(&self) -> Result<()> {
        let flint_root_dir = self.out_dir.join("flint");

        Command::new("cp")
            .arg("--recursive")
            .arg("--archive")   // avoids to trigger unnecessary rebuild
            .arg("--update")
            .arg("flint")
            .arg(&self.out_dir)
            .output()
            .context("Could not copy FLINT source tree")?;

        if !flint_root_dir.join("configure").is_file() {
            Command::new("sh")
                .current_dir(&flint_root_dir)
                .arg("./bootstrap.sh")
                .output()
                .context("FLINT compilation: ./bootstrap.sh failed")?;
        }

        if !flint_root_dir.join("Makefile").is_file() {
            let mut configure = Command::new("sh");

            configure
                .current_dir(&flint_root_dir)
                .arg("./configure")
                .arg("--prefix")
                .arg(&self.out_dir)
                .arg("--disable-shared"); // it is not advised to generate dynamic libraries in crates

            if cfg!(feature = "gmp-mpfr-sys") {
                configure
                    .arg(format!(
                        "--with-gmp-lib={}",
                        std::env::var("DEP_GMP_LIB_DIR")?
                    ))
                    .arg(format!(
                        "--with-gmp-include={}",
                        std::env::var("DEP_GMP_INCLUDE_DIR")?
                    ));
            }

            configure
                .output()
                .context("FLINT compilation: ./configure failed")?;
        }

        if !flint_root_dir.join("libflint.a").is_file() {
            Command::new("make")
                .current_dir(&flint_root_dir)
                .arg("-j")
                .output()
                .context("FLINT compilation: make failed")?;
        }

        Command::new("make")
            .current_dir(&flint_root_dir)
            .arg("install")
            .output()
            .context("FLINT compilation: make install failed")?;

        println!("cargo::metadata=LIB_DIR={}", self.flint_lib_dir.display());
        println!(
            "cargo::metadata=INCLUDE_DIR={}",
            self.flint_include_dir.display()
        );

        Ok(())
    }

    fn build_extern(&self) -> Result<()> {
        cc::Build::new()
            .file(&self.bindgen_extern_c)
            .include(&self.flint_include_dir)
            .flags(["-lflint", "-lgmp", "-lmpfr"])
            .flags([
                "-Wno-old-style-declaration",
                "-Wno-unused-parameter",
                "-Wno-sign-compare",
            ])
            .try_compile("extern")?;

        Ok(())
    }
}

#[cfg(not(feature = "force-bindgen"))]
impl Conf {
    fn bindgen(&self) -> Result<()> {
        println!(
            "cargo::rerun-if-changed={}",
            &self.bindgen_flint_rs.display()
        );
        println!(
            "cargo::rerun-if-changed={}",
            &self.bindgen_extern_c.display()
        );
        Command::new("cp")
            .arg("--archive")
            .arg("./bindgen/flint.rs")
            .arg(&self.bindgen_flint_rs)
            .output()
            .context("Failed to copy `bindgen/flint.rs`")?;
        Command::new("cp")
            .arg("--archive")
            .arg("./bindgen/extern.c")
            .arg(&self.bindgen_extern_c)
            .output()
            .context("Failed to copy `bindgen/extern.c`")?;
        Ok(())
    }
}

#[cfg(feature = "force-bindgen")]
impl Conf {
    // Compute the list of all FLINT headers (minus the one we skip)
    fn flint_headers(&self) -> Result<Vec<PathBuf>> {
        use std::{collections::HashSet, ffi::OsStr};

        let flint_header_dir = self.flint_include_dir.join("flint");
        anyhow::ensure!(
            flint_header_dir.join("flint.h").is_file(),
            "Cannot find `flint.h`"
        );

        let mut headers = Vec::new();

        let mut skip = HashSet::new();
        for file in SKIP_HEADERS {
            skip.insert(OsStr::new(*file));
        }

        let entries = flint_header_dir.read_dir()?;
        let header_extension = OsStr::new("h");
        for entry in entries {
            let entry = entry?;
            let path = entry.path();
            if path.extension() != Some(header_extension) {
                continue;
            }
            if skip.contains(&path.file_name().unwrap()) {
                continue;
            }
            headers.push(path)
        }

        Ok(headers)
    }

    fn bindgen(&self) -> Result<()> {
        // All relevant FLINT headers
        let headers: Vec<_> = self.flint_headers()?;

        let mut builder = bindgen::Builder::default();
        for header in &headers {
            let h = header.to_str().context("Non unicode path")?;
            builder = builder.allowlist_file(h).header(h);
            // We are using bindgen in allowlisting mode, see
            // https://rust-lang.github.io/rust-bindgen/allowlisting.html
            // This avoids bringing all of GMP in the binding
        }

        let extern_tmp = self.out_dir.join("extern-abs.c");

        // It will probably never be the case, but bindgen does not create the
        // file if there are no inline fuctions. So we make sure it is created.
        std::fs::write(&extern_tmp, b"")?;

        let bindings = builder
            // .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) // useful to echo some cargo:force
            .derive_default(false) // use mem::zeroed() instead, or MaybeUninit
            .derive_copy(false) // nothing (?) in FLINT is Copy
            .derive_debug(false) // useless
            .wrap_static_fns(true) // deal with inline functions
            .wrap_static_fns_path(&extern_tmp) // idem
            .generate_cstr(true) // recommended by bindgen's doc
            .merge_extern_blocks(true) // why would we not?
            .blocklist_function("__.*") // block internal items
            .blocklist_var("__.*") // block internal items
            .rust_target(bindgen::RustTarget::stable(82, 0).ok().unwrap())
            // There are still issues with 2024 edition
            // https://github.com/rust-lang/rust-bindgen/issues/3180
            .rust_edition(bindgen::RustEdition::Edition2021)
            .formatter(bindgen::Formatter::Prettyplease)
            .generate()?;

        bindings.write_to_file(&self.bindgen_flint_rs)?;

        {
            // The file `extern-abs.c` contains #include directives with absolute
            // paths, we need to relativize them.
            use std::io::Write;
            let mut extern_rel = std::io::BufWriter::new(
                std::fs::File::create(&self.bindgen_extern_c).context("Cannot open `extern.c`")?,
            );

            let include_re = regex::Regex::new(r##"^#include\s+".+(flint/[^/]+\.h)""##)?;

            // This is the file where bindgen outputs the inline functions.
            let extern_tmp = std::fs::read_to_string(&extern_tmp)
                .context(format!("Cannot read `{}`", extern_tmp.display()))?;

            for line in extern_tmp.lines() {
                match include_re.captures(&line) {
                    Some(capt) => writeln!(
                        extern_rel,
                        r##"#include "{}""##,
                        capt.get(1).context("no capturing group")?.as_str()
                    )?,
                    None => writeln!(extern_rel, "{line}")?,
                }
            }
        }

        // The maintenaire of `flint-ffi-sys` may use the environment variable
        // KEEP_BINDGEN_OUTPUT to save the result of bindgen and release it,
        // so that the use downstream do not have to run bindgen themselves.
        println!("cargo::rerun-if-env-changed=KEEP_BINDGEN_OUTPUT");
        if std::env::var("KEEP_BINDGEN_OUTPUT").is_ok() {
            std::fs::copy(
                &self.bindgen_flint_rs,
                &std::path::Path::new("./bindgen/flint.rs"),
            )
            .context(format!(
                "Failed to copy `{}`",
                self.bindgen_flint_rs.display()
            ))?;
            std::fs::copy(
                &self.bindgen_extern_c,
                &std::path::Path::new("./bindgen/extern.c"),
            )
            .context(format!(
                "Failed to copy `{}`",
                self.bindgen_extern_c.display()
            ))?;
        }

        Ok(())
    }
}

fn main() -> Result<()> {
    let conf = Conf::new();

    /////////////////
    // build FLINT //
    /////////////////

    conf.build_flint()?;

    // make sure that we have the correct files at the correct place
    anyhow::ensure!(
        conf.flint_include_dir.join("flint/flint.h").is_file(),
        "Compilation is successful, but `flint/flint.h` is not where it should"
    );

    // idem
    anyhow::ensure!(
        conf.flint_lib_dir.join("libflint.a").is_file(),
        "Compilation is successful, but `libflint.a` is not where it should"
    );

    // Instruct cargo that he has to link against libflint.a and its dependencies.
    println!("cargo::rustc-link-lib=flint");
    println!("cargo::rustc-link-lib=gmp");
    println!("cargo::rustc-link-lib=mpfr");
    println!(
        "cargo::rustc-link-search=native={}",
        conf.flint_lib_dir.display()
    );


    ////////////////////////
    // binding generation //
    ////////////////////////

    // Unless the feature `force-bindgen` is unable, this simply takes the files
    // from the directory `./bindgen`.
    conf.bindgen()?;

    anyhow::ensure!(conf.bindgen_extern_c.is_file(), "Cannot find `extern.c`");
    anyhow::ensure!(conf.bindgen_flint_rs.is_file(), "Cannot find `flint.rs`");


    ////////////////////////////////
    // Compiling inline functions //
    ////////////////////////////////

    conf.build_extern()?;
    // No linking instruction is required here, this is handled by `cc`.

    Ok(())
}