nd300 3.0.11

Cross-platform network diagnostic tool
Documentation
use clap::CommandFactory;
use std::fs;
use std::path::Path;

// Include the cli module types for man page generation.
// We reference the library's cli module via the crate path.
#[path = "src/cli.rs"]
mod cli;

// Stub the dependency so cli.rs compiles in the build script context.
// cli.rs uses `crate::speedtest::TestDuration` — we replicate just enough here.
mod speedtest {
    #[derive(Clone)]
    #[allow(dead_code)]
    pub enum TestDuration {
        Seconds(u64),
        Auto,
    }
}

fn normalize_manpage(buf: Vec<u8>) -> Vec<u8> {
    let rendered = String::from_utf8(buf).expect("clap_mangen renders UTF-8 man pages");
    rendered
        .lines()
        .map(str::trim_end)
        .collect::<Vec<_>>()
        .join("\n")
        .into_bytes()
}

/// Decide whether `manifest_dir` is a live developer checkout (vs. a registry
/// extraction, vendored copy, or git-dependency checkout).
///
/// When `cargo install nd300` builds the crate, `CARGO_MANIFEST_DIR` points
/// inside Cargo's registry/cache (read-only on hardened or sandboxed machines),
/// and writing the committed `man/*.1` there is both pointless (the release
/// archive already ships them via `include = ["man/**"]`) and dangerous (a
/// failed write would panic the install). We refuse to write into any path that
/// looks like one of those non-source locations.
///
/// The substrings checked are the canonical Cargo layout on every platform:
/// extracted registry crates live under `…/registry/src/…`, the compressed
/// cache under `…/registry/cache/…`, git dependencies under `…/git/checkouts/…`,
/// and `cargo vendor` output under `…/vendor/…`. We also treat the
/// `cargo publish`/`cargo package` verify-build directory (`…/target/package/…`)
/// as non-source: writing the committed `man/*.1` there during the verification
/// build would trip cargo's "source directory was modified by build.rs" check
/// (and only succeeds today by coincidence when the regenerated bytes match the
/// packaged ones). Backslashes are normalized to forward slashes first so the
/// same check works on Windows.
fn is_dev_source_tree(manifest_dir: &Path) -> bool {
    let normalized = manifest_dir.to_string_lossy().replace('\\', "/");
    const NON_SOURCE_MARKERS: [&str; 5] = [
        "/registry/src/",
        "/registry/cache/",
        "/git/checkouts/",
        "/vendor/",
        "/target/package/",
    ];
    !NON_SOURCE_MARKERS
        .iter()
        .any(|marker| normalized.contains(marker))
}

fn main() {
    // Render both man pages into in-memory buffers first. These steps are
    // deterministic: a failure here is a genuine bug (a broken CLI definition
    // or a non-UTF-8 render), so we keep the panics.
    let nd300_cmd = cli::Nd300Cli::command();
    let nd300_man = clap_mangen::Man::new(nd300_cmd);
    let mut nd300_buf = Vec::new();
    nd300_man.render(&mut nd300_buf).unwrap();
    let nd300_buf = normalize_manpage(nd300_buf);

    let speedqx_cmd = cli::SpeedQXCli::command();
    let speedqx_man = clap_mangen::Man::new(speedqx_cmd);
    let mut speedqx_buf = Vec::new();
    speedqx_man.render(&mut speedqx_buf).unwrap();
    let speedqx_buf = normalize_manpage(speedqx_buf);

    // 1) OUT_DIR/man — always attempt, but strictly best-effort. `cargo install`
    //    on a read-only/sandboxed cache must NEVER panic on a man-page write.
    //    Nothing reads OUT_DIR/man (the release archive uses the committed
    //    copies), so a skipped write here is harmless.
    let out_man_dir =
        std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap_or_else(|_| "man".to_string()))
            .join("man");
    if fs::create_dir_all(&out_man_dir).is_ok() {
        let _ = fs::write(out_man_dir.join("nd300.1"), &nd300_buf);
        let _ = fs::write(out_man_dir.join("speedqx.1"), &speedqx_buf);
    }

    // 2) Repo man/ — only when building inside a live source checkout, and even
    //    then best-effort. This keeps the committed `man/*.1` fresh on a normal
    //    `cargo build` (so the dev tree stays clean) without ever writing into a
    //    registry extraction, vendored copy, or git-dep checkout.
    if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        let manifest_dir = std::path::PathBuf::from(manifest_dir);
        if is_dev_source_tree(&manifest_dir) {
            let root_man_dir = manifest_dir.join("man");
            if fs::create_dir_all(&root_man_dir).is_ok() {
                let _ = fs::write(root_man_dir.join("nd300.1"), &nd300_buf);
                let _ = fs::write(root_man_dir.join("speedqx.1"), &speedqx_buf);
            }
        }
    }

    println!("cargo::rerun-if-changed=src/cli.rs");
}