use std::{ sync::{Mutex, OnceLock}, collections::{HashSet, HashMap}, time::SystemTime, path::PathBuf, fs, env, process::Command, }; pub fn fresh_logs_folder(first: bool, label: &str) -> String { let logs_path = [std::env::current_dir().unwrap().to_str().unwrap(), ".test-logs", label] .iter() .collect::<std::path::PathBuf>(); if first { let _ = fs::remove_dir_all(&logs_path); fs::create_dir_all(&logs_path).expect("couldn't create logs directory"); assert!( fs::read_dir(&logs_path).expect("couldn't read the logs folder").next().is_none(), "logs folder wasn't empty, despite removing it at the start of the run", ); } logs_path.to_str().unwrap().to_string() } // TODO: Merge this with what's in serai-orchestrator/have serai-orchestrator perform building static BUILT: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new(); pub fn build(name: String) { let built = BUILT.get_or_init(|| Mutex::new(HashMap::new())); // Only one call to build will acquire this lock let mut built_lock = built.lock().unwrap(); if built_lock.contains_key(&name) { // If it was built, return return; } // Else, hold the lock while we build let mut repo_path = env::current_exe().unwrap(); repo_path.pop(); assert!(repo_path.as_path().ends_with("deps")); repo_path.pop(); assert!(repo_path.as_path().ends_with("debug")); repo_path.pop(); assert!(repo_path.as_path().ends_with("target")); repo_path.pop(); // Run the orchestrator to ensure the most recent files exist if !Command::new("cargo") .current_dir(&repo_path) .arg("run") .arg("-p") .arg("serai-orchestrator") .arg("--") .arg("key_gen") .arg("dev") .spawn() .unwrap() .wait() .unwrap() .success() { panic!("failed to run the orchestrator"); } if !Command::new("cargo") .current_dir(&repo_path) .arg("run") .arg("-p") .arg("serai-orchestrator") .arg("--") .arg("setup") .arg("dev") .spawn() .unwrap() .wait() .unwrap() .success() { panic!("failed to run the orchestrator"); } let mut orchestration_path = repo_path.clone(); orchestration_path.push("orchestration"); if name != "runtime" { orchestration_path.push("dev"); } let mut dockerfile_path = orchestration_path.clone(); if HashSet::from(["bitcoin", "ethereum", "monero"]).contains(name.as_str()) { dockerfile_path = dockerfile_path.join("coins"); } if name.contains("-processor") { dockerfile_path = dockerfile_path.join("processor").join(name.split('-').next().unwrap()).join("Dockerfile"); } else if name == "serai-fast-epoch" { dockerfile_path = dockerfile_path.join("serai").join("Dockerfile.fast-epoch"); } else { dockerfile_path = dockerfile_path.join(&name).join("Dockerfile"); } // If this Docker image was created after this repo was last edited, return here // This should have better performance than Docker and allows running while offline if let Ok(res) = Command::new("docker") .arg("inspect") .arg("-f") .arg("{{ .Metadata.LastTagTime }}") .arg(format!("serai-dev-{name}")) .output() { let last_tag_time_buf = String::from_utf8(res.stdout).expect("docker had non-utf8 output"); let last_tag_time = last_tag_time_buf.trim(); if !last_tag_time.is_empty() { let created_time = SystemTime::from( chrono::DateTime::parse_and_remainder(last_tag_time, "%F %T.%f %z") .unwrap_or_else(|_| { panic!("docker formatted last tag time unexpectedly: {last_tag_time}") }) .0, ); // For all services, if the Dockerfile was edited after the image was built we should rebuild let mut last_modified = fs::metadata(&dockerfile_path).ok().and_then(|meta| meta.modified().ok()); // Check any additionally specified paths let meta = |path: PathBuf| (path.clone(), fs::metadata(path)); let mut metadatas = match name.as_str() { "bitcoin" | "monero" => vec![], "message-queue" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), meta(repo_path.join("substrate").join("primitives")), meta(repo_path.join("message-queue")), ], "bitcoin-processor" | "ethereum-processor" | "monero-processor" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), meta(repo_path.join("coins")), meta(repo_path.join("substrate")), meta(repo_path.join("message-queue")), meta(repo_path.join("processor")), ], "coordinator" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), meta(repo_path.join("coins")), meta(repo_path.join("substrate")), meta(repo_path.join("message-queue")), meta(repo_path.join("coordinator")), ], "runtime" | "serai" | "serai-fast-epoch" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), meta(repo_path.join("substrate")), ], _ => panic!("building unrecognized docker image"), }; while !metadatas.is_empty() { if let (path, Ok(metadata)) = metadatas.pop().unwrap() { if metadata.is_file() { if let Ok(modified) = metadata.modified() { if modified > last_modified .expect("got when source was last modified yet not when the Dockerfile was") { last_modified = Some(modified); } } } else { // Recursively crawl since we care when the folder's contents were edited, not the // folder itself for entry in fs::read_dir(path.clone()).expect("couldn't read directory") { metadatas.push(meta( path.join(entry.expect("couldn't access item in directory").file_name()), )); } } } } if let Some(last_modified) = last_modified { if last_modified < created_time { println!("{name} was built after the most recent source code edits, assuming built."); built_lock.insert(name, true); return; } } } } println!("Building {}...", &name); // Version which always prints if !Command::new("docker") .current_dir(&repo_path) .arg("build") .arg("-f") .arg(dockerfile_path) .arg(".") .arg("-t") .arg(format!("serai-dev-{name}")) .spawn() .unwrap() .wait() .unwrap() .success() { panic!("failed to build {name}"); } // Version which only prints on error /* let res = Command::new("docker") .current_dir(dockerfile_path) .arg("build") .arg(".") .arg("-t") .arg(format!("serai-dev-{name}")) .output() .unwrap(); if !res.status.success() { println!("failed to build {name}\n"); println!("-- stdout --"); println!( "{}\r\n", String::from_utf8(res.stdout) .unwrap_or_else(|_| "stdout had non-utf8 characters".to_string()) ); println!("-- stderr --"); println!( "{}\r\n", String::from_utf8(res.stderr) .unwrap_or_else(|_| "stderr had non-utf8 characters".to_string()) ); panic!("failed to build {name}"); } */ println!("Built!"); if std::env::var("GITHUB_CI").is_ok() { println!("In CI, so clearing cache to prevent hitting the storage limits."); if !Command::new("docker") .arg("builder") .arg("prune") .arg("--all") .arg("--force") .output() .unwrap() .status .success() { println!("failed to clear cache after building {name}\n"); } } // Set built built_lock.insert(name, true); }