//! Compiles the unmodified C ircd tree into `libircd_c.a` and links it.
//!
//! Strategy: drive the project's *own* autoconf `configure` + `Makefile` so the
//! per-object `-D` defines (which embed machine-specific install paths) and the
//! autoconf-detected include set (e.g. the zlib include path `@IRC_ZLIB_INCLUDE@`)
//! stay correct by construction, rather than re-deriving them in Rust. We inject
//! the two required overrides through `CC` — NOT by replacing `S_CFLAGS` — so the
//! Makefile's own `S_CFLAGS` (including any zlib include path) is preserved:
//!   * `-std=gnu17`: this codebase predates C23; gcc >= 15 defaults to C23, under
//!     which patricia.c's K&R callback pointers fail to compile.
//!   * `-Dmain=c_ircd_main`: so Rust owns the real `main` (safe: `main` is an
//!     identifier only in ircd.c among the compiled objects).
//!
//! To "drop" a C module in a later phase: add its `.o` to `PORTED` and define
//! its exported symbols in Rust. The archive then lacks those symbols, so there
//! is no duplicate definition.
//!
//! Known limitations (acceptable for the P0a single-developer baseline):
//!   * `make` only rebuilds a `.o` when its *source* is newer, not when these
//!     flags change. If you edit `EXTRA_CFLAGS`/`CREF_OBJS`, run `rm -rf cbuild` once.
//!   * `cbuild/` is shared across cargo profiles; running `cargo build` and
//!     `cargo build --release` concurrently can race on the `.o` files.

use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

/// The FULL module set — every common/ + ircd/ object, including modules that
/// have been ported to Rust. Always compiled; this is what the `cref_*` oracle
/// archive in ircd-testkit is built from (exported via `cargo:objs=`), so the
/// reference C of a ported module stays available to diff the Rust against.
/// Mirrors IRCD_COMMON_OBJS + IRCD_OBJS from support/Makefile.in:144-149.
/// (P8t: `version.o` is no longer built — its data globals are Rust-owned statics
/// in ircd-common/src/version.rs, so the generated `version.c` is gone from the
/// build.) `config_read.c` is `#include`-d by `s_conf.c`, not a separate TU.
const CREF_OBJS: &[&str] = &[
    // IRCD_COMMON_OBJS (from common/)
    "bsd.o", "dbuf.o", "packet.o", "send.o", "match.o", "parse.o", "support.o",
    // IRCD_OBJS (from ircd/)
    "channel.o", "class.o", "hash.o", "ircd.o", "list.o", "res.o", "s_auth.o",
    "s_bsd.o", "s_conf.o", "s_debug.o", "s_err.o", "s_id.o", "s_misc.o",
    "s_numeric.o", "s_send.o", "s_serv.o", "s_service.o", "s_user.o", "s_zip.o",
    "whowas.o", "res_init.o", "res_comp.o", "res_mkquery.o", "patricia.o",
];

/// Objects whose symbols are now provided by Rust (`ircd-common`). They are
/// COMPILED (they stay in CREF_OBJS for the oracle) but NOT archived into
/// libircd_c.a, so the Rust `#[no_mangle]` is the sole definition at link time.
/// Grows one module per P1 sub-phase. (`support.o` is handled specially — see the
/// `support_link.o` step in P1c — so it is NOT listed here.)
const PORTED: &[&str] = &[
    // each entry is dropped from libircd_c.a; Rust provides the symbols instead.
    "bsd.o",
    "packet.o",
    "match.o",
    "dbuf.o",
    "patricia.o",
    "class.o",
    "list.o",
    "s_err.o",
    "s_id.o",
    "s_numeric.o",
    "parse.o",
    "s_service.o",
    "s_debug.o",
    "s_zip.o",
    "s_misc.o",
    "channel.o",
    "s_serv.o",
    "s_user.o",
    "whowas.o",
    "hash.o",
    "s_conf.o",
    "res_comp.o",
    "res_mkquery.o",
    "res_init.o",
    "res.o",
    // P8m: s_send.c (the variadic esendto_* service-routing senders) is fully ported to
    // ircd-common/s_send.rs → dropped outright. USE_SERVICES off → zero callers, so no
    // -DPORT_* guard and no s_send_link.o; cref keeps the unguarded s_send.o for L1.
    "s_send.o",
    // P8n: s_auth.c is now FULLY ported — its last C remnant, the five iauth data globals
    // (iauth_options/iauth_spawn/iauth_version/iauth_conf/iauth_stats), now live in
    // ircd-common/s_auth.rs as #[no_mangle] statics → s_auth.o is dropped outright (no more
    // s_auth_link.o). The cref oracle keeps the unguarded s_auth.o so cref_iauth_* +
    // cref_report_iauth_* + cref_sendto_iauth survive for the L1 differentials.
    "s_auth.o",
    // P8o: send.c is now FULLY ported — every sender went Rust over P3d/P8b..P8l, leaving
    // send_link.o defining only the module-private `rcsid` string (an unreferenced static),
    // so send.o is dropped outright: no more send_link.o second-compile, no -DPORT_SEND_*
    // guard chain. The cref oracle keeps the unguarded send.o so cref_sendto_* + the L1 send
    // differentials survive.
    "send.o",
    // P8p: s_bsd.c is now FULLY ported — every function went Rust over P7d..P7ff, leaving
    // s_bsd_link.o defining only its data globals (local[]/fdas/fdall/highest_fd/readcalls/
    // udpfd/resfd/adfd/timeofday/mysk + the module-private `rcsid`). Those globals now live
    // in ircd-common/s_bsd.rs as #[no_mangle] statics, so s_bsd.o is dropped outright: no
    // more s_bsd_link.o second-compile, no -DPORT_S_BSD_* guard chain. The cref oracle keeps
    // the unguarded s_bsd.o so cref_local/cref_highest_fd/… + the L1 s_bsd differentials
    // survive.
    "s_bsd.o",
    // P8q: ircd.c is now FULLY ported — every function went Rust over P7c..P7oo, leaving
    // ircd_link.o defining only its data globals (me/client/istat/iconf/myargv/rehashed/
    // portnum/configfile/debuglevel/bootopt/serverbooting/firstrejoindone/sbrk0/tunefile/
    // dorehash/dorestart/restart_iauth/the next* timers/ListenerLL + the module-private
    // `rcsid`). Those globals now live in ircd-common/ircd.rs as #[no_mangle] statics, so
    // ircd.o is dropped outright: no more ircd_link.o second-compile, no -DPORT_IRCD_* guard
    // chain. The Rust statics read IRCDCONF_PATH/IRCDTUNE_PATH via the IRCD_CONF_PATH/
    // IRCD_TUNE_PATH rustc-envs below; the cref oracle keeps the unguarded ircd.o so cref_me/
    // cref_try_connections/… + the L1 ircd differentials survive.
    "ircd.o",
    // P8r: support.c is now FULLY ported — the last C-logic TU. Over P1c/P2e its pure subset
    // (mystrdup/strtoken/myctime/inetntop/… + the MyMalloc trio) went Rust behind PORT_SUPPORT_P1/
    // P2, leaving support_link.o defining only the format remnants dgets/make_isupport +
    // the data globals ipv6string/minus_one (+ the dead-in-Rust variadic snprintf_append and the
    // module-private rcsid). dgets/make_isupport are now ported and ipv6string/minus_one defined
    // as #[no_mangle] statics in ircd-common/support.rs, so support.o is dropped outright: no more
    // support_link.o second-compile, no -DPORT_SUPPORT_* guard chain. snprintf_append has no live
    // Rust caller (WHOX is built field-by-field in s_user.rs) so it dies with the .o. The cref
    // oracle keeps the unguarded support.o so cref_dgets/cref_make_isupport/cref_mystrdup/… + the
    // L1 support differentials survive.
    "support.o",
];

/// Flags we must add on top of whatever the Makefile already passes. Injected via
/// `CC` so the Makefile's `S_CFLAGS` (include paths, zlib include) is untouched.
///
/// `-DNO_DNS_LOOKUP`: reverse-DNS lookups on inbound client connections are
/// disabled project-wide (see `s_bsd.c` `add_connection`). The resolver's
/// behaviour is intentionally not maintained -- it is nondeterministic (the
/// resolved hostname depends on the host's DNS) and is the only thing that held
/// client registration across the event loop, which broke under the L2 golden
/// harness's pinned clock. Clients now register immediately with their numeric
/// host. Defined here so the cref oracle, the link set, and the Rust binary all
/// share one definition (the PLAN's single-source-of-truth rule).
const EXTRA_CFLAGS: &str = "-std=gnu17 -Dmain=c_ircd_main -fPIC -DNO_DNS_LOOKUP";

fn run(cmd: &mut Command, what: &str) {
    let status = cmd.status().unwrap_or_else(|e| panic!("failed to spawn {what}: {e}"));
    assert!(status.success(), "{what} failed: {status}");
}

/// Read a `VAR = value` assignment from a generated Makefile (first match wins).
fn makefile_var(makefile: &Path, var: &str) -> Option<String> {
    let text = fs::read_to_string(makefile).ok()?;
    let prefix = format!("{var} = ");
    text.lines()
        .find_map(|l| l.strip_prefix(&prefix))
        .map(|v| v.trim().to_string())
}

/// The link set = CREF_OBJS minus the ported modules. These are the objects
/// archived into libircd_c.a and linked into the final ircd binary. As of P8r
/// every C TU is either in PORTED (dropped outright) or pure-data — there is no
/// longer any `*_link.o` second-compile variant, so this is a plain set difference.
fn link_objs() -> Vec<&'static str> {
    CREF_OBJS
        .iter()
        .copied()
        .filter(|o| !PORTED.contains(o))
        .collect()
}

fn main() {
    let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let repo = manifest.parent().expect("ircd-sys must live under the repo root").to_path_buf();
    let cbuild = repo.join("cbuild");
    let out = PathBuf::from(env::var("OUT_DIR").unwrap());

    // 1. Configure once into a fixed-name, repo-child build dir. The relative
    //    `../ircd`/`../common` paths baked into Makefile.in require this dir to be
    //    a direct child of the repo root. Guard on BOTH setup.h and Makefile so a
    //    configure run that died partway through is retried rather than half-used.
    if !cbuild.join("setup.h").exists() || !cbuild.join("Makefile").exists() {
        fs::create_dir_all(&cbuild).unwrap();
        fs::copy(repo.join("support/config.h.dist"), cbuild.join("config.h")).unwrap();
        fs::copy(repo.join("support/configure"), cbuild.join("configure")).unwrap();
        run(Command::new("./configure").current_dir(&cbuild), "configure");
    }

    // P8t: version.c is no longer generated/compiled. Its data globals
    // (generation/creation/pass_version/infotext/isupport) are now Rust-owned
    // `#[no_mangle]` statics in ircd-common/src/version.rs (generation/creation
    // sourced from crate metadata at build time), so version.o is dropped from the
    // archive — see the `ar` step below. version.c was the last *generated* C TU.

    // The compiler configure chose, with our overrides appended. Passing this as
    // `CC` makes every recipe `<cc> <EXTRA_CFLAGS> $(S_CFLAGS) [per-object -D] ...`,
    // preserving the Makefile's S_CFLAGS (incl. the autoconf zlib include path).
    let base_cc = makefile_var(&cbuild.join("Makefile"), "CC").unwrap_or_else(|| "gcc".into());
    let cc_override = format!("{base_cc} {EXTRA_CFLAGS}");

    // 3. Compile every C object via the project's Makefile recipes.
    run(
        Command::new("make")
            .current_dir(&cbuild)
            .arg(format!("CC={cc_override}"))
            .args(CREF_OBJS),
        "make C objects",
    );

    // 4. (P8t) version.o is no longer compiled — see the version.c note above. We
    //    still need S_CFLAGS for the cref res.o re-compile and ctruth.o below.
    let s_cflags = makefile_var(&cbuild.join("Makefile"), "S_CFLAGS").unwrap_or_default();

    // P8r: support.c is now FULLY ported (dgets/make_isupport → Rust; ipv6string/minus_one →
    // Rust statics; the pure subset + MyMalloc trio went Rust in P1c/P2e). support.o is in
    // PORTED and dropped outright — no more support_link.o second-compile. The cref oracle keeps
    // the unguarded support.o (in CREF_OBJS) so cref_dgets/cref_make_isupport/etc. survive for L1.

    // P5(whowas): whowas.c is now FULLY ported (m_whowas — the last C remnant — now
    // lives in ircd-common/whowas.rs) → whowas.o is in PORTED and dropped outright.
    // The P2c `whowas_link.o` second-compile (-DPORT_WHOWAS_P2) is no longer needed;
    // the cref oracle keeps the full unguarded whowas.o for the L1 whowas_diff test.

    // P5hash: m_hash is now ported to Rust (ircd-common/hash.rs) → hash.o is in
    // PORTED and dropped outright; the P2d `hash_link.o` second-compile is gone.
    // The cref oracle keeps the unguarded hash.o so the cref_* hash symbols survive
    // for the L1 hash_diff test.

    // P8o: send.c is now FULLY ported. Over P3d + P8b..P8l every sender (and dead_link,
    // the logfile cluster, the svchans component, the anon machinery) was #ifdef'd out of
    // send_link.o and reimplemented in ircd-common/send.rs, leaving send_link.o defining
    // only the module-private `rcsid` (an unreferenced static). So send.o is in PORTED and
    // dropped outright — no send_link.o second-compile, no -DPORT_SEND_* guard chain. The
    // Rust logfiles_open (ircd-common/send.rs) still reads FNAME_USERLOG/CONNLOG via the
    // IRCD_USERLOG_PATH/IRCD_CONNLOG_PATH rustc-envs below; the cref oracle keeps the
    // unguarded send.o (native Makefile recipe, FNAME_* expanded) for the L1 send diffs.

    // P8q: ircd.c is now FULLY ported. Over P7c..P7oo every function was #ifdef'd out of
    // ircd_link.o (the long -DPORT_IRCD_* chain) and reimplemented in ircd-common/ircd.rs,
    // leaving ircd_link.o defining only its data globals (me/client/istat/iconf/myargv/
    // rehashed/portnum/configfile/debuglevel/bootopt/serverbooting/firstrejoindone/sbrk0/
    // tunefile/dorehash/dorestart/restart_iauth/the next* timers/ListenerLL) + the
    // module-private `rcsid`. Those globals now live in ircd-common/ircd.rs as #[no_mangle]
    // statics, so ircd.o is in PORTED and dropped outright — no more ircd_link.o
    // second-compile, no -DPORT_IRCD_* guard chain. The Rust statics read IRCDCONF_PATH/
    // IRCDTUNE_PATH via the IRCD_CONF_PATH/IRCD_TUNE_PATH rustc-envs below; the cref oracle
    // keeps the full unguarded ircd.o (native Makefile recipe, paths expanded) so cref_me/
    // cref_try_connections/cref_dorehash/… survive for the L1 ircd differentials.

    // P8p: s_bsd.c is now FULLY ported. Over P7d..P7ff every function was #ifdef'd out of
    // s_bsd_link.o and reimplemented in ircd-common/s_bsd.rs, leaving s_bsd_link.o defining
    // only its data globals (local[]/fdas/fdall/highest_fd/readcalls/udpfd/resfd/adfd/
    // timeofday/mysk) + the module-private `rcsid`. Those globals now live in
    // ircd-common/s_bsd.rs as #[no_mangle] statics, so s_bsd.o is in PORTED and dropped
    // outright — no more s_bsd_link.o second-compile, no -DPORT_S_BSD_* guard chain. The
    // Rust write_pidfile/start_iauth still read IRCDPID_PATH/IAUTH_PATH/IAUTH via the
    // IRCD_PID_PATH/IRCD_IAUTH_PATH/IRCD_IAUTH rustc-envs below; the cref oracle keeps the
    // unguarded s_bsd.o (native Makefile recipe) for the L1 s_bsd diffs.

    // P8n: s_auth.c is now FULLY ported — the five iauth data globals (the last C remnant)
    // are Rust-owned `#[no_mangle]` statics in ircd-common/s_auth.rs, so s_auth.o is in
    // PORTED and dropped outright; the P7u..P8a `s_auth_link.o` second-compile (the long
    // -DPORT_S_AUTH_* chain) is no longer needed. The cref oracle keeps the full unguarded
    // s_auth.o (in CREF_OBJS) so cref_report_iauth_* + cref_iauth_* + cref_sendto_iauth
    // survive for the L1 differentials.

    // P6m: s_conf.c is now FULLY ported (the final rehash/ipv6_convert/lookup_confhost
    // functions + the conf/kconf/networkname data globals now live in
    // ircd-common/s_conf.rs) → s_conf.o is in PORTED and dropped outright. The P6a..P6l
    // `s_conf_link.o` second-compile (-DPORT_*_P6) is no longer needed; the cref oracle
    // keeps the full unguarded s_conf.o so the cref_* symbols survive for the L1 diffs.

    // P6gg: res.c is now FULLY ported. The resolver was migrated bottom-up over
    // P6b/P6q..P6ff (m_dns → cache → request queue → query builders/senders →
    // answer parser → recvfrom driver → bootstrap), and finally P6gg moves the
    // last remnant — the resolver's private data globals (reinfo/cachetop/cainfo/
    // hashtable/incache/first/last) — into ircd-common/res.rs → res.o is in PORTED
    // and dropped outright. The P6b..P6ff `res_link.o` second-compile (the long
    // -DPORT_RES_* chain) is no longer needed; the cref oracle keeps the full
    // unguarded res.o (below) so the cref_* symbols survive for the L1 res_*_diff
    // tests.

    // The cref oracle's res.o must expose the otherwise-static cache/resolver
    // helpers + data globals as GLOBAL so the L1 differentials can reach cref_*
    // (cref_hash_name, cref_reinfo, cref_get_res, ...); -DRES_CACHE_EXPOSE blanks
    // their `static` linkage (behavior-neutral). res.o is consumed ONLY by
    // libcref.a (it is in PORTED → dropped from the link set), so this never
    // reaches the real binary; it overwrites the plain res.o `make` built above.
    run(
        Command::new("sh").current_dir(&cbuild).arg("-c").arg(format!(
            "{cc_override} {s_cflags} -DRES_CACHE_EXPOSE -c ../ircd/res.c -o res.o"
        )),
        "compile cref res.o (RES_CACHE_EXPOSE)",
    );

    // P5ccc (s_user hunt_server / next_client cluster): s_user.c is now FULLY ported
    // (every symbol it defines — the final routing callees hunt_server/next_client
    // lives in ircd-common) → s_user.o is in PORTED and dropped outright. The
    // P5e..P5s `s_user_link.o` second-compile (-DPORT_USER_P5) is no longer needed;
    // the cref oracle keeps the full unguarded s_user.o for differential testing.

    // P5bbb (s_serv server-introduction / link-establishment / burst cluster):
    // s_serv.c is now FULLY ported (every symbol it defines — including the last
    // server-link handlers m_server/m_server_estab/m_smask + the burst statics +
    // the check_servername_errors table — lives in ircd-common) → s_serv.o is in
    // PORTED and dropped outright. The P5ii..P5aaa `s_serv_link.o` second-compile
    // (-DPORT_SERV_P5, which also carried -DIRCDMOTD_PATH for the then-still-C
    // m_motd) is no longer needed; the cref oracle keeps the unguarded s_serv.o
    // for differential testing.

    // P5hh (channel lifecycle group): channel.c is now FULLY ported (every symbol it
    // defines lives in ircd-common) → channel.o is in PORTED and dropped outright. The
    // P5t..P5gg `channel_link.o` second-compile (-DPORT_CHANNEL_P5) is no longer needed;
    // the cref oracle keeps the unguarded channel.o for differential testing.

    // 4b. Compile the C truth exporter with the SAME compiler + flags as the
    //     ircd objects (cc_override carries -std=gnu17; s_cflags carries the
    //     project include paths). `-I{manifest}` lets it find wrapper.h. This is
    //     what guarantees its reported sizes/offsets are the real ABI.
    // NOTE: paths must not contain spaces; a CARGO_MANIFEST_DIR with spaces would
    // break this sh -c invocation (same assumption as the version.o block above).
    run(
        Command::new("sh").current_dir(&cbuild).arg("-c").arg(format!(
            "{cc_override} {s_cflags} -I{manifest} -c {src} -o ctruth.o",
            manifest = manifest.display(),
            src = manifest.join("ctruth.c").display(),
        )),
        "compile ctruth.o",
    );

    // 5. Archive all objects into OUT_DIR/libircd_c.a.
    let archive = out.join("libircd_c.a");
    let _ = fs::remove_file(&archive);
    let mut ar = Command::new("ar");
    ar.current_dir(&cbuild).arg("crs").arg(&archive);
    let link_set = link_objs();
    for o in &link_set {
        ar.arg(o);
    }
    // P8t: version.o dropped (its globals are Rust now). `link_set` is empty (every
    // hand-written TU is ported), so libircd_c.a holds only the L1 harness ctruth.o.
    ar.arg("ctruth.o");
    run(&mut ar, "ar");

    // 5b. Generate Rust bindings for the full C ABI surface.
    //
    // Faithful define set: bindgen sees the SAME configured headers the C objects
    // were built with (config.h + setup.h via `-I cbuild`, reached through os.h),
    // plus `-std=gnu17`. No layout-gating `-D` is re-typed here — they all live in
    // config.h. The ctruth.c cross-check test (Task 3) fails loudly if this set ever
    // drifts from the real compile.
    //
    // `-x c` is REQUIRED: bindgen defaults to C++, but these are C headers that use
    // `class` as a struct field name (common/class_def.h:25, common/struct_def.h:323)
    // and the `register` storage class — both are C++ errors that leave structs
    // incomplete. bindgen 0.72 (not 0.70) is REQUIRED: 0.70 mishandles the system
    // libclang 22 AST for the structs forward-declared via `typedef struct X Y;` in
    // common/struct_def.h:23-40, emitting them as opaque stubs instead of full structs.
    //
    // Build requirement: bindgen links the system libclang at runtime. If this build
    // panics in `bindgen ... .expect(...)` below, install libclang (e.g. the distro's
    // `clang`/`libclang-dev` package); there is no vendored fallback.
    let bindings = bindgen::Builder::default()
        .header(
            manifest
                .join("wrapper.h")
                .to_str()
                .unwrap_or_else(|| panic!("wrapper.h path is not valid UTF-8")),
        )
        .clang_arg("-x")
        .clang_arg("c")
        .clang_arg("-std=gnu17")
        .clang_arg(format!("-I{}", cbuild.display()))
        .clang_arg(format!("-I{}", repo.join("ircd").display()))
        .clang_arg(format!("-I{}", repo.join("common").display()))
        // Emit only project types/functions (+ their transitive deps); skip the
        // rest of libc. Paths are matched against the defining header.
        .allowlist_file(r".*/(common|ircd)/[^/]*\.h")
        // MAXCONNECTIONS lives in config.h (cbuild), not a project header.
        .allowlist_var("MAXCONNECTIONS")
        // bindgen's auto-generated layout tests (size/offset self-consistency) now
        // compile and pass since the structs are complete; keep them on as a free check.
        .layout_tests(true)
        .generate()
        .expect("bindgen failed to generate bindings for wrapper.h");
    bindings
        .write_to_file(out.join("bindings.rs"))
        .expect("failed to write bindings.rs");

    // 6. Link directives. `+whole-archive` forces every object in, exactly like the
    //    native `gcc -o ircd *.o -lz -lm -lcrypt` link (objects are referenced via
    //    function-pointer tables, so we must not let the linker drop any).
    println!("cargo:rustc-link-search=native={}", out.display());
    println!("cargo:rustc-link-lib=static:+whole-archive=ircd_c");
    println!("cargo:rustc-link-lib=z");
    println!("cargo:rustc-link-lib=m");
    println!("cargo:rustc-link-lib=crypt");

    // Metadata for dependents (e.g. ircd-testkit's L1 harness): where the compiled
    // C objects live and which ones constitute the ircd ABI surface. Exposed as
    // DEP_IRCD_C_CBUILD / DEP_IRCD_C_OBJS via the `links = "ircd_c"` channel.
    println!("cargo:cbuild={}", cbuild.display());
    println!("cargo:objs={}", CREF_OBJS.join(";"));

    // P6m: the Rust `rehash` (ircd-common/s_conf.rs) calls `read_motd(IRCDMOTD_PATH)`,
    // exactly as the C did. IRCDMOTD_PATH is a recursively-expanded Makefile path var
    // (not a header #define, so it is absent from bindgen). Expand it via make and hand
    // it to the crate as IRCD_MOTD_PATH so the Rust reload reads the identical file the
    // reference-C build does (the L2 REHASH golden depends on this byte-for-byte).
    let motd_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_motd:\n\t@echo $(IRCDMOTD_PATH)")
        .arg("__ircd_print_motd")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_MOTD_PATH={motd_path}");

    // P7d: the Rust `write_pidfile` (ircd-common/s_bsd.rs) writes IRCDPID_PATH, exactly
    // as the C did. IRCDPID_PATH is a recursively-expanded Makefile path var (passed
    // only on s_bsd.o's command line, absent from bindgen). Expand it via make and hand
    // it to the crate as IRCD_PID_PATH so the Rust pidfile lands at the identical path
    // the reference-C build uses (the L1 s_bsd_leaves_diff write compares bytes at it).
    let pid_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_pid:\n\t@echo $(IRCDPID_PATH)")
        .arg("__ircd_print_pid")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_PID_PATH={pid_path}");

    // P8i: the Rust `logfiles_open` (ircd-common/send.rs) opens FNAME_USERLOG/FNAME_CONNLOG,
    // exactly as the C did. Both are recursively-expanded Makefile path vars (passed only on
    // send.o's command line, absent from bindgen). Expand them via make and hand them to the
    // crate as IRCD_USERLOG_PATH/IRCD_CONNLOG_PATH so the Rust open()s hit the identical files
    // the reference-C build does (the L1 sendto_flog_diff byte-compares at them).
    let userlog_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_userlog:\n\t@echo $(FNAME_USERLOG)")
        .arg("__ircd_print_userlog")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_USERLOG_PATH={userlog_path}");
    let connlog_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_connlog:\n\t@echo $(FNAME_CONNLOG)")
        .arg("__ircd_print_connlog")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_CONNLOG_PATH={connlog_path}");

    // P7dd: the Rust `start_iauth` (ircd-common/s_bsd.rs) `execl(IAUTH_PATH, IAUTH, NULL)`s
    // the iauth helper, exactly as the C did. IAUTH_PATH (= $(server_bin_dir)/$(IAUTH))
    // and IAUTH are recursively-expanded Makefile vars (passed only on s_bsd.o's command
    // line, absent from bindgen). Expand both via make and hand them to the crate as
    // IRCD_IAUTH_PATH / IRCD_IAUTH so the Rust spawn execs the identical binary the
    // reference-C build does.
    let iauth_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_iauth_path:\n\t@echo $(IAUTH_PATH)")
        .arg("__ircd_print_iauth_path")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_IAUTH_PATH={iauth_path}");
    let iauth = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_iauth:\n\t@echo $(IAUTH)")
        .arg("__ircd_print_iauth")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_IAUTH={iauth}");

    // P7kk: the Rust `server_reboot` (ircd-common/src/ircd.rs) `execv(IRCD_PATH, myargv)`s
    // the server image to restart, exactly as the C did. IRCD_PATH (= $(server_bin_dir)/
    // $(IRCD_BIN)) is a recursively-expanded Makefile path var (passed only on ircd.o's
    // command line, absent from bindgen). Expand it via make and hand it to the crate as
    // IRCD_SERVER_PATH so the Rust re-exec targets the identical binary the reference-C
    // build does.
    let server_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_server:\n\t@echo $(IRCD_PATH)")
        .arg("__ircd_print_server")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_SERVER_PATH={server_path}");

    // P8q: the Rust `configfile`/`tunefile` data globals (ircd-common/src/ircd.rs) default to
    // IRCDCONF_PATH/IRCDTUNE_PATH, exactly as the C did. Both are recursively-expanded Makefile
    // path vars (passed only on ircd.o's command line, absent from bindgen). Expand them via
    // make and hand them to the crate as IRCD_CONF_PATH/IRCD_TUNE_PATH so the Rust statics carry
    // the identical default paths the reference-C build uses.
    let conf_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_conf:\n\t@echo $(IRCDCONF_PATH)")
        .arg("__ircd_print_conf")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_CONF_PATH={conf_path}");
    let tune_path = Command::new("make")
        .current_dir(&cbuild)
        .arg("-s")
        .arg("--eval=__ircd_print_tune:\n\t@echo $(IRCDTUNE_PATH)")
        .arg("__ircd_print_tune")
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_default();
    println!("cargo:rustc-env=IRCD_TUNE_PATH={tune_path}");

    // 7. Rebuild triggers.
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed={}", manifest.join("wrapper.h").display());
    println!("cargo:rerun-if-changed={}", manifest.join("ctruth.c").display());
    println!("cargo:rerun-if-changed={}", repo.join("support/config.h.dist").display());
    println!("cargo:rerun-if-changed={}", repo.join("support/Makefile.in").display());
    println!("cargo:rerun-if-changed={}", repo.join("support/configure").display());
    println!("cargo:rerun-if-changed={}", repo.join("common").display());
    println!("cargo:rerun-if-changed={}", repo.join("ircd").display());
}
