dotfiles-guix/scripts/install-system

764 lines
23 KiB
Text
Raw Normal View History

2024-12-11 02:15:14 +00:00
#!/run/current-system/profile/bin/guile -s
!#
;;; file: install-system
;;; author: Freya Murphy <freya@freyacat.org>
;;; desc: Installs Guix onto your system
;;
;; root check
;;
;; we need to be root to access block devices and mounts
(if (not (string=? (getenv "USER") "root"))
(begin
(display "Requesting root user access...\n")
(system* "sudo" "-E" (current-filename))
;; dont continue as orig user... exit
(exit 0)))
;;
;; environment
;;
(setenv "TERM" "xterm") ; this should fix more problems then it might cause
; ...always can remove it if not the case
(setenv "PATH" (string-append (getenv "PATH") ;; needed for guix-retry
":"
(dirname (current-filename))))
(setenv "GUILE_HISTORY"
(string-append (getenv "HOME")
"/.guix_installer_history"))
;;
;; import modules
;;
(use-modules (guix records)
(os process)
(ice-9 popen)
(ice-9 textual-ports)
(ice-9 readline)
(ice-9 match)
(ice-9 regex)
(ice-9 format)
(srfi srfi-1) ; fold, last
(srfi srfi-9) ; records
(srfi srfi-34)) ; exceptions
(activate-readline)
;;
;; config
;;
(define %dotfiles-dir
(dirname (dirname (current-filename))))
(define %user
"freya")
(define %block-device-min
;; smallest size in GiB a block device is allowed to be
8)
(define %bypass-block-dev-check
;; bypass checking if a file is a block device
(getenv "DEBUG"))
;;
;; option
;;
(define (option? option)
(and (list? option)
(not (null? option))
(eq? 'option (car option))))
(define (some? option)
(and (option? option)
(= (length option) 2)))
(define (none)
`(option))
(define (some value)
`(option ,value))
(define (match-option option onSome onNone)
(if (option? option)
(if (some? option)
(onSome (cadr option))
(onNone))
(error "Expected option:" option)))
(define (unwrap-option option)
(if (some? option)
(cadr option)
(error "Expected some:" option)))
(define (wrap-option value check?)
(if (check? value)
(some value)
(none)))
;;
;; shell functions
;;
(define* (invoke command #:key (stdin #f))
;; invokes a shell command with `args`
;; and returns stdout as a string
(let* (;; Spawn process
(command* (map (lambda (value)
(cond ((string? value) value)
((number? value) (number->string value))
((symbol? value) (symbol->string value))
(#t (throw 'invalid-command-arg value))))
(filter
(lambda (value)
(not (unspecified? value)))
command)))
(proc (apply run-with-pipe (cons* "r+" command*)))
(pid (car proc))
(in (cadr proc))
(out (cddr proc)))
;; write stdin
(when stdin
(put-string out stdin)
(force-output out))
;; close stdin
(close-port out)
;; read stdout
(let* ((output (get-string-all in)))
;; close stdout
(close-port in)
;; return
output)))
(define* (invoke* prog . args)
(invoke (cons* prog args)))
(define (read-file path)
(call-with-input-file path get-string-all))
(define (write-file path contents)
(call-with-output-file path (lambda (port)
(put-string port contents))))
(define (string-trim-full content)
(fold (lambda (char content)
(string-trim-both content char))
content
(list #\tab
#\space
#\newline)))
;;
;; exception functions
;;
(define (catch-or thunk default)
;; runs the provided `thunk` callback
;; and returns `default` on any exception
(with-exception-handler
(lambda _ default)
thunk
#:unwind? #t))
(define-syntax-rule (catch-or* default . exprs)
;; macro to not have to type lambda _
(catch-or (lambda _ . exprs) default))
;;
;; prompt functions
;;
(define* (prompt* #:key
(heading #f) ; header message
(prompt ": ") ; prompt
(err #f) ; error message to print on invalid input
(fn some) ; transition function
(confirm? #f) ; confirm input before returning value
(clear? #t) ; clear terminal before prompt
(passwd? #f) ; if we are prompting for a password
(helper #f)) ; readline autocompletion helper
;; prompts for user input
;; - returns result of `fn` if some
;; - retries on none
(let inner ((err? #f))
(let* (; reset function - sets up prompt terminal view
(reset
(lambda (err?)
(if clear? (system "clear"))
(if heading (display heading))
(if err? (format (current-error-port) "\n~a\n\n" err))))
; confirm function - confirms if input is what user wants
(confirm
(lambda (content)
(if confirm?
(prompt*
#:prompt (string-append "confirm (" content ") y/N: ")
#:fn (lambda (str)
(some (string=? str "y")))
#:clear? #f)
#t)))
; helper* - readline helper
(helper*
(cond ((list? helper)
(make-completion-function helper))
((procedure? helper)
helper)
(#t
(lambda (text state) #f))))
; readline* function
(readline*
(lambda (prompt)
(if passwd?
; dont use readline if prompting for password
(getpass prompt)
; use readline
(with-readline-completion-function
helper*
(lambda () (readline prompt)))))))
; setup terminal view
(reset err?)
; get input
(let* ((content (readline* prompt))
(content? (string? content))
(content* (string-trim-full (if content? content ""))))
;; exit script on EOF
(if (eof-object? content)
(exit 0))
;; update readline history
(if (and content?
(not passwd?))
(add-history content))
;; handle result
(match-option (fn content*)
(lambda (value) ; onSome
(if (confirm content*)
value
(inner #f)))
(lambda _ ; onNone
(inner #t)))))))
(define* (prompt-passwd #:key
(heading #f) ; header message
(fn some) ; transition function
(confirm? #t) ; should the user confirm the password
(clear? #t)) ; clear terminal before prompt
(prompt*
#:heading heading
#:prompt "passwd: "
#:err "Passwords did not match or do not meet requirements"
#:fn (lambda (passwd)
(if confirm?
(let ((confirm-passwd (getpass "confirm: ")))
(if (not (string=? passwd confirm-passwd))
(none)
(fn passwd)))
(fn passwd)))
#:clear? clear?
#:passwd? #t))
(define* (prompt-bool #:key
(heading #f) ; header message
(prompt "y/n: ") ; prompt
(fn some) ; transition function
(confirm? #f) ; confirm input before returning value
(clear? #t) ; clear terminal before prompt
(passwd? #f)) ; if we are prompting for a password
(prompt*
#:heading heading
#:prompt prompt
#:err "Must type 'y' for yes, 'n' for no"
#:fn (lambda (str)
(match str
("y" (fn #t))
("n" (fn #f))
(_ (none))))
#:confirm? confirm?
#:clear? clear?
#:passwd? passwd?
#:helper (list "y" "n")))
(define* (prompt-number #:key
(heading #f) ; header message
(prompt "#: ") ; prompt
(fn some) ; transition function
(confirm? #f) ; confirm input before returning value
(clear? #t) ; clear terminal before prompt
(passwd? #f)) ; if we are prompting for a password
(prompt*
#:heading heading
#:prompt prompt
#:err "Must type a valid integer"
#:fn (lambda (str)
(let ((num (string->number str)))
(if (integer? num)
(fn num)
(none))))
#:confirm? confirm?
#:clear? clear?
#:passwd? passwd?))
;;
;; storage functions
;;
(define (is-valid-block-device? path)
;; #t if the path is a valid block device, otherwise #f
(catch-or* #f
(let* ((info (stat path))
(type (stat:type info)))
(cond ((and %bypass-block-dev-check
(eq? type 'regular)) #t)
((eq? type 'block-special) #t)
(#t #f)))))
(define (block-device-size path)
;; returns size of block device in GiB
(catch-or* 0
(let* ((info (stat path))
(result (invoke* "blockdev" "--getsize64" path))
(result* (string-trim-full result))
(bytes (or (string->number result*) 0))
(bytes* (if (and %bypass-block-dev-check
(eq? (stat:type info) 'regular))
(stat:size info)
bytes)))
(round (/ bytes* (* 1024 1024 1024))))))
(define (block-device-uuid path)
;; returns uuid of block device
(string-trim-full
(invoke* "blkid" "-s" "UUID" "-o" "value" path)))
;;
;; config
;;
(define-record-type* <installer-config>
installer-config make-installer-config
installer-config?
(dev config-dev)
(filesystem config-filesystem)
(crypt? config-crypt?)
(crypt-passwd config-crypt-passwd)
(swap? config-swap?)
(swap-size config-swap-size)
(hostname config-hostname))
;;
;; partition
;;
(define-record-type* <partition>
partition make-partition
partition?
(dev partition-dev) ; block device (string)
(label partition-label) ; symbol
(index partition-index) ; integer
(start partition-start) ; n MiB
(end partition-end) ; n MiB
(type partition-type) ; symbol
(boot? partition-boot?
(default #f))
(swap? partition-swap?
(default #f)))
(define (partition-name part)
;; TODO: sata
(string-append (partition-dev part) "p" (number->string (partition-index part))))
(define (partition-crypt? config part)
(and (config-crypt? config)
(eq? 'root (partition-label part))))
(define (partition-crypt-label part)
(string-append "installer"
(symbol->string (partition-label part))))
(define (find-partition partitions label)
(cond ((null? partitions) #f)
((eq? label (partition-label (car partitions))) (car partitions))
(#t (find-partition (cdr partitions) label))))
(define (config->partitions config)
(let* (; read needed config values
(dev (config-dev config))
(filesystem (config-filesystem config))
(swap? (config-swap? config))
(swap-size (config-swap-size config))
; boot
(boot (partition
(dev dev)
(label 'boot)
(index 1)
(start 1)
(end 1024)
(type 'fat32)
(boot? #t)))
; swap
(swap (partition
(dev dev)
(label 'swap)
(index 2)
(start (partition-end boot))
(end (+ (partition-end boot) (* swap-size 1024)))
(type 'linux-swap)
(swap? #t)))
; root
(root (partition
(dev dev)
(label 'root)
(index (+ (partition-index swap) (if swap? 1 0)))
(start (partition-end swap))
(end #f)
(type filesystem))))
(if swap?
(list boot swap root)
(list boot root))))
;;
;; step 0. get all user input
;;
(define (prompt-block-device)
(prompt*
#:heading "Which block device to install GUIX on?\n"
#:prompt "Disk: "
#:err "Invalid Block Device"
#:fn (lambda (dev)
(cond ((not (is-valid-block-device? dev)) (none))
((not (>= (block-device-size dev) %block-device-min)) (none))
(#t (some dev))))
#:confirm? #t
#:helper filename-completion-function))
(define (prompt-filesystem)
(let ((supported (list 'fat32 'btrfs 'ext4)))
(prompt*
#:heading "What filesystem do you want to use?\n"
#:prompt "Filesystem: "
#:err "Invalid or unsupported filesystem"
#:fn (lambda (name)
(wrap-option (string->symbol name)
(lambda (type)
(member type supported))))
#:helper (map symbol->string supported))))
(define (prompt-crypt?)
(prompt-bool
#:heading "Do you want an encrypted system?\n"))
(define (prompt-crypt-passwd)
(prompt-passwd
#:heading "Enter crypt device password\n"
#:fn (lambda (passwd)
(wrap-option passwd (lambda (passwd)
;; dont allow empty password
(>= (string-length passwd) 1))))))
(define (prompt-crypt-reconfirm-passwd)
(prompt-passwd
#:heading "\nReconfirm crypt password\n"
#:clear? #f
#:confirm? #f))
(define (prompt-swap?)
(prompt-bool
#:heading "Do you want a swap partition?\n"))
(define (prompt-swap-size dev-size)
(let* ((max* (round (/ (- dev-size 1) 2))) ; (GiB - 1) / 2
(heading (format #f "How large should the swap parition be? [1-~a] GiB\n" max*)))
(prompt-number
#:heading heading
#:fn (lambda (num)
(cond ((> num max*) (none))
((< num 1) (none))
(#t (some num)))))))
(define (prompt-hostname)
(prompt*
#:heading "What will the hostname of this device be?\n"
#:prompt "Hostname: "
#:err "Invalid Hostname"
#:fn (lambda (hostname)
(wrap-option hostname (lambda (hostname)
(string-match "[a-zA-Z]{1,63}" hostname))))
#:confirm? #t))
(define (prompt-installer-config)
(let inner ()
(let* (; get all config values
(dev (prompt-block-device))
(filesystem (prompt-filesystem))
(crypt? (prompt-crypt?))
(crypt-passwd (if crypt?
(prompt-crypt-passwd) #f))
(swap? (prompt-swap?))
(swap-size (if swap?
(prompt-swap-size (block-device-size dev)) 0))
(hostname (prompt-hostname))
; confirm result
(boolean->string (lambda (bool)
(if bool "true" "false")))
(heading
(string-append
(format #f "Disk: ~a\n" dev)
(format #f "Filesystem: ~a\n" filesystem)
(format #f "Crypt: ~a\n" (boolean->string crypt?))
(if crypt?
(format #f "Crypt passwd: ~a\n" "********") "")
(format #f "Swap: ~a\n" (boolean->string swap?))
(if swap?
(format #f "Swap Size: ~d (GiB)\n" swap-size) "")
(format #f "Hostname: ~a\n" hostname)
"\nAre these options correct?\n"))
(confirm?
;; make sure this is what the user wants
(cond ((not (prompt-bool #:heading heading)) #f)
;; make sure the user acutally knows the crypt password
((not (or (not crypt?)
(string=? crypt-passwd
(prompt-crypt-reconfirm-passwd)))) #f)
;; approved
(#t #t))))
(if confirm?
; config approved
(installer-config
(dev dev)
(filesystem filesystem)
(crypt? crypt?)
(crypt-passwd crypt-passwd)
(swap? swap?)
(swap-size swap-size)
(hostname hostname))
; re-prompt values
(inner)))))
;;
;; step 1. partition
;;
(define* (parted dev . args)
(invoke (cons* "parted"
dev
"--script"
args)))
(define (create-partition part)
(let* (;; partition info
(dev (partition-dev part))
(label (partition-label part))
(index (partition-index part))
(type (partition-type part))
;; start and end
(start (partition-start part))
(start* (string-append (number->string start)
"MiB"))
(end (partition-end part))
(end* (if end
(string-append (number->string end)
"MiB")
"100%")))
;; create partition
(parted dev
"mkpart" label type start* end*)
;; when boot?
(when (partition-boot? part)
(parted dev
"set" index "boot" "on"))
;; when swap?
(when (partition-swap? part)
(parted dev
"set" index "swap" "on"))))
(define (create-partitions partitions)
(let ((dev (partition-dev (car partitions))))
(parted dev "mklabel" "gpt"))
(for-each create-partition
partitions))
;;
;; step 2. format
;;
(define (format-luks config part)
(let* (;read needed config values
(name (partition-name part))
(label (partition-crypt-label part))
(crypt-passwd (config-crypt-passwd config)))
(format #t "formatting ~a with luks\n" name)
;; format root partition with luksFormat
(invoke `("cryptsetup" "-q" "luksFormat" ,name)
#:stdin (string-append crypt-passwd "\n"
crypt-passwd "\n"))
;; open luks
(invoke `("cryptsetup" "open" ,name ,label)
#:stdin (string-append crypt-passwd "\n"))
;; format luks mapper
(format-path (partition-type part)
(string-append "/dev/mapper/" label))))
(define (format-path type path)
(format #t "formatting ~a with ~a\n" path type)
(match type
('fat32 (invoke* "mkfs.vfat" "-F" "32" path))
('btrfs (invoke* "mkfs.btrfs" "--force" path))
('ext4 (invoke* "mkfs.ext4" path))
('linux-swap (invoke* "mkswap" path))
(_ (throw 'unknown-fs (string-append "Unhandeled partition type: "
(symbol->string type))))))
(define (format-partition config part)
(let* (;; partition info
(label (partition-label part))
(name (partition-name part))
(type (partition-type part)))
(if (partition-crypt? config part)
(format-luks config part)
(format-path type name))))
(define (format-partitions config partitions)
(for-each (lambda (part)
(format-partition config part))
partitions))
;;
;; step 3. mount
;;
(define (mount-path from to)
(format #t "mounting ~a to ~a\n" from to)
(invoke* "mkdir" "-p" to)
(invoke* "mount" from to))
(define (mount-swap from)
(format #t "swapping ~a\n" from)
(invoke* "swapon" from))
(define (mount-partitions config partitions)
(let* ((root (find-partition partitions 'root))
(swap (find-partition partitions 'swap))
(boot (find-partition partitions 'boot)))
;; root
(let ((path (if (partition-crypt? config root)
(string-append "/dev/mapper/" (partition-crypt-label root))
(partition-name root))))
(mount-path path "/mnt"))
;; swap
(when swap
(mount-swap (partition-name swap)))
;; boot
(mount-path (partition-name boot) "/mnt/boot")))
;;
;; step 4. create specification
;;
(define %base-scm-path
(string-append %dotfiles-dir
"/files/base.scm.m4"))
(define (get-specification config partitions)
(let* (;; config values
(host (config-hostname config))
(fsys (symbol->string (config-filesystem config)))
(swap? (config-swap? config))
(crypt? (config-crypt? config))
;; uuid helper
(getuuid (lambda (label)
(let ((part (find-partition partitions label)))
(if part
(block-device-uuid
(partition-name part))
#f))))
;; uuids
(root (getuuid 'root))
(swap (getuuid 'swap))
(boot (getuuid 'boot)))
(invoke* "m4"
; Required
(string-append "--define=HOST=" host)
(string-append "--define=ROOT=" root)
(string-append "--define=BOOT=" boot)
(string-append "--define=FSYS=" fsys)
; Optional
(if crypt?
"--define=CRYPT")
(if swap?
(string-append "--define=SWAP=" swap))
; Pass in file
%base-scm-path)))
;;
;; step 5. install system
;;
(define %channels-path
(string-append %dotfiles-dir
"/channels.scm"))
(define %modules-path
(string-append %dotfiles-dir
"/modules"))
(define %substitute-urls
(list "https://substitutes.nonguix.org"
"https://substitutes.freya.cat"
"https://bordeaux.guix.gnu.org"
"https://ci.guix.gnu.org"))
(define (install-system config scm)
(let* ((hostname (config-hostname config))
(system-scm (string-append %dotfiles-dir
"/systems/"
hostname
".scm")))
;; write scm to system-scm
(write-file system-scm scm)
;; start cow store
(system* "herd" "start" "cow-store" "/mnt")
;; approve signing keys
(for-each (lambda (key)
(invoke '("guix" "archive" "--authorize")
#:stdin (read-file key)))
(map (lambda (name)
(string-append %dotfiles-dir
"/files/keys/"
name))
(list "nonguix.pub"
"sakura.pub")))
;; install base system
(system* "guix-retry" ; fix TLS 'write_to_session_record_port' failures
"guix" "time-machine" "-C" %channels-path "--"
"system" "-L" %modules-path
(string-append "--substitute-urls='"
(string-join %substitute-urls " ")
"'")
"init" system-scm "/mnt")))
;;
;; entry
;;
(let* (; step 0 - prompt
(config (prompt-installer-config))
(partitions (config->partitions config))
; step 1 - partition
(_ (create-partitions partitions))
; step 2 - format
(_ (format-partitions config partitions))
; step 3 - mount
(_ (mount-partitions config partitions))
;; step 4 - create specification
(scm (get-specification config partitions))
; step 5 - install
(_ (install-system config scm)))
#t)