code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018, 2020 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2020 Florian Pelz <pelzflorian@pelzflorian.de>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer record)
  #:use-module (guix records)
  #:use-module (srfi srfi-1)
  #:export (<installer>
            installer
            make-installer
            installer?
            installer-name
            installer-init
            installer-exit
            installer-exit-error
            installer-final-page
            installer-keymap-page
            installer-locale-page
            installer-menu-page
            installer-network-page
            installer-substitutes-page
            installer-timezone-page
            installer-hostname-page
            installer-user-page
            installer-partition-page
            installer-services-page
            installer-welcome-page
            installer-parameters-menu
            installer-parameters-page))

;;;
;;; Installer record.
;;;

;; The <installer> record contains pages that will be run to prompt the user
;; for the system configuration. The goal of the installer is to produce a
;; complete <operating-system> record and install it.

(define-record-type* <installer>
  installer make-installer
  installer?
  ;; symbol
  (name installer-name)
  ;; procedure: void -> void
  (init installer-init)
  ;; procedure: void -> void
  (exit installer-exit)
  ;; procedure (key arguments) -> void
  (exit-error installer-exit-error)
  ;; procedure void -> void
  (final-page installer-final-page)
  ;; procedure (layouts context) -> (list layout variant options)
  (keymap-page installer-keymap-page)
  ;; procedure: (#:key supported-locales iso639-languages iso3166-territories)
  ;; -> glibc-locale
  (locale-page installer-locale-page)
  ;; procedure: (steps) -> step-id
  (menu-page installer-menu-page)
  ;; procedure void -> void
  (network-page installer-network-page)
  ;; procedure void -> void
  (substitutes-page installer-substitutes-page)
  ;; procedure (zonetab) -> posix-timezone
  (timezone-page installer-timezone-page)
  ;; procedure void -> void
  (hostname-page installer-hostname-page)
  ;; procedure void -> void
  (user-page installer-user-page)
  ;; procedure void -> void
  (partition-page installer-partition-page)
  ;; procedure void -> void
  (services-page installer-services-page)
  ;; procedure (logo) -> void
  (welcome-page installer-welcome-page)
  ;; procedure (menu-proc) -> void
  (parameters-menu installer-parameters-menu)
  ;; procedure (keyboard-layout-selection) -> void
  (parameters-page installer-parameters-page))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
;;; Copyright © 2021 Tobias Geerinckx-Rice <me@tobias.gr>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer services)
  #:use-module (guix records)
  #:use-module (srfi srfi-1)
  #:export (system-service?
            system-service-name
            system-service-type
            system-service-recommended?
            system-service-snippet
            system-service-packages

            desktop-system-service?

            %system-services
            system-services->configuration))

(define-record-type* <system-service>
  system-service make-system-service
  system-service?
  (name            system-service-name)           ;string
  (type            system-service-type)           ;'desktop|'networking|…
  (recommended?    system-service-recommended?    ;Boolean
                   (default #f))
  (snippet         system-service-snippet         ;list of sexps
                   (default '()))
  (packages        system-service-packages        ;list of sexps
                   (default '())))

(define %system-services
  (let-syntax ((desktop-environment (syntax-rules ()
                                      ((_ fields ...)
                                       (system-service
                                        (type 'desktop)
                                        fields ...))))
               (G_ (syntax-rules ()               ;for xgettext
                     ((_ str) str))))
    (list
     ;; This is the list of desktop environments supported as services.
     (desktop-environment
      (name "GNOME")
      (snippet '((service gnome-desktop-service-type))))
     (desktop-environment
      (name "Xfce")
      (snippet '((service xfce-desktop-service-type))))
     (desktop-environment
      (name "MATE")
      (snippet '((service mate-desktop-service-type))))
     (desktop-environment
      (name "Enlightenment")
      (snippet '((service enlightenment-desktop-service-type))))
     (desktop-environment
      (name "Openbox")
      (packages '((specification->package "openbox"))))
     (desktop-environment
      (name "awesome")
      (packages '((specification->package "awesome"))))
     (desktop-environment
      (name "i3")
      (packages (map (lambda (package)
                       `(specification->package ,package))
                     '("i3-wm" "i3status" "dmenu" "st"))))
     (desktop-environment
      (name "ratpoison")
      (packages '((specification->package "ratpoison")
                  (specification->package "xterm"))))
     (desktop-environment
      (name "Emacs EXWM")
      (packages '((specification->package "emacs")
                  (specification->package "emacs-exwm")
                  (specification->package "emacs-desktop-environment"))))

     ;; Networking.
     (system-service
      (name (G_ "OpenSSH secure shell daemon (sshd)"))
      (type 'networking)
      (snippet '((service openssh-service-type))))
     (system-service
      (name (G_ "Tor anonymous network router"))
      (type 'networking)
      (snippet '((service tor-service-type))))
     (system-service
      (name (G_ "Mozilla NSS certificates, for HTTPS access"))
      (type 'networking)
      (packages '((specification->package "nss-certs")))
      (recommended? #t))

     ;; Network connectivity management.
     (system-service
      (name (G_ "NetworkManager network connection manager"))
      (type 'network-management)
      (snippet '((service network-manager-service-type)
                 (service wpa-supplicant-service-type))))
     (system-service
      (name (G_ "Connman network connection manager"))
      (type 'network-management)
      (snippet '((service connman-service-type)
                 (service wpa-supplicant-service-type))))
     (system-service
      (name (G_ "DHCP client (dynamic IP address assignment)"))
      (type 'network-management)
      (snippet '((service dhcp-client-service-type))))

     ;; Dealing with documents.
     (system-service
      (name (G_ "CUPS printing system (no Web interface by default)"))
      (type 'document)
      (snippet '((service cups-service-type)))))))

(define (desktop-system-service? service)
  "Return true if SERVICE is a desktop environment service."
  (eq? 'desktop (system-service-type service)))

(define (system-services->configuration services)
  "Return the configuration field for SERVICES."
  (let* ((snippets (append-map system-service-snippet services))
         (packages (append-map system-service-packages services))
         (desktop? (find desktop-system-service? services))
         (base     (if desktop?
                       '%desktop-services
                       '%base-services)))
    (if (null? snippets)
        `(,@(if (null? packages)
                '()
                `((packages (append (list ,@packages)
                                    %base-packages))))
          (services ,base))
        `(,@(if (null? packages)
                '()
                `((packages (append (list ,@packages)
                                    %base-packages))))
          (services (append (list ,@snippets

                                  ,@(if desktop?
                                        ;; XXX: Assume 'keyboard-layout' is in
                                        ;; scope.
                                        '((set-xorg-configuration
                                           (xorg-configuration
                                            (keyboard-layout keyboard-layout))))
                                        '()))
                           ,base))))))

repo_name: maximed/guix

license: mit


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018, 2019 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2020, 2021 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer steps)
  #:use-module (guix records)
  #:use-module (guix build utils)
  #:use-module (guix i18n)
  #:use-module (gnu installer utils)
  #:use-module (ice-9 match)
  #:use-module (ice-9 pretty-print)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (rnrs io ports)
  #:export (&installer-step-abort
            installer-step-abort?

            &installer-step-break
            installer-step-break?

            <installer-step>
            installer-step
            make-installer-step
            installer-step?
            installer-step-id
            installer-step-description
            installer-step-compute
            installer-step-configuration-formatter

            run-installer-steps
            find-step-by-id
            result->step-ids
            result-step
            result-step-done?

            %installer-configuration-file
            %installer-target-dir
            format-configuration
            configuration->file))

;; This condition may be raised to abort the current step.
(define-condition-type &installer-step-abort &condition
  installer-step-abort?)

;; This condition may be raised to break out from the steps execution.
(define-condition-type &installer-step-break &condition
  installer-step-break?)

;; An installer-step record is basically an id associated to a compute
;; procedure. The COMPUTE procedure takes exactly one argument, an association
;; list containing the results of previously executed installer-steps (see
;; RUN-INSTALLER-STEPS description). The value returned by the COMPUTE
;; procedure will be stored in the results list passed to the next
;; installer-step and so on.
(define-record-type* <installer-step>
  installer-step make-installer-step
  installer-step?
  (id                         installer-step-id) ;symbol
  (description                installer-step-description ;string
                              (default #f)

                              ;; Make it thunked so that 'G_' is called at the
                              ;; right time, as opposed to being called once
                              ;; when the installer starts.
                              (thunked))
  (compute                    installer-step-compute) ;procedure
  (configuration-formatter    installer-step-configuration-formatter ;procedure
                              (default #f)))

(define* (run-installer-steps #:key
                              steps
                              (rewind-strategy 'previous)
                              (menu-proc (const #f)))
  "Run the COMPUTE procedure of all <installer-step> records in STEPS
sequentially.  If the &installer-step-abort condition is raised, fallback to a
previous install-step, accordingly to the specified REWIND-STRATEGY.

REWIND-STRATEGY possible values are 'previous, 'menu and 'start.  If 'previous
is selected, the execution will resume at the previous installer-step. If
'menu is selected, the MENU-PROC procedure will be called. Its return value
has to be an installer-step ID to jump to. The ID has to be the one of a
previously executed step. It is impossible to jump forward. Finally if 'start
is selected, the execution will resume at the first installer-step.

The result of every COMPUTE procedures is stored in an association list, under
the form:

		'((STEP-ID . COMPUTE-RESULT) ...)

where STEP-ID is the ID field of the installer-step and COMPUTE-RESULT the
result of the associated COMPUTE procedure. This result association list is
passed as argument of every COMPUTE procedure. It is finally returned when the
computation is over.

If the &installer-step-break condition is raised, stop the computation and
return the accumalated result so far."
  (define (pop-result list)
    (cdr list))

  (define (first-step? steps step)
    (match steps
      ((first-step . rest-steps)
       (equal? first-step step))))

  (define* (skip-to-step step result
                         #:key todo-steps done-steps)
    (match todo-steps
      ((todo . rest-todo)
       (let ((found? (eq? (installer-step-id todo)
                          (installer-step-id step))))
         (cond
          (found?
           (run result
                #:todo-steps todo-steps
                #:done-steps done-steps))
          ((and (not found?)
                (null? done-steps))
           (error (format #f "Step ~a not found" (installer-step-id step))))
          (else
           (match done-steps
             ((prev-done ... last-done)
              (skip-to-step step (pop-result result)
                            #:todo-steps (cons last-done todo-steps)
                            #:done-steps prev-done)))))))))

  (define* (run result #:key todo-steps done-steps)
    (match todo-steps
      (() (reverse result))
      ((step . rest-steps)
       (guard (c ((installer-step-abort? c)
                  (case rewind-strategy
                    ((previous)
                     (match done-steps
                       (()
                        ;; We cannot go previous the first step. So re-raise
                        ;; the exception. It might be useful in the case of
                        ;; nested run-installer-steps. Abort to 'raise-above
                        ;; prompt to prevent the condition from being catched
                        ;; by one of the previously installed guard.
                        (abort-to-prompt 'raise-above c))
                       ((prev-done ... last-done)
                        (run (pop-result result)
                             #:todo-steps (cons last-done todo-steps)
                             #:done-steps prev-done))))
                    ((menu)
                     (let ((goto-step (menu-proc
                                       (append done-steps (list step)))))
                       (if (eq? goto-step step)
                           (run result
                                #:todo-steps todo-steps
                                #:done-steps done-steps)
                           (skip-to-step goto-step result
                                         #:todo-steps todo-steps
                                         #:done-steps done-steps))))
                    ((start)
                     (if (null? done-steps)
                         ;; Same as above, it makes no sense to jump to start
                         ;; when we are at the first installer-step. Abort to
                         ;; 'raise-above prompt to re-raise the condition.
                         (abort-to-prompt 'raise-above c)
                         (run '()
                              #:todo-steps steps
                              #:done-steps '())))))
                 ((installer-step-break? c)
                  (reverse result)))
         (syslog "running step '~a'~%" (installer-step-id step))
         (let* ((id (installer-step-id step))
                (compute (installer-step-compute step))
                (res (compute result done-steps)))
           (run (alist-cons id res result)
                #:todo-steps rest-steps
                #:done-steps (append done-steps (list step))))))))

  ;; Ignore SIGPIPE so that we don't die if a client closes the connection
  ;; prematurely.
  (sigaction SIGPIPE SIG_IGN)

  (with-server-socket
    (call-with-prompt 'raise-above
      (lambda ()
        (run '()
             #:todo-steps steps
             #:done-steps '()))
      (lambda (k condition)
        (raise condition)))))

(define (find-step-by-id steps id)
  "Find and return the step in STEPS whose id is equal to ID."
  (find (lambda (step)
          (eq? (installer-step-id step) id))
        steps))

(define (result-step results step-id)
  "Return the result of the installer-step specified by STEP-ID in
RESULTS."
  (assoc-ref results step-id))

(define (result-step-done? results step-id)
  "Return #t if the installer-step specified by STEP-ID has a COMPUTE value
stored in RESULTS. Return #f otherwise."
  (and (assoc step-id results) #t))

(define %installer-configuration-file (make-parameter "/mnt/etc/config.scm"))
(define %installer-target-dir (make-parameter "/mnt"))

(define (format-configuration steps results)
  "Return the list resulting from the application of the procedure defined in
CONFIGURATION-FORMATTER field of <installer-step> on the associated result
found in RESULTS."
  (let ((configuration
         (append-map
          (lambda (step)
            (let* ((step-id (installer-step-id step))
                   (conf-formatter
                    (installer-step-configuration-formatter step))
                   (result-step (result-step results step-id)))
              (if (and result-step conf-formatter)
                  (conf-formatter result-step)
                  '())))
          steps))
        (modules '((use-modules (gnu))
                   (use-service-modules cups desktop networking ssh xorg))))
    `(,@modules
      ()
      (operating-system ,@configuration))))

(define* (configuration->file configuration
                              #:key (filename (%installer-configuration-file)))
  "Write the given CONFIGURATION to FILENAME."
  (mkdir-p (dirname filename))
  (call-with-output-file filename
    (lambda (port)
      ;; TRANSLATORS: This is a comment within a Scheme file.  Each line must
      ;; start with ";; " (two semicolons and a space).  Please keep line
      ;; length below 60 characters.
      (display (G_ "\
;; This is an operating system configuration generated
;; by the graphical installer.\n")
               port)
      (newline port)
      (for-each (lambda (part)
                  (if (null? part)
                      (newline port)
                      (pretty-print part port)))
                configuration)
      (flush-output-port port))))

;;; Local Variables:
;;; eval: (put 'with-server-socket 'scheme-indent-function 0)
;;; End:


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer substitutes)
  #:use-module (gnu installer utils)
  #:use-module (gnu services herd)
  #:export (enable-discovery
            disable-discovery))

(define (enable-discovery)
  (with-silent-shepherd
    (with-shepherd-action 'guix-daemon
        ('discover "on")
        result
      result)))

(define (disable-discovery)
  (with-silent-shepherd
    (with-shepherd-action 'guix-daemon
        ('discover "off")
        result
      result)))

;; Local Variables:
;; eval: (put 'with-silent-shepherd 'scheme-indent-function 0)
;; End:


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer tests)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 match)
  #:use-module (ice-9 regex)
  #:use-module (ice-9 pretty-print)
  #:export (&pattern-not-matched
            pattern-not-matched?

            %installer-socket-file
            open-installer-socket

            converse
            conversation-log-port

            choose-locale+keyboard
            enter-host-name+passwords
            choose-services
            choose-partitioning
            start-installation
            complete-installation

            edit-configuration-file))

;;; Commentary:
;;;
;;; This module provides tools to test the guided "graphical" installer in a
;;; non-interactive fashion.  The core of it is 'converse': it allows you to
;;; state Expect-style dialogues, which happen over the Unix-domain socket the
;;; installer listens to.  Higher-level procedures such as
;;; 'choose-locale+keyboard' are provided to perform specific parts of the
;;; dialogue.
;;;
;;; Code:

(define %installer-socket-file
  ;; Socket the installer listens to.
  "/var/guix/installer-socket")

(define* (open-installer-socket #:optional (file %installer-socket-file))
  "Return a socket connected to the installer."
  (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
    (connect sock AF_UNIX file)
    sock))

(define-condition-type &pattern-not-matched &error
  pattern-not-matched?
  (pattern pattern-not-matched-pattern)
  (sexp    pattern-not-matched-sexp))

(define (pattern-error pattern sexp)
  (raise (condition
          (&pattern-not-matched
           (pattern pattern) (sexp sexp)))))

(define conversation-log-port
  ;; Port where debugging info is logged
  (make-parameter (current-error-port)))

(define (converse-debug pattern)
  (format (conversation-log-port)
          "conversation expecting pattern ~s~%"
          pattern))

(define-syntax converse
  (lambda (s)
    "Convert over PORT: read sexps from there, match them against each
PATTERN, and send the corresponding REPLY.  Raise to '&pattern-not-matched'
when one of the PATTERNs is not matched."

    ;; XXX: Strings that appear in PATTERNs must be in the language the
    ;; installer is running in.  In the future, we should add support to allow
    ;; writing English strings in PATTERNs and have the pattern matcher
    ;; automatically translate them.

    ;; Here we emulate 'pmatch' syntax on top of 'match'.  This is ridiculous
    ;; but that's because 'pmatch' compares objects with 'eq?', making it
    ;; pretty useless, and it doesn't support ellipses and such.

    (define (quote-pattern s)
      ;; Rewrite the pattern S from pmatch style (a ,b) to match style like
      ;; ('a b).
      (with-ellipsis :::
        (syntax-case s (unquote _ ...)
          ((unquote id) #'id)
          (_ #'_)
          (... #'...)
          (id
           (identifier? #'id)
           #''id)
          ((lst :::) (map quote-pattern #'(lst :::)))
          (pattern #'pattern))))

    (define (match-pattern s)
      ;; Match one pattern without a guard.
      (syntax-case s ()
        ((port (pattern reply) continuation)
         (with-syntax ((pattern (quote-pattern #'pattern)))
           #'(let ((pat 'pattern))
               (converse-debug pat)
               (match (read port)
                 (pattern
                  (let ((data (call-with-values (lambda () reply)
                                list)))
                    (for-each (lambda (obj)
                                (write obj port)
                                (newline port))
                              data)
                    (force-output port)
                    (continuation port)))
                 (sexp
                  (pattern-error pat sexp))))))))

    (syntax-case s ()
      ((_ port (pattern reply) rest ...)
       (match-pattern #'(port (pattern reply)
                              (lambda (port)
                                (converse port rest ...)))))
      ((_ port (pattern guard reply) rest ...)
       #`(let ((skip? (not guard))
               (next  (lambda (p)
                        (converse p rest ...))))
           (if skip?
               (next port)
               #,(match-pattern #'(port (pattern reply) next)))))
      ((_ port)
       #t))))

(define* (choose-locale+keyboard port
                                 #:key
                                 (language "English")
                                 (location "Hong Kong")
                                 (timezone '("Europe" "Zagreb"))
                                 (keyboard
                                  '("English (US)"
                                    "English (intl., with AltGr dead keys)")))
  "Converse over PORT with the guided installer to choose the specified
LANGUAGE, LOCATION, TIMEZONE, and KEYBOARD."
  (converse port
    ((list-selection (title "Locale language")
                     (multiple-choices? #f)
                     (items _))
     language)
    ((list-selection (title "Locale location")
                     (multiple-choices? #f)
                     (items _))
     location)
    ((menu (title "GNU Guix install")
           (text _)
           (items (,guided _ ...)))           ;"Guided graphical installation"
     guided)
    ((list-selection (title "Timezone")
                     (multiple-choices? #f)
                     (items _))
     (first timezone))
    ((list-selection (title "Timezone")
                     (multiple-choices? #f)
                     (items _))
     (second timezone))
    ((list-selection (title "Layout")
                     (multiple-choices? #f)
                     (items _))
     (first keyboard))
    ((list-selection (title "Variant")
                     (multiple-choices? #f)
                     (items _))
     (second keyboard))))

(define* (enter-host-name+passwords port
                                    #:key
                                    (host-name "guix")
                                    (root-password "foo")
                                    (users '(("alice" "pass1")
                                             ("bob" "pass2")
                                             ("charlie" "pass3"))))
  "Converse over PORT with the guided installer to choose HOST-NAME,
ROOT-PASSWORD, and USERS."
  (converse port
    ((input (title "Hostname") (text _) (default _))
     host-name)
    ((input (title "System administrator password") (text _) (default _))
     root-password)
    ((input (title "Password confirmation required") (text _) (default _))
     root-password)
    ((add-users)
     (match users
       (((names passwords) ...)
        (map (lambda (name password)
               `(user (name ,name) (real-name ,(string-titlecase name))
                      (home-directory ,(string-append "/home/" name))
                      (password ,password)))
             names passwords))))))

(define* (choose-services port
                          #:key
                          (choose-desktop-environment? (const #f))
                          (choose-network-service?
                           (lambda (service)
                             (or (string-contains service "SSH")
                                 (string-contains service "NSS"))))
                          (choose-network-management-tool?
                           (lambda (service)
                             (string-contains service "DHCP")))
                          (choose-other-service? (const #f)))
  "Converse over PORT to choose services."
  (define desktop-environments '())

  (converse port
    ((checkbox-list (title "Desktop environment") (text _)
                    (items ,services))
     (let ((desktops (filter choose-desktop-environment? services)))
       (set! desktop-environments desktops)
       desktops))
    ((checkbox-list (title "Network service") (text _)
                    (items ,services))
     (filter choose-network-service? services))

    ;; The "Network management" dialog shows up only when no desktop
    ;; environments have been selected, hence the guard.
    ((list-selection (title "Network management")
                     (multiple-choices? #f)
                     (items ,services))
     (null? desktop-environments)
     (find choose-network-management-tool? services))

    ((checkbox-list (title "Other services") (text _)
                    (items ,services))
     (filter choose-other-service? services))))

(define (edit-configuration-file file)
  "Edit FILE, an operating system configuration file generated by the
installer, by adding a marionette service such that the installed OS is
instrumented for further testing."
  (define (read-expressions port)
    (let loop ((result '()))
      (match (read port)
        ((? eof-object?)
         (reverse result))
        (exp
         (loop (cons exp result))))))

  (define (edit exp)
    (match exp
      (('operating-system _ ...)
       `(marionette-operating-system ,exp
                                     #:imported-modules
                                     '((gnu services herd)
                                       (guix build utils)
                                       (guix combinators))))
      (_
       exp)))

  (let ((content (call-with-input-file file read-expressions)))
    (call-with-output-file file
      (lambda (port)
        (format port "\
;; Operating system configuration edited for automated testing.~%~%")

        (pretty-print '(use-modules (gnu tests)) port)
        (for-each (lambda (exp)
                    (pretty-print (edit exp) port)
                    (newline port))
                  content)))

    #t))

(define* (choose-partitioning port
                              #:key
                              (encrypted? #t)
                              (uefi-support? #f)
                              (passphrase "thepassphrase")
                              (edit-configuration-file
                               edit-configuration-file))
  "Converse over PORT to choose the partitioning method.  When ENCRYPTED? is
true, choose full-disk encryption with PASSPHRASE as the LUKS passphrase.

When UEFI-SUPPORT? is true, assume that we are running the installation tests
on an UEFI capable machine.

This conversation stops when the user partitions have been formatted, right
before the installer generates the configuration file and shows it in a dialog
box. "
  (converse port
    ((list-selection (title "Partitioning method")
                     (multiple-choices? #f)
                     (items (,not-encrypted ,encrypted _ ...)))
     (if encrypted?
         encrypted
         not-encrypted))
    ((list-selection (title "Disk") (multiple-choices? #f)
                     (items (,disks ...)))
     ;; When running the installation from an ISO image, the CD/DVD drive
     ;; shows up in the list.  Avoid it.
     (find (lambda (disk)
             (not (or (string-contains disk "DVD")
                      (string-contains disk "CD-ROM"))))
           disks))

    ;; The "Partition table" dialog pops up only if there's not already a
    ;; partition table and if the system does not support UEFI.
    ((list-selection (title "Partition table")
                     (multiple-choices? #f)
                     (items _))
     ;; When UEFI is supported, the partition is forced to GPT by the
     ;; installer.
     (not uefi-support?)
     "gpt")

    ((list-selection (title "Partition scheme")
                     (multiple-choices? #f)
                     (items (,one-partition _ ...)))
     one-partition)
    ((list-selection (title "Guided partitioning")
                     (multiple-choices? #f)
                     (items (,disk _ ...)))
     disk)
    ((input (title "Password required")
            (text _) (default #f))
     encrypted?                                   ;only when ENCRYPTED?
     passphrase)
    ((input (title "Password confirmation required")
            (text _) (default #f))
     encrypted?
     passphrase)
    ((confirmation (title "Format disk?") (text _))
     #t)
    ((info (title "Preparing partitions") _ ...)
     (values))                                    ;nothing to return
    ((starting-final-step)
     ;; Do not return anything.  The reply will be sent by
     ;; 'conclude-installation' and in the meantime the installer just waits
     ;; for us, giving us a chance to do things such as changing partition
     ;; UUIDs before it generates the configuration file.
     (values))))

(define (start-installation port)
  "Start the installation by checking over PORT that we get the generated
configuration file, accepting it and starting the installation, and then
receiving the pause message once the 'guix system init' process has
completed."
  ;; Assume the previous message received was 'starting-final-step'; here we
  ;; send the reply to that message, which lets the installer continue.
  (write #t port)
  (newline port)
  (force-output port)

  (converse port
    ((file-dialog (title "Configuration file")
                  (text _)
                  (file ,configuration-file))
     (edit-configuration-file configuration-file))
    ((pause)                                      ;"Press Enter to continue."
     (values))))

(define (complete-installation port)
  "Complete the installation by replying to the installer pause message and
waiting for the installation-complete message."
  ;; Assume the previous message received was 'pause'; here we send the reply
  ;; to that message, which lets the installer continue.
  (write #t port)
  (newline port)
  (force-output port)

  (converse port
    ((installation-complete)
     (values))))

;;; Local Variables:
;;; eval: (put 'converse 'scheme-indent-function 1)
;;; eval: (put 'with-ellipsis 'scheme-indent-function 1)
;;; End:


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer timezone)
  #:use-module (gnu installer utils)
  #:use-module (guix i18n)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 match)
  #:use-module (ice-9 receive)
  #:export (locate-children
            timezone->posix-tz
            timezone-has-child?
            zonetab->timezone-tree
            posix-tz->configuration))

(define %not-blank
  (char-set-complement char-set:blank))

(define (posix-tz->timezone tz)
  "Convert given TZ in Posix format like \"Europe/Paris\" into a list like
(\"Europe\" \"Paris\")."
  (string-split tz #\/))

(define (timezone->posix-tz timezone)
  "Convert given TIMEZONE like (\"Europe\" \"Paris\") into a Posix timezone
like \"Europe/Paris\"."
  (string-join timezone "/"))

(define (zonetab->timezones zonetab)
  "Parse ZONETAB file and return the corresponding list of timezones."

  (define (zonetab-line->posix-tz line)
    (let ((tokens (string-tokenize line %not-blank)))
      (match tokens
        ((code coordinates tz _ ...)
         tz))))

  (call-with-input-file zonetab
    (lambda (port)
      (let* ((lines (read-lines port))
             ;; Filter comment lines starting with '#' character.
             (tz-lines (filter (lambda (line)
                                 (not (eq? (string-ref line 0)
                                           #\#)))
                               lines)))
        (map (lambda (line)
               (posix-tz->timezone
                (zonetab-line->posix-tz line)))
             tz-lines)))))

(define (timezones->timezone-tree timezones)
  "Convert the list of timezones, TIMEZONES into a tree under the form:

	(\"America\" (\"North_Dakota\" \"New_Salem\" \"Center\"))

representing America/North_Dakota/New_Salem and America/North_Dakota/Center
timezones."

  (define (remove-first lists)
    "Remove the first element of every sublists in the argument LISTS."
    (map (lambda (list)
           (if (null? list) list (cdr list)))
         lists))

  (let loop ((cur-timezones timezones))
    (match cur-timezones
      (() '())
      (((region . rest-region) . rest-timezones)
       (if (null? rest-region)
           (cons (list region) (loop rest-timezones))
           (receive (same-region other-region)
               (partition (lambda (timezone)
                            (string=? (car timezone) region))
                          cur-timezones)
             (acons region
                    (loop (remove-first same-region))
                    (loop other-region))))))))

(define (locate-children tree path)
  "Return the children of the timezone indicated by PATH in the given
TREE. Raise a condition if the PATH could not be found."
  (let ((extract-proc (cut map car <>)))
    (match path
      (() (sort (extract-proc tree) string<?))
      ((region . rest)
       (or (and=> (assoc-ref tree region)
                  (cut locate-children <> rest))
           (raise
            (condition
             (&message
              (message
               (format #f (G_ "Unable to locate path: ~a.") path))))))))))

(define (timezone-has-child? tree timezone)
  "Return #t if the given TIMEZONE any child in TREE and #f otherwise."
  (not (null? (locate-children tree timezone))))

(define* (zonetab->timezone-tree zonetab)
  "Return the timezone tree corresponding to the given ZONETAB file."
  (timezones->timezone-tree (zonetab->timezones zonetab)))

;;;
;;; Configuration formatter.
;;;

(define (posix-tz->configuration timezone)
  "Return the configuration field for TIMEZONE."
  `((timezone ,timezone)))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer user)
  #:use-module (guix records)
  #:use-module (srfi srfi-1)
  #:export (<user>
            user
            make-user
            user-name
            user-real-name
            user-group
            user-home-directory
            user-password

            users->configuration))

(define-record-type* <user>
  user make-user
  user?
  (name            user-name)
  (real-name       user-real-name
                   (default ""))
  (group           user-group
                   (default "users"))
  (password        user-password)
  (home-directory  user-home-directory))

(define (users->configuration users)
  "Return the configuration field for USERS."
  (define (user->sexp user)
    `(user-account
      (name ,(user-name user))
      (comment ,(user-real-name user))
      (group ,(user-group user))
      (home-directory ,(user-home-directory user))
      (supplementary-groups '("wheel" "netdev"
                              "audio" "video"))))

  `((users (cons*
            ,@(filter-map (lambda (user)
                            ;; Do not emit a 'user-account' form for "root".
                            (and (not (string=? (user-name user) "root"))
                                 (user->sexp user)))
                          users)
            %base-user-accounts))))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018, 2020 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2019, 2020 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu installer utils)
  #:use-module (gnu services herd)
  #:use-module (guix utils)
  #:use-module (guix build utils)
  #:use-module (guix i18n)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-19)
  #:use-module (srfi srfi-34)
  #:use-module (ice-9 match)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 regex)
  #:use-module (ice-9 format)
  #:use-module (ice-9 textual-ports)
  #:export (read-lines
            read-all
            nearest-exact-integer
            read-percentage
            run-command

            syslog-port
            syslog
            call-with-time
            let/time

            with-server-socket
            current-server-socket
            current-clients
            send-to-clients

            with-silent-shepherd))

(define* (read-lines #:optional (port (current-input-port)))
  "Read lines from PORT and return them as a list."
  (let loop ((line (read-line port))
             (lines '()))
    (if (eof-object? line)
        (reverse lines)
        (loop (read-line port)
              (cons line lines)))))

(define (read-all file)
  "Return the content of the given FILE as a string."
  (call-with-input-file file
    get-string-all))

(define (nearest-exact-integer x)
  "Given a real number X, return the nearest exact integer, with ties going to
the nearest exact even integer."
  (inexact->exact (round x)))

(define (read-percentage percentage)
  "Read PERCENTAGE string and return the corresponding percentage as a
number. If no percentage is found, return #f"
  (let ((result (string-match "^([0-9]+)%$" percentage)))
    (and result
         (string->number (match:substring result 1)))))

(define* (run-command command #:key locale)
  "Run COMMAND, a list of strings, in the given LOCALE.  Return true if
COMMAND exited successfully, #f otherwise."
  (define env (environ))

  (define (pause)
    (format #t (G_ "Press Enter to continue.~%"))
    (send-to-clients '(pause))
    (environ env)                               ;restore environment variables
    (match (select (cons (current-input-port) (current-clients))
             '() '())
      (((port _ ...) _ _)
       (read-line port))))

  (setenv "PATH" "/run/current-system/profile/bin")

  (when locale
    (let ((supported? (false-if-exception
                       (setlocale LC_ALL locale))))
      ;; If LOCALE is not supported, then set LANGUAGE, which might at
      ;; least give us translated messages.
      (if supported?
          (setenv "LC_ALL" locale)
          (setenv "LANGUAGE"
                  (string-take locale
                               (or (string-index locale #\_)
                                   (string-length locale)))))))

  (guard (c ((invoke-error? c)
             (newline)
             (format (current-error-port)
                     (G_ "Command failed with exit code ~a.~%")
                     (invoke-error-exit-status c))
             (syslog "command ~s failed with exit code ~a"
                     command (invoke-error-exit-status c))
             (pause)
             #f))
    (syslog "running command ~s~%" command)
    (apply invoke command)
    (syslog "command ~s succeeded~%" command)
    (newline)
    (pause)
    #t))

;;;
;;; Logging.
;;;

(define (call-with-time thunk kont)
  "Call THUNK and pass KONT the elapsed time followed by THUNK's return
values."
  (let* ((start  (current-time time-monotonic))
         (result (call-with-values thunk list))
         (end    (current-time time-monotonic)))
    (apply kont (time-difference end start) result)))

(define-syntax-rule (let/time ((time result exp)) body ...)
  (call-with-time (lambda () exp) (lambda (time result) body ...)))

(define (open-syslog-port)
  "Return an open port (a socket) to /dev/log or #f if that wasn't possible."
  (let ((sock (socket AF_UNIX SOCK_DGRAM 0)))
    (catch 'system-error
      (lambda ()
        (connect sock AF_UNIX "/dev/log")
        (setvbuf sock 'line)
        sock)
      (lambda args
        (close-port sock)
        #f))))

(define syslog-port
  (let ((port #f))
    (lambda ()
      "Return an output port to syslog."
      (unless port
        (set! port (open-syslog-port)))
      (or port (%make-void-port "w")))))

(define-syntax syslog
  (lambda (s)
    "Like 'format', but write to syslog."
    (syntax-case s ()
      ((_ fmt args ...)
       (string? (syntax->datum #'fmt))
       (with-syntax ((fmt (string-append "installer[~d]: "
                                         (syntax->datum #'fmt))))
         #'(format (syslog-port) fmt (getpid) args ...))))))

;;;
;;; Client protocol.
;;;

(define %client-socket-file
  ;; Unix-domain socket where the installer accepts connections.
  "/var/guix/installer-socket")

(define current-server-socket
  ;; Socket on which the installer is currently accepting connections, or #f.
  (make-parameter #f))

(define current-clients
  ;; List of currently connected clients.
  (make-parameter '()))

(define* (open-server-socket
          #:optional (socket-file %client-socket-file))
  "Open SOCKET-FILE as a Unix-domain socket to accept incoming connections and
return it."
  (mkdir-p (dirname socket-file))
  (when (file-exists? socket-file)
    (delete-file socket-file))
  (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
    (bind sock AF_UNIX socket-file)
    (listen sock 0)
    sock))

(define (call-with-server-socket thunk)
  (if (current-server-socket)
      (thunk)
      (let ((socket (open-server-socket)))
        (dynamic-wind
          (const #t)
          (lambda ()
            (parameterize ((current-server-socket socket))
              (thunk)))
          (lambda ()
            (close-port socket))))))

(define-syntax-rule (with-server-socket exp ...)
  "Evaluate EXP with 'current-server-socket' parameterized to a currently
accepting socket."
  (call-with-server-socket (lambda () exp ...)))

(define* (send-to-clients exp)
  "Send EXP to all the current clients."
  (define remainder
    (fold (lambda (client remainder)
            (catch 'system-error
              (lambda ()
                (write exp client)
                (newline client)
                (force-output client)
                (cons client remainder))
              (lambda args
                ;; We might get EPIPE if the client disconnects; when that
                ;; happens, remove CLIENT from the set of available clients.
                (let ((errno (system-error-errno args)))
                  (if (memv errno (list EPIPE ECONNRESET ECONNABORTED))
                      (begin
                        (syslog "removing client ~s due to ~s while replying~%"
                                (fileno client) (strerror errno))
                        (false-if-exception (close-port client))
                        remainder)
                      (cons client remainder))))))
          '()
          (current-clients)))

  (current-clients (reverse remainder))
  exp)

(define-syntax-rule (with-silent-shepherd exp ...)
  "Evaluate EXP while discarding shepherd messages."
  (parameterize ((shepherd-message-port
                  (%make-void-port "w")))
    exp ...))