11__author__ = "desultory"
2- __version__ = "3.5 .0"
2+ __version__ = "4.1 .0"
33
44from pathlib import Path
55from platform import uname
6+ from re import search
67from struct import error as StructError
78from struct import unpack
89from subprocess import run
910from typing import Union
1011
1112from ugrd .exceptions import AutodetectError , ValidationError
12- from ugrd .kmod import (
13- BuiltinModuleError ,
14- DependencyResolutionError ,
15- IgnoredModuleError ,
16- MissingModuleError ,
17- )
13+ from ugrd .kmod import BuiltinModuleError , DependencyResolutionError , IgnoredModuleError , MissingModuleError
1814from zenlib .util import colorize as c_
1915from zenlib .util import contains , unset
2016
17+ _KMOD_ALIASES = {}
2118MODULE_METADATA_FILES = ["modules.order" , "modules.builtin" , "modules.builtin.modinfo" ]
2219
20+
2321def _normalize_kmod_name (self , module : Union [str , list ]) -> str :
2422 """Replaces -'s with _'s in a kernel module name.
2523 ignores modules defined in kmod_no_normalize.
@@ -34,6 +32,32 @@ def _normalize_kmod_name(self, module: Union[str, list]) -> str:
3432 return module .replace ("-" , "_" )
3533
3634
35+ def _normalize_kmod_alias (self , alias : str ) -> str :
36+ """Gets the base alias name from kmod alias info
37+ gets data after : and , if present.
38+ """
39+ if not alias :
40+ return ""
41+ alias = alias .split (":" , 1 )[- 1 ] # Strip bus type
42+ alias = alias .split ("," , 1 )[- 1 ]
43+ return _normalize_kmod_name (self , alias )
44+
45+
46+ def _resolve_kmod_alias (self , module : str ) -> str :
47+ """Attempts to resolve a kernel module alias to a module name.
48+ Uses /lib/modules/<kernel_version>/modules.alias to find the module name.
49+ normalizes - to _ then replaces _ with [_-] to allow for both _ and - in the module name.
50+ """
51+ module = module .replace ("-" , "_" )
52+ module = module .replace ("_" , "[_-]" ) # Allow for both _ and - in the module name
53+ for alias , kmod in _KMOD_ALIASES .items ():
54+ if search (module , alias ):
55+ self .logger .info (f"Resolved kernel module alias: { c_ (alias , 'blue' )} -> { c_ (kmod , 'cyan' )} " )
56+ return kmod
57+
58+ raise MissingModuleError (f"Failed to resolve kernel module alias: { module } " )
59+
60+
3761def _process_kernel_modules_multi (self , module : str ) -> None :
3862 """Adds kernel modules to self['kernel_modules']."""
3963 module = _normalize_kmod_name (self , module )
@@ -81,14 +105,22 @@ def _process__kmod_auto_multi(self, module: str) -> None:
81105 self ["_kmod_auto" ].append (module )
82106
83107
84- def _get_kmod_info (self , module : str ):
108+ def _get_kmod_info (self , module : str ) -> dict :
85109 """
86110 Runs modinfo on a kernel module, parses the output and stored the results in self['_kmod_modinfo'].
87111 !!! Should be run after metadata is processed so the kver is set properly !!!
112+
113+ Returns the module info as a dictionary with the following keys:
114+ - filename: The path to the module file.
115+ - depends: A list of module dependencies.
116+ - softdep: A list of soft dependencies.
117+ - firmware: A list of firmware files required by the module.
118+ Raises:
119+ DependencyResolutionError: If the modinfo command fails, returns no output, or the module name can't be resolved.
88120 """
89121 module = _normalize_kmod_name (self , module )
90122 if module in self ["_kmod_modinfo" ]:
91- return self . logger . debug ( "[%s] Module info already exists." % module )
123+ return module , self [ "_kmod_modinfo" ][ module ]
92124 args = ["modinfo" , module , "--set-version" , self ["kernel_version" ]]
93125
94126 try :
@@ -98,9 +130,15 @@ def _get_kmod_info(self, module: str):
98130 raise DependencyResolutionError ("[%s] Failed to run modinfo command: %s" % (module , " " .join (args ))) from e
99131
100132 if not cmd .stdout and cmd .stderr :
101- raise DependencyResolutionError ("[%s] Modinfo returned no output." % module )
133+ try :
134+ resolved_module = _resolve_kmod_alias (self , module )
135+ return _get_kmod_info (self , resolved_module )
136+ except MissingModuleError :
137+ raise DependencyResolutionError (
138+ "[%s] Modinfo returned no output and the alias name could no be resolved." % module
139+ )
102140
103- module_info = {}
141+ module_info = {"filename" : None , "depends" : [], "softdep" : [], "firmware" : [] }
104142 for line in cmd .stdout .decode ().split ("\n " ):
105143 line = line .strip ()
106144 if line .startswith ("filename:" ):
@@ -125,6 +163,51 @@ def _get_kmod_info(self, module: str):
125163
126164 self .logger .debug ("[%s] Module info: %s" % (module , module_info ))
127165 self ["_kmod_modinfo" ][module ] = module_info
166+ return module , module_info
167+
168+
169+ @unset ("no_kmod" , "no_kmod is enabled, skipping module alias enumeration." , log_level = 30 )
170+ def get_module_aliases (self ):
171+ """Processes the kernel module aliases from /lib/modules/<kernel_version>/modules.alias."""
172+ alias_file = Path ("/lib/modules" ) / self ["kernel_version" ] / "modules.alias"
173+ if not alias_file .exists ():
174+ self .logger .error (f"Kernel module alias file does not exist: { c_ (alias_file , 'red' , bold = True )} " )
175+ else :
176+ for line in alias_file .read_text ().splitlines ():
177+ _ , alias , module = line .strip ().split (" " , 2 )
178+ _KMOD_ALIASES [_normalize_kmod_alias (self , alias )] = _normalize_kmod_name (self , module )
179+
180+
181+ @unset ("no_kmod" , "no_kmod is enabled, skipping builtin module enumeration." , log_level = 30 )
182+ def get_builtin_module_info (self ) -> None :
183+ """Gets the kernel module aliases from /lib/modules/<kernel_version>/modules.builtin.modinfo.
184+ puts it in _kmod_modinfo.
185+ also populates the _KMOD_ALIASES global variable with the aliases.
186+ """
187+
188+ builtin_modinfo_file = Path ("/lib/modules" ) / self ["kernel_version" ] / "modules.builtin.modinfo"
189+ if not builtin_modinfo_file .exists ():
190+ self .logger .error (f"Builtin modinfo file does not exist: { c_ (builtin_modinfo_file , 'red' , bold = True )} " )
191+ else :
192+ for line in builtin_modinfo_file .read_bytes ().split (b"\x00 " ):
193+ """ Lines are in the format <name>.<parameter>=<value>"""
194+ line = line .decode ("utf-8" , errors = "ignore" ).strip ()
195+ if not line or "." not in line or "=" not in line :
196+ continue
197+ name , parameter = line .split ("." , 1 )
198+ name = _normalize_kmod_name (self , name )
199+ parameter , value = parameter .split ("=" , 1 )
200+ modinfo = self ["_kmod_modinfo" ].get (
201+ name , {"filename" : "(builtin)" , "depends" : [], "softdep" : [], "firmware" : []}
202+ )
203+ if parameter == "firmware" :
204+ modinfo ["firmware" ].append (value )
205+ elif parameter != "alias" :
206+ continue
207+
208+ alias = _normalize_kmod_alias (self , value )
209+ self ["_kmod_modinfo" ][name ] = modinfo
210+ _KMOD_ALIASES [alias ] = name # Store the alias in the global aliases dict
128211
129212
130213@contains ("kmod_autodetect_lspci" , "kmod_autodetect_lspci is not enabled, skipping." )
@@ -159,12 +242,14 @@ def _autodetect_modules_lsmod(self) -> None:
159242 modules = [line .split ()[0 ] for line in f .readlines ()]
160243
161244 if len (modules ) > 25 :
162- self .logger .warning (f"[{ len (modules )} ] More than 25 kernel modules were autodetected from the running kernel. If lsmod detection is required for your use case, please file a bug report so more appropriate detection methods can be implemented." )
245+ self .logger .warning (
246+ f"[{ len (modules )} ] More than 25 kernel modules were autodetected from the running kernel. If lsmod detection is required for your use case, please file a bug report so more appropriate detection methods can be implemented."
247+ )
163248 for module in modules :
164249 self ["_kmod_auto" ] = module .split ()[0 ]
165250
166251
167- @unset ("no_kmod" , "no_kmod is enabled, skipping." , log_level = 30 )
252+ @unset ("no_kmod" , "no_kmod is enabled, skipping module detection ." , log_level = 30 )
168253@contains ("hostonly" , "Skipping kmod autodetection, hostonly is disabled." , log_level = 30 )
169254def autodetect_modules (self ) -> None :
170255 """Autodetects kernel modules from lsmod and/or lspci -k."""
@@ -309,24 +394,24 @@ def _add_kmod_firmware(self, kmod: str) -> None:
309394
310395 Attempts to run even if no_kmod is set; this will not work if there are no kmods/no kernel version set
311396 """
312- kmod = _normalize_kmod_name ( self , kmod )
313-
314- if kmod not in self [ "_kmod_modinfo" ] :
397+ try :
398+ kmod , modinfo = _get_kmod_info ( self , kmod )
399+ except DependencyResolutionError as e :
315400 if self ["no_kmod" ]:
316401 return self .logger .warning (
317402 "[%s] Kernel module info for firmware detection does not exist, but no_kmod is set." % kmod
318403 )
319- raise DependencyResolutionError ("Kernel module info does not exist: %s" % kmod )
404+ raise DependencyResolutionError ("Kernel module info does not exist: %s" % kmod ) from e
320405
321- if self [ "_kmod_modinfo" ][ kmod ]. get ( " firmware") and not self ["kmod_pull_firmware" ]:
406+ if modinfo [ " firmware"] and not self ["kmod_pull_firmware" ]:
322407 # Log a warning if the kernel module has firmware files, but kmod_pull_firmware is not set
323408 self .logger .warning ("[%s] Kernel module has firmware files, but kmod_pull_firmware is not set." % kmod )
324409
325- if not self [ "_kmod_modinfo" ][ kmod ]. get ( " firmware") or not self .get ("kmod_pull_firmware" ):
410+ if not modinfo [ " firmware"] or not self .get ("kmod_pull_firmware" ):
326411 # No firmware files to add, or kmod_pull_firmware is not set
327412 return
328413
329- for firmware in self [ "_kmod_modinfo" ][ kmod ] ["firmware" ]:
414+ for firmware in modinfo ["firmware" ]:
330415 _add_firmware_dep (self , kmod , firmware )
331416
332417
@@ -356,17 +441,18 @@ def _process_kmod_dependencies(self, kmod: str, mod_tree=None) -> None:
356441
357442 Iterate over dependencies, adding them to kernel_mdules if they (or sub-dependencies) are not in the ignore list.
358443 If the dependency is already in the module tree, skip it to prevent infinite recursion.
444+
445+ returns the name of the kernel module (in case it was an alias) and the list of dependencies.
359446 """
360447 mod_tree = mod_tree or set ()
361- kmod = _normalize_kmod_name (self , kmod )
362- _get_kmod_info (self , kmod )
448+ kmod , modinfo = _get_kmod_info (self , kmod )
363449
364450 # Get kernel module dependencies, softedeps if not ignored
365451 dependencies = []
366- if harddeps := self [ "_kmod_modinfo" ][ kmod ]. get ( " depends") :
452+ if harddeps := modinfo [ " depends"] :
367453 dependencies += harddeps
368454
369- if sofdeps := self [ "_kmod_modinfo" ][ kmod ]. get ( " softdep") :
455+ if sofdeps := modinfo [ " softdep"] :
370456 if self .get ("kmod_ignore_softdeps" , False ):
371457 self .logger .warning ("[%s] Soft dependencies were detected, but are being ignored: %s" % (kmod , sofdeps ))
372458 else :
@@ -378,12 +464,11 @@ def _process_kmod_dependencies(self, kmod: str, mod_tree=None) -> None:
378464 if dependency in mod_tree :
379465 self .logger .debug ("[%s] Dependency is already in mod_tree: %s" % (kmod , dependency ))
380466 continue
467+ dependency , dep_modinfo = _get_kmod_info (self , dependency ) # Get modinfo for the dependency
381468 if dependency in self ["kmod_ignore" ]: # Don't add modules with ignored dependencies
382- _get_kmod_info (self , dependency ) # Make sure modinfo is queried in case it's built-in
383- if modinfo := self ["_kmod_modinfo" ].get (dependency ):
384- if modinfo ["filename" ] == "(builtin)" : # If it's ignored because builtin, that's fine
385- self .logger .debug ("[%s] Ignored dependency is a built-in module: %s" % (kmod , dependency ))
386- continue
469+ if dep_modinfo ["filename" ] == "(builtin)" :
470+ self .logger .debug ("[%s] Ignored dependency is a built-in module: %s" % (kmod , dependency ))
471+ continue
387472 # If modinfo doesn't exist, or it's not builtin, simply raise an ignored module error
388473 raise IgnoredModuleError ("[%s] Kernel module dependency is in ignore list: %s" % (kmod , dependency ))
389474 if dependency in self ["kernel_modules" ]:
@@ -398,10 +483,12 @@ def _process_kmod_dependencies(self, kmod: str, mod_tree=None) -> None:
398483 continue
399484 self ["kernel_modules" ] = dependency
400485
401- if self [ "_kmod_modinfo" ][ kmod ] ["filename" ] == "(builtin)" : # for built-in modules, just add firmware and return
486+ if modinfo ["filename" ] == "(builtin)" : # for built-in modules, just add firmware and return
402487 _add_kmod_firmware (self , kmod )
403488 raise BuiltinModuleError ("Not adding built-in module to dependencies: %s" % kmod )
404489
490+ return kmod , dependencies
491+
405492
406493def add_kmod_deps (self ):
407494 """Adds all kernel modules to the initramfs dependencies.
@@ -420,7 +507,8 @@ def add_kmod_deps(self):
420507 continue
421508
422509 # Add the kmod file to the initramfs dependenceis
423- filename = self ["_kmod_modinfo" ][kmod ]["filename" ]
510+ kmod , modinfo = _get_kmod_info (self , kmod )
511+ filename = modinfo ["filename" ]
424512 if filename .endswith (".ko" ):
425513 self ["dependencies" ] = filename
426514 elif filename .endswith (".ko.xz" ):
@@ -440,16 +528,19 @@ def process_ignored_module(self, module: str) -> None:
440528 for key in ["kmod_init" , "kernel_modules" , "_kmod_auto" ]:
441529 if module in self [key ]:
442530 if key == "kmod_init" :
443- if module in self ["_kmod_modinfo" ] and self ["_kmod_modinfo" ][module ]["filename" ] == "(builtin)" :
444- self .logger .debug ("Removing built-in module from kmod_init: %s" % module )
445- elif module == "zfs" :
446- self .logger .critical ("ZFS module is required but missing." )
447- self .logger .critical ("Please build/install the required kmods before running this script." )
448- self .logger .critical ("Detected kernel version: %s" % self ["kernel_version" ])
449- # https://github.com/projg2/installkernel-gentoo/commit/1c70dda8cd2700e5306d2ed74886b66ad7ccfb42
450- exit (77 )
451- else :
452- raise MissingModuleError ("Required module cannot be imported and is not builtin: %s" % module )
531+ try :
532+ module , modinfo = _get_kmod_info (self , module )
533+ if modinfo ["filename" ] == "(builtin)" :
534+ self .logger .debug ("Removing built-in module from kmod_init: %s" % module )
535+ except DependencyResolutionError :
536+ if module == "zfs" :
537+ self .logger .critical ("ZFS module is required but missing." )
538+ self .logger .critical ("Please build/install the required kmods before running this script." )
539+ self .logger .critical ("Detected kernel version: %s" % self ["kernel_version" ])
540+ # https://github.com/projg2/installkernel-gentoo/commit/1c70dda8cd2700e5306d2ed74886b66ad7ccfb42
541+ exit (77 )
542+ else :
543+ raise MissingModuleError ("Required module cannot be imported and is not builtin: %s" % module )
453544 else :
454545 self .logger .debug ("Removing ignored kernel module from %s: %s" % (key , module ))
455546 self [key ].remove (module )
@@ -469,15 +560,17 @@ def _process_optional_modules(self) -> None:
469560 self .logger .debug (f"Optional kmod_init module is already in kmod_init: { c_ (kmod , 'yellow' , bold = True )} " )
470561 continue
471562 try :
472- _process_kmod_dependencies (self , kmod )
563+ kmod , dependencies = _process_kmod_dependencies (self , kmod )
473564 self ["kmod_init" ] = kmod # add to kmod_init so it will be loaded
474565 except IgnoredModuleError as e :
475566 self .logger .warning (e )
476567 except BuiltinModuleError :
477568 self .logger .debug (f"Optional kmod_init module is built-in, skipping: { c_ (kmod , 'yellow' )} " )
478569 continue
479570 except DependencyResolutionError as e :
480- self .logger .warning (f"[{ c_ (kmod , 'yellow' , bold = True )} ] Failed to process optional kernel module dependencies: { e } " )
571+ self .logger .warning (
572+ f"[{ c_ (kmod , 'yellow' , bold = True )} ] Failed to process optional kernel module dependencies: { e } "
573+ )
481574
482575
483576@unset ("no_kmod" , "no_kmod is enabled, skipping." , log_level = 30 )
@@ -486,6 +579,11 @@ def process_modules(self) -> None:
486579 _process_optional_modules (self )
487580 self .logger .debug ("Processing kernel modules: %s" % self ["kernel_modules" ])
488581 for kmod in self ["kernel_modules" ].copy ():
582+ """ Process all kernel modules
583+ for kmod_init modules, log an error if info can't be retreived, but continue processing.
584+ in successful cases, continue, if he module processing fails, add to the ignore list.
585+ Later, when ignored modules are processed, an exception is raised if the module is required.
586+ """
489587 self .logger .debug ("Processing kernel module: %s" % kmod )
490588 try :
491589 _process_kmod_dependencies (self , kmod )
@@ -507,13 +605,14 @@ def process_modules(self) -> None:
507605 self ["kmod_ignore" ] = kmod
508606
509607 for kmod in self ["_kmod_auto" ]:
608+ """ Do similar for automatic modules, but log warnings insead of errors if dependencies are missing. """
510609 if kmod in self ["kernel_modules" ]:
511610 self .logger .debug ("Autodetected module is already in kernel_modules: %s" % kmod )
512611 continue
513612 self .logger .debug ("Processing autodetected kernel module: %s" % kmod )
514613 try :
515- _process_kmod_dependencies (self , kmod )
516- self ["kmod_init" ] = kmod
614+ kmod_name , dependencies = _process_kmod_dependencies (self , kmod )
615+ self ["kmod_init" ] = kmod_name
517616 continue
518617 except BuiltinModuleError :
519618 continue # Don't add built-in modules to the ignore list
0 commit comments