cxx-dlang 0.2.2

Reusable D-side cxx.rs `rust::*` type bindings + LDC2 build glue
Documentation
//! Build-script glue for downstream consumers.
//!
//! Replaces ~80 lines of LDC2 toolchain detection + safety-preview wiring +
//! `cxx_build::bridge` invocation with three lines:
//!
//! ```no_run
//! # // build.rs in a downstream crate
//! cxx_dlang::build::bridge("src/main.rs")
//!     .include("include")
//!     .d_module("d/extra.d")
//!     .compile("my_bridge");
//! ```
//!
//! Discovery for `ldc2` follows the D-toolchain convention:
//! `$DC` → `$LDC2_PATH` → walk `$PATH` → `~/.dlang/ldc2-*/bin/ldc2`.
//!
//! D modules are compiled under the curated LDC2 `--preview=` safety flag
//! set, with `-I` pointing at this crate's `d/` directory so `import cxx_d;`
//! resolves to the shipped bindings.

use std::env;
use std::path::PathBuf;

/// Directory containing `cxx_d.d` — pass to `ldc2` as `-I=<this>` so that
/// `import cxx_d;` resolves from the consumer crate's D files.
pub const D_BINDINGS_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/d");

/// LDC2 `--preview=` safety flags applied to every D compilation unit.
pub const LDC2_SAFETY_FLAGS: &[&str] = &[
    "--edition=2025",
    "--preview=safer",
    "--preview=dip1000",
    "--preview=nosharedaccess",
    "--preview=fixImmutableConv",
    "--preview=systemVariables",
];

/// Start building a cxx bridge with attached D modules.
pub fn bridge(rs_path: impl Into<PathBuf>) -> CxxDBuilder {
    CxxDBuilder {
        rs_path: rs_path.into(),
        includes: Vec::new(),
        d_modules: Vec::new(),
        extern_std: "c++17",
    }
}

/// Builder for the cxx + D bridge pipeline.
pub struct CxxDBuilder {
    rs_path: PathBuf,
    includes: Vec<PathBuf>,
    d_modules: Vec<PathBuf>,
    extern_std: &'static str,
}

impl CxxDBuilder {
    /// Add a C++ include directory (forwarded to `cxx_build`'s `cc::Build`).
    pub fn include(mut self, dir: impl Into<PathBuf>) -> Self {
        self.includes.push(dir.into());
        self
    }

    /// Add one D source file to compile. The resulting object is linked into
    /// the cxx bridge's static archive so the consumer's `extern(C++, "ns")`
    /// implementations are visible to the cxx-generated callers.
    pub fn d_module(mut self, path: impl Into<PathBuf>) -> Self {
        self.d_modules.push(path.into());
        self
    }

    /// Override the C++ standard ("c++17" by default; matches LDC2's
    /// `--extern-std=`).
    pub fn extern_std(mut self, std: &'static str) -> Self {
        self.extern_std = std;
        self
    }

    /// Compile every D source, run `cxx_build::bridge`, attach the objects,
    /// and emit the static archive `lib<name>.a` (or `<name>.lib` on MSVC).
    pub fn compile(self, name: &str) {
        let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
        let d_objs_dir = out_dir.join(format!("{name}-d-objs"));
        std::fs::create_dir_all(&d_objs_dir).expect("create d_objs dir");

        let ldc2 = find_ldc2();
        let extern_std_flag = format!("--extern-std={}", self.extern_std);

        let mut objects: Vec<PathBuf> = Vec::with_capacity(self.d_modules.len());
        for src in &self.d_modules {
            println!("cargo:rerun-if-changed={}", src.display());
            let stem = src.file_stem().and_then(|s| s.to_str()).unwrap_or("dmod");
            let obj = d_objs_dir.join(format!("{stem}.o"));
            let status = std::process::Command::new(&ldc2)
                .args(LDC2_SAFETY_FLAGS)
                .arg(&extern_std_flag)
                .arg("-relocation-model=pic")
                .arg("-c")
                .arg(format!("-of={}", obj.display()))
                .arg(format!("-I={D_BINDINGS_DIR}"))
                .arg("-Id")
                .arg(src)
                .status()
                .unwrap_or_else(|e| panic!("Failed to invoke ldc2: {e}"));
            assert!(
                status.success(),
                "LDC2 compile failed for {}",
                src.display()
            );
            objects.push(obj);
        }

        let mut build = cxx_build::bridge(&self.rs_path);
        for inc in &self.includes {
            build.include(inc);
        }
        build.flag_if_supported(format!("-std={}", self.extern_std));
        for obj in &objects {
            build.object(obj);
        }
        build.compile(name);

        println!("cargo:rerun-if-changed={}", self.rs_path.display());
        println!("cargo:rerun-if-env-changed=DC");
        println!("cargo:rerun-if-env-changed=LDC2_PATH");
    }
}

/// Resolve the `ldc2` binary path.
/// Priority: `$DC` → `$LDC2_PATH` → walk `$PATH` → `~/.dlang/ldc2-*/bin/ldc2`.
pub fn find_ldc2() -> PathBuf {
    for var in ["DC", "LDC2_PATH"] {
        if let Ok(path) = env::var(var) {
            let p = PathBuf::from(&path);
            if p.is_file() {
                return p;
            }
            let p2 = p.join("ldc2");
            if p2.is_file() {
                return p2;
            }
        }
    }
    if let Some(p) = which_on_path("ldc2") {
        return p;
    }
    let home = env::var("HOME").unwrap_or_default();
    let dlang_dir = PathBuf::from(&home).join(".dlang");
    if dlang_dir.is_dir()
        && let Ok(entries) = std::fs::read_dir(&dlang_dir)
    {
        let mut candidates: Vec<PathBuf> = entries
            .flatten()
            .filter(|e| {
                e.file_name()
                    .to_str()
                    .map(|n| n.starts_with("ldc2-"))
                    .unwrap_or(false)
            })
            .map(|e| e.path().join("bin").join("ldc2"))
            .filter(|p| p.is_file())
            .collect();
        candidates.sort();
        if let Some(p) = candidates.pop() {
            return p;
        }
    }
    panic!(
        "ldc2 not found; set $DC or $LDC2_PATH, or install LDC2 >= 1.40 via https://dlang.org/download.html"
    );
}

fn which_on_path(name: &str) -> Option<PathBuf> {
    let path = env::var("PATH").ok()?;
    let exts: &[&str] = if cfg!(target_os = "windows") {
        &["", ".exe"]
    } else {
        &[""]
    };
    for dir in env::split_paths(&path) {
        for ext in exts {
            let candidate = dir.join(format!("{name}{ext}"));
            if candidate.is_file() {
                return Some(candidate);
            }
        }
    }
    None
}