Add a basic usable installer TUI and tests
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
(define-module (fruix installer)
|
||||
#:use-module (fruix installer state)
|
||||
#:use-module (fruix installer steps)
|
||||
#:use-module (fruix installer flow)
|
||||
#:use-module (fruix installer final)
|
||||
#:use-module (fruix installer newt)
|
||||
#:re-export (default-installer-network-mode
|
||||
@@ -35,6 +36,10 @@
|
||||
next-installer-step-id
|
||||
previous-installer-step-id
|
||||
validate-installer-state-for-step
|
||||
installer-network-mode-options
|
||||
installer-target-device-candidates
|
||||
installer-command-apply
|
||||
run-installer-command-script
|
||||
installer-final-summary-text
|
||||
newt-required-bindings
|
||||
missing-newt-bindings
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
(format #f "current step: ~a\n\n" (installer-state-selected-step state))
|
||||
layout-summary
|
||||
"\n\n"
|
||||
"This prototype only validates the installer state and presents a Newt-based\n"
|
||||
"review flow. It does not yet invoke the final install action from the TUI.\n")))
|
||||
"Selecting Install in the TUI hands control back to the Fruix installer\n"
|
||||
"engine, which then applies the shared storage/install backend.\n")))
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
(define-module (fruix installer flow)
|
||||
#:use-module (fruix installer state)
|
||||
#:use-module (fruix installer steps)
|
||||
#:use-module (fruix system storage)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-13)
|
||||
#:export (installer-network-mode-options
|
||||
installer-target-device-candidates
|
||||
installer-command-apply
|
||||
run-installer-command-script))
|
||||
|
||||
(define installer-network-mode-options
|
||||
'(none dhcp))
|
||||
|
||||
(define invalid-target-device-prefixes
|
||||
'("cd" "md" "pass" "ses" "fd" "ugen" "lo" "pflog" "tap" "tun"))
|
||||
|
||||
(define (target-device-name-candidate? name)
|
||||
(and (non-empty-string? name)
|
||||
(not (any (lambda (prefix)
|
||||
(string-prefix? prefix name))
|
||||
invalid-target-device-prefixes))))
|
||||
|
||||
(define (normalize-target-device value)
|
||||
(cond
|
||||
((not (non-empty-string? value)) #f)
|
||||
((string-prefix? "/dev/" value) value)
|
||||
(else (string-append "/dev/" value))))
|
||||
|
||||
(define* (installer-target-device-candidates state #:key kern-disks)
|
||||
(let* ((explicit-target (normalize-target-device (installer-state-target-device state)))
|
||||
(discovered (if kern-disks
|
||||
(string-tokenize kern-disks)
|
||||
'()))
|
||||
(devices (append (if explicit-target (list explicit-target) '())
|
||||
(map (lambda (name)
|
||||
(string-append "/dev/" name))
|
||||
(filter target-device-name-candidate? discovered)))))
|
||||
(delete-duplicates devices string=?)))
|
||||
|
||||
(define (step-transition state next-step)
|
||||
(installer-state-update state #:selected-step next-step))
|
||||
|
||||
(define (command-success state result target-devices)
|
||||
`((state . ,state)
|
||||
(result . ,result)
|
||||
(target-devices . ,target-devices)))
|
||||
|
||||
(define (update-state-field state field value)
|
||||
(match field
|
||||
('storage-layout (installer-state-update state #:storage-layout value))
|
||||
('target-device (installer-state-update state #:target-device (normalize-target-device value)))
|
||||
('host-name (installer-state-update state #:host-name value))
|
||||
('root-size (installer-state-update state #:root-size value))
|
||||
('disk-capacity (installer-state-update state #:disk-capacity value))
|
||||
('network-mode (installer-state-update state #:network-mode value))
|
||||
('selected-step (installer-state-update state #:selected-step value))
|
||||
('metadata (installer-state-update state #:metadata value))
|
||||
(_
|
||||
(error "unsupported installer state field" field))))
|
||||
|
||||
(define* (installer-command-apply state command #:key target-devices)
|
||||
(let ((target-devices (or target-devices
|
||||
(installer-target-device-candidates state))))
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(match command
|
||||
(('set field value)
|
||||
(command-success (update-state-field state field value)
|
||||
'continue
|
||||
target-devices))
|
||||
(('action 'next)
|
||||
(let* ((current (installer-state-selected-step state))
|
||||
(_ (validate-installer-state-for-step state current))
|
||||
(next (next-installer-step-id current)))
|
||||
(command-success (if next
|
||||
(step-transition state next)
|
||||
state)
|
||||
'continue
|
||||
target-devices)))
|
||||
(('action 'back)
|
||||
(let ((prev (previous-installer-step-id (installer-state-selected-step state))))
|
||||
(command-success (if prev
|
||||
(step-transition state prev)
|
||||
state)
|
||||
'continue
|
||||
target-devices)))
|
||||
(('action 'proceed)
|
||||
(let* ((current (installer-state-selected-step state))
|
||||
(_ (unless (memq current '(summary install))
|
||||
(error "installer proceed is only valid from summary/install" current state)))
|
||||
(_ (validate-installer-state-for-step state 'install))
|
||||
(final-state (step-transition state 'install)))
|
||||
(command-success final-state 'proceed target-devices)))
|
||||
(('action 'abort)
|
||||
(command-success state 'abort target-devices))
|
||||
(('action 'goto step-id)
|
||||
(unless (find-installer-step step-id)
|
||||
(error "unknown installer step" step-id))
|
||||
(command-success (step-transition state step-id)
|
||||
'continue
|
||||
target-devices))
|
||||
(_
|
||||
(error "unsupported installer command" command))))
|
||||
(lambda args
|
||||
`((state . ,state)
|
||||
(result . error)
|
||||
(error . ,args)
|
||||
(target-devices . ,target-devices))))))
|
||||
|
||||
(define* (run-installer-command-script state commands #:key target-devices)
|
||||
(let loop ((state state)
|
||||
(pending commands)
|
||||
(transcript '()))
|
||||
(if (null? pending)
|
||||
`((state . ,state)
|
||||
(result . completed)
|
||||
(transcript . ,(reverse transcript))
|
||||
(target-devices . ,(or target-devices
|
||||
(installer-target-device-candidates state))))
|
||||
(let* ((command (car pending))
|
||||
(response (installer-command-apply state command
|
||||
#:target-devices target-devices))
|
||||
(next-state (assoc-ref response 'state))
|
||||
(result (assoc-ref response 'result))
|
||||
(entry `((command . ,command)
|
||||
(result . ,result)
|
||||
(selected-step . ,(installer-state-selected-step next-state))
|
||||
(state . ,(installer-state-spec next-state))
|
||||
(error . ,(or (assoc-ref response 'error) #f)))))
|
||||
(if (eq? result 'continue)
|
||||
(loop next-state
|
||||
(cdr pending)
|
||||
(cons entry transcript))
|
||||
`((state . ,next-state)
|
||||
(result . ,result)
|
||||
(transcript . ,(reverse (cons entry transcript)))
|
||||
(error . ,(or (assoc-ref response 'error) #f))
|
||||
(target-devices . ,(assoc-ref response 'target-devices))))))))
|
||||
@@ -1,8 +1,14 @@
|
||||
(define-module (fruix installer newt)
|
||||
#:use-module (fruix installer final)
|
||||
#:use-module (fruix installer flow)
|
||||
#:use-module (fruix installer state)
|
||||
#:use-module (fruix installer steps)
|
||||
#:use-module (fruix system storage)
|
||||
#:use-module (ice-9 match)
|
||||
#:use-module (ice-9 popen)
|
||||
#:use-module (rnrs io ports)
|
||||
#:use-module (srfi srfi-1)
|
||||
#:use-module (srfi srfi-13)
|
||||
#:export (newt-required-bindings
|
||||
missing-newt-bindings
|
||||
newt-available?
|
||||
@@ -14,7 +20,25 @@
|
||||
newt-finish
|
||||
clear-screen
|
||||
message-window
|
||||
choice-window))
|
||||
choice-window
|
||||
make-centered-window
|
||||
pop-window
|
||||
make-form
|
||||
add-components-to-form
|
||||
set-current-component
|
||||
run-form
|
||||
destroy-form
|
||||
make-button
|
||||
make-label
|
||||
make-textbox
|
||||
set-textbox-text
|
||||
make-entry
|
||||
entry-value
|
||||
make-radio-button
|
||||
current-radio-button
|
||||
components=?
|
||||
FLAG-BORDER
|
||||
FLAG-SCROLL))
|
||||
|
||||
(define (newt-interface)
|
||||
(false-if-exception (resolve-interface '(newt))))
|
||||
@@ -33,27 +57,353 @@
|
||||
(define (newt-available?)
|
||||
(null? (missing-newt-bindings)))
|
||||
|
||||
(define (call-newt name . args)
|
||||
(let ((proc (newt-binding name)))
|
||||
(unless proc
|
||||
(define (newt-ref name)
|
||||
(let ((value (newt-binding name)))
|
||||
(unless value
|
||||
(error "missing required Newt binding" name))
|
||||
value))
|
||||
|
||||
(define (call-newt name . args)
|
||||
(let ((proc (newt-ref name)))
|
||||
(apply proc args)))
|
||||
|
||||
(define (newt-result base-fields mode available? result error missing)
|
||||
(define (render-error args)
|
||||
(match args
|
||||
((_ message . _) (if (string? message)
|
||||
message
|
||||
(call-with-output-string
|
||||
(lambda (port)
|
||||
(write args port)))))
|
||||
(_
|
||||
(call-with-output-string
|
||||
(lambda (port)
|
||||
(write args port))))))
|
||||
|
||||
(define (state-fields state steps)
|
||||
`((state . ,(installer-state-spec state))
|
||||
(step-count . ,(length steps))
|
||||
(step-ids . ,(map installer-step-id steps))))
|
||||
|
||||
(define (newt-result state mode available? result error missing steps)
|
||||
(append `((ui . newt)
|
||||
(mode . ,mode)
|
||||
(available? . ,available?)
|
||||
(result . ,result)
|
||||
(error . ,error)
|
||||
(missing-bindings . ,missing))
|
||||
base-fields))
|
||||
(state-fields state steps)))
|
||||
|
||||
(define (show-error-page title text)
|
||||
(call-newt 'message-window title "OK" text)
|
||||
'ok)
|
||||
|
||||
(define (button-x-position count index)
|
||||
(cond
|
||||
((= count 1) 30)
|
||||
((= count 2) (list-ref '(20 42) index))
|
||||
((= count 3) (list-ref '(8 30 52) index))
|
||||
(else (+ 4 (* index 14)))))
|
||||
|
||||
(define (make-buttons button-specs y)
|
||||
(let ((count (length button-specs)))
|
||||
(let loop ((rest button-specs)
|
||||
(index 0)
|
||||
(result '()))
|
||||
(match rest
|
||||
(() (reverse result))
|
||||
((spec . tail)
|
||||
(loop tail
|
||||
(+ index 1)
|
||||
(cons (cons (car spec)
|
||||
(call-newt 'make-button
|
||||
(button-x-position count index)
|
||||
y
|
||||
(cdr spec)))
|
||||
result)))))))
|
||||
|
||||
(define (button-action buttons component)
|
||||
(let ((match-entry (find (lambda (entry)
|
||||
(call-newt 'components=? (cdr entry) component))
|
||||
buttons)))
|
||||
(and match-entry (car match-entry))))
|
||||
|
||||
(define (run-form-page title width height components focus buttons)
|
||||
(let ((form (call-newt 'make-form)))
|
||||
(dynamic-wind
|
||||
(lambda ()
|
||||
(call-newt 'make-centered-window width height title))
|
||||
(lambda ()
|
||||
(apply call-newt (append (list 'add-components-to-form form) components))
|
||||
(when focus
|
||||
(call-newt 'set-current-component form focus))
|
||||
(let loop ()
|
||||
(call-with-values (lambda ()
|
||||
(call-newt 'run-form form))
|
||||
(lambda (reason payload)
|
||||
(match reason
|
||||
('exit-component
|
||||
(or (button-action buttons payload)
|
||||
(loop)))
|
||||
(_
|
||||
(loop)))))))
|
||||
(lambda ()
|
||||
(false-if-exception
|
||||
(call-newt 'destroy-form form))
|
||||
(false-if-exception
|
||||
(call-newt 'pop-window))))))
|
||||
|
||||
(define* (run-text-page title info-text button-specs
|
||||
#:key
|
||||
(width 72)
|
||||
(height 16)
|
||||
(textbox-height 8))
|
||||
(let* ((textbox (call-newt 'make-textbox 2 1 66 textbox-height
|
||||
(+ (newt-ref 'FLAG-BORDER)
|
||||
(newt-ref 'FLAG-SCROLL))))
|
||||
(_ (call-newt 'set-textbox-text textbox info-text))
|
||||
(buttons (make-buttons button-specs (- height 3))))
|
||||
(run-form-page title width height
|
||||
(append (list textbox) (map cdr buttons))
|
||||
(cdr (car buttons))
|
||||
buttons)))
|
||||
|
||||
(define* (run-entry-page title info-text field-label initial-value button-specs
|
||||
#:key
|
||||
(width 72)
|
||||
(height 16))
|
||||
(let* ((textbox (call-newt 'make-textbox 2 1 66 4
|
||||
(+ (newt-ref 'FLAG-BORDER)
|
||||
(newt-ref 'FLAG-SCROLL))))
|
||||
(_ (call-newt 'set-textbox-text textbox info-text))
|
||||
(label (call-newt 'make-label 2 7 field-label))
|
||||
(entry (call-newt 'make-entry 2 8 66 #:initial-value (or initial-value "")))
|
||||
(buttons (make-buttons button-specs (- height 3)))
|
||||
(action (run-form-page title width height
|
||||
(append (list textbox label entry) (map cdr buttons))
|
||||
entry
|
||||
buttons)))
|
||||
`((action . ,action)
|
||||
(value . ,(call-newt 'entry-value entry)))))
|
||||
|
||||
(define* (run-radio-page title info-text items current-value button-specs
|
||||
#:key
|
||||
(width 72)
|
||||
(height 18))
|
||||
(if (null? items)
|
||||
'((action . abort) (value . #f))
|
||||
(let* ((textbox (call-newt 'make-textbox 2 1 66 4
|
||||
(+ (newt-ref 'FLAG-BORDER)
|
||||
(newt-ref 'FLAG-SCROLL))))
|
||||
(_ (call-newt 'set-textbox-text textbox info-text))
|
||||
(radios
|
||||
(let loop ((rest items)
|
||||
(index 0)
|
||||
(previous #f)
|
||||
(result '()))
|
||||
(match rest
|
||||
(() (reverse result))
|
||||
(((value . text) . tail)
|
||||
(let* ((radio (if previous
|
||||
(call-newt 'make-radio-button 4 (+ 6 index) text
|
||||
(and current-value (equal? value current-value))
|
||||
previous)
|
||||
(call-newt 'make-radio-button 4 (+ 6 index) text
|
||||
(and current-value (equal? value current-value))))))
|
||||
(loop tail
|
||||
(+ index 1)
|
||||
radio
|
||||
(cons (cons value radio) result)))))))
|
||||
(focus (cdar radios))
|
||||
(buttons (make-buttons button-specs (- height 3)))
|
||||
(action (run-form-page title width height
|
||||
(append (list textbox)
|
||||
(map cdr radios)
|
||||
(map cdr buttons))
|
||||
focus
|
||||
buttons))
|
||||
(selected-radio (call-newt 'current-radio-button focus))
|
||||
(selected-entry (find (lambda (entry)
|
||||
(call-newt 'components=? (cdr entry) selected-radio))
|
||||
radios)))
|
||||
`((action . ,action)
|
||||
(value . ,(and selected-entry (car selected-entry)))))))
|
||||
|
||||
(define (apply-installer-commands state commands target-devices)
|
||||
(let loop ((current-state state)
|
||||
(pending commands))
|
||||
(if (null? pending)
|
||||
`((state . ,current-state)
|
||||
(result . continue)
|
||||
(target-devices . ,target-devices))
|
||||
(let* ((response (installer-command-apply current-state
|
||||
(car pending)
|
||||
#:target-devices target-devices))
|
||||
(result (assoc-ref response 'result))
|
||||
(next-state (assoc-ref response 'state)))
|
||||
(if (and (eq? result 'continue)
|
||||
(pair? (cdr pending)))
|
||||
(loop next-state (cdr pending))
|
||||
response)))))
|
||||
|
||||
(define (handle-page-response state commands target-devices)
|
||||
(let ((response (apply-installer-commands state commands target-devices)))
|
||||
(if (eq? (assoc-ref response 'result) 'error)
|
||||
(begin
|
||||
(show-error-page "Installer validation error"
|
||||
(render-error (assoc-ref response 'error)))
|
||||
`((state . ,(assoc-ref response 'state))
|
||||
(result . retry)))
|
||||
response)))
|
||||
|
||||
(define (welcome-text)
|
||||
(string-append
|
||||
"Welcome to the Fruix installer.\n\n"
|
||||
"This first interactive installer guides you through target disk selection,\n"
|
||||
"review of the default GPT + EFI + UFS storage layout, hostname setup,\n"
|
||||
"and a minimal network policy before handing control back to the installer\n"
|
||||
"engine to apply the plan."))
|
||||
|
||||
(define (storage-layout-text state)
|
||||
(let ((layout (installer-state-effective-storage-layout state)))
|
||||
(string-append
|
||||
"The default Fruix v1 layout uses a single GPT disk with an EFI system\n"
|
||||
"partition and a UFS root filesystem.\n\n"
|
||||
(if layout
|
||||
(render-storage-layout-summary layout)
|
||||
"storage-layout <unset>"))))
|
||||
|
||||
(define (network-mode-text mode)
|
||||
(case mode
|
||||
((dhcp) "Use DHCP when networking is needed during installation")
|
||||
(else "Do not make installer networking changes")))
|
||||
|
||||
(define (run-welcome-page state)
|
||||
(let ((action (run-text-page "Fruix installer" (welcome-text)
|
||||
'((next . "Continue")
|
||||
(abort . "Abort"))
|
||||
#:height 15
|
||||
#:textbox-height 7)))
|
||||
(case action
|
||||
((next) (handle-page-response state '((action next)) '()))
|
||||
(else `((state . ,state) (result . abort))))))
|
||||
|
||||
(define (read-kern-disks)
|
||||
(false-if-exception
|
||||
(let ((port (open-pipe* OPEN_READ "sysctl" "-n" "kern.disks")))
|
||||
(let ((text (get-string-all port)))
|
||||
(close-pipe port)
|
||||
text))))
|
||||
|
||||
(define (run-target-disk-page state)
|
||||
(let* ((target-devices (installer-target-device-candidates state #:kern-disks (or (read-kern-disks) "")))
|
||||
(items (map (lambda (device)
|
||||
(cons device device))
|
||||
target-devices)))
|
||||
(if (null? items)
|
||||
(begin
|
||||
(show-error-page "No install target disks"
|
||||
"No suitable target disks were discovered. Re-run the installer with --install-target-device /dev/NAME or boot inside the installer environment.")
|
||||
`((state . ,state) (result . abort)))
|
||||
(let* ((response (run-radio-page "Target disk"
|
||||
"Choose the disk that Fruix should overwrite and install to. The selected disk will be repartitioned."
|
||||
items
|
||||
(or (installer-state-target-device state)
|
||||
(caar items))
|
||||
'((back . "Back")
|
||||
(next . "Next")
|
||||
(abort . "Abort"))))
|
||||
(action (assoc-ref response 'action))
|
||||
(value (assoc-ref response 'value)))
|
||||
(case action
|
||||
((back) (handle-page-response state '((action back)) target-devices))
|
||||
((next) (handle-page-response state
|
||||
`((set target-device ,value)
|
||||
(action next))
|
||||
target-devices))
|
||||
(else `((state . ,state) (result . abort))))))))
|
||||
|
||||
(define (run-storage-layout-page state)
|
||||
(let ((action (run-text-page "Storage layout"
|
||||
(storage-layout-text state)
|
||||
'((back . "Back")
|
||||
(next . "Next")
|
||||
(abort . "Abort"))
|
||||
#:height 18
|
||||
#:textbox-height 11)))
|
||||
(case action
|
||||
((back) (handle-page-response state '((action back)) '()))
|
||||
((next) (handle-page-response state '((action next)) '()))
|
||||
(else `((state . ,state) (result . abort))))))
|
||||
|
||||
(define (run-hostname-page state)
|
||||
(let* ((response (run-entry-page "Hostname"
|
||||
"Choose the hostname that the installed Fruix system should use on first boot."
|
||||
"Host name"
|
||||
(installer-state-host-name state)
|
||||
'((back . "Back")
|
||||
(next . "Next")
|
||||
(abort . "Abort"))))
|
||||
(action (assoc-ref response 'action))
|
||||
(value (string-trim-both (assoc-ref response 'value))))
|
||||
(case action
|
||||
((back) (handle-page-response state '((action back)) '()))
|
||||
((next) (handle-page-response state
|
||||
`((set host-name ,value)
|
||||
(action next))
|
||||
'()))
|
||||
(else `((state . ,state) (result . abort))))))
|
||||
|
||||
(define (run-network-page state)
|
||||
(let* ((items '((none . "No installer networking changes")
|
||||
(dhcp . "Use wired DHCP when networking is needed")))
|
||||
(response (run-radio-page "Network"
|
||||
"Choose the simple installer networking policy. Fruix keeps the first interactive installer deliberately small and wired-first."
|
||||
items
|
||||
(installer-state-network-mode state)
|
||||
'((back . "Back")
|
||||
(next . "Next")
|
||||
(abort . "Abort"))))
|
||||
(action (assoc-ref response 'action))
|
||||
(value (assoc-ref response 'value)))
|
||||
(case action
|
||||
((back) (handle-page-response state '((action back)) '()))
|
||||
((next) (handle-page-response state
|
||||
`((set network-mode ,value)
|
||||
(action next))
|
||||
'()))
|
||||
(else `((state . ,state) (result . abort))))))
|
||||
|
||||
(define (run-summary-page state)
|
||||
(let* ((summary-text
|
||||
(string-append
|
||||
(installer-final-summary-text state)
|
||||
"\n"
|
||||
(format #f "network policy: ~a\n" (network-mode-text (installer-state-network-mode state)))
|
||||
"\nSelect Install to hand control back to the Fruix installer engine.\n"
|
||||
"The engine will apply the shared storage/install backend outside this UI."))
|
||||
(action (run-text-page "Review and install"
|
||||
summary-text
|
||||
'((back . "Back")
|
||||
(proceed . "Install")
|
||||
(abort . "Abort"))
|
||||
#:height 20
|
||||
#:textbox-height 13)))
|
||||
(case action
|
||||
((back) (handle-page-response state '((action back)) '()))
|
||||
((proceed) (handle-page-response state '((action proceed)) '()))
|
||||
(else `((state . ,state) (result . abort))))))
|
||||
|
||||
(define* (run-newt-self-test #:key (steps %default-installer-steps))
|
||||
(let* ((missing (missing-newt-bindings))
|
||||
(base-fields `((step-count . ,(length steps))
|
||||
(step-ids . ,(map installer-step-id steps)))))
|
||||
(let ((missing (missing-newt-bindings)))
|
||||
(if (pair? missing)
|
||||
(newt-result base-fields 'self-test #f 'unavailable #f missing)
|
||||
`((ui . newt)
|
||||
(mode . self-test)
|
||||
(available? . #f)
|
||||
(result . unavailable)
|
||||
(error . #f)
|
||||
(missing-bindings . ,missing)
|
||||
(step-count . ,(length steps))
|
||||
(step-ids . ,(map installer-step-id steps)))
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(dynamic-wind
|
||||
@@ -61,20 +411,31 @@
|
||||
(call-newt 'newt-init)
|
||||
(call-newt 'clear-screen))
|
||||
(lambda ()
|
||||
(newt-result base-fields 'self-test #t 'self-test-ok #f '()))
|
||||
`((ui . newt)
|
||||
(mode . self-test)
|
||||
(available? . #t)
|
||||
(result . self-test-ok)
|
||||
(error . #f)
|
||||
(missing-bindings . ())
|
||||
(step-count . ,(length steps))
|
||||
(step-ids . ,(map installer-step-id steps))))
|
||||
(lambda ()
|
||||
(false-if-exception
|
||||
(call-newt 'newt-finish)))))
|
||||
(lambda args
|
||||
(newt-result base-fields 'self-test #t 'error args '()))))))
|
||||
`((ui . newt)
|
||||
(mode . self-test)
|
||||
(available? . #t)
|
||||
(result . error)
|
||||
(error . ,args)
|
||||
(missing-bindings . ())
|
||||
(step-count . ,(length steps))
|
||||
(step-ids . ,(map installer-step-id steps))))))))
|
||||
|
||||
(define* (run-newt-installer state #:key (steps %default-installer-steps))
|
||||
(let* ((missing (missing-newt-bindings))
|
||||
(base-fields `((state . ,(installer-state-spec state))
|
||||
(step-count . ,(length steps))
|
||||
(step-ids . ,(map installer-step-id steps)))))
|
||||
(let ((missing (missing-newt-bindings)))
|
||||
(if (pair? missing)
|
||||
(newt-result base-fields 'interactive #f 'unavailable #f missing)
|
||||
(newt-result state 'interactive #f 'unavailable #f missing steps)
|
||||
(catch #t
|
||||
(lambda ()
|
||||
(dynamic-wind
|
||||
@@ -82,26 +443,38 @@
|
||||
(call-newt 'newt-init)
|
||||
(call-newt 'clear-screen))
|
||||
(lambda ()
|
||||
(call-newt 'message-window
|
||||
"Fruix installer"
|
||||
"Continue"
|
||||
(installer-state-summary state))
|
||||
(let ((result (call-newt 'choice-window
|
||||
"Fruix installer review"
|
||||
"Proceed"
|
||||
"Abort"
|
||||
(installer-final-summary-text state))))
|
||||
(newt-result base-fields
|
||||
'interactive
|
||||
#t
|
||||
(match result
|
||||
(1 'proceed)
|
||||
(2 'abort)
|
||||
(_ 'unknown))
|
||||
#f
|
||||
'())))
|
||||
(let loop ((current-state state))
|
||||
(let* ((step-id (installer-state-selected-step current-state))
|
||||
(response (case step-id
|
||||
((welcome) (run-welcome-page current-state))
|
||||
((target-disk) (run-target-disk-page current-state))
|
||||
((storage-layout) (run-storage-layout-page current-state))
|
||||
((hostname) (run-hostname-page current-state))
|
||||
((network) (run-network-page current-state))
|
||||
((summary install complete) (run-summary-page current-state))
|
||||
(else
|
||||
`((state . ,current-state)
|
||||
(result . error)
|
||||
(error . (unknown-step ,step-id))))))
|
||||
(result (assoc-ref response 'result))
|
||||
(next-state (assoc-ref response 'state)))
|
||||
(case result
|
||||
((continue retry)
|
||||
(loop next-state))
|
||||
((proceed abort)
|
||||
(newt-result next-state 'interactive #t result #f '() steps))
|
||||
((error)
|
||||
(newt-result next-state 'interactive #t 'error
|
||||
(assoc-ref response 'error)
|
||||
'()
|
||||
steps))
|
||||
(else
|
||||
(newt-result next-state 'interactive #t 'error
|
||||
`(unexpected-result ,result)
|
||||
'()
|
||||
steps))))))
|
||||
(lambda ()
|
||||
(false-if-exception
|
||||
(call-newt 'newt-finish)))))
|
||||
(lambda args
|
||||
(newt-result base-fields 'interactive #t 'error args '()))))))
|
||||
(newt-result state 'interactive #t 'error args '() steps))))))
|
||||
|
||||
@@ -497,6 +497,8 @@
|
||||
. "share/fruix/node/modules/fruix/installer.scm")
|
||||
(,(string-append repo-root "/modules/fruix/installer/final.scm")
|
||||
. "share/fruix/node/modules/fruix/installer/final.scm")
|
||||
(,(string-append repo-root "/modules/fruix/installer/flow.scm")
|
||||
. "share/fruix/node/modules/fruix/installer/flow.scm")
|
||||
(,(string-append repo-root "/modules/fruix/installer/newt.scm")
|
||||
. "share/fruix/node/modules/fruix/installer/newt.scm")
|
||||
(,(string-append repo-root "/modules/fruix/installer/state.scm")
|
||||
|
||||
+29
-22
@@ -627,28 +627,35 @@ Common options:\n\
|
||||
(installer_store_item_count . ,(length installer-store-items))))))
|
||||
|
||||
(define (emit-installer-tui-metadata os-file resolved-symbol store-dir state result)
|
||||
(emit-metadata
|
||||
`((action . "installer-tui")
|
||||
(os_file . ,os-file)
|
||||
(system_variable . ,resolved-symbol)
|
||||
(store_dir . ,store-dir)
|
||||
(newt_mode . ,(assoc-ref result 'mode))
|
||||
(newt_available . ,(assoc-ref result 'available?))
|
||||
(newt_result . ,(assoc-ref result 'result))
|
||||
(newt_error . ,(or (assoc-ref result 'error) #f))
|
||||
(missing_bindings . ,(assoc-ref result 'missing-bindings))
|
||||
(step_count . ,(assoc-ref result 'step-count))
|
||||
(step_ids . ,(assoc-ref result 'step-ids))
|
||||
(selected_step . ,(installer-state-selected-step state))
|
||||
(host_name . ,(installer-state-host-name state))
|
||||
(target_device . ,(installer-state-target-device state))
|
||||
(root_size . ,(installer-state-root-size state))
|
||||
(disk_capacity . ,(installer-state-disk-capacity state))
|
||||
(network_mode . ,(installer-state-network-mode state))
|
||||
(storage_layout . ,(and (installer-state-storage-layout state)
|
||||
(storage-layout-spec (installer-state-storage-layout state))))
|
||||
(effective_storage_layout . ,(and (installer-state-effective-storage-layout state)
|
||||
(storage-layout-spec (installer-state-effective-storage-layout state)))))))
|
||||
(let* ((state-spec (or (assoc-ref result 'state)
|
||||
(installer-state-spec state)))
|
||||
(state-value (lambda (key fallback)
|
||||
(let ((entry (assoc key state-spec)))
|
||||
(if entry (cdr entry) fallback)))))
|
||||
(emit-metadata
|
||||
`((action . "installer-tui")
|
||||
(os_file . ,os-file)
|
||||
(system_variable . ,resolved-symbol)
|
||||
(store_dir . ,store-dir)
|
||||
(newt_mode . ,(assoc-ref result 'mode))
|
||||
(newt_available . ,(assoc-ref result 'available?))
|
||||
(newt_result . ,(assoc-ref result 'result))
|
||||
(newt_error . ,(or (assoc-ref result 'error) #f))
|
||||
(missing_bindings . ,(assoc-ref result 'missing-bindings))
|
||||
(step_count . ,(assoc-ref result 'step-count))
|
||||
(step_ids . ,(assoc-ref result 'step-ids))
|
||||
(selected_step . ,(state-value 'selected-step (installer-state-selected-step state)))
|
||||
(host_name . ,(state-value 'host-name (installer-state-host-name state)))
|
||||
(target_device . ,(state-value 'target-device (installer-state-target-device state)))
|
||||
(root_size . ,(state-value 'root-size (installer-state-root-size state)))
|
||||
(disk_capacity . ,(state-value 'disk-capacity (installer-state-disk-capacity state)))
|
||||
(network_mode . ,(state-value 'network-mode (installer-state-network-mode state)))
|
||||
(storage_layout . ,(state-value 'storage-layout
|
||||
(and (installer-state-storage-layout state)
|
||||
(storage-layout-spec (installer-state-storage-layout state)))))
|
||||
(effective_storage_layout . ,(state-value 'effective-storage-layout
|
||||
(and (installer-state-effective-storage-layout state)
|
||||
(storage-layout-spec (installer-state-effective-storage-layout state)))))))))
|
||||
|
||||
(define (emit-native-build-promotion-metadata store-dir result-root result)
|
||||
(emit-metadata
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
(use-modules (srfi srfi-64)
|
||||
(srfi srfi-13)
|
||||
(fruix installer)
|
||||
(fruix system freebsd))
|
||||
|
||||
(define (make-test-state)
|
||||
(installer-state
|
||||
#:operating-system #f
|
||||
#:storage-layout (default-freebsd-efi-ufs-storage-layout #:device #f)
|
||||
#:target-device #f
|
||||
#:host-name #f
|
||||
#:root-size #f
|
||||
#:disk-capacity #f
|
||||
#:network-mode default-installer-network-mode
|
||||
#:selected-step 'welcome
|
||||
#:metadata '()))
|
||||
|
||||
(define (response-state response)
|
||||
(assoc-ref response 'state))
|
||||
|
||||
(define (response-result response)
|
||||
(assoc-ref response 'result))
|
||||
|
||||
(test-begin "installer-basic")
|
||||
|
||||
(let ((candidates (installer-target-device-candidates
|
||||
(installer-state #:storage-layout (default-freebsd-efi-ufs-storage-layout #:device #f)
|
||||
#:target-device "/dev/ada0"
|
||||
#:host-name "seed"
|
||||
#:selected-step 'target-disk)
|
||||
#:kern-disks "cd0 ada1 ada0 md0 pass0 nvd0")))
|
||||
(test-equal "target-device candidate filtering"
|
||||
'("/dev/ada0" "/dev/ada1" "/dev/nvd0")
|
||||
candidates))
|
||||
|
||||
(let* ((response (run-installer-command-script
|
||||
(make-test-state)
|
||||
'((action next)
|
||||
(set target-device "/dev/ada0")
|
||||
(action next)
|
||||
(action next)
|
||||
(set host-name "apollo")
|
||||
(action next)
|
||||
(set network-mode dhcp)
|
||||
(action next)
|
||||
(action proceed))
|
||||
#:target-devices '("/dev/ada0" "/dev/ada1")))
|
||||
(state (response-state response)))
|
||||
(test-equal "happy-path script result" 'proceed (response-result response))
|
||||
(test-equal "happy-path final step" 'install (installer-state-selected-step state))
|
||||
(test-equal "happy-path target device" "/dev/ada0" (installer-state-target-device state))
|
||||
(test-equal "happy-path host name" "apollo" (installer-state-host-name state))
|
||||
(test-equal "happy-path network mode" 'dhcp (installer-state-network-mode state))
|
||||
(test-assert "happy-path ready for install"
|
||||
(installer-state-ready-for-install? state))
|
||||
(test-assert "happy-path review text mentions host"
|
||||
(string-contains (installer-final-summary-text state) "host-name: apollo")))
|
||||
|
||||
(let* ((response (run-installer-command-script
|
||||
(make-test-state)
|
||||
'((action next)
|
||||
(action next))
|
||||
#:target-devices '("/dev/ada0")))
|
||||
(state (response-state response)))
|
||||
(test-equal "missing target device yields error" 'error (response-result response))
|
||||
(test-equal "missing target device stays on target step" 'target-disk (installer-state-selected-step state)))
|
||||
|
||||
(let* ((response (run-installer-command-script
|
||||
(make-test-state)
|
||||
'((action next)
|
||||
(set target-device "/dev/ada0")
|
||||
(action next)
|
||||
(action back))
|
||||
#:target-devices '("/dev/ada0")))
|
||||
(state (response-state response)))
|
||||
(test-equal "back navigation returns to target-disk" 'target-disk (installer-state-selected-step state)))
|
||||
|
||||
(let* ((response (installer-command-apply (make-test-state)
|
||||
'(action proceed)
|
||||
#:target-devices '("/dev/ada0"))))
|
||||
(test-equal "proceed only valid from summary/install" 'error (response-result response)))
|
||||
|
||||
(test-end "installer-basic")
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
|
||||
. "$HOME/.local/opt/fruix-builder/env.sh"
|
||||
|
||||
guile_version=$($GUILE_BIN -c '(display (effective-version))')
|
||||
guile_load_path="$repo_root/modules:$GUIX_SOURCE_DIR:$HOME/.local/opt/fruix-builder/shepherd/share/guile/site/$guile_version${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}"
|
||||
guile_load_compiled_path="$HOME/.local/opt/fruix-builder/shepherd/lib/guile/$guile_version/site-ccache${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}"
|
||||
|
||||
env \
|
||||
GUILE_AUTO_COMPILE=0 \
|
||||
GUILE_LOAD_PATH="$guile_load_path" \
|
||||
GUILE_LOAD_COMPILED_PATH="$guile_load_compiled_path" \
|
||||
"$GUILE_BIN" --no-auto-compile "$repo_root/tests/installer-basic.scm"
|
||||
Reference in New Issue
Block a user