sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
⎇ d797fc472d7c648b433d956ade3fe7713e4e93e4
crates/hammer-core/src/kernel/modmap.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
kernel: el gate contra un .config producido — ve los huecos que la receta base ya traía
1//! Mapa **driver → símbolo de Kconfig**, leído de los Makefiles de kbuild.
2//!
3//! Es la pieza que le faltaba al gate de no-regresión (#6 del handoff). El kernel sabe qué driver
4//! tiene bindeado cada dispositivo (`/sys/bus/*/devices/*/driver`), pero **no** dice de qué
5//! `CONFIG_*` salió: esa relación sólo existe en los Makefiles, en la forma
6//!
7//! ```make
8//! obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o
9//! ```
10//!
11//! ## Dos trampas de nombres
12//! 1. **El módulo cargado usa `_` donde el fichero usa `-`**: `snd-hda-intel.o` se ve en `/sys` como
13//! `snd_hda_intel`. Se indexa normalizado, o el gate no encontraría nada y diría que todo está
14//! bien — que es el peor resultado posible para un portón.
15//! 2. **Un módulo puede salir de más de un símbolo** (se declara en varios Makefiles o bajo varias
16//! condiciones). Se guardan todos: el driver sobrevive si sobrevive **cualquiera** de ellos.
17//!
18//! ## Lo que este mapa NO sabe
19//! `obj-y += foo.o` (built-in incondicional) no lleva símbolo: ese driver está siempre y no hay
20//! nada que proteger. Y hay drivers que ningún `obj-$(CONFIG_…)` nombra — los que el mapa no
21//! cubre se reportan como **hueco**, nunca como «está bien». Un gate que calla lo que no pudo
22//! comprobar no es un gate.
23
24use std::collections::{BTreeMap, BTreeSet};
25use std::path::Path;
26
27/// `driver normalizado → símbolos que lo construyen`.
28#[derive(Debug, Clone, Default)]
29pub struct ModMap {
30 pub by_module: BTreeMap<String, BTreeSet<String>>,
31 /// Cuántos Makefiles se leyeron.
32 pub files: usize,
33}
34
35impl ModMap {
36 /// Recorre el árbol buscando `Makefile*` y extrae las reglas `obj-$(CONFIG_X) += y.o`.
37 pub fn parse(root: &Path) -> std::io::Result<ModMap> {
38 let mut m = ModMap::default();
39 walk(root, &mut m)?;
40 Ok(m)
41 }
42
43 /// Símbolos que construyen este driver. Acepta el nombre tal cual sale de `/sys` (con `_`).
44 pub fn symbols_for(&self, driver: &str) -> Option<&BTreeSet<String>> {
45 self.by_module.get(&normalize(driver))
46 }
47
48 pub fn len(&self) -> usize {
49 self.by_module.len()
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.by_module.is_empty()
54 }
55}
56
57fn walk(dir: &Path, m: &mut ModMap) -> std::io::Result<()> {
58 let Ok(entries) = std::fs::read_dir(dir) else {
59 return Ok(());
60 };
61 for e in entries.flatten() {
62 let p = e.path();
63 if p.is_dir() {
64 walk(&p, m)?;
65 continue;
66 }
67 let name = e.file_name();
68 let name = name.to_string_lossy();
69 if !name.starts_with("Makefile") && !name.starts_with("Kbuild") {
70 continue;
71 }
72 if let Ok(text) = std::fs::read_to_string(&p) {
73 m.files += 1;
74 parse_makefile(&text, m);
75 }
76 }
77 Ok(())
78}
79
80fn parse_makefile(text: &str, m: &mut ModMap) {
81 for line in joined_lines(text) {
82 let t = line.trim();
83 let Some(rest) = t.strip_prefix("obj-$(CONFIG_") else {
84 continue;
85 };
86 let Some(close) = rest.find(')') else { continue };
87 let symbol = &rest[..close];
88 if symbol.is_empty() || !symbol.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
89 continue;
90 }
91 // `+=`, `:=` o `=`.
92 let after = &rest[close + 1..];
93 let Some(eq) = after.find('=') else { continue };
94 let targets = &after[eq + 1..];
95 for tok in targets.split_whitespace() {
96 // Los directorios (`foo/`) los cubre el Makefile de dentro.
97 let Some(obj) = tok.strip_suffix(".o") else {
98 continue;
99 };
100 // `subdir/mod.o` — el nombre del módulo es el último componente.
101 let obj = obj.rsplit('/').next().unwrap_or(obj);
102 if obj.is_empty() || obj.contains('$') {
103 continue;
104 }
105 m.by_module
106 .entry(normalize(obj))
107 .or_default()
108 .insert(symbol.to_string());
109 }
110 }
111}
112
113/// Une las continuaciones `\` de make.
114fn joined_lines(text: &str) -> Vec<String> {
115 let mut out = Vec::new();
116 let mut acc = String::new();
117 for line in text.lines() {
118 match line.strip_suffix('\\') {
119 Some(head) => {
120 acc.push_str(head);
121 acc.push(' ');
122 }
123 None => {
124 acc.push_str(line);
125 out.push(std::mem::take(&mut acc));
126 }
127 }
128 }
129 if !acc.is_empty() {
130 out.push(acc);
131 }
132 out
133}
134
135/// El nombre con el que el módulo aparece en `/sys`: guiones a guiones bajos.
136fn normalize(s: &str) -> String {
137 s.replace('-', "_")
138}
139
140/// Alimentar un `ModMap` desde texto, para los tests de otros módulos.
141#[cfg(test)]
142pub(crate) mod tests_support {
143 pub fn feed(m: &mut super::ModMap, texto: &str) {
144 super::parse_makefile(texto, m);
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn extrae_las_tres_formas_de_asignacion() {
154 let mut m = ModMap::default();
155 parse_makefile(
156 "obj-$(CONFIG_SND_HDA) := snd-hda-codec.o\n\
157 obj-$(CONFIG_E1000) += e1000.o\n\
158 obj-$(CONFIG_FOO)=foo.o\n",
159 &mut m,
160 );
161 assert!(m.symbols_for("snd_hda_codec").unwrap().contains("SND_HDA"));
162 assert!(m.symbols_for("e1000").unwrap().contains("E1000"));
163 assert!(m.symbols_for("foo").unwrap().contains("FOO"));
164 }
165
166 #[test]
167 fn el_guion_bajo_de_sys_encuentra_al_guion_del_fichero() {
168 let mut m = ModMap::default();
169 parse_makefile("obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o\n", &mut m);
170 // Así es como sale de /sys/bus/pci/devices/*/driver.
171 assert!(m.symbols_for("snd_hda_intel").is_some());
172 }
173
174 #[test]
175 fn un_modulo_puede_venir_de_varios_simbolos() {
176 let mut m = ModMap::default();
177 parse_makefile("obj-$(CONFIG_A) += x.o\n", &mut m);
178 parse_makefile("obj-$(CONFIG_B) += x.o\n", &mut m);
179 let s = m.symbols_for("x").unwrap();
180 assert_eq!(s.len(), 2);
181 }
182
183 #[test]
184 fn los_directorios_y_los_obj_y_no_entran() {
185 let mut m = ModMap::default();
186 parse_makefile(
187 "obj-$(CONFIG_NET) += core/\nobj-y += siempre.o\nobj-$(CONFIG_X) += $(VAR).o\n",
188 &mut m,
189 );
190 assert!(m.is_empty(), "{:?}", m.by_module);
191 }
192
193 #[test]
194 fn la_continuacion_de_barra_se_une() {
195 let mut m = ModMap::default();
196 parse_makefile("obj-$(CONFIG_X) += a.o \\\n\tb.o\n", &mut m);
197 assert!(m.symbols_for("a").is_some());
198 assert!(m.symbols_for("b").is_some());
199 }
200
201 #[test]
202 fn el_modulo_en_subdirectorio_se_nombra_por_el_ultimo_componente() {
203 let mut m = ModMap::default();
204 parse_makefile("obj-$(CONFIG_X) += sub/dir/mod.o\n", &mut m);
205 assert!(m.symbols_for("mod").is_some());
206 }
207}