1use alloc::{
16 string::{String, ToString},
17 sync::Arc,
18 vec::Vec,
19};
20use core::{fmt::Write, hash::BuildHasher};
21use cubecl_ir::{debug::DebugState, settings::DebugInfo};
22use md5::{Digest, Md5};
23use std::{
24 collections::HashMap,
25 path::{Component, Path, PathBuf},
26 sync::{Mutex, OnceLock, PoisonError},
27};
28
29pub const SOURCE_ROOT_VAR: &str = "CUBECL_SOURCE_ROOT";
31
32#[must_use]
35pub fn source_md5s(debug: &DebugState, level: DebugInfo) -> HashMap<&str, Arc<str>> {
36 match level {
37 DebugInfo::Full => debug
38 .sources()
39 .iter()
40 .map(|(path, text)| (path.as_str(), source_md5(text)))
41 .collect(),
42 _ => HashMap::new(),
43 }
44}
45
46#[must_use]
55pub fn kernel_source_root<S: BuildHasher>(
56 debug: &DebugState,
57 md5s: &HashMap<&str, Arc<str>, S>,
58) -> Option<String> {
59 let files = debug
60 .files()
61 .iter()
62 .map(|path| (path.as_str(), md5s.get(path.as_str()).map(AsRef::as_ref)));
63 source_root(files)
64 .or_else(|| {
65 let texts = debug
66 .sources()
67 .iter()
68 .filter_map(|(path, text)| Some((path.as_str(), *text, md5s.get(path.as_str())?)));
69 cached_root(source_cache()?, texts)
70 })
71 .and_then(|root| root.to_str().map(str::to_string))
72}
73
74fn source_root<'a>(files: impl IntoIterator<Item = (&'a str, Option<&'a str>)>) -> Option<PathBuf> {
77 type Found = HashMap<(String, Option<String>), Option<PathBuf>>;
79 static FOUND: OnceLock<Mutex<Found>> = OnceLock::new();
80 let found = FOUND.get_or_init(Mutex::default);
81
82 files
83 .into_iter()
84 .filter(|(path, _)| Path::new(path).is_relative())
85 .find_map(|(path, md5)| {
86 let key = (path.to_string(), md5.map(str::to_string));
87 found
88 .lock()
89 .unwrap_or_else(PoisonError::into_inner)
90 .entry(key)
91 .or_insert_with(|| find_root(path, md5, search_roots()))
92 .clone()
93 })
94}
95
96fn search_roots() -> &'static [PathBuf] {
98 static ROOTS: OnceLock<Vec<PathBuf>> = OnceLock::new();
99 ROOTS.get_or_init(|| {
100 let var = std::env::var_os(SOURCE_ROOT_VAR).map(PathBuf::from);
101 let cwd = std::env::current_dir().ok();
102 let parents = cwd
103 .iter()
104 .flat_map(|cwd| cwd.ancestors().map(Path::to_path_buf));
105 var.into_iter().chain(parents).collect()
106 })
107}
108
109fn find_root<'a>(
111 path: &str,
112 md5: Option<&str>,
113 roots: impl IntoIterator<Item = &'a PathBuf>,
114) -> Option<PathBuf> {
115 roots
116 .into_iter()
117 .find(|root| {
118 let file = root.join(path);
119 match md5 {
120 None => file.is_file(),
121 Some(md5) => std::fs::read(&file).is_ok_and(|bytes| md5_hex(&bytes) == md5),
122 }
123 })
124 .cloned()
125}
126
127fn source_cache() -> Option<&'static Path> {
129 use crate::config::{CubeClRuntimeConfig, RuntimeConfig};
130
131 static CACHE: OnceLock<Option<PathBuf>> = OnceLock::new();
132 CACHE
133 .get_or_init(|| CubeClRuntimeConfig::get().compilation.source_cache.clone())
134 .as_deref()
135}
136
137fn cached_root<'a>(
144 cache: &Path,
145 texts: impl IntoIterator<Item = (&'a str, &'a str, &'a Arc<str>)>,
146) -> Option<PathBuf> {
147 type Trees = HashMap<(PathBuf, String), Option<PathBuf>>;
149 static TREES: OnceLock<Mutex<Trees>> = OnceLock::new();
150
151 let mut texts: Vec<_> = texts
152 .into_iter()
153 .filter(|(path, ..)| is_under(Path::new(path)))
154 .collect();
155 if texts.is_empty() {
156 return None;
157 }
158 texts.sort_unstable_by_key(|(path, ..)| *path);
159 let tree = md5_hex(texts.iter().fold(String::new(), |mut key, (path, _, md5)| {
160 let _ = writeln!(key, "{path}\0{md5}");
161 key
162 }));
163 TREES
164 .get_or_init(Mutex::default)
165 .lock()
166 .unwrap_or_else(PoisonError::into_inner)
167 .entry((cache.to_path_buf(), tree))
168 .or_insert_with_key(|(cache, tree)| {
169 let root = cache.join(tree);
170 let written = texts
171 .iter()
172 .filter(|(path, text, _)| write_once(&root.join(path), text))
173 .count();
174 (written > 0).then_some(root)
175 })
176 .clone()
177}
178
179fn is_under(path: &Path) -> bool {
181 path.components().all(|c| matches!(c, Component::Normal(_)))
182 && path.components().next().is_some()
183}
184
185fn write_once(file: &Path, text: &str) -> bool {
188 if file.is_file() {
189 return true;
190 }
191 let mut temporary = std::ffi::OsString::from(file);
192 temporary.push(alloc::format!(".{}.tmp", std::process::id()));
193 let result = file
194 .parent()
195 .map_or(Ok(()), std::fs::create_dir_all)
196 .and_then(|()| std::fs::write(&temporary, text))
197 .and_then(|()| std::fs::rename(&temporary, file));
198 if let Err(err) = result {
199 let _ = std::fs::remove_file(&temporary);
200 log::warn!("The source cache cannot write {}: {err}", file.display());
201 }
202 file.is_file()
203}
204
205fn source_md5(text: &'static str) -> Arc<str> {
211 type Md5s = HashMap<(usize, usize), Arc<str>>;
213 static MD5S: OnceLock<Mutex<Md5s>> = OnceLock::new();
214 MD5S.get_or_init(Mutex::default)
215 .lock()
216 .unwrap_or_else(PoisonError::into_inner)
217 .entry((text.as_ptr().addr(), text.len()))
218 .or_insert_with(|| md5_hex(text).into())
219 .clone()
220}
221
222pub fn md5_hex(text: impl AsRef<[u8]>) -> String {
224 Md5::digest(text)
225 .iter()
226 .fold(String::with_capacity(32), |mut hex, byte| {
227 let _ = write!(hex, "{byte:02x}");
228 hex
229 })
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn temporary(name: &str) -> PathBuf {
238 let dir = std::env::temp_dir().join(alloc::format!(
239 "cubecl-debug-source-{}-{name}",
240 std::process::id()
241 ));
242 let _ = std::fs::remove_dir_all(&dir);
243 std::fs::create_dir_all(&dir).unwrap();
244 dir
245 }
246
247 fn tree(name: &str, text: &str) -> PathBuf {
249 let root = temporary(name);
250 std::fs::create_dir_all(root.join("src")).unwrap();
251 std::fs::write(root.join("src/k.rs"), text).unwrap();
252 root
253 }
254
255 #[test]
256 fn the_first_root_that_has_the_file() {
257 let root = tree("first", "fn k() {}");
258 let roots = [root.join("src"), root.clone(), PathBuf::from("/")];
259 assert_eq!(find_root("src/k.rs", None, &roots), Some(root.clone()));
260 assert_eq!(find_root("src/none.rs", None, &roots), None);
261 std::fs::remove_dir_all(root).unwrap();
262 }
263
264 #[test]
266 fn a_file_with_a_different_text_is_not_the_source() {
267 let old = tree("old", "fn k() {}");
268 let new = tree("new", "fn k() { changed }");
269 let md5 = md5_hex("fn k() {}");
270 let roots = [new.clone(), old.clone()];
271 assert_eq!(find_root("src/k.rs", Some(&md5), &roots), Some(old.clone()));
272 std::fs::remove_dir_all(old).unwrap();
273 std::fs::remove_dir_all(new).unwrap();
274 }
275
276 #[test]
279 fn an_absolute_path_has_no_root() {
280 assert_eq!(source_root([("/abs/k.rs", None)]), None);
281 }
282
283 #[test]
286 fn the_cache_has_the_texts_under_one_directory() {
287 let cache = temporary("cache");
288 let (k, f) = ("fn k() {}", "fn f() {}");
289 let (k_md5, f_md5): (Arc<str>, Arc<str>) = (md5_hex(k).into(), md5_hex(f).into());
290 let files = [("src/k.rs", k, &k_md5), ("lib/f.rs", f, &f_md5)];
291
292 let root = cached_root(&cache, files).expect("a directory");
293 assert!(root.starts_with(&cache), "{}", root.display());
294 assert_eq!(std::fs::read_to_string(root.join("src/k.rs")).unwrap(), k);
295 assert_eq!(std::fs::read_to_string(root.join("lib/f.rs")).unwrap(), f);
296 let [k_file, f_file] = files;
297 assert_eq!(cached_root(&cache, [f_file, k_file]), Some(root.clone()));
298
299 let changed = "fn k() { changed }";
300 let changed_md5: Arc<str> = md5_hex(changed).into();
301 let other = cached_root(&cache, [("src/k.rs", changed, &changed_md5)]);
302 assert!(other.is_some_and(|other| other != root));
303 std::fs::remove_dir_all(cache).unwrap();
304 }
305
306 #[test]
308 fn the_cache_writes_only_under_itself() {
309 let cache = temporary("escape");
310 let text = "fn k() {}";
311 let md5: Arc<str> = md5_hex(text).into();
312 for path in ["../k.rs", "/k.rs", "./k.rs", ""] {
313 assert_eq!(cached_root(&cache.join("c"), [(path, text, &md5)]), None);
314 }
315 assert!(!cache.join("k.rs").exists());
316 assert!(!cache.join("c").exists());
317 std::fs::remove_dir_all(cache).unwrap();
318 }
319}