mod build_delegation;
use std::env;
use std::path::{Path, PathBuf};
const OCCT_VERSION: &str = "V8_0_0_rc5";
#[cfg(not(feature = "source-build"))]
const OCCT_PREBUILT_TAG: &str = "occt-v800rc5";
fn slug(version: &str) -> String {
version.to_ascii_lowercase().replace('_', "")
}
fn main() {
println!("cargo:rerun-if-env-changed=OCCT_ROOT");
println!("cargo:rerun-if-env-changed=CADRUM_PREBUILT_URL");
println!("cargo:rerun-if-changed=src/traits.rs");
println!("cargo:rerun-if-changed=build_delegation.rs");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let traits_src = std::fs::read_to_string("src/traits.rs").expect("read src/traits.rs");
build_delegation::build_delegation(&traits_src, &out_dir);
if env::var("DOCS_RS").is_ok() {
return;
}
let target = env::var("TARGET").unwrap();
let target_dir = target_dir_from_out_dir(&out_dir, &target);
let default_root = target_dir.join(format!("cadrum-occt-{}-{}", slug(OCCT_VERSION), &target));
let effective_root = env::var("OCCT_ROOT")
.map(|r| {
let p = PathBuf::from(r);
if p.is_relative() { env::current_dir().unwrap().join(p) } else { p }
})
.unwrap_or(default_root);
let [occt_include, occt_lib_dir] = resolve_occt(&effective_root, &target);
link_occt_libraries(&occt_include, &occt_lib_dir);
}
fn target_dir_from_out_dir(out_dir: &Path, target: &str) -> PathBuf {
let above_profile = out_dir.ancestors().nth(4).expect("unexpected OUT_DIR layout");
if above_profile.file_name().map_or(false, |n| n == target) {
above_profile.parent().unwrap().to_path_buf()
} else {
above_profile.to_path_buf()
}
}
fn resolve_occt(effective_root: &Path, target: &str) -> [PathBuf; 2] {
let _ = target; println!("cargo:rerun-if-changed={}", effective_root.display());
match find_occt_dirs(effective_root) {
Some(dirs) => return dirs,
None => {
#[cfg(feature = "source-build")]
{
eprintln!("cargo:warning=OCCT cache miss at {} — building from source (this may take 10-30 minutes)", effective_root.display());
return source::build_from_source(effective_root)
.expect("Failed to build OCCT from source");
}
#[cfg(not(feature = "source-build"))]
{
return download_prebuilt(effective_root, target)
.unwrap_or_else(|| panic!(
"\nFailed to download prebuilt OCCT for target `{}`.\n\
See README for the list of supported prebuilt targets, or enable\n\
the `source-build` feature to build OCCT from upstream sources:\n\
\n cargo build --features source-build\n",
target
));
}
}
}
}
fn find_occt_dirs(occt_root: &Path) -> Option<[PathBuf; 2]> {
let pick = |cands: &[PathBuf]| cands.iter().find(|p| p.exists()).cloned();
let inc = pick(&[occt_root.join("include").join("opencascade"), occt_root.join("inc"), occt_root.join("include")])?;
let lib = pick(&[occt_root.join("lib"), occt_root.join("win64").join("gcc").join("lib"), occt_root.join("win64").join("clang").join("lib"), occt_root.join("win64").join("vc14").join("lib")])?;
Some([inc, lib])
}
const OCC_LIBS: &[&str] = &[
"TKernel", "TKMath", "TKBRep", "TKTopAlgo", "TKPrim", "TKBO", "TKBool",
"TKShHealing", "TKMesh", "TKGeomBase", "TKGeomAlgo", "TKG3d", "TKG2d",
"TKBin", "TKXSBase", "TKDE", "TKDECascade", "TKOffset", "TKFillet", "TKDESTEP",
#[cfg(feature = "color")] "TKLCAF",
#[cfg(feature = "color")] "TKXCAF",
#[cfg(feature = "color")] "TKCAF",
#[cfg(feature = "color")] "TKCDF",
];
fn link_occt_libraries(occt_include: &Path, occt_lib_dir: &Path) {
println!("cargo:rustc-link-search=native={}", occt_lib_dir.display());
for lib in OCC_LIBS {
println!("cargo:rustc-link-lib=static={}", lib);
}
for entry in walkdir::WalkDir::new(occt_lib_dir).min_depth(1).max_depth(1).into_iter().flatten() {
let Some(name) = entry.file_name().to_str() else { continue };
if name.contains("cadrum") {
let name=name.strip_prefix("lib").unwrap_or(name).strip_suffix(".a").or(name.strip_suffix(".lib")).unwrap_or(name);
println!("cargo:rustc-link-lib=static={}", name);
}
}
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let is_mingw_like = target_env == "gnu" || target_env == "gnullvm";
if is_mingw_like {
println!("cargo:rustc-link-arg=-Wl,--allow-multiple-definition");
}
if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") && is_mingw_like {
println!("cargo:rustc-link-arg=-static");
}
let mut build = cxx_build::bridge("src/occt/ffi.rs");
build.file("cpp/wrapper.cpp").include(occt_include).std("c++17").define("_USE_MATH_DEFINES", None);
if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") {
build.flag("/utf-8");
}
#[cfg(feature = "color")]
build.define("CADRUM_COLOR", None);
build.compile("cadrum_cpp");
println!("cargo:rerun-if-changed=src/occt/ffi.rs");
println!("cargo:rerun-if-changed=cpp/wrapper.h");
println!("cargo:rerun-if-changed=cpp/wrapper.cpp");
}
#[cfg(not(feature = "source-build"))]
fn download_prebuilt(dest: &Path, target: &str) -> Option<[PathBuf; 2]> {
let slug_ver = slug(OCCT_VERSION);
let top_name = format!("cadrum-occt-{}-{}", slug_ver, target);
let tarball_name = format!("{}.tar.gz", top_name);
let url = env::var("CADRUM_PREBUILT_URL").unwrap_or_else(|_| format!("https://github.com/lzpel/cadrum/releases/download/{}/{}", OCCT_PREBUILT_TAG, tarball_name));
eprintln!("cargo:warning=Downloading prebuilt OCCT from {}", url);
let parent = dest.parent()?;
std::fs::create_dir_all(parent).ok()?;
if let Err(e) = download_and_extract_tar_gz(&url, parent) {
eprintln!("cargo:warning=prebuilt fetch failed: {}", e);
return None;
}
let extracted = parent.join(&top_name);
if !extracted.is_dir() {
eprintln!("cargo:warning=prebuilt tarball missing expected top-level dir `{}`", top_name);
return None;
}
if extracted != *dest {
let _ = std::fs::remove_dir_all(dest);
if let Err(e) = std::fs::rename(&extracted, dest) {
eprintln!("cargo:warning=failed to move extracted OCCT into {}: {}", dest.display(), e);
return None;
}
}
find_occt_dirs(dest)
}
fn download_and_extract_tar_gz(url: &str, dest: &Path) -> Result<(), String> {
let bytes = fetch_bytes(url)?;
let gz = libflate::gzip::Decoder::new(&bytes[..]).map_err(|e| format!("gzip decode failed: {e}"))?;
tar::Archive::new(gz).unpack(dest).map_err(|e| format!("tar unpack failed: {e}"))?;
Ok(())
}
fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
if let Some(rest) = url.strip_prefix("file://") {
let path: PathBuf = if rest.len() >= 3 && rest.starts_with('/') && rest.as_bytes()[2] == b':' {
PathBuf::from(&rest[1..])
} else {
PathBuf::from(rest)
};
std::fs::read(&path).map_err(|e| format!("read {}: {}", path.display(), e))
} else {
let resp = minreq::get(url).send().map_err(|e| e.to_string())?;
Ok(resp.into_bytes())
}
}
#[cfg(feature = "source-build")]
mod source {
use super::{download_and_extract_tar_gz, find_occt_dirs, OCCT_VERSION};
use std::env;
use std::path::{Path, PathBuf};
pub fn build_from_source(effective_root: &Path) -> Option<[PathBuf; 2]> {
if let Some(dirs) = find_occt_dirs(effective_root) {
return Some(dirs);
}
let occt_version = OCCT_VERSION;
let occt_url = format!("https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/{}.tar.gz", occt_version);
let extraction_sentinel = effective_root.join(".occt_extraction_done");
if !extraction_sentinel.exists() {
std::fs::create_dir_all(effective_root).unwrap();
if let Ok(entries) = std::fs::read_dir(effective_root) {
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 {} from {} ...", occt_version, occt_url);
download_and_extract_tar_gz(&occt_url, effective_root).expect("Failed to download/extract OCCT source tarball");
std::fs::write(&extraction_sentinel, "done").unwrap();
eprintln!("OCCT source extracted successfully.");
}
let source_dir = std::fs::read_dir(effective_root)
.expect("Failed to read effective_root 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");
walk_occt_sources(&source_dir, |path| {
if let Some(patched) = patch_or_none(path) {
std::fs::write(path, patched).expect("patch write failed");
eprintln!("Patched {}", path.file_name().unwrap().to_string_lossy());
}
});
eprintln!("Building OCCT with CMake (this may take a while)...");
let built = cmake::Config::new(&source_dir)
.profile("Release")
.define("BUILD_LIBRARY_TYPE", "Static")
.define("CMAKE_INSTALL_PREFIX", effective_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")
.define("CMAKE_RC_FLAGS_INIT", "-C 1252")
.build();
eprintln!("OCCT built at: {}", built.display());
walk_occt_sources(&source_dir, |path| {
if path.is_dir() {
let _ = std::fs::remove_dir_all(path);
} else if patch_or_none(path).is_none() {
let _ = std::fs::remove_file(path);
}
});
find_occt_dirs(effective_root)
}
fn walk_occt_sources(source_dir: &Path, mut f: impl FnMut(&Path)) {
for entry in walkdir::WalkDir::new(source_dir).min_depth(1).max_depth(1).into_iter().flatten() {
match entry {
entry if "src|adm".contains(&*entry.file_name().to_string_lossy()) => {
for child in walkdir::WalkDir::new(entry.path()).into_iter().flatten() {
if child.file_type().is_file() {
f(child.path());
}
}
}
entry if entry.file_type().is_dir() => f(entry.path()),
_ => {},
}
}
}
fn patch_or_none(path: &Path) -> Option<String> {
let name = path.file_name()?.to_str()?;
let is_windows = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows");
match name {
"XCAFDoc_VisMaterial.cxx" => Some(stub_content(path, true)),
"XCAFPrs_Texture.cxx" => Some(stub_content(path, false)),
"Standard_StackTrace.cxx" => {
let stubbed = stub_content(path, true);
Some(comment_out_include_in(&stubbed, "execinfo.h"))
}
"OSD_WNT.cxx" if is_windows => Some(stub_content(path, false)),
"OSD_File.cxx" | "OSD_Protection.cxx" | "OSD_signal.cxx"
| "OSD_FileNode.cxx" | "OSD_Process.cxx"
if is_windows =>
{
Some(stub_content(path, true))
}
"occt_defs_flags.cmake" if is_windows => {
let content = std::fs::read_to_string(path).ok()?;
let needle = "add_definitions(-DOCC_CONVERT_SIGNALS)";
let replacement = "# add_definitions(-DOCC_CONVERT_SIGNALS) # patched out by cadrum build.rs";
if content.contains(needle) {
Some(content.replace(needle, replacement))
} else if content.contains(replacement) {
Some(content) } else {
None
}
}
_ => None,
}
}
fn stub_content(path: &Path, keep_signatures: bool) -> String {
let unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "unknown".to_string());
let description = if keep_signatures { "method bodies stubbed" } else { "file emptied" };
let header = format!("// Stubbed by cadrum build.rs at unix={unix}: {description}.\n");
if keep_signatures {
let content = std::fs::read_to_string(path).expect("Failed to read file for stubbing");
header + &stub_all_top_level_bodies(&content)
} else {
header
}
}
fn comment_out_include_in(content: &str, header: &str) -> String {
let needle = format!("#include <{}>", header);
let replacement = format!("// {} (patched out by cadrum build.rs)", needle);
content.replace(&needle, &replacement)
}
fn lex_normalize(content: &str) -> String {
let bytes = content.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
let mut at_line_start = true;
let push_blank = |out: &mut Vec<u8>, b: u8| {
out.push(if b == b'\n' { b'\n' } else { b' ' });
};
while i < bytes.len() {
let c = bytes[i];
if c == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
while i < bytes.len() && bytes[i] != b'\n' {
out.push(b' ');
i += 1;
}
continue;
}
if c == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
out.push(b' ');
out.push(b' ');
i += 2;
while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
push_blank(&mut out, bytes[i]);
i += 1;
}
if i + 1 < bytes.len() {
out.push(b' ');
out.push(b' ');
i += 2;
} else {
while i < bytes.len() {
push_blank(&mut out, bytes[i]);
i += 1;
}
}
continue;
}
if c == b'"' {
out.push(b' ');
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
out.push(b' ');
push_blank(&mut out, bytes[i + 1]);
i += 2;
} else {
push_blank(&mut out, bytes[i]);
i += 1;
}
}
if i < bytes.len() {
out.push(b' ');
i += 1;
}
continue;
}
if c == b'\'' {
out.push(b' ');
i += 1;
while i < bytes.len() && bytes[i] != b'\'' {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
out.push(b' ');
out.push(b' ');
i += 2;
} else {
out.push(b' ');
i += 1;
}
}
if i < bytes.len() {
out.push(b' ');
i += 1;
}
continue;
}
if at_line_start && c == b'#' {
while i < bytes.len() {
if bytes[i] == b'\n' {
let mut k = i;
while k > 0 && (bytes[k - 1] == b' ' || bytes[k - 1] == b'\t') {
k -= 1;
}
let continued = k > 0 && bytes[k - 1] == b'\\';
out.push(b'\n');
i += 1;
if !continued {
break;
}
} else {
out.push(b' ');
i += 1;
}
}
at_line_start = true;
continue;
}
if c == b'\n' {
at_line_start = true;
} else if !c.is_ascii_whitespace() {
at_line_start = false;
}
out.push(c);
i += 1;
}
debug_assert_eq!(out.len(), bytes.len(), "lex_normalize must preserve byte length");
String::from_utf8(out).expect("lex_normalize produced invalid utf-8")
}
fn stub_body_for_sig(sig: &str) -> &'static str {
let sig_norm: String = {
let mut s = sig.to_string();
loop {
let next = s.replace(" ::", "::").replace(":: ", "::");
if next == s {
break s;
}
s = next;
}
};
let paren_pos = {
let bytes = sig_norm.as_bytes();
let mut cursor = 0;
loop {
let Some(off) = sig_norm[cursor..].find('(') else { return "{}"; };
let pos = cursor + off;
let before = sig_norm[..pos].trim_end();
let id_start = before.rfind(|c: char| !(c.is_ascii_alphanumeric() || c == '_')).map(|p| p + 1).unwrap_or(0);
let ident = &before[id_start..];
let is_macro = !ident.is_empty()
&& ident.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
&& ident.chars().any(|c| c.is_ascii_uppercase());
if !is_macro {
break pos;
}
let mut depth = 1;
let mut j = pos + 1;
while j < bytes.len() && depth > 0 {
match bytes[j] {
b'(' => depth += 1,
b')' => depth -= 1,
_ => {}
}
j += 1;
}
cursor = j;
}
};
let head_full = sig_norm[..paren_pos].trim();
let head = head_full.rsplit('\n').next().unwrap_or(head_full).trim();
if head.is_empty() {
return "{}";
}
let hb = head.as_bytes();
let mut start = hb.len();
while start > 0 {
let c = hb[start - 1];
if c.is_ascii_alphanumeric() || c == b'_' || c == b':' || c == b'~' {
start -= 1;
} else {
break;
}
}
let name = &head[start..];
let return_part = head[..start].trim();
if name.contains('~') {
return "{}";
}
let segs: Vec<&str> = name.split("::").collect();
if segs.len() >= 2 && segs[segs.len() - 1] == segs[segs.len() - 2] {
return "{}";
}
if return_part.is_empty() {
return "{}";
}
let rb = return_part.as_bytes();
let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
let mut idx = 0;
while let Some(off) = return_part[idx..].find("void") {
let pos = idx + off;
let end = pos + 4;
let before_ok = pos == 0 || !is_ident(rb[pos - 1]);
let after_ok = end >= rb.len() || !is_ident(rb[end]);
if before_ok && after_ok {
let mut j = end;
while j < rb.len() && rb[j].is_ascii_whitespace() {
j += 1;
}
if j >= rb.len() || (rb[j] != b'*' && rb[j] != b'&') {
return "{}";
}
}
idx = end;
}
"{ return {}; }"
}
fn stub_all_top_level_bodies(content: &str) -> String {
let normalized = lex_normalize(content);
let nb = normalized.as_bytes();
let mut result = String::new();
let mut depth = 0usize;
let mut i = 0;
let mut last_end = 0;
while i < nb.len() {
match nb[i] {
b'{' if depth == 0 => {
let brace_pos = i;
let prefix_norm = &normalized[last_end..brace_pos];
let sig = prefix_norm.rfind(|c| c == ';' || c == '}').map(|p| &prefix_norm[p + 1..]).unwrap_or(prefix_norm);
let trimmed = sig.trim_end();
let last_line = trimmed.rsplit('\n').next().unwrap_or(trimmed).trim();
let is_function = {
let mut t = last_line;
loop {
let prev_len = t.len();
for kw in ["const", "override", "final", "noexcept", "mutable", "volatile", "= 0", "=0"] {
if t.ends_with(kw) {
t = t[..t.len() - kw.len()].trim_end();
break;
}
}
if t.len() == prev_len {
break;
}
}
t.ends_with(')')
};
let is_var_init = trimmed.ends_with('=') || !is_function;
depth = 1;
i += 1;
while i < nb.len() && depth > 0 {
match nb[i] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
i += 1;
}
if is_var_init {
continue;
}
let stub_body = stub_body_for_sig(sig);
result.push_str(&content[last_end..brace_pos]);
result.push_str(stub_body);
last_end = i;
continue;
}
b'{' => depth += 1,
b'}' => {
if depth > 0 {
depth -= 1;
}
}
_ => {}
}
i += 1;
}
result.push_str(&content[last_end..]);
result
}
}