libasdf-rs 0.2.1

Drop-in C ABI replacement for libasdf, implemented in Rust
Documentation
//! Build script for the C ABI layer.
//!
//! Three jobs:
//!
//! 1. Generate `asdf/config.h`, which the vendored headers include and which
//!    records what this build supports.
//! 2. Compile `shim.c`, which carries the handful of entry points Rust cannot
//!    express on stable (see the file's own comment).
//! 3. Export the include directory to dependent crates and to the ABI
//!    conformance tests.

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

fn main() {
    println!("cargo:rerun-if-changed=shim.c");
    println!("cargo:rerun-if-changed=include");
    println!("cargo:rerun-if-changed=build.rs");

    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
    let manifest_dir =
        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
    let vendored_include = manifest_dir.join("include");

    let have_float16 = probe_float16();
    let generated_include = out_dir.join("include");
    write_config_h(&generated_include, have_float16);

    // Build the shim against both the vendored headers and the generated
    // config.h.
    let mut build = cc::Build::new();
    build
        .file("shim.c")
        .include(&vendored_include)
        .include(&generated_include)
        .std("c11")
        .warnings(true)
        .flag_if_supported("-fvisibility=hidden");

    if have_float16 {
        build.define("ASDF_HAVE_FLOAT16", "1");
    }

    // Emit the link directives by hand so the archive can be linked with
    // `+whole-archive`. Without it the linker drops every object in the
    // archive, because nothing in the Rust code references the shim's
    // functions -- they exist purely to be exported to C callers.
    build.cargo_metadata(false);
    build.compile("asdf_shim");
    println!("cargo:rustc-link-search=native={}", out_dir.display());
    println!("cargo:rustc-link-lib=static:+whole-archive=asdf_shim");

    // Downstream crates and the conformance harness need these paths.
    println!("cargo:include={}", vendored_include.display());
    println!("cargo:generated_include={}", generated_include.display());
    // Two variables rather than one joined list: a Windows path contains the
    // separator any list would use.
    println!("cargo:rustc-env=ASDF_VENDORED_INCLUDE={}", vendored_include.display());
    println!("cargo:rustc-env=ASDF_GENERATED_INCLUDE={}", generated_include.display());
    if have_float16 {
        println!("cargo:rustc-cfg=asdf_have_float16");
    }
    println!("cargo::rustc-check-cfg=cfg(asdf_have_float16)");
}

/// Does the target's C compiler support `_Float16`?
///
/// When it does not, upstream's headers leave `asdf_ndarray_read_float16_at`
/// undeclared, and the `float16` datatype can still be read as long as it is
/// converted to another type on the way out.
fn probe_float16() -> bool {
    let probe = r#"
        _Float16 probe(_Float16 x) { return x + (_Float16)1.0f; }
        int main(void) { return (int)probe((_Float16)1.0f) == 2 ? 0 : 1; }
    "#;
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
    let src = out_dir.join("probe_float16.c");
    if std::fs::write(&src, probe).is_err() {
        return false;
    }

    let mut build = cc::Build::new();
    build.file(&src).warnings(false).cargo_metadata(false).cargo_warnings(false);
    // `try_compile` reports failure rather than panicking, which is what we
    // want for a capability probe.
    build.try_compile("probe_float16").is_ok()
}

/// Write the `asdf/config.h` the vendored headers expect.
fn write_config_h(include_dir: &Path, have_float16: bool) {
    let dir = include_dir.join("asdf");
    std::fs::create_dir_all(&dir).expect("create generated include dir");

    let float16 =
        if have_float16 { "#define ASDF_HAVE_FLOAT16 1" } else { "/* #undef ASDF_HAVE_FLOAT16 */" };

    let contents = format!(
        "\
/*
 * Generated by libasdf-rs's build.rs. Do not edit.
 *
 * Records the build-time configuration of this libasdf implementation, in the
 * same shape upstream's `config.h.in` produces, because the vendored public
 * headers include it and gate declarations on it.
 */

#ifndef ASDF_CONFIG_H
#define ASDF_CONFIG_H

/* libasdf's internal log statements are compiled in. */
#define ASDF_LOG_ENABLED 1

/* Default runtime log level when none is set explicitly. */
#define ASDF_LOG_DEFAULT_LEVEL ASDF_LOG_WARN

/* Compile-time minimum log level. */
#define ASDF_LOG_MIN_LEVEL ASDF_LOG_TRACE

/* Log output is colorized when written to a terminal. */
#define ASDF_LOG_COLOR 1

/* Whether the `_Float16` type is usable on this target. */
{float16}

#endif /* ASDF_CONFIG_H */
"
    );

    let path = dir.join("config.h");
    // Avoid rewriting an unchanged file so downstream rebuilds stay quiet.
    if std::fs::read_to_string(&path).ok().as_deref() != Some(contents.as_str()) {
        std::fs::write(&path, contents).expect("write config.h");
    }
}