use alloc::{
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use core::{fmt::Write, hash::BuildHasher};
use cubecl_ir::{debug::DebugState, settings::DebugInfo};
use md5::{Digest, Md5};
use std::{
collections::HashMap,
path::{Component, Path, PathBuf},
sync::{Mutex, OnceLock, PoisonError},
};
pub const SOURCE_ROOT_VAR: &str = "CUBECL_SOURCE_ROOT";
#[must_use]
pub fn source_md5s(debug: &DebugState, level: DebugInfo) -> HashMap<&str, Arc<str>> {
match level {
DebugInfo::Full => debug
.sources()
.iter()
.map(|(path, text)| (path.as_str(), source_md5(text)))
.collect(),
_ => HashMap::new(),
}
}
#[must_use]
pub fn kernel_source_root<S: BuildHasher>(
debug: &DebugState,
md5s: &HashMap<&str, Arc<str>, S>,
) -> Option<String> {
let files = debug
.files()
.iter()
.map(|path| (path.as_str(), md5s.get(path.as_str()).map(AsRef::as_ref)));
source_root(files)
.or_else(|| {
let texts = debug
.sources()
.iter()
.filter_map(|(path, text)| Some((path.as_str(), *text, md5s.get(path.as_str())?)));
cached_root(source_cache()?, texts)
})
.and_then(|root| root.to_str().map(str::to_string))
}
fn source_root<'a>(files: impl IntoIterator<Item = (&'a str, Option<&'a str>)>) -> Option<PathBuf> {
type Found = HashMap<(String, Option<String>), Option<PathBuf>>;
static FOUND: OnceLock<Mutex<Found>> = OnceLock::new();
let found = FOUND.get_or_init(Mutex::default);
files
.into_iter()
.filter(|(path, _)| Path::new(path).is_relative())
.find_map(|(path, md5)| {
let key = (path.to_string(), md5.map(str::to_string));
found
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry(key)
.or_insert_with(|| find_root(path, md5, search_roots()))
.clone()
})
}
fn search_roots() -> &'static [PathBuf] {
static ROOTS: OnceLock<Vec<PathBuf>> = OnceLock::new();
ROOTS.get_or_init(|| {
let var = std::env::var_os(SOURCE_ROOT_VAR).map(PathBuf::from);
let cwd = std::env::current_dir().ok();
let parents = cwd
.iter()
.flat_map(|cwd| cwd.ancestors().map(Path::to_path_buf));
var.into_iter().chain(parents).collect()
})
}
fn find_root<'a>(
path: &str,
md5: Option<&str>,
roots: impl IntoIterator<Item = &'a PathBuf>,
) -> Option<PathBuf> {
roots
.into_iter()
.find(|root| {
let file = root.join(path);
match md5 {
None => file.is_file(),
Some(md5) => std::fs::read(&file).is_ok_and(|bytes| md5_hex(&bytes) == md5),
}
})
.cloned()
}
fn source_cache() -> Option<&'static Path> {
use crate::config::{CubeClRuntimeConfig, RuntimeConfig};
static CACHE: OnceLock<Option<PathBuf>> = OnceLock::new();
CACHE
.get_or_init(|| CubeClRuntimeConfig::get().compilation.source_cache.clone())
.as_deref()
}
fn cached_root<'a>(
cache: &Path,
texts: impl IntoIterator<Item = (&'a str, &'a str, &'a Arc<str>)>,
) -> Option<PathBuf> {
type Trees = HashMap<(PathBuf, String), Option<PathBuf>>;
static TREES: OnceLock<Mutex<Trees>> = OnceLock::new();
let mut texts: Vec<_> = texts
.into_iter()
.filter(|(path, ..)| is_under(Path::new(path)))
.collect();
if texts.is_empty() {
return None;
}
texts.sort_unstable_by_key(|(path, ..)| *path);
let tree = md5_hex(texts.iter().fold(String::new(), |mut key, (path, _, md5)| {
let _ = writeln!(key, "{path}\0{md5}");
key
}));
TREES
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry((cache.to_path_buf(), tree))
.or_insert_with_key(|(cache, tree)| {
let root = cache.join(tree);
let written = texts
.iter()
.filter(|(path, text, _)| write_once(&root.join(path), text))
.count();
(written > 0).then_some(root)
})
.clone()
}
fn is_under(path: &Path) -> bool {
path.components().all(|c| matches!(c, Component::Normal(_)))
&& path.components().next().is_some()
}
fn write_once(file: &Path, text: &str) -> bool {
if file.is_file() {
return true;
}
let mut temporary = std::ffi::OsString::from(file);
temporary.push(alloc::format!(".{}.tmp", std::process::id()));
let result = file
.parent()
.map_or(Ok(()), std::fs::create_dir_all)
.and_then(|()| std::fs::write(&temporary, text))
.and_then(|()| std::fs::rename(&temporary, file));
if let Err(err) = result {
let _ = std::fs::remove_file(&temporary);
log::warn!("The source cache cannot write {}: {err}", file.display());
}
file.is_file()
}
fn source_md5(text: &'static str) -> Arc<str> {
type Md5s = HashMap<(usize, usize), Arc<str>>;
static MD5S: OnceLock<Mutex<Md5s>> = OnceLock::new();
MD5S.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry((text.as_ptr().addr(), text.len()))
.or_insert_with(|| md5_hex(text).into())
.clone()
}
pub fn md5_hex(text: impl AsRef<[u8]>) -> String {
Md5::digest(text)
.iter()
.fold(String::with_capacity(32), |mut hex, byte| {
let _ = write!(hex, "{byte:02x}");
hex
})
}
#[cfg(test)]
mod tests {
use super::*;
fn temporary(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(alloc::format!(
"cubecl-debug-source-{}-{name}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn tree(name: &str, text: &str) -> PathBuf {
let root = temporary(name);
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/k.rs"), text).unwrap();
root
}
#[test]
fn the_first_root_that_has_the_file() {
let root = tree("first", "fn k() {}");
let roots = [root.join("src"), root.clone(), PathBuf::from("/")];
assert_eq!(find_root("src/k.rs", None, &roots), Some(root.clone()));
assert_eq!(find_root("src/none.rs", None, &roots), None);
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn a_file_with_a_different_text_is_not_the_source() {
let old = tree("old", "fn k() {}");
let new = tree("new", "fn k() { changed }");
let md5 = md5_hex("fn k() {}");
let roots = [new.clone(), old.clone()];
assert_eq!(find_root("src/k.rs", Some(&md5), &roots), Some(old.clone()));
std::fs::remove_dir_all(old).unwrap();
std::fs::remove_dir_all(new).unwrap();
}
#[test]
fn an_absolute_path_has_no_root() {
assert_eq!(source_root([("/abs/k.rs", None)]), None);
}
#[test]
fn the_cache_has_the_texts_under_one_directory() {
let cache = temporary("cache");
let (k, f) = ("fn k() {}", "fn f() {}");
let (k_md5, f_md5): (Arc<str>, Arc<str>) = (md5_hex(k).into(), md5_hex(f).into());
let files = [("src/k.rs", k, &k_md5), ("lib/f.rs", f, &f_md5)];
let root = cached_root(&cache, files).expect("a directory");
assert!(root.starts_with(&cache), "{}", root.display());
assert_eq!(std::fs::read_to_string(root.join("src/k.rs")).unwrap(), k);
assert_eq!(std::fs::read_to_string(root.join("lib/f.rs")).unwrap(), f);
let [k_file, f_file] = files;
assert_eq!(cached_root(&cache, [f_file, k_file]), Some(root.clone()));
let changed = "fn k() { changed }";
let changed_md5: Arc<str> = md5_hex(changed).into();
let other = cached_root(&cache, [("src/k.rs", changed, &changed_md5)]);
assert!(other.is_some_and(|other| other != root));
std::fs::remove_dir_all(cache).unwrap();
}
#[test]
fn the_cache_writes_only_under_itself() {
let cache = temporary("escape");
let text = "fn k() {}";
let md5: Arc<str> = md5_hex(text).into();
for path in ["../k.rs", "/k.rs", "./k.rs", ""] {
assert_eq!(cached_root(&cache.join("c"), [(path, text, &md5)]), None);
}
assert!(!cache.join("k.rs").exists());
assert!(!cache.join("c").exists());
std::fs::remove_dir_all(cache).unwrap();
}
}