use std::io::{Read, Write};
use blueprint_std::{
env, fs,
path::{Path, PathBuf},
process::Command,
};
pub fn build_contracts(contract_dirs: Vec<&str>) {
let root = workspace_or_manifest_dir();
let forge_executable = find_forge_executable();
for dir in contract_dirs {
let full_path = root.join(dir).canonicalize().unwrap_or_else(|_| {
println!(
"Directory not found or inaccessible: {}",
root.join(dir).display()
);
root.join(dir)
});
if full_path.exists() {
if full_path != root.join("./contracts") {
let foundry_toml_path = full_path.join("foundry.toml");
if foundry_toml_path.exists() {
let mut content = String::new();
std::fs::File::open(&foundry_toml_path)
.expect("Failed to open foundry.toml")
.read_to_string(&mut content)
.expect("Failed to read foundry.toml");
if !content.contains("evm_version") {
if let Some(pos) = content.find("[profile.default]") {
let mut new_content = content.clone();
let insert_pos = content[pos..]
.find('\n')
.map_or(content.len(), |p| p + pos + 1);
new_content.insert_str(insert_pos, " evm_version = \"shanghai\"\n");
std::fs::write(&foundry_toml_path, new_content)
.expect("Failed to write to foundry.toml");
} else {
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&foundry_toml_path)
.expect("Failed to open foundry.toml for appending");
file.write_all(b"\n[profile.default]\nevm_version = \"shanghai\"\n")
.expect("Failed to append to foundry.toml");
}
}
} else {
panic!("Failed to read dependency foundry.toml");
}
}
let status = Command::new(&forge_executable)
.current_dir(&full_path)
.arg("build")
.arg("--evm-version")
.arg("shanghai")
.arg("--use")
.arg("0.8.27")
.status()
.expect("Failed to execute Forge build");
assert!(
status.success(),
"Forge build failed for directory: {}",
full_path.display()
);
} else {
panic!(
"Directory not found or does not exist: {}",
full_path.display()
);
}
}
}
fn is_directory_empty(path: &Path) -> bool {
fs::read_dir(path)
.map(|mut i| i.next().is_none())
.unwrap_or(true)
}
fn workspace_or_manifest_dir() -> PathBuf {
let dir = env::var("CARGO_WORKSPACE_DIR")
.or_else(|_| env::var("CARGO_MANIFEST_DIR"))
.expect("neither CARGO_WORKSPACE_DIR nor CARGO_MANIFEST_DIR is set");
PathBuf::from(dir)
}
fn run_soldeer_with_retry(args: &[&str], label: &str) {
let root = workspace_or_manifest_dir();
let forge_executable = find_forge_executable();
let attempts = 5u32;
let mut delay = std::time::Duration::from_secs(10);
for attempt in 1..=attempts {
let status = Command::new(&forge_executable)
.current_dir(&root)
.args(args)
.status()
.unwrap_or_else(|e| panic!("Failed to execute 'forge {label}': {e}"));
if status.success() {
if attempt > 1 {
println!("'forge {label}' succeeded on attempt {attempt}/{attempts}");
}
return;
}
if attempt < attempts {
println!(
"'forge {label}' attempt {attempt}/{attempts} failed; sleeping {}s",
delay.as_secs()
);
std::thread::sleep(delay);
delay *= 2;
}
}
panic!("'forge {label}' failed after {attempts} attempts");
}
pub fn soldeer_install() {
let root = workspace_or_manifest_dir();
let dependencies_dir = root.join("dependencies");
if !dependencies_dir.exists() || is_directory_empty(&dependencies_dir) {
println!("Populating dependencies directory");
run_soldeer_with_retry(&["soldeer", "install"], "soldeer install");
} else {
println!("Dependencies directory exists or is not empty. Skipping soldeer install.");
}
}
pub fn soldeer_update() {
run_soldeer_with_retry(&["soldeer", "update", "-d"], "soldeer update");
}
#[must_use]
pub fn find_forge_executable() -> String {
match Command::new("which").arg("forge").output() {
Ok(output) => {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(
!path.is_empty(),
"Forge executable not found. Make sure Foundry is installed."
);
path
}
Err(e) => panic!("Failed to find `forge` executable: {e}"),
}
}