diff --git a/gnu/bootloader/grub.scm b/gnu/bootloader/grub.scm index 6e71f30f0d..b57c78e156 100644 --- a/gnu/bootloader/grub.scm +++ b/gnu/bootloader/grub.scm @@ -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 . - ((? 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 + (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 . + ((? 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 + (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 + (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 - (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