gnu: bootloader: grub: Define grub-menu-entry configuration.

* gnu/bootloader/grub.scm (serialize-grub-theme, serialize-string,
normalize-file, serialize-file-like, string-list?, file-like-list?,
serialize-string-list, serialize-file-like-list, grub-device?,
serialize-grub-device, serialize-linux-directive,
serialize-multiboot-directive, directive-or-file-like?,
serialize-directive-or-file-like, menu-entry->grub-menu-entry,
serialize-grub-menu-entry): Add procedures.
(make-grub.cfg): Use really-mixed-text-file to reduce indentation.
(%boot): Add variable.  Define maybe-string.
(linux-directive, multiboot-directive, grub-menu-entry): Add records.

Change-Id: Ie1e56e04d5c8ae7ab07741ef7b6909f306398f28
This commit is contained in:
Herman Rimm 2024-09-21 20:17:22 +02:00 committed by Ryan Schanzenbacher
parent fd4e074d61
commit 38cb6dee66
Signed by: ryan77627
GPG key ID: 81B0E222A3E2308E

View file

@ -34,6 +34,7 @@ (define-module (gnu bootloader grub)
#:use-module (gnu packages bootloaders)
#:autoload (gnu packages gtk) (guile-cairo guile-rsvg)
#:autoload (gnu packages xorg) (xkeyboard-config)
#:use-module (gnu services configuration)
#:use-module (gnu system boot)
#:use-module (gnu system file-systems)
#:use-module (gnu system keyboard)
@ -83,6 +84,13 @@ (define (sanitize str)
(list->string (fold-right glycerin '()
(map isopropyl (string->list #$str))))))
;; Name of variable with GRUB search result for boot device.
(define %boot "boot")
;; TODO: Use device-mount-point and device-subvol.
(define (normalize-file file)
#~(string-append "($" #$%boot ")" #$(sanitize file)))
(define* (search/target type targets var #:optional (port #f))
"Returns a gexp of a GRUB search command for target TYPE, storing the
result in VAR. Optionally outputs to the gexp PORT instead of returning
@ -97,43 +105,6 @@ (define (form name val)
(label (form "fs_label" label))
(else (form "file" (sanitize path)))))))
(define* (search/menu-entry device file var #:optional (port #f))
"Return the GRUB 'search' command to look for DEVICE, which contains
FILE, a gexp. The result is a gexp that can be inserted in the
grub.cfg-generation code to set the variable VAR. This procedure is
able to handle DEVICEs unmounted at evaltime."
(match device
;; Preferably refer to DEVICE by its UUID or label. This is more
;; efficient and less ambiguous, see <http://bugs.gnu.org/22281>.
((? uuid? idfk) ; calling idfk uuid here errors for some reason
#~(format #$port "search.fs_uuid ~a ~a~%" #$(uuid->string device) #$var))
((? file-system-label? label)
#~(format #$port "search.fs_label \"~a\" ~a~%"
#$(sanitize (file-system-label->string label)) #$var))
((? (lambda (device)
(and (string? device) (string-contains device ":/"))) nfs-uri)
;; If the device is an NFS share, then we assume that the expected
;; file on that device (e.g. the GRUB background image or the kernel)
;; has to be loaded over the network. Otherwise we would need an
;; additional device information for some local disk to look for that
;; file, which we do not have.
;;
;; TFTP is preferred to HTTP because it is used more widely and
;; specified in standards more widely--especially BOOTP/DHCPv4
;; defines a TFTP server for DHCP option 66, but not HTTP.
;;
;; Note: DHCPv6 specifies option 59 to contain a boot-file-url,
;; which can contain a HTTP or TFTP URL.
;;
;; Note: It is assumed that the file paths are of a similar
;; setup on both the TFTP server and the NFS server (it is
;; not possible to search for files on TFTP).
;;
;; TODO: Allow HTTP.
#~(format #$port "set ~a=tftp~%" #$var))
((or #f (? string?))
#~(format #$port "search.file \"~a\" ~a~%" #$(sanitize file) #$var))))
(define (when-list . xs) (filter identity xs))
@ -173,6 +144,24 @@ (define (grub-theme-png theme)
#:height #$height)))))))
(_ image))))
(define (serialize-grub-theme value install)
(match-record value <grub-theme>
(image color-normal color-highlight)
(define (colors->str colors)
(format #f "~a/~a" (assoc-ref colors 'fg) (assoc-ref colors 'bg)))
(and image
#~(format #f "insmod png
if background_image \"($root)~a/image.png\"; then
set color_normal=~a
set color_highlight=~a
else
set menu_color_normal=cyan/blue
set menu_color_highlight=white/blue
fi~%"
#$(sanitize install)
#$(colors->str color-normal)
#$(colors->str color-highlight)))))
;;;
;;; Core config.
@ -288,140 +277,263 @@ (define* (core.img grub format #:key bootloader-config store-crypto-devices
;;; This is what does the heavy lifting after core.img finds it.
;;;
;; TODO: use define-configuration.
(define (menu-entry->gexp entry extra-initrd port)
(match-menu-entry
entry
(label device linux linux-arguments initrd multiboot-kernel
multiboot-arguments multiboot-modules chain-loader)
(let ((normalize-file
(compose sanitize (cut normalize-file entry <>))))
#~(begin
(format #$port "menuentry ~s {~% " #$label)
#$(search/menu-entry
device (or linux multiboot-kernel chain-loader) "boot" port)
#$@(cond
(linux
(list #~(format
#$port " linux \"($boot)~a\" ~a~%"
#$(normalize-file linux)
;; GRUB passes rest of the line _verbatim_.
(string-join (list #$@linux-arguments)))
#~(format #$port " initrd ~a \"($boot)~a\"~%"
(if #$extra-initrd
(string-append "($boot)\""
(normalize-file
#$extra-initrd)
"\"")
"")
#$(normalize-file initrd))))
;; Previously, this provided a (wrong) root= argument.
;; Just don't bother anymore; better less info than
;; wrong info.
(multiboot-kernel
(cons
#~(format #$port " multiboot \"($boot)~a\" ~a~%"
#$(normalize-file multiboot-kernel)
(string-join (list #$@multiboot-arguments)))
(map (lambda (mod)
#~(format port " module \"($boot)~a\"~%"
#$(normalize-file mod)))
multiboot-modules)))
(chain-loader
(list #~(format #$port " chainloader \"~a\"~%"
#$(normalize-file chain-loader)))))
(format #$port "}~%")))))
(define (serialize-string value)
(match value
((key value) #~(string-append #$key "=" #$value))
((? string?) #~#$value)
(x x)))
(define-maybe string)
(define (serialize-file-like value)
#~(format #f " module ~s~%" #$(normalize-file value)))
(define (string-list? x)
(and (list? x)
(and-map (match-lambda
(((? string?) (? file-like?)) #t)
;; TODO: Remove gexps from linux-arguments.
(x (or (string? x) (gexp? x))))
x)))
(define (file-like-list? x)
(and (list? x) (and-map file-like? x)))
(define (serialize-string-list value)
#~(string-join (list #$@(map serialize-string value)) " "))
(define (serialize-file-like-list value)
#~(string-concatenate (list #$@(map serialize-file-like value))))
(define (grub-device? x)
(or (not x) (string? x) (uuid? x) (file-system-label? x)))
(define (serialize-grub-device device file)
"Return the GRUB 'search' command to look for DEVICE, which contains
FILE, a gexp. The result is a gexp that can be inserted in the
grub.cfg-generation code to set the variable VAR. This procedure is
able to handle DEVICEs unmounted at evaltime."
(define (host-prefix? device)
(and (string? device) (string-contains device ":/")))
(match device
;; Preferably refer to DEVICE by its UUID or label. This is more
;; efficient and less ambiguous, see <http://bugs.gnu.org/22281>.
((? uuid?)
#~(format #f "search.fs_uuid ~a ~a~%"
#$(uuid->string device) #$%boot))
((? file-system-label?)
#~(format #f "search.fs_label ~s ~a~%"
#$(sanitize (file-system-label->string device)) #$%boot))
((? host-prefix?)
;; If the device is an NFS share, then we assume that the expected
;; file on that device (e.g. the GRUB background image or the kernel)
;; has to be loaded over the network. Otherwise we would need an
;; additional device information for some local disk to look for that
;; file, which we do not have.
;;
;; TFTP is preferred to HTTP because it is used more widely and
;; specified in standards more widely--especially BOOTP/DHCPv4
;; defines a TFTP server for DHCP option 66, but not HTTP.
;;
;; Note: DHCPv6 specifies option 59 to contain a boot-file-url,
;; which can contain an HTTP or TFTP URL.
;;
;; Note: It is assumed that the file paths are of a similar
;; setup on both the TFTP server and the NFS server (it is
;; not possible to search for files on TFTP).
;;
;; TODO: Allow HTTP.
#~(format #f "set ~a=tftp~%" #$%boot))
((or #f (? string?))
#~(format #f "search.file ~s ~a~%" #$(sanitize file) #$%boot))))
(define-configuration linux-directive
;; XXX: Does normalize-file work on strings?
(extra-initrd maybe-string "Path to out-of-store initrd with secrets.")
(initrd file-like "The initial RAM disk to use.")
(kernel file-like "The Linux kernel image to boot.")
(arguments
string-list
"The list of extra Linux kernel command-line arguments."))
(define (serialize-linux-directive value normalize-file)
(match-record value <linux-directive>
(arguments extra-initrd initrd kernel)
;; TODO: Convert menu-entry-initrd to file-like.
(let ((initrd #~(readlink #$initrd))
(kernel #~(readlink #$kernel)))
#~(format #f " linux ~s ~a~% initrd ~s~%"
#$(normalize-file kernel)
;; GRUB passes the rest of the line _verbatim_.
#$(serialize-string-list arguments)
(if #$(maybe-value-set? extra-initrd)
;; XXX: Do not normalize extra-initrd?
(string-append #$(normalize-file extra-initrd)
"\" \"" #$(normalize-file initrd))
#$(normalize-file initrd))))))
(define-configuration multiboot-directive
(kernel file-like "The Multiboot kernel image to boot.")
(arguments
(string-list '())
"The list of Multiboot kernel command-line arguments.")
(modules
(file-like-list '())
"The list of commands for loading Multiboot modules."))
(define (serialize-multiboot-directive value normalize-file)
(match-record value <multiboot-directive>
(arguments kernel modules)
;; Previously, this provided a (wrong) root= argument.
;; Just don't bother anymore; better less info than
;; wrong info.
#~(format #f " multiboot ~s ~a~%~a"
#$(normalize-file kernel)
#$(serialize-string-list arguments)
#$(serialize-file-like-list modules))))
(define (directive-or-file-like? x)
(or (linux-directive? x) (multiboot-directive? x) (file-like? x)))
(define (serialize-directive-or-file-like value)
;; TODO: Use mount-point and subvol to normalize file.
(match value
((? linux-directive?)
(serialize-linux-directive value normalize-file))
((? multiboot-directive?)
(serialize-multiboot-directive value normalize-file))
((? file-like?)
#~(format #f " chainloader ~s~%" #$(normalize-file value)))))
(define-configuration grub-menu-entry
(label string "Entry label with e.g. rank, kernel, and date.")
(device
grub-device
;; XXX: Is NFS exclusive to Linux?
"Device UUID or label, NFS path, or block device path, with payload.")
(device-mount-point maybe-string "Remove prefix from relevant paths.")
(device-subvol maybe-string "Access files from this Btrfs subvolume.")
(directive
directive-or-file-like
"Configuration for a Linux or Multiboot directive, or a file for the
chainloader directive."))
(define (menu-entry->grub-menu-entry entry extra-initrd)
"Temporary compatibility function just for trying out a new thing."
(match-menu-entry entry
(label device device-mount-point device-subvol linux linux-arguments
initrd multiboot-kernel multiboot-arguments multiboot-modules
chain-loader)
(grub-menu-entry
(label label)
(device device)
(device-mount-point (or device-mount-point %unset-value))
(device-subvol (or device-subvol %unset-value))
;; Prefer linux to multiboot to chainloader.
(directive (cond
(linux
(linux-directive
(arguments linux-arguments)
(extra-initrd (or extra-initrd %unset-value))
;; Refer to single file by (sym)link, not string.
;; TODO: Convert menu-entry-initrd to file-like.
(initrd (computed-file "initrd.cpio.gz"
#~(symlink #$initrd #$output)))
(kernel (computed-file "bzImage"
#~(symlink #$linux #$output)))))
(multiboot-kernel
(multiboot-directive
(arguments multiboot-arguments)
(kernel multiboot-kernel)
(modules multiboot-modules)))
(chain-loader chain-loader))))))
(define (serialize-grub-menu-entry value)
(let* ((directive (grub-menu-entry-directive value))
(file (match directive
((? linux-directive?)
(linux-directive-kernel directive))
((? multiboot-directive?)
(multiboot-directive-kernel directive))
((? file-like?) directive)))
(device (grub-menu-entry-device value))
;; XXX: Decouple from directive (file).
(search (serialize-grub-device device file))
(label (grub-menu-entry-label value))
(directive (serialize-directive-or-file-like directive)))
#~(format #f "menuentry ~s {~% ~a~a}~%"
#$label #$search #$directive)))
;; E.g. build this file, appended with:
;; (mixed-text-file "grub-menu-entry.texi"
;; (generate-grub-menu-entry-documentation))
(define (generate-grub-menu-entry-documentation)
(generate-documentation
`((grub-menu-entry
,grub-menu-entry-fields
(directive linux-directive)
(directive multiboot-directive))
(linux-directive ,linux-directive-fields)
(multiboot-directive ,multiboot-directive-fields))
'grub-menu-entry))
;; TODO: use define-configuration.
(define (make-grub.cfg bootloader-config locale install menu-entries
old-entries terms->str outputs inputs theme)
(define (colors->str c)
(format #f "~a/~a" (assoc-ref c 'fg) (assoc-ref c 'bg)))
old-entries gfxterm? outputs inputs theme)
(define (really-mixed-text-file . xs)
"Ignores #f arguments and flattens list arguments."
(apply mixed-text-file (flatten (filter identity xs))))
(match-bootloader-configuration
bootloader-config
;; XXX: Separate these fields into another record?
(default-entry timeout serial-unit serial-speed)
#~(call-with-output-file #$output
(lambda (port)
;; preamble
(format port "\
(really-mixed-text-file "grub.cfg" "\
# This file was generated from your Guix configuration. Any changes
# will be lost upon reconfiguration~%")
#$@(when-list
;; menu settings
(and default-entry
#~(format port "set default=~a~%" #$default-entry))
(and timeout
#~(format port "set timeout=~a~%" #$timeout))
;; gfxterm setup
(and (memq 'gfxterm outputs)
#~(format
port "\
# will be lost upon reconfiguration\n"
;; menu settings
(format #f "~@[set default=~a~%~]" default-entry)
(format #f "~@[set timeout=~a~%~]" timeout)
;; gfxterm setup
(and gfxterm?
(format #f "\
if loadfont unicode; then
set gfxmode=~a
insmod all_video
insmod gfxterm
fi~%"
#$(string-join (grub-theme-gfxmode theme) ";")))
;; io
(and (or serial-unit serial-speed)
#~(format
port "serial --unit=~a --speed=~a~%"
;; Documented defaults are unit 0 at 9600 baud.
#$(number->string (or serial-unit 0))
#$(number->string (or serial-speed 9600))))
(and (pair? outputs)
#~(format port "terminal_output ~a~%"
#$(terms->str outputs)))
(and (pair? inputs)
#~(format port "terminal_input ~a~%"
#$(terms->str inputs)))
;; locale
(and locale
#~(format port "\
set \"locale_dir=($root)~a/locales\"
(string-join (grub-theme-gfxmode theme) ";")))
;; io
(and (or serial-unit serial-speed)
(format #f "serial~@[ --unit=~d~]~@[ --speed=~d~]~%"
serial-unit serial-speed))
(format #f "~@[terminal_output ~a~%~]" outputs)
(format #f "~@[terminal_input ~a~%~]" inputs)
;; locale
(and locale
#~(format #f "\
set locale_dir=\"($root)~a/locales\"
set lang=~a~%"
#$(sanitize install)
#$(locale-definition-source
(locale-name->definition locale))))
;; keyboard layout
(and (bootloader-configuration-keyboard-layout
bootloader-config)
#~(format port "\
#$(sanitize install)
#$(locale-definition-source
(locale-name->definition locale))))
;; keyboard layout
(and (bootloader-configuration-keyboard-layout bootloader-config)
#~(format #f "\
insmod keylayouts
keymap \"($root)~a/keymap~%\""
#$(sanitize install)))
;; theme
(match-record theme <grub-theme>
(image color-normal color-highlight)
(and image
#~(format port "\
insmod png
if background_image \"($root)~a/image.png\"; then
set color_normal=~a
set color_highlight=~a
else
set menu_color_normal=cyan/blue
set menu_color_highlight=white/blue
fi~%" #$(sanitize install)
#$(colors->str color-normal)
#$(colors->str color-highlight)))))
;; menu entries
#$@menu-entries
#$@(if (pair? old-entries)
(append (list #~(format
port "submenu ~s {~%"
"GNU system, old configurations..."))
old-entries
(list #~(format port "}~%")))
'())
(format port "\
keymap \"($root)~a/keymap\"~%"
#$(sanitize install)))
(serialize-grub-theme theme install)
;; menu entries
menu-entries
(and (pair? old-entries)
(cons "submenu \"GNU system, old configurations...\" {\n"
;; Do not indent: initrd line is >80 columns anyway.
(append old-entries (list "}\n"))))
"\
if [ \"${grub_platform}\" == efi ]; then
menuentry \"Firmware setup\" {
fwsetup
}
fi~%")))))
fi\n")))
(define* (grub.cfg #:key bootloader-config
current-boot-alternative
@ -437,28 +549,26 @@ (define* (grub.cfg #:key bootloader-config
(menu-entries targets extra-initrd theme terminal-outputs
terminal-inputs)
(define (entries->gexp entries)
(map (cut menu-entry->gexp <> extra-initrd #~port)
(map (compose serialize-grub-menu-entry
(cut menu-entry->grub-menu-entry <> extra-initrd))
entries))
(define (terms->str terms)
(and (pair? terms) (format #f "~{~a~^ ~}" terms)))
(let* ((current-entry (boot-alternative->menu-entry
current-boot-alternative))
(entries (entries->gexp (cons current-entry menu-entries)))
(old-entries (entries->gexp (map boot-alternative->menu-entry
old-boot-alternatives)))
(terms->str (compose string-join (cut map symbol->string <>)))
(gfxterm? (or (not terminal-outputs)
(memq 'gfxterm terminal-outputs)))
;; Use the values provided, or the defaults otherwise.
(outputs (or terminal-outputs '(gfxterm)))
(inputs (or terminal-inputs '()))
(outputs (or (and=> terminal-outputs terms->str) "gfxterm"))
(inputs (and=> terminal-inputs terms->str))
(theme (or theme (grub-theme))))
(and=>
(with-targets targets
(('install => (install :devpath))
(make-grub.cfg bootloader-config locale install entries
old-entries terms->str outputs inputs theme)))
(cut computed-file "grub.cfg" <>
;; Since this file is rather unique, there's no point in
;; trying to substitute it.
#:options '(#:local-build? #t #:substitutable? #f))))))
(with-targets targets
(('install => (install :devpath))
(make-grub.cfg bootloader-config locale install entries
old-entries gfxterm? outputs inputs theme))))))
(define (keyboard-layout-file layout grub)
"Process the X keyboard layout description LAYOUT, a <keyboard-layout>