diff --git a/modules/fruix/installer.scm b/modules/fruix/installer.scm index b9bb530..57d4467 100644 --- a/modules/fruix/installer.scm +++ b/modules/fruix/installer.scm @@ -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 diff --git a/modules/fruix/installer/final.scm b/modules/fruix/installer/final.scm index dfc48a6..9367e18 100644 --- a/modules/fruix/installer/final.scm +++ b/modules/fruix/installer/final.scm @@ -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"))) diff --git a/modules/fruix/installer/flow.scm b/modules/fruix/installer/flow.scm new file mode 100644 index 0000000..71f3ebc --- /dev/null +++ b/modules/fruix/installer/flow.scm @@ -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)))))))) diff --git a/modules/fruix/installer/newt.scm b/modules/fruix/installer/newt.scm index f21bdbc..942883c 100644 --- a/modules/fruix/installer/newt.scm +++ b/modules/fruix/installer/newt.scm @@ -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 ")))) + +(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)))))) diff --git a/modules/fruix/system/freebsd/render.scm b/modules/fruix/system/freebsd/render.scm index dd3f1b4..18e73b6 100644 --- a/modules/fruix/system/freebsd/render.scm +++ b/modules/fruix/system/freebsd/render.scm @@ -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") diff --git a/scripts/fruix.scm b/scripts/fruix.scm index 366b7e8..8a6d017 100644 --- a/scripts/fruix.scm +++ b/scripts/fruix.scm @@ -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 diff --git a/tests/installer-basic.scm b/tests/installer-basic.scm new file mode 100644 index 0000000..1db427d --- /dev/null +++ b/tests/installer-basic.scm @@ -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") diff --git a/tests/run-installer-basic.sh b/tests/run-installer-basic.sh new file mode 100755 index 0000000..be71863 --- /dev/null +++ b/tests/run-installer-basic.sh @@ -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"