#!/run/current-system/profile/bin/guile -s !# ;;; file: install-system ;;; author: Freya Murphy ;;; 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 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 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)