use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn detect_gpu() -> bool {
if cfg!(feature = "cpu-only") {
return false;
}
let nvcc_in_cuda = Path::new("/usr/local/cuda/bin/nvcc").exists();
let nvcc_in_path = Command::new("nvcc").arg("--version").output().map(|o| o.status.success()).unwrap_or(false);
nvcc_in_cuda || nvcc_in_path
}
fn main() {
println!("cargo:rerun-if-env-changed=CUDA_ARCHS");
println!("cargo:rerun-if-env-changed=CUDA_ARCH");
println!("cargo:rerun-if-env-changed=CUDA_GENCODE_FLAGS");
let use_gpu = if cfg!(feature = "cpu-only") {
println!("cargo:warning=[BUILD INFO] STARKS compiled with CPU-only support (feature enabled)");
false
} else if detect_gpu() {
println!("cargo:warning=[BUILD INFO] STARKS compiled with GPU support");
true
} else {
println!("cargo:warning=[BUILD INFO] STARKS compiled with CPU-only support (CUDA not detected)");
false
};
if use_gpu {
println!("cargo:rustc-env=STARKS_BUILD_MODE=GPU");
} else {
println!("cargo:rustc-env=STARKS_BUILD_MODE=CPU");
}
let vendored = proofman_starks_src::source_dir();
let in_place = is_writable(&vendored);
let pil2_stark_path = if in_place {
vendored.canonicalize().unwrap_or(vendored)
} else {
let dst = Path::new(&env::var("OUT_DIR").unwrap()).join("pil2-stark");
sync_tree(&vendored, &dst);
dst.canonicalize().unwrap_or(dst)
};
let library_folder = if use_gpu { pil2_stark_path.join("lib-gpu") } else { pil2_stark_path.join("lib") };
let library_name = if use_gpu { "starksgpu" } else { "starks" };
let lib_file = library_folder.join(format!("lib{library_name}.a"));
let lock_path = pil2_stark_path.join(".build_lock");
let build_lock = fs::File::options()
.create(true)
.truncate(false)
.write(true)
.open(&lock_path)
.unwrap_or_else(|e| panic!("Failed to open build lock {}: {e}", lock_path.display()));
build_lock.lock().unwrap_or_else(|e| panic!("Failed to acquire build lock {}: {e}", lock_path.display()));
if use_gpu {
if in_place {
ensure_gpu_submodules_initialized(&pil2_stark_path);
}
ensure_blst_compiled(&pil2_stark_path);
}
let tracked_files = find_tracked_files(&pil2_stark_path);
for file in &tracked_files {
println!("cargo:rerun-if-changed={}", file.display());
}
println!("cargo:rerun-if-changed={}", lib_file.display());
let makefile_path = pil2_stark_path.join("Makefile");
let makefile_stamp_path = library_folder.join(".makefile_stamp");
let current_makefile = fs::read(&makefile_path).ok();
let stored_makefile = fs::read(&makefile_stamp_path).ok();
let makefile_changed = current_makefile.is_some() && current_makefile != stored_makefile;
let target = if use_gpu { "starks_lib_gpu" } else { "starks_lib" };
if makefile_changed {
eprintln!("Makefile changed — running clean rebuild...");
run_command("make", &[if use_gpu { "clean_gpu" } else { "clean_cpu" }], &pil2_stark_path);
}
eprintln!("Running make -j {target}...");
run_command("make", &["-j", target], &pil2_stark_path);
if let Some(content) = ¤t_makefile {
if let Err(e) = fs::write(&makefile_stamp_path, content) {
eprintln!(
"Warning: failed to write Makefile stamp {:?}: {e} — next build will recompile",
makefile_stamp_path
);
}
}
let abs_lib_path = library_folder.canonicalize().unwrap_or_else(|_| library_folder.clone());
if !lib_file.exists() {
if use_gpu {
panic!("`libstarksgpu.a` was not found at {}", lib_file.display());
} else {
panic!("`libstarks.a` was not found at {}", lib_file.display());
}
}
if cfg!(target_os = "macos") {
let homebrew_prefix = Command::new("brew")
.arg("--prefix")
.output()
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
.unwrap_or_else(|_| "/opt/homebrew".to_string());
println!("cargo:rustc-link-search=native={homebrew_prefix}/lib");
println!("cargo:rustc-link-search=native={homebrew_prefix}/opt/libomp/lib");
println!("cargo:rustc-link-search=native={homebrew_prefix}/opt/libsodium/lib");
println!("cargo:rustc-link-search=native={homebrew_prefix}/opt/gmp/lib");
println!("cargo:rustc-link-search=native={homebrew_prefix}/opt/openssl/lib");
println!("cargo:rustc-link-search=native=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib");
} else if cfg!(target_os = "linux") {
println!("cargo:rustc-link-search=native=/usr/lib");
println!("cargo:rustc-link-search=native=/usr/local/lib");
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu");
}
println!("cargo:rustc-link-search=native={}", abs_lib_path.display());
println!("cargo:rustc-link-lib=static={library_name}");
if use_gpu {
let cuda_path = "/usr/local/cuda/lib64"; println!("cargo:rustc-link-search=native={cuda_path}");
println!("cargo:rustc-link-lib=static=cudart_static"); println!("cargo:rustc-link-lib=dylib=dl");
println!("cargo:rustc-link-lib=dylib=rt");
let blst_path = pil2_stark_path.join("external/blst");
let blst_lib_path = blst_path.canonicalize().unwrap_or_else(|_| blst_path.clone());
println!("cargo:rustc-link-search=native={}", blst_lib_path.display());
println!("cargo:rustc-link-lib=static=blst");
}
if cfg!(target_os = "macos") {
for lib in &["sodium", "pthread", "gmp", "gmpxx", "c++", "omp"] {
println!("cargo:rustc-link-lib={lib}");
}
} else {
for lib in &["sodium", "pthread", "gmp", "stdc++", "gmpxx", "crypto", "iomp5"] {
println!("cargo:rustc-link-lib={lib}");
}
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/openmpi/lib");
println!("cargo:rustc-link-lib=mpi");
}
}
fn run_command(cmd: &str, args: &[&str], dir: &Path) {
let status = Command::new(cmd)
.args(args)
.current_dir(dir)
.status()
.unwrap_or_else(|e| panic!("Failed to execute `{cmd}`: {e}"));
if !status.success() {
panic!("Command `{}` failed with exit code {:?}", cmd, status.code());
}
}
fn find_tracked_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Some(name) = dir.file_name().and_then(|n| n.to_str()) {
if matches!(name, "build" | "build-gpu" | "build_gpu" | "lib" | "lib-gpu" | ".vscode" | ".git") {
return files;
}
}
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(find_tracked_files(&path));
} else {
let ext = path.extension().and_then(|e| e.to_str());
if matches!(ext, Some("mk" | "d")) {
continue;
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name == ".build_lock" || name.ends_with("_stamp") {
continue;
}
files.push(path);
}
}
}
files
}
fn ensure_gpu_submodules_initialized(pil2_stark_path: &Path) {
let blst_path = pil2_stark_path.join("external/blst");
let sppark_path = pil2_stark_path.join("external/sppark");
if !is_submodule_initialized(&blst_path) || !is_submodule_initialized(&sppark_path) {
eprintln!("GPU submodules not fully initialized, running git submodule update...");
let workspace_root = pil2_stark_path.parent().unwrap_or(pil2_stark_path);
run_command("git", &["submodule", "update", "--init", "--recursive"], workspace_root);
}
}
fn is_writable(dir: &Path) -> bool {
fs::OpenOptions::new().write(true).open(dir.join("Makefile")).is_ok()
}
#[cfg(unix)]
fn make_writable(mut perms: fs::Permissions) -> fs::Permissions {
use std::os::unix::fs::PermissionsExt;
perms.set_mode(perms.mode() | 0o200);
perms
}
#[cfg(not(unix))]
fn make_writable(mut perms: fs::Permissions) -> fs::Permissions {
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
perms
}
fn sync_tree(src: &Path, dst: &Path) {
fs::create_dir_all(dst).expect("create OUT_DIR source mirror");
for entry in fs::read_dir(src).expect("read vendored source dir").flatten() {
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
sync_tree(&from, &to);
} else if from.is_file() {
let src_mtime = fs::metadata(&from).and_then(|m| m.modified()).ok();
let dst_mtime = fs::metadata(&to).and_then(|m| m.modified()).ok();
let stale = match (src_mtime, dst_mtime) {
(Some(s), Some(d)) => s > d,
_ => true,
};
if stale {
fs::copy(&from, &to).expect("copy vendored source file");
if let Ok(meta) = fs::metadata(&to) {
let perms = meta.permissions();
if perms.readonly() {
let _ = fs::set_permissions(&to, make_writable(perms));
}
}
if let (Some(s), Ok(f)) = (src_mtime, fs::File::options().write(true).open(&to)) {
let _ = f.set_modified(s);
}
}
}
}
}
fn ensure_blst_compiled(pil2_stark_path: &Path) {
let blst_path = pil2_stark_path.join("external/blst");
let blst_lib = blst_path.join("libblst.a");
println!("cargo:rerun-if-changed={}", blst_lib.display());
if blst_lib.exists() {
eprintln!("blst library already exists at {}", blst_lib.display());
return;
}
eprintln!("blst library not found at {}, compiling...", blst_lib.display());
let build_script = blst_path.join("build.sh");
println!("cargo:rerun-if-changed={}", build_script.display());
println!("cargo:rerun-if-changed={}", blst_path.join("src").display());
println!("cargo:rerun-if-changed={}", blst_path.join("build").display());
if !build_script.exists() {
panic!("blst build.sh not found at {} — vendored blst sources are incomplete.", build_script.display());
}
let status = Command::new("sh")
.arg("build.sh")
.current_dir(&blst_path)
.status()
.unwrap_or_else(|e| panic!("Failed to execute blst build.sh: {e}"));
if !status.success() {
panic!("blst build.sh failed with exit code {:?}", status.code());
}
if !blst_lib.exists() {
panic!("blst compilation completed but libblst.a was not created at {}", blst_lib.display());
}
eprintln!("blst library successfully compiled at {}", blst_lib.display());
}
fn is_submodule_initialized(path: &Path) -> bool {
if path.join(".git").exists() {
return true;
}
if let Ok(mut entries) = fs::read_dir(path) {
return entries.next().is_some();
}
false
}