use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const SDK_HEADERS: [&str; 1] = ["ThostFtdcMdApi.h"];
const SHIM_SOURCES: [&str; 2] = ["ek_ctp_shim.cpp", "ek_ctp_pod.cpp"];
const SDK_LIBS: [&str; 2] = ["thostmduserapi_se", "thosttraderapi_se"];
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",
"native/sdk/include",
] {
println!("cargo:rerun-if-changed={p}");
}
println!("cargo:rerun-if-env-changed=EK_CTP_FORCE_VSWHERE");
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");
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."
);
}
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();
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");
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") .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/)");
}
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)");
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}");
}
if let Some(profile_dir) = out_dir.ancestors().nth(3) {
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);
}
}
}
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}");
}
}
struct MsvcToolchain {
cl: PathBuf,
lib: PathBuf,
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) }
}
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()
}