use std::env;
use std::io::Read;
use std::path::{Path, PathBuf};
fn main() {
if env::var("DOCS_RS").is_ok() {
return;
}
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let (occt_include, occt_lib_dir) = if cfg!(feature = "bundled") {
build_occt_from_source(&out_dir, &manifest_dir)
} else if cfg!(feature = "prebuilt") {
use_system_occt()
} else {
panic!("Either 'bundled' or 'prebuilt' feature must be enabled");
};
link_occt_libraries(&occt_include, &occt_lib_dir, cfg!(feature = "color"));
}
fn link_occt_libraries(occt_include: &Path, occt_lib_dir: &Path, color: bool) {
let occ_libs = &[
"TKernel",
"TKMath",
"TKBRep",
"TKTopAlgo",
"TKPrim",
"TKBO",
"TKBool",
"TKShHealing", "TKMesh",
"TKGeomBase",
"TKGeomAlgo",
"TKG3d",
"TKG2d",
"TKBin", "TKXSBase",
"TKDE", "TKDECascade", "TKDESTEP", ];
println!("cargo:rustc-link-search=native={}", occt_lib_dir.display());
for lib in occ_libs {
println!("cargo:rustc-link-lib=static={}", lib);
}
if color {
for lib in &["TKLCAF", "TKXCAF", "TKCAF", "TKCDF"] {
println!("cargo:rustc-link-lib=static={}", lib);
}
}
println!("cargo:rustc-link-arg=-Wl,--allow-multiple-definition");
if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
println!("cargo:rustc-link-arg=-ladvapi32");
}
let mut build = cxx_build::bridge("src/ffi.rs");
build
.file("cpp/wrapper.cpp")
.include(occt_include)
.std("c++17")
.define("_USE_MATH_DEFINES", None);
if color {
build.define("CHIJIN_COLOR", None);
}
if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
&& env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("gnu")
{
build.define("OCC_CONVERT_SIGNALS", None);
}
build.compile("chijin_cpp");
println!("cargo:rerun-if-changed=src/ffi.rs");
println!("cargo:rerun-if-changed=cpp/wrapper.h");
println!("cargo:rerun-if-changed=cpp/wrapper.cpp");
}
fn build_occt_from_source(out_dir: &Path, manifest_dir: &Path) -> (PathBuf, PathBuf) {
let occt_version = "V7_9_3";
let occt_url = format!(
"https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/{}.tar.gz",
occt_version
);
let download_dir = out_dir.join("occt-source");
let extraction_sentinel = download_dir.join(".extraction_done");
if !extraction_sentinel.exists() {
std::fs::create_dir_all(&download_dir).unwrap();
if let Ok(entries) = std::fs::read_dir(&download_dir) {
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with("OCCT") && entry.path().is_dir() {
eprintln!("Removing partial OCCT extraction: {:?}", name);
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
eprintln!("Downloading OCCT {} ...", occt_version);
let response = ureq::get(&occt_url)
.call()
.expect("Failed to download OCCT source tarball");
let mut body = Vec::new();
response
.into_body()
.into_reader()
.read_to_end(&mut body)
.expect("Failed to read OCCT download response body");
eprintln!("Downloaded {} bytes. Extracting...", body.len());
let gz_decoder =
libflate::gzip::Decoder::new(&body[..]).expect("Failed to initialize gzip decoder");
let mut archive = tar::Archive::new(gz_decoder);
archive
.unpack(&download_dir)
.expect("Failed to extract OCCT source tarball");
std::fs::write(&extraction_sentinel, "done").unwrap();
eprintln!("OCCT source extracted successfully.");
}
let source_dir = std::fs::read_dir(&download_dir)
.expect("Failed to read occt-source directory")
.flatten()
.find(|e| e.file_name().to_string_lossy().starts_with("OCCT") && e.path().is_dir())
.map(|e| e.path())
.expect("OCCT source directory not found after extraction");
patch_occt_sources(&source_dir);
let occt_root = manifest_dir.join("target").join("occt");
let lib_dir = find_occt_lib_dir(&occt_root);
if !lib_dir.exists() {
eprintln!("Building OCCT with CMake (this may take a while)...");
let built = cmake::Config::new(&source_dir)
.define("BUILD_LIBRARY_TYPE", "Static")
.define("CMAKE_INSTALL_PREFIX", occt_root.to_str().unwrap())
.define("USE_FREETYPE", "OFF")
.define("USE_FREEIMAGE", "OFF")
.define("USE_OPENVR", "OFF")
.define("USE_FFMPEG", "OFF")
.define("USE_TBB", "OFF")
.define("USE_VTK", "OFF")
.define("USE_RAPIDJSON", "OFF")
.define("USE_DRACO", "OFF")
.define("USE_TK", "OFF")
.define("USE_TCL", "OFF")
.define("USE_XLIB", "OFF")
.define("USE_OPENGL", "OFF")
.define("USE_GLES2", "OFF")
.define("USE_EGL", "OFF")
.define("USE_D3D", "OFF")
.define("BUILD_MODULE_FoundationClasses", "ON")
.define("BUILD_MODULE_ModelingData", "ON")
.define("BUILD_MODULE_ModelingAlgorithms", "ON")
.define("BUILD_MODULE_DataExchange", "ON")
.define("BUILD_MODULE_Visualization", "OFF")
.define("BUILD_MODULE_ApplicationFramework", "OFF")
.define("BUILD_MODULE_Draw", "OFF")
.define("BUILD_DOC_Overview", "OFF")
.define("BUILD_DOC_RefMan", "OFF")
.define("BUILD_YACCLEX", "OFF")
.define("BUILD_RESOURCES", "OFF")
.define("BUILD_SAMPLES_MFC", "OFF")
.define("BUILD_SAMPLES_QT", "OFF")
.define("BUILD_Inspector", "OFF")
.define("BUILD_ENABLE_FPE_SIGNAL_HANDLER", "OFF")
.build();
eprintln!("OCCT built at: {}", built.display());
}
let lib_dir = find_occt_lib_dir(&occt_root);
let include_dir = find_occt_include_dir(&occt_root);
(include_dir, lib_dir)
}
fn find_occt_include_dir(occt_root: &Path) -> PathBuf {
let candidates = [
occt_root.join("include").join("opencascade"),
occt_root.join("inc"),
occt_root.join("include"),
];
for dir in &candidates {
if dir.exists() {
return dir.clone();
}
}
occt_root.join("include")
}
fn find_occt_lib_dir(occt_root: &Path) -> PathBuf {
let candidates = [
occt_root.join("lib"),
occt_root.join("win64").join("gcc").join("lib"),
occt_root.join("win64").join("vc14").join("lib"),
];
for dir in &candidates {
if dir.exists() {
return dir.clone();
}
}
occt_root.join("lib")
}
fn use_system_occt() -> (PathBuf, PathBuf) {
let occt_root = env::var("OCCT_ROOT")
.or_else(|_| env::var("CASROOT"))
.expect(
"OCCT_ROOT or CASROOT environment variable must be set \
when using the 'prebuilt' feature",
);
let occt_root = PathBuf::from(occt_root);
let include_dir = find_occt_include_dir(&occt_root);
let lib_dir = find_occt_lib_dir(&occt_root);
assert!(
include_dir.exists(),
"OCCT include directory not found at {}",
include_dir.display()
);
assert!(
lib_dir.exists(),
"OCCT lib directory not found at {}",
lib_dir.display()
);
(include_dir, lib_dir)
}
fn patch_occt_sources(source_dir: &Path) {
patch_remove_includes_and_stub_methods(
&source_dir.join("src/XCAFDoc/XCAFDoc_VisMaterial.cxx"),
&[
"Graphic3d_Aspects.hxx",
"Graphic3d_MaterialAspect.hxx",
"XCAFPrs_Texture.hxx",
],
&[
"XCAFDoc_VisMaterial::FillMaterialAspect",
"XCAFDoc_VisMaterial::FillAspect",
"XCAFDoc_VisMaterial::ConvertToPbrMaterial",
"XCAFDoc_VisMaterial::ConvertToCommonMaterial",
],
);
let texture_cxx = source_dir.join("src/XCAFPrs/XCAFPrs_Texture.cxx");
if texture_cxx.exists() {
std::fs::write(&texture_cxx, "// Stubbed: TKService not built\n")
.expect("Failed to patch XCAFPrs_Texture.cxx");
eprintln!("Patched XCAFPrs_Texture.cxx");
}
}
fn patch_remove_includes_and_stub_methods(
path: &Path,
includes_to_remove: &[&str],
methods_to_stub: &[&str],
) {
if !path.exists() {
return;
}
let content = std::fs::read_to_string(path).expect("Failed to read file for patching");
let patched: String = content
.lines()
.filter(|line| {
let t = line.trim();
if !t.starts_with("#include") {
return true;
}
!includes_to_remove.iter().any(|pat| t.contains(pat))
})
.collect::<Vec<_>>()
.join("\n") + "\n";
let patched = methods_to_stub
.iter()
.fold(patched, |s, m| empty_method_body(&s, m));
std::fs::write(path, patched).expect("Failed to write patched file");
eprintln!("Patched {}", path.file_name().unwrap().to_string_lossy());
}
fn empty_method_body(content: &str, method_name: &str) -> String {
let Some(name_pos) = content.find(method_name) else {
return content.to_string();
};
let sig_start = content[..name_pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
let stub_body = if content[sig_start..name_pos].contains("void") {
"{}"
} else {
"{ return {}; }"
};
let after_name = &content[name_pos..];
let Some(brace_offset) = after_name.find('{') else {
return content.to_string();
};
let brace_start = name_pos + brace_offset;
let bytes = content.as_bytes();
let mut depth = 0usize;
let mut i = brace_start;
while i < bytes.len() {
match bytes[i] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return format!("{}{}{}",
&content[..brace_start],
stub_body,
&content[i + 1..]);
}
}
_ => {}
}
i += 1;
}
content.to_string()
}