1use std::collections::BTreeMap;
31use std::path::{Path, PathBuf};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum LayoutError {
35 UnknownHardware(String),
37 Manifest { path: PathBuf, message: String },
40 UnknownVendor { hardware: String, vendor: String },
42 Inherits { hardware: String, message: String },
46 Redirect { model_dir: PathBuf, message: String },
49 UnknownModel(PathBuf),
51 NoKernelDirectory(String),
53 Symlink(PathBuf),
57 Use {
59 manifest: PathBuf,
60 entry: String,
61 message: String,
62 },
63 UseCollides {
66 manifest: PathBuf,
67 name: String,
68 existing: PathBuf,
69 },
70 UndeclaredShadow {
73 name: String,
74 winner: PathBuf,
75 loser: PathBuf,
76 manifest: PathBuf,
77 },
78 DeadShadow { manifest: PathBuf, stem: String },
80 SelfShadow { name: String, source: PathBuf },
83 RetiredKey { path: PathBuf, key: String },
86}
87
88impl std::fmt::Display for LayoutError {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Self::UnknownHardware(hw) => write!(f, "kernels/{hw}: no HARDWARE.toml"),
92 Self::Manifest { path, message } => write!(f, "{}: {message}", path.display()),
93 Self::UnknownVendor { hardware, vendor } => write!(
94 f,
95 "kernels/{hardware}/HARDWARE.toml: vendor {vendor:?} has no kernel source extension"
96 ),
97 Self::Inherits { hardware, message } => {
98 write!(
99 f,
100 "kernels/{hardware}/HARDWARE.toml: [hardware] inherits: {message}"
101 )
102 }
103 Self::Redirect { model_dir, message } => {
104 write!(
105 f,
106 "{}/MODEL.toml: [model] kernel_source: {message}",
107 model_dir.display()
108 )
109 }
110 Self::UnknownModel(dir) => write!(f, "{}: no such model directory", dir.display()),
111 Self::NoKernelDirectory(t) => write!(f, "{t}: no kernel directory in any layer"),
112 Self::Symlink(p) => write!(
113 f,
114 "{}: is a symlink — symlinks are not a sharing mechanism under kernels/; \
115 declare the file in [sources] use of the KERNEL.toml beside it",
116 p.display()
117 ),
118 Self::Use {
119 manifest,
120 entry,
121 message,
122 } => {
123 write!(
124 f,
125 "{}: [sources] use {entry:?}: {message}",
126 manifest.display()
127 )
128 }
129 Self::UseCollides {
130 manifest,
131 name,
132 existing,
133 } => write!(
134 f,
135 "{}: [sources] use brings {name} but this layer already holds {}",
136 manifest.display(),
137 existing.display()
138 ),
139 Self::UndeclaredShadow {
140 name,
141 winner,
142 loser,
143 manifest,
144 } => write!(
145 f,
146 "{} shadows {} and {} does not declare it: add `[shadow] {} = \"<reason>\"`",
147 winner.display(),
148 loser.display(),
149 manifest.display(),
150 stem_of(name)
151 ),
152 Self::DeadShadow { manifest, stem } => write!(
153 f,
154 "{}: [shadow] {stem} is declared but this directory's {stem} shadows nothing",
155 manifest.display()
156 ),
157 Self::SelfShadow { name, source } => write!(
158 f,
159 "{name} resolves to {} in two layers — a use of a file the target already reaches",
160 source.display()
161 ),
162 Self::RetiredKey { path, key } => write!(
163 f,
164 "{}: `{key}` is retired; an overlay's own files need no inventory, and one that \
165 replaces a parent's file declares it in [shadow] of its KERNEL.toml",
166 path.display()
167 ),
168 }
169 }
170}
171
172impl std::error::Error for LayoutError {}
173
174pub fn stem_of(name: &str) -> &str {
177 name.rsplit_once('.').map_or(name, |(stem, _)| stem)
178}
179
180pub fn source_ext(vendor: &str) -> Option<&'static str> {
183 match vendor {
184 "nvidia" | "cuda" | "amd" | "rocm" | "scale" | "hip" => Some("cu"),
185 "apple" | "metal" => Some("metal"),
186 _ => None,
187 }
188}
189
190pub const HEADER_EXTS: &[&str] = &["cuh", "h"];
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct Hardware {
197 pub name: String,
198 pub vendor: String,
199 pub arch: Option<String>,
201 pub inherits: Option<String>,
202 pub source_ext: &'static str,
203}
204
205fn read_toml(path: &Path) -> Result<toml::Value, LayoutError> {
206 let text = std::fs::read_to_string(path).map_err(|e| LayoutError::Manifest {
207 path: path.to_path_buf(),
208 message: e.to_string(),
209 })?;
210 toml::from_str(&text).map_err(|e| LayoutError::Manifest {
211 path: path.to_path_buf(),
212 message: format!("bad TOML: {e}"),
213 })
214}
215
216fn string_at(value: &toml::Value, path: &Path, key: &str) -> Result<Option<String>, LayoutError> {
217 let (table, field) = key.split_once('.').unwrap_or(("", key));
218 let holder = if table.is_empty() {
219 Some(value)
220 } else {
221 value.get(table)
222 };
223 match holder.and_then(|t| t.get(field)) {
224 None => Ok(None),
225 Some(v) => v
226 .as_str()
227 .map(|s| Some(s.to_string()))
228 .ok_or_else(|| LayoutError::Manifest {
229 path: path.to_path_buf(),
230 message: format!("{key} must be a string"),
231 }),
232 }
233}
234
235pub(crate) fn hardware_raw(kernels: &Path, hw: &str) -> Result<Hardware, LayoutError> {
238 let path = kernels.join(hw).join("HARDWARE.toml");
239 if !path.is_file() {
240 return Err(LayoutError::UnknownHardware(hw.to_string()));
241 }
242 let value = read_toml(&path)?;
243 if value
244 .get("kernels")
245 .and_then(|k| k.get("overrides"))
246 .is_some()
247 {
248 return Err(LayoutError::RetiredKey {
249 path,
250 key: "[kernels] overrides".into(),
251 });
252 }
253 let vendor = string_at(&value, &path, "hardware.vendor")?.unwrap_or_else(|| "nvidia".into());
254 let source_ext = source_ext(&vendor).ok_or_else(|| LayoutError::UnknownVendor {
255 hardware: hw.to_string(),
256 vendor: vendor.clone(),
257 })?;
258 Ok(Hardware {
259 name: hw.to_string(),
260 vendor,
261 arch: string_at(&value, &path, "hardware.arch")?,
262 inherits: string_at(&value, &path, "hardware.inherits")?,
263 source_ext,
264 })
265}
266
267pub fn hardware(kernels: &Path, hw: &str) -> Result<Hardware, LayoutError> {
271 let own = hardware_raw(kernels, hw)?;
272 if let Some(parent) = &own.inherits {
273 let fail = |message: String| LayoutError::Inherits {
274 hardware: hw.to_string(),
275 message,
276 };
277 if parent == hw {
278 return Err(fail("a tree cannot inherit itself".into()));
279 }
280 let parent_hw =
281 hardware_raw(kernels, parent).map_err(|e| fail(format!("parent {parent:?}: {e}")))?;
282 if let Some(grand) = parent_hw.inherits {
283 return Err(fail(format!(
284 "{parent:?} itself inherits {grand:?} — chains are refused; inherit the tree that owns the sources"
285 )));
286 }
287 if parent_hw.source_ext != own.source_ext {
288 return Err(fail(format!(
289 "{parent:?} compiles .{} sources, this tree .{}",
290 parent_hw.source_ext, own.source_ext
291 )));
292 }
293 }
294 Ok(own)
295}
296
297pub fn kernel_source_dir(model_dir: &Path) -> Result<PathBuf, LayoutError> {
303 let Some(src) = kernel_source(model_dir)? else {
304 return Ok(model_dir.to_path_buf());
305 };
306 let fail = |message: String| LayoutError::Redirect {
307 model_dir: model_dir.to_path_buf(),
308 message,
309 };
310 let hw_dir = model_dir
311 .parent()
312 .ok_or_else(|| fail("model dir has no parent".into()))?;
313 let src_dir = hw_dir.join(&src);
314 if !src_dir.is_dir() || !src_dir.join("MODEL.toml").is_file() {
315 return Err(fail(format!(
316 "{src:?} does not name a kernel target directory under {}",
317 hw_dir.display()
318 )));
319 }
320 if kernel_source(&src_dir)?.is_some() {
321 return Err(fail(format!(
322 "{src:?} itself redirects — chains are not allowed; point at the target that owns the sources"
323 )));
324 }
325 Ok(src_dir)
326}
327
328fn kernel_source(model_dir: &Path) -> Result<Option<String>, LayoutError> {
329 let path = model_dir.join("MODEL.toml");
330 if !path.is_file() {
331 return Ok(None);
332 }
333 let value = read_toml(&path)?;
334 let src = string_at(&value, &path, "model.kernel_source")?;
335 if let Some(s) = &src
336 && s.trim().is_empty()
337 {
338 return Err(LayoutError::Manifest {
339 path,
340 message: "[model] kernel_source must name a kernel target directory".into(),
341 });
342 }
343 Ok(src)
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Default)]
348pub struct KernelManifest {
349 pub path: PathBuf,
350 pub uses: Vec<String>,
352 pub shadow: BTreeMap<String, String>,
354}
355
356pub fn kernel_manifest(dir: &Path) -> Result<Option<KernelManifest>, LayoutError> {
358 let path = dir.join("KERNEL.toml");
359 if !path.is_file() {
360 return Ok(None);
361 }
362 let value = read_toml(&path)?;
363 let fail = |message: String| LayoutError::Manifest {
364 path: path.clone(),
365 message,
366 };
367 let mut uses = Vec::new();
368 if let Some(sources) = value.get("sources") {
369 let table = sources
370 .as_table()
371 .ok_or_else(|| fail("[sources] must be a table".into()))?;
372 for key in table.keys() {
373 if key != "use" {
374 return Err(fail(format!("[sources] has no key `{key}`; only `use`")));
375 }
376 }
377 if let Some(list) = table.get("use") {
378 let arr = list
379 .as_array()
380 .ok_or_else(|| fail("[sources] use must be an array of paths".into()))?;
381 for v in arr {
382 let s = v
383 .as_str()
384 .ok_or_else(|| fail("[sources] use entries must be strings".into()))?;
385 uses.push(s.to_string());
386 }
387 }
388 }
389 let mut shadow = BTreeMap::new();
390 if let Some(decl) = value.get("shadow") {
391 let table = decl
392 .as_table()
393 .ok_or_else(|| fail("[shadow] must be a table of `stem = \"reason\"`".into()))?;
394 for (stem, reason) in table {
395 let reason = reason
396 .as_str()
397 .ok_or_else(|| fail(format!("[shadow] {stem} must be a reason string")))?;
398 if reason.trim().is_empty() {
399 return Err(fail(format!("[shadow] {stem} needs a stated reason")));
400 }
401 shadow.insert(stem.clone(), reason.to_string());
402 }
403 }
404 Ok(Some(KernelManifest { path, uses, shadow }))
405}