Subject: [PATCH 2/5] linux-modules: Add 'device-module-aliases' and related procedures.

From: Ludovic Courtès <[email-masked]>
Date: Tue, 27 Feb 2018 15:22:42 +0100

* gnu/build/linux-modules.scm (readlink*, stat->device-major)
(stat->device-minor): New procedures.
(%not-slash): New variable.
(read-uevent, device-module-aliases, read-module-aliases)
(current-alias-file, known-module-aliases, matching-modules): New
procedures.
---
 gnu/build/linux-modules.scm | 161 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 159 insertions(+), 2 deletions(-)

diff --git a/gnu/build/linux-modules.scm b/gnu/build/linux-modules.scm
index 5ca7bf8e3..51b3e0871 100644
--- a/gnu/build/linux-modules.scm
+++ b/gnu/build/linux-modules.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2014, 2016 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2014, 2016, 2018 Ludovic Courtès <[email-masked]>
 ;;; Copyright © 2017 Mathieu Othacehe <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -19,6 +19,7 @@
 
 (define-module (gnu build linux-modules)
   #:use-module (guix elf)
+  #:use-module (guix glob)
   #:use-module (guix build syscalls)
   #:use-module (rnrs io ports)
   #:use-module (rnrs bytevectors)
@@ -26,6 +27,7 @@
   #:use-module (srfi srfi-26)
   #:use-module (ice-9 vlist)
   #:use-module (ice-9 match)
+  #:use-module (ice-9 rdelim)
   #:export (dot-ko
             ensure-dot-ko
             module-dependencies
@@ -34,7 +36,11 @@
             module-loaded?
             load-linux-module*
 
-            current-module-debugging-port))
+            current-module-debugging-port
+
+            device-module-aliases
+            known-module-aliases
+            matching-modules))
 
 ;;; Commentary:
 ;;;
@@ -213,4 +219,155 @@ appears in BLACK-LIST are not loaded."
              (or (and recursive? (= EEXIST (system-error-errno args)))
                  (apply throw args)))))))
 
+
+;;;
+;;; Device modules.
+;;;
+
+;; Copied from (guix utils).  FIXME: Factorize.
+(define (readlink* file)
+  "Call 'readlink' until the result is not a symlink."
+  (define %max-symlink-depth 50)
+
+  (let loop ((file  file)
+             (depth 0))
+    (define (absolute target)
+      (if (absolute-file-name? target)
+          target
+          (string-append (dirname file) "/" target)))
+
+    (if (>= depth %max-symlink-depth)
+        file
+        (call-with-values
+            (lambda ()
+              (catch 'system-error
+                (lambda ()
+                  (values #t (readlink file)))
+                (lambda args
+                  (let ((errno (system-error-errno args)))
+                    (if (or (= errno EINVAL))
+                        (values #f file)
+                        (apply throw args))))))
+          (lambda (success? target)
+            (if success?
+                (loop (absolute target) (+ depth 1))
+                file))))))
+
+;; See 'major' and 'minor' in <sys/sysmacros.h>.
+
+(define (stat->device-major st)
+  (ash (logand #xfff00 (stat:rdev st)) -8))
+
+(define (stat->device-minor st)
+  (logand #xff (stat:rdev st)))
+
+(define %not-slash
+  (char-set-complement (char-set #\/)))
+
+(define (read-uevent port)
+  "Read a /sys 'uevent' file from PORT and return an alist where each car is a
+key such as 'MAJOR or 'DEVTYPE and each cdr is the corresponding value."
+  (let loop ((result '()))
+    (match (read-line port)
+      ((? eof-object?)
+       (reverse result))
+      (line
+       (loop (cons (key=value->pair line) result))))))
+
+(define (device-module-aliases device)
+  "Return the list of module aliases required by DEVICE, a /dev file name, as
+in this example:
+
+  (device-module-aliases \"/dev/sda\")
+  => (\"scsi:t-0x00\" \"pci:v00008086d00009D03sv0000103Csd000080FAbc01sc06i01\")
+
+The modules corresponding to these aliases can then be found using
+'matching-modules'."
+  ;; The approach is adapted from
+  ;; <https://unix.stackexchange.com/questions/97676/how-to-find-the-driver-module-associated-with-a-device-on-linux>.
+  (let* ((st        (stat device))
+         (type      (stat:type st))
+         (major     (stat->device-major st))
+         (minor     (stat->device-minor st))
+         (sys-name  (string-append "/sys/dev/"
+                                   (case type
+                                     ((block-special) "block")
+                                     ((char-special)  "char")
+                                     (else (symbol->string type)))
+                                   "/" (number->string major) ":"
+                                   (number->string minor)))
+         (directory (canonicalize-path (readlink* sys-name))))
+    (let loop ((components (string-tokenize directory %not-slash))
+               (aliases    '()))
+      (match components
+        (("sys" "devices" _)
+         (reverse aliases))
+        ((head ... _)
+         (let ((uevent (string-append (string-join components "/" 'prefix)
+                                      "/uevent")))
+           (if (file-exists? uevent)
+               (let ((props (call-with-input-file uevent read-uevent)))
+                 (match (assq-ref props 'MODALIAS)
+                   (#f    (loop head aliases))
+                   (alias (loop head (cons alias aliases)))))
+               (loop head aliases))))))))
+
+(define (read-module-aliases port)
+  "Read from PORT data in the Linux 'modules.alias' file format.  Return a
+list of alias/module pairs where each alias is a glob pattern as like the
+result of:
+
+  (compile-glob-pattern \"scsi:t-0x01*\")
+
+and each module is a module name like \"snd_hda_intel\"."
+  (define (comment? str)
+    (string-prefix? "#" str))
+
+  (define (tokenize str)
+    ;; Lines have the form "alias ALIAS MODULE", where ALIAS can contain
+    ;; whitespace.  This is why we don't use 'string-tokenize'.
+    (let* ((str   (string-trim-both str))
+           (left  (string-index str #\space))
+           (right (string-rindex str #\space)))
+      (list (string-take str left)
+            (string-trim-both (substring str left right))
+            (string-trim-both (string-drop str right)))))
+
+  (let loop ((aliases '()))
+    (match (read-line port)
+      ((? eof-object?)
+       (reverse aliases))
+      ((? comment?)
+       (loop aliases))
+      (line
+       (match (tokenize line)
+         (("alias" alias module)
+          (loop (alist-cons (compile-glob-pattern alias) module
+                            aliases)))
+         (()                                      ;empty line
+          (loop aliases)))))))
+
+(define (current-alias-file)
+  "Return the absolute file name of the default 'modules.alias' file."
+  (string-append (or (getenv "LINUX_MODULE_DIRECTORY")
+                     "/run/booted-system/kernel/lib/modules")
+                 "/" (utsname:release (uname))
+                 "/" "modules.alias"))
+
+(define* (known-module-aliases #:optional (alias-file (current-alias-file)))
+  "Return the list of alias/module pairs read from ALIAS-FILE.  Each alias is
+actually a pattern."
+  (call-with-input-file alias-file read-module-aliases))
+
+(define* (matching-modules alias
+                           #:optional (known-aliases (known-module-aliases)))
+  "Return the list of modules that match ALIAS according to KNOWN-ALIASES.
+ALIAS is a string like \"scsi:t-0x00\" as returned by
+'device-module-aliases'."
+  (filter-map (match-lambda
+                ((pattern . module)
+                 (and (glob-match? pattern alias)
+                      module)))
+              known-aliases))
+
 ;;; linux-modules.scm ends here
-- 
2.16.2