use std::{
env, fs,
path::{Path, PathBuf},
process::Command,
};
const TORA_ASSET_URLS: &[&str] = &[
"https://ctp-api.ruiqilei.com/tora/API_Stock_C%2B%2B_td%20v4.1.8_20260422.zip",
"https://ctp-api.ruiqilei.com/tora/Api_Stock_lv1_C%2B%2B_md%20v1.0.9_20250825.zip",
"https://ctp-api.ruiqilei.com/tora/API_Stock_lv2_C%2B%2B%20v4.0.8_20251126.zip",
];
const WRAPPER_SRCS: &[&str] = &[
"wrapper/src/TraderConverter.cpp",
"wrapper/src/CTraderSpi.cpp",
"wrapper/src/TraderApi.cpp",
"wrapper/src/XMdConverter.cpp",
"wrapper/src/CXMdSpi.cpp",
"wrapper/src/XMdApi.cpp",
"wrapper/src/Lev2MdConverter.cpp",
"wrapper/src/CLev2MdSpi.cpp",
"wrapper/src/Lev2MdApi.cpp",
];
const WRAPPER_HEADERS: &[&str] = &[
"wrapper/include/TraderConverter.h",
"wrapper/include/CTraderSpi.h",
"wrapper/include/TraderApi.h",
"wrapper/include/XMdConverter.h",
"wrapper/include/CXMdSpi.h",
"wrapper/include/XMdApi.h",
"wrapper/include/Lev2MdConverter.h",
"wrapper/include/CLev2MdSpi.h",
"wrapper/include/Lev2MdApi.h",
];
fn main() {
if env::var_os("DOCS_RS").is_some() {
return;
}
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let lib_dir = out_dir.join("lib");
ensure_lib_dir(&lib_dir);
assert!(
lib_dir.join("TORATstpTraderApi.h").exists(),
"TORA SDK headers missing under {} after download — the bundle layout \
may have changed; run `cargo clean` and rebuild to re-fetch",
lib_dir.display()
);
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let fast_trader = env::var_os("CARGO_FEATURE_FAST_TRADER").is_some();
let fast_xmd = env::var_os("CARGO_FEATURE_FAST_XMD").is_some();
let trader_lib = if fast_trader {
"fasttraderapi"
} else {
"traderapi"
};
let xmd_lib = if fast_xmd { "xfastmdapi" } else { "xmdapi" };
let lev2md_lib = "lev2mdapi";
let link_libs = [trader_lib, xmd_lib, lev2md_lib];
println!("cargo:rustc-link-search=native={}", lib_dir.display());
for lib in link_libs {
println!("cargo:rustc-link-lib=dylib={}", lib);
}
if target_os == "linux" {
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
}
let mut build = cxx_build::bridges(["src/trader.rs", "src/xmd.rs", "src/lev2md.rs"]);
build.include(&lib_dir); build
.flag_if_supported("/EHsc")
.flag_if_supported("/std:c++20")
.flag_if_supported("/utf-8")
.flag_if_supported("/w")
.flag_if_supported("-std=c++20")
.flag_if_supported("-w");
build.files(WRAPPER_SRCS).compile("tora_rs");
println!("cargo:rerun-if-changed=src/lib.rs");
for f in WRAPPER_SRCS.iter().chain(WRAPPER_HEADERS) {
println!("cargo:rerun-if-changed={}", f);
}
copy_runtime_libs(&lib_dir, &target_os, &link_libs);
}
fn ensure_lib_dir(lib_dir: &Path) {
if lib_dir.exists() {
return;
}
let scratch = lib_dir.with_file_name("lib.scratch");
let staging = lib_dir.with_file_name("lib.partial");
for d in [&scratch, &staging] {
if d.exists() {
fs::remove_dir_all(d)
.unwrap_or_else(|e| panic!("failed to clear {}: {}", d.display(), e));
}
}
let raw = scratch.join("raw");
fs::create_dir_all(&raw)
.unwrap_or_else(|e| panic!("failed to create {}: {}", raw.display(), e));
fs::create_dir_all(&staging)
.unwrap_or_else(|e| panic!("failed to create {}: {}", staging.display(), e));
eprintln!("tora-rs: downloading TORA SDK bundles — this happens once per OUT_DIR");
for (i, url) in TORA_ASSET_URLS.iter().enumerate() {
let zip_path = scratch.join(format!("asset-{i}.zip"));
let status = Command::new("curl")
.arg("-fsSL")
.arg("--retry")
.arg("3")
.arg("-o")
.arg(&zip_path)
.arg(url)
.status()
.expect("`curl` not found — required to fetch TORA SDK bundles (see README)");
assert!(
status.success(),
"failed to download TORA SDK bundle from {}",
url
);
unzip_into(&zip_path, &raw);
}
for inner in find_zips(&raw) {
unzip_into(&inner, &raw);
}
collect_sdk_files(&raw, &staging);
let _ = fs::remove_dir_all(&scratch);
fs::rename(&staging, lib_dir).unwrap_or_else(|e| {
panic!(
"failed to rename {} → {}: {}",
staging.display(),
lib_dir.display(),
e
)
});
}
fn unzip_into(zip: &Path, dest: &Path) {
let status = if cfg!(target_os = "windows") {
Command::new("tar")
.arg("-xf")
.arg(zip)
.arg("-C")
.arg(dest)
.status()
.expect("`tar` not found — required to extract on Windows (see README)")
} else {
Command::new("unzip")
.arg("-q")
.arg("-o")
.arg(zip)
.arg("-d")
.arg(dest)
.status()
.expect("`unzip` not found — required to extract TORA bundles (see README)")
};
assert!(
status.success(),
"failed to extract {} into {}",
zip.display(),
dest.display()
);
}
fn find_zips(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
for entry in fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {}", dir.display(), e)) {
let p = entry.unwrap().path();
if p.extension().and_then(|s| s.to_str()) == Some("zip") {
out.push(p);
}
}
out
}
fn collect_sdk_files(raw: &Path, staging: &Path) {
visit_files(raw, &mut |path| {
let in_x86 = path_has_component(path, "x86");
let in_x64 = path_has_component(path, "x64");
if in_x86 {
return;
}
let name = match path.file_name().and_then(|s| s.to_str()) {
Some(n) => n,
None => return,
};
let lower = name.to_lowercase();
let want = lower.ends_with(".h")
|| lower.ends_with(".so")
|| ((lower.ends_with(".dll") || lower.ends_with(".lib")) && in_x64);
if want {
let dst = staging.join(name);
fs::copy(path, &dst)
.unwrap_or_else(|e| panic!("copy {} -> {}: {}", path.display(), dst.display(), e));
}
});
}
fn path_has_component(path: &Path, needle: &str) -> bool {
path.components()
.any(|c| c.as_os_str().to_str() == Some(needle))
}
fn visit_files(dir: &Path, f: &mut dyn FnMut(&Path)) {
for entry in fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {}", dir.display(), e)) {
let p = entry.unwrap().path();
if p.is_dir() {
visit_files(&p, f);
} else {
f(&p);
}
}
}
fn copy_runtime_libs(lib_dir: &Path, target_os: &str, libs: &[&str]) {
let target_dir = {
let mut p = PathBuf::from(env::var("OUT_DIR").unwrap());
let _ = p.pop() && p.pop() && p.pop();
p
};
for lib in libs {
let file = match target_os {
"windows" => format!("{lib}.dll"),
"linux" => format!("lib{lib}.so"),
_ => return,
};
let src = lib_dir.join(&file);
let dst = target_dir.join(&file);
fs::copy(&src, &dst)
.unwrap_or_else(|e| panic!("copy {} -> {}: {}", src.display(), dst.display(), e));
}
}