//! Mapa **driver → símbolo de Kconfig**, leído de los Makefiles de kbuild. //! //! Es la pieza que le faltaba al gate de no-regresión (#6 del handoff). El kernel sabe qué driver //! tiene bindeado cada dispositivo (`/sys/bus/*/devices/*/driver`), pero **no** dice de qué //! `CONFIG_*` salió: esa relación sólo existe en los Makefiles, en la forma //! //! ```make //! obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o //! ``` //! //! ## Dos trampas de nombres //! 1. **El módulo cargado usa `_` donde el fichero usa `-`**: `snd-hda-intel.o` se ve en `/sys` como //! `snd_hda_intel`. Se indexa normalizado, o el gate no encontraría nada y diría que todo está //! bien — que es el peor resultado posible para un portón. //! 2. **Un módulo puede salir de más de un símbolo** (se declara en varios Makefiles o bajo varias //! condiciones). Se guardan todos: el driver sobrevive si sobrevive **cualquiera** de ellos. //! //! ## Lo que este mapa NO sabe //! `obj-y += foo.o` (built-in incondicional) no lleva símbolo: ese driver está siempre y no hay //! nada que proteger. Y hay drivers que ningún `obj-$(CONFIG_…)` nombra — los que el mapa no //! cubre se reportan como **hueco**, nunca como «está bien». Un gate que calla lo que no pudo //! comprobar no es un gate. use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; /// `driver normalizado → símbolos que lo construyen`. #[derive(Debug, Clone, Default)] pub struct ModMap { pub by_module: BTreeMap>, /// Cuántos Makefiles se leyeron. pub files: usize, } impl ModMap { /// Recorre el árbol buscando `Makefile*` y extrae las reglas `obj-$(CONFIG_X) += y.o`. pub fn parse(root: &Path) -> std::io::Result { let mut m = ModMap::default(); walk(root, &mut m)?; Ok(m) } /// Símbolos que construyen este driver. Acepta el nombre tal cual sale de `/sys` (con `_`). pub fn symbols_for(&self, driver: &str) -> Option<&BTreeSet> { self.by_module.get(&normalize(driver)) } pub fn len(&self) -> usize { self.by_module.len() } pub fn is_empty(&self) -> bool { self.by_module.is_empty() } } fn walk(dir: &Path, m: &mut ModMap) -> std::io::Result<()> { let Ok(entries) = std::fs::read_dir(dir) else { return Ok(()); }; for e in entries.flatten() { let p = e.path(); if p.is_dir() { walk(&p, m)?; continue; } let name = e.file_name(); let name = name.to_string_lossy(); if !name.starts_with("Makefile") && !name.starts_with("Kbuild") { continue; } if let Ok(text) = std::fs::read_to_string(&p) { m.files += 1; parse_makefile(&text, m); } } Ok(()) } fn parse_makefile(text: &str, m: &mut ModMap) { for line in joined_lines(text) { let t = line.trim(); let Some(rest) = t.strip_prefix("obj-$(CONFIG_") else { continue; }; let Some(close) = rest.find(')') else { continue }; let symbol = &rest[..close]; if symbol.is_empty() || !symbol.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') { continue; } // `+=`, `:=` o `=`. let after = &rest[close + 1..]; let Some(eq) = after.find('=') else { continue }; let targets = &after[eq + 1..]; for tok in targets.split_whitespace() { // Los directorios (`foo/`) los cubre el Makefile de dentro. let Some(obj) = tok.strip_suffix(".o") else { continue; }; // `subdir/mod.o` — el nombre del módulo es el último componente. let obj = obj.rsplit('/').next().unwrap_or(obj); if obj.is_empty() || obj.contains('$') { continue; } m.by_module .entry(normalize(obj)) .or_default() .insert(symbol.to_string()); } } } /// Une las continuaciones `\` de make. fn joined_lines(text: &str) -> Vec { let mut out = Vec::new(); let mut acc = String::new(); for line in text.lines() { match line.strip_suffix('\\') { Some(head) => { acc.push_str(head); acc.push(' '); } None => { acc.push_str(line); out.push(std::mem::take(&mut acc)); } } } if !acc.is_empty() { out.push(acc); } out } /// El nombre con el que el módulo aparece en `/sys`: guiones a guiones bajos. fn normalize(s: &str) -> String { s.replace('-', "_") } /// Alimentar un `ModMap` desde texto, para los tests de otros módulos. #[cfg(test)] pub(crate) mod tests_support { pub fn feed(m: &mut super::ModMap, texto: &str) { super::parse_makefile(texto, m); } } #[cfg(test)] mod tests { use super::*; #[test] fn extrae_las_tres_formas_de_asignacion() { let mut m = ModMap::default(); parse_makefile( "obj-$(CONFIG_SND_HDA) := snd-hda-codec.o\n\ obj-$(CONFIG_E1000) += e1000.o\n\ obj-$(CONFIG_FOO)=foo.o\n", &mut m, ); assert!(m.symbols_for("snd_hda_codec").unwrap().contains("SND_HDA")); assert!(m.symbols_for("e1000").unwrap().contains("E1000")); assert!(m.symbols_for("foo").unwrap().contains("FOO")); } #[test] fn el_guion_bajo_de_sys_encuentra_al_guion_del_fichero() { let mut m = ModMap::default(); parse_makefile("obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o\n", &mut m); // Así es como sale de /sys/bus/pci/devices/*/driver. assert!(m.symbols_for("snd_hda_intel").is_some()); } #[test] fn un_modulo_puede_venir_de_varios_simbolos() { let mut m = ModMap::default(); parse_makefile("obj-$(CONFIG_A) += x.o\n", &mut m); parse_makefile("obj-$(CONFIG_B) += x.o\n", &mut m); let s = m.symbols_for("x").unwrap(); assert_eq!(s.len(), 2); } #[test] fn los_directorios_y_los_obj_y_no_entran() { let mut m = ModMap::default(); parse_makefile( "obj-$(CONFIG_NET) += core/\nobj-y += siempre.o\nobj-$(CONFIG_X) += $(VAR).o\n", &mut m, ); assert!(m.is_empty(), "{:?}", m.by_module); } #[test] fn la_continuacion_de_barra_se_une() { let mut m = ModMap::default(); parse_makefile("obj-$(CONFIG_X) += a.o \\\n\tb.o\n", &mut m); assert!(m.symbols_for("a").is_some()); assert!(m.symbols_for("b").is_some()); } #[test] fn el_modulo_en_subdirectorio_se_nombra_por_el_ultimo_componente() { let mut m = ModMap::default(); parse_makefile("obj-$(CONFIG_X) += sub/dir/mod.o\n", &mut m); assert!(m.symbols_for("mod").is_some()); } }