ek-ctp 0.12.4

CTP gateway for the enkeep ek ecosystem: MD/TD session drivers mapping CTP wire rows onto ek-data objects over the ek-event bus (pure-logic core; the CTP SDK ships inside this package and native bridging lands behind the optional `native` feature).
Documentation
//! ek-ctp 构建脚本 —— 只服务 `native` 特性(手写,仅 std,零第三方 crate)。
//!
//! 三条硬规则:
//! 1. **特性关闭时完全 no-op**:纯逻辑消费方与 docs.rs 不触碰任何 C++ 工具链。
//! 2. **只支持 x86_64-pc-windows-msvc**:CTP 的导入库是 MSVC 产物;其他目标直接给
//!    清晰报错,而不是让用户在链接期面对一堆看不懂的符号错误。
//! 3. **SDK 随包**:头/导入库/DLL 都在 `native/sdk/`(见 NOTICE/D-SDK-SHIP),
//!    下游无需自备;MSVC 环境优先用环境变量,缺失时用 vswhere 自建
//!    (不要求"VS 开发者命令行")。
//!
//! 错误信息一律 ASCII:控制台码页可能是 GBK,中文会显示成乱码。

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

/// shim 源码里包含的 SDK 头(存在性检查用)。
const SDK_HEADERS: [&str; 1] = ["ThostFtdcMdApi.h"];

/// shim 源码清单(逐文件编译;分文件便于并行落地,见 M2)。
/// `ek_ctp_shim.cpp` 必须存在(下面单独校验);其余存在才编。
const SHIM_SOURCES: [&str; 2] = ["ek_ctp_shim.cpp", "ek_ctp_pod.cpp"];

/// 需要链接的 CTP 导入库(内核名,不带 .lib)。MD 与 TD 各一。
const SDK_LIBS: [&str; 2] = ["thostmduserapi_se", "thosttraderapi_se"];

/// 运行期需要随 target 目录投放的 DLL(M1 用 MD;TD 先备好,M2 直接用)。
const SDK_DLLS: [&str; 2] = ["thostmduserapi_se.dll", "thosttraderapi_se.dll"];

const VSWHERE: &str = r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe";
const WINSDK_ROOT: &str = r"C:\Program Files (x86)\Windows Kits\10";

fn main() {
    // 输入变化触发重跑
    for p in [
        "build.rs",
        "native/shim/ek_ctp_shim.cpp",
        "native/shim/ek_ctp_shim.h",
        "native/shim/ek_ctp_pod.cpp",
        "native/shim/ek_ctp_pod.h",
        // SDK 头变了 shim 必须重编;SDK 随包不可变,但这条依赖不能不记
        "native/sdk/include",
    ] {
        println!("cargo:rerun-if-changed={p}");
    }
    println!("cargo:rerun-if-env-changed=EK_CTP_FORCE_VSWHERE");

    // ---- 规则 1:特性关闭 → no-op ----
    if env::var_os("CARGO_FEATURE_NATIVE").is_none() {
        return;
    }

    let manifest = PathBuf::from(
        env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is always set by cargo"),
    );
    let sdk = manifest.join("native").join("sdk");
    let include_dir = sdk.join("include");
    let win64 = sdk.join("win64");
    let import_libs = win64.join("libs");
    let shim_dir = manifest.join("native").join("shim");
    let shim_cpp = shim_dir.join("ek_ctp_shim.cpp");

    // ---- 规则 2:目标检查 ----
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
    let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
    if target_os != "windows" || target_env != "msvc" || target_arch != "x86_64" {
        panic!(
            "ek-ctp `native` supports only x86_64-pc-windows-msvc (got {target_arch}-{target_os}-{target_env}). \
             The pure-logic layer builds everywhere without this feature; \
             the CTP import libraries are MSVC x64 artefacts."
        );
    }

    // ---- SDK 存在性(随包发布,理论上必然存在;缺了就明确报错) ----
    for h in SDK_HEADERS {
        let p = include_dir.join(h);
        if !p.is_file() {
            panic!(
                "ek-ctp `native`: missing CTP header {} (see NOTICE)",
                p.display()
            );
        }
    }
    for l in SDK_LIBS {
        let p = import_libs.join(format!("{l}.lib"));
        if !p.is_file() {
            panic!(
                "ek-ctp `native`: missing CTP import library {} (see NOTICE)",
                p.display()
            );
        }
    }
    if !shim_cpp.is_file() {
        panic!(
            "ek-ctp `native`: missing shim source {}",
            shim_cpp.display()
        );
    }

    let toolchain = MsvcToolchain::discover();
    // 强制自建路径属诊断行为:把解析到的工具链打出来,便于验证 vswhere 分支(平时保持安静)。
    if env::var_os("EK_CTP_FORCE_VSWHERE").is_some() {
        println!(
            "cargo:warning=ek-ctp native: forced MSVC discovery -> cl={} lib={}",
            toolchain.cl.display(),
            toolchain.lib.display()
        );
    }
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is always set by cargo"));
    let shim_lib = out_dir.join("ek_ctp_shim.lib");

    // ---- 编译 shim(cl.exe → .obj,逐个源文件) ----
    let mut objs: Vec<PathBuf> = Vec::new();
    for name in SHIM_SOURCES {
        let src = shim_dir.join(name);
        if !src.is_file() {
            continue; // 分文件增量落地:尚未添加的源文件跳过
        }
        let obj = out_dir.join(format!("{}.obj", name.trim_end_matches(".cpp")));
        let mut cl = Command::new(&toolchain.cl);
        cl.arg("/nologo")
            .arg("/c")
            .arg("/O2")
            .arg("/MD")
            .arg("/wd4819") // CTP 头是 GBK:非中文码页机器上的编码告警,与本工程无关
            .arg("/D_CRT_SECURE_NO_WARNINGS")
            .arg(format!("/I{}", include_dir.display()))
            .arg(format!("/Fo{}", obj.display()))
            .arg(&src);
        if let Some(inc) = &toolchain.include_dirs {
            cl.env("INCLUDE", join_paths(inc));
        }
        run(&mut cl, &format!("cl.exe (compile {name})"));
        objs.push(obj);
    }
    if objs.is_empty() {
        panic!("ek-ctp `native`: no shim source was compiled (check native/shim/)");
    }

    // ---- 归档成静态库(lib.exe → .lib) ----
    let mut lib = Command::new(&toolchain.lib);
    lib.arg("/nologo")
        .arg(format!("/OUT:{}", shim_lib.display()));
    for o in &objs {
        lib.arg(o);
    }
    if let Some(libdirs) = &toolchain.lib_dirs {
        lib.env("LIB", join_paths(libdirs));
    }
    run(&mut lib, "lib.exe (archive shim)");

    // ---- 链接指令:shim 静态库 + CTP 导入库 ----
    println!("cargo:rustc-link-search=native={}", out_dir.display());
    println!("cargo:rustc-link-lib=static=ek_ctp_shim");
    println!("cargo:rustc-link-search=native={}", import_libs.display());
    for l in SDK_LIBS {
        println!("cargo:rustc-link-lib=dylib={l}");
    }

    // ---- 运行期 DLL 投放:测试/示例的可执行文件在 target/<profile>/deps/ 与
    //      target/<profile>/examples/,而 Windows 只在 exe 所在目录、系统目录、
    //      cwd、PATH 里找 DLL,所以三处都要放(0.11.0 第6刀:补 examples 这一档,
    //      示例不再需要手工把 target/debug 塞进 PATH)。 ----
    if let Some(profile_dir) = out_dir.ancestors().nth(3) {
        // 先把两个落点目录立起来:build script 跑在编译之前,examples/ 要等
        // 示例开始编译才存在 —— 不在这里建好,DLL 就永远进不去 examples/(首次
        // `cargo build --examples` 拿不到 DLL 的那一口坑)。
        let _ = fs::create_dir_all(profile_dir.join("deps"));
        let _ = fs::create_dir_all(profile_dir.join("examples"));
        for dll in SDK_DLLS {
            let src = win64.join(dll);
            if !src.is_file() {
                panic!("ek-ctp `native`: missing CTP runtime {}", src.display());
            }
            copy_into(profile_dir, dll, &src);
            copy_into(&profile_dir.join("deps"), dll, &src);
            copy_into(&profile_dir.join("examples"), dll, &src);
        }
    }
}

/// 把 DLL 复制到目标目录(不存在就跳过,不视为错误)。
fn copy_into(dir: &Path, name: &str, src: &Path) {
    if !dir.is_dir() {
        return;
    }
    let dst = dir.join(name);
    if fs::copy(src, &dst).is_err() {
        panic!(
            "ek-ctp `native`: failed to copy {} -> {}",
            src.display(),
            dst.display()
        );
    }
}

fn join_paths(paths: &[PathBuf]) -> std::ffi::OsString {
    env::join_paths(paths).expect("path list contains a separator, which cannot happen here")
}

fn run(cmd: &mut Command, what: &str) {
    let out = match cmd.output() {
        Ok(o) => o,
        Err(e) => panic!(
            "ek-ctp `native`: could not run {what} ({e}). \
             Install Visual Studio with the C++ build tools, or run from a developer prompt."
        ),
    };
    if !out.status.success() {
        let mut msg = format!("ek-ctp `native`: {what} failed with {}", out.status);
        let stdout = String::from_utf8_lossy(&out.stdout);
        let stderr = String::from_utf8_lossy(&out.stderr);
        if !stdout.trim().is_empty() {
            msg.push_str("\n--- stdout ---\n");
            msg.push_str(stdout.trim_end());
        }
        if !stderr.trim().is_empty() {
            msg.push_str("\n--- stderr ---\n");
            msg.push_str(stderr.trim_end());
        }
        panic!("{msg}");
    }
}

/// MSVC 工具链定位:优先用环境里现成的 cl.exe/INCLUDE/LIB(已配好的机器零成本),
/// 否则用 vswhere 自建(不要求用户先开"VS 开发者命令行")。
struct MsvcToolchain {
    cl: PathBuf,
    lib: PathBuf,
    /// None = 沿用环境里已有的 INCLUDE/LIB
    include_dirs: Option<Vec<PathBuf>>,
    lib_dirs: Option<Vec<PathBuf>>,
}

impl MsvcToolchain {
    fn discover() -> Self {
        let forced = env::var_os("EK_CTP_FORCE_VSWHERE").is_some();
        if !forced {
            let ambient_ok = env::var_os("INCLUDE").is_some() && env::var_os("LIB").is_some();
            if ambient_ok {
                if let (Some(cl), Some(lib)) = (find_on_path("cl.exe"), find_on_path("lib.exe")) {
                    return Self {
                        cl,
                        lib,
                        include_dirs: None,
                        lib_dirs: None,
                    };
                }
            }
        }
        Self::from_vswhere(forced)
    }

    fn from_vswhere(forced: bool) -> Self {
        let vs = run_capture(
            Path::new(VSWHERE),
            &[
                "-latest",
                "-products",
                "*",
                "-requires",
                "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
                "-property",
                "installationPath",
            ],
        )
        .filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| {
            panic!(
                "ek-ctp `native`: could not locate MSVC. Either run from a Visual Studio developer \
                 prompt (so INCLUDE/LIB/cl.exe are set) or install the C++ build tools{}.",
                if forced {
                    " (EK_CTP_FORCE_VSWHERE was set)"
                } else {
                    ""
                }
            )
        });
        let vs = PathBuf::from(vs.trim());

        let ver_file = vs.join("VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt");
        let ver = fs::read_to_string(&ver_file)
            .ok()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .unwrap_or_else(|| {
                panic!(
                    "ek-ctp `native`: cannot read MSVC version from {}",
                    ver_file.display()
                )
            });

        let vc_tools = vs.join("VC").join("Tools").join("MSVC").join(&ver);
        let bin = vc_tools.join("bin/Hostx64/x64");
        let cl = bin.join("cl.exe");
        let lib = bin.join("lib.exe");
        if !cl.is_file() {
            panic!("ek-ctp `native`: cl.exe not found at {}", cl.display());
        }

        let mut include_dirs = vec![vc_tools.join("include")];
        let mut lib_dirs = vec![vc_tools.join("lib/x64")];

        let win_sdk = env::var_os("WindowsSdkDir")
            .map(PathBuf::from)
            .unwrap_or_else(|| PathBuf::from(WINSDK_ROOT));
        if let Some(ver) = latest_child_dir(&win_sdk.join("Include")) {
            for sub in ["ucrt", "shared", "um"] {
                let p = ver.join(sub);
                if p.is_dir() {
                    include_dirs.push(p);
                }
            }
        }
        if let Some(ver) = latest_child_dir(&win_sdk.join("Lib")) {
            for sub in ["ucrt/x64", "um/x64"] {
                let p = ver.join(sub);
                if p.is_dir() {
                    lib_dirs.push(p);
                }
            }
        }

        Self {
            cl,
            lib,
            include_dirs: Some(include_dirs),
            lib_dirs: Some(lib_dirs),
        }
    }
}

fn find_on_path(exe: &str) -> Option<PathBuf> {
    let path = env::var_os("PATH")?;
    for dir in env::split_paths(&path) {
        let cand = dir.join(exe);
        if cand.is_file() {
            return Some(cand);
        }
    }
    None
}

fn run_capture(cmd: &Path, args: &[&str]) -> Option<String> {
    let out = Command::new(cmd).args(args).output().ok()?;
    if !out.status.success() {
        return None;
    }
    let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
    if s.is_empty() { None } else { Some(s) }
}

/// 取版本号最大的子目录("10.0.26100.0" 这类字典序即版本序)。
fn latest_child_dir(root: &Path) -> Option<PathBuf> {
    let mut dirs: Vec<PathBuf> = fs::read_dir(root)
        .ok()?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.is_dir())
        .collect();
    dirs.sort();
    dirs.pop()
}