aboutsummaryrefslogtreecommitdiff
path: root/gnu/installer
diff options
context:
space:
mode:
Diffstat (limited to 'gnu/installer')
-rw-r--r--gnu/installer/aux-files/SUPPORTED484
-rw-r--r--gnu/installer/aux-files/logo.txt19
-rw-r--r--gnu/installer/connman.scm400
-rw-r--r--gnu/installer/final.scm36
-rw-r--r--gnu/installer/hostname.scm23
-rw-r--r--gnu/installer/keymap.scm172
-rw-r--r--gnu/installer/locale.scm210
-rw-r--r--gnu/installer/newt.scm128
-rw-r--r--gnu/installer/newt/ethernet.scm81
-rw-r--r--gnu/installer/newt/final.scm86
-rw-r--r--gnu/installer/newt/hostname.scm26
-rw-r--r--gnu/installer/newt/keymap.scm122
-rw-r--r--gnu/installer/newt/locale.scm217
-rw-r--r--gnu/installer/newt/menu.scm44
-rw-r--r--gnu/installer/newt/network.scm173
-rw-r--r--gnu/installer/newt/page.scm530
-rw-r--r--gnu/installer/newt/partition.scm766
-rw-r--r--gnu/installer/newt/services.scm48
-rw-r--r--gnu/installer/newt/timezone.scm83
-rw-r--r--gnu/installer/newt/user.scm175
-rw-r--r--gnu/installer/newt/utils.scm43
-rw-r--r--gnu/installer/newt/welcome.scm118
-rw-r--r--gnu/installer/newt/wifi.scm243
-rw-r--r--gnu/installer/parted.scm1312
-rw-r--r--gnu/installer/record.scm84
-rw-r--r--gnu/installer/services.scm59
-rw-r--r--gnu/installer/steps.scm237
-rw-r--r--gnu/installer/timezone.scm127
-rw-r--r--gnu/installer/user.scm50
-rw-r--r--gnu/installer/utils.scm63
30 files changed, 6159 insertions, 0 deletions
diff --git a/gnu/installer/aux-files/SUPPORTED b/gnu/installer/aux-files/SUPPORTED
new file mode 100644
index 0000000000..24aae1e089
--- /dev/null
+++ b/gnu/installer/aux-files/SUPPORTED
@@ -0,0 +1,484 @@
+aa_DJ.UTF-8 UTF-8
+aa_DJ ISO-8859-1
+aa_ER UTF-8
+aa_ER@saaho UTF-8
+aa_ET UTF-8
+af_ZA.UTF-8 UTF-8
+af_ZA ISO-8859-1
+agr_PE UTF-8
+ak_GH UTF-8
+am_ET UTF-8
+an_ES.UTF-8 UTF-8
+an_ES ISO-8859-15
+anp_IN UTF-8
+ar_AE.UTF-8 UTF-8
+ar_AE ISO-8859-6
+ar_BH.UTF-8 UTF-8
+ar_BH ISO-8859-6
+ar_DZ.UTF-8 UTF-8
+ar_DZ ISO-8859-6
+ar_EG.UTF-8 UTF-8
+ar_EG ISO-8859-6
+ar_IN UTF-8
+ar_IQ.UTF-8 UTF-8
+ar_IQ ISO-8859-6
+ar_JO.UTF-8 UTF-8
+ar_JO ISO-8859-6
+ar_KW.UTF-8 UTF-8
+ar_KW ISO-8859-6
+ar_LB.UTF-8 UTF-8
+ar_LB ISO-8859-6
+ar_LY.UTF-8 UTF-8
+ar_LY ISO-8859-6
+ar_MA.UTF-8 UTF-8
+ar_MA ISO-8859-6
+ar_OM.UTF-8 UTF-8
+ar_OM ISO-8859-6
+ar_QA.UTF-8 UTF-8
+ar_QA ISO-8859-6
+ar_SA.UTF-8 UTF-8
+ar_SA ISO-8859-6
+ar_SD.UTF-8 UTF-8
+ar_SD ISO-8859-6
+ar_SS UTF-8
+ar_SY.UTF-8 UTF-8
+ar_SY ISO-8859-6
+ar_TN.UTF-8 UTF-8
+ar_TN ISO-8859-6
+ar_YE.UTF-8 UTF-8
+ar_YE ISO-8859-6
+ayc_PE UTF-8
+az_AZ UTF-8
+az_IR UTF-8
+as_IN UTF-8
+ast_ES.UTF-8 UTF-8
+ast_ES ISO-8859-15
+be_BY.UTF-8 UTF-8
+be_BY CP1251
+be_BY@latin UTF-8
+bem_ZM UTF-8
+ber_DZ UTF-8
+ber_MA UTF-8
+bg_BG.UTF-8 UTF-8
+bg_BG CP1251
+bhb_IN.UTF-8 UTF-8
+bho_IN UTF-8
+bho_NP UTF-8
+bi_VU UTF-8
+bn_BD UTF-8
+bn_IN UTF-8
+bo_CN UTF-8
+bo_IN UTF-8
+br_FR.UTF-8 UTF-8
+br_FR ISO-8859-1
+br_FR@euro ISO-8859-15
+brx_IN UTF-8
+bs_BA.UTF-8 UTF-8
+bs_BA ISO-8859-2
+byn_ER UTF-8
+ca_AD.UTF-8 UTF-8
+ca_AD ISO-8859-15
+ca_ES.UTF-8 UTF-8
+ca_ES ISO-8859-1
+ca_ES@euro ISO-8859-15
+ca_ES@valencia UTF-8
+ca_FR.UTF-8 UTF-8
+ca_FR ISO-8859-15
+ca_IT.UTF-8 UTF-8
+ca_IT ISO-8859-15
+ce_RU UTF-8
+chr_US UTF-8
+cmn_TW UTF-8
+crh_UA UTF-8
+cs_CZ.UTF-8 UTF-8
+cs_CZ ISO-8859-2
+csb_PL UTF-8
+cv_RU UTF-8
+cy_GB.UTF-8 UTF-8
+cy_GB ISO-8859-14
+da_DK.UTF-8 UTF-8
+da_DK ISO-8859-1
+de_AT.UTF-8 UTF-8
+de_AT ISO-8859-1
+de_AT@euro ISO-8859-15
+de_BE.UTF-8 UTF-8
+de_BE ISO-8859-1
+de_BE@euro ISO-8859-15
+de_CH.UTF-8 UTF-8
+de_CH ISO-8859-1
+de_DE.UTF-8 UTF-8
+de_DE ISO-8859-1
+de_DE@euro ISO-8859-15
+de_IT.UTF-8 UTF-8
+de_IT ISO-8859-1
+de_LI.UTF-8 UTF-8
+de_LU.UTF-8 UTF-8
+de_LU ISO-8859-1
+de_LU@euro ISO-8859-15
+doi_IN UTF-8
+dv_MV UTF-8
+dz_BT UTF-8
+el_GR.UTF-8 UTF-8
+el_GR ISO-8859-7
+el_GR@euro ISO-8859-7
+el_CY.UTF-8 UTF-8
+el_CY ISO-8859-7
+en_AG UTF-8
+en_AU.UTF-8 UTF-8
+en_AU ISO-8859-1
+en_BW.UTF-8 UTF-8
+en_BW ISO-8859-1
+en_CA.UTF-8 UTF-8
+en_CA ISO-8859-1
+en_DK.UTF-8 UTF-8
+en_DK ISO-8859-1
+en_GB.UTF-8 UTF-8
+en_GB ISO-8859-1
+en_HK.UTF-8 UTF-8
+en_HK ISO-8859-1
+en_IE.UTF-8 UTF-8
+en_IE ISO-8859-1
+en_IE@euro ISO-8859-15
+en_IL UTF-8
+en_IN UTF-8
+en_NG UTF-8
+en_NZ.UTF-8 UTF-8
+en_NZ ISO-8859-1
+en_PH.UTF-8 UTF-8
+en_PH ISO-8859-1
+en_SC.UTF-8 UTF-8
+en_SG.UTF-8 UTF-8
+en_SG ISO-8859-1
+en_US.UTF-8 UTF-8
+en_US ISO-8859-1
+en_ZA.UTF-8 UTF-8
+en_ZA ISO-8859-1
+en_ZM UTF-8
+en_ZW.UTF-8 UTF-8
+en_ZW ISO-8859-1
+eo UTF-8
+es_AR.UTF-8 UTF-8
+es_AR ISO-8859-1
+es_BO.UTF-8 UTF-8
+es_BO ISO-8859-1
+es_CL.UTF-8 UTF-8
+es_CL ISO-8859-1
+es_CO.UTF-8 UTF-8
+es_CO ISO-8859-1
+es_CR.UTF-8 UTF-8
+es_CR ISO-8859-1
+es_CU UTF-8
+es_DO.UTF-8 UTF-8
+es_DO ISO-8859-1
+es_EC.UTF-8 UTF-8
+es_EC ISO-8859-1
+es_ES.UTF-8 UTF-8
+es_ES ISO-8859-1
+es_ES@euro ISO-8859-15
+es_GT.UTF-8 UTF-8
+es_GT ISO-8859-1
+es_HN.UTF-8 UTF-8
+es_HN ISO-8859-1
+es_MX.UTF-8 UTF-8
+es_MX ISO-8859-1
+es_NI.UTF-8 UTF-8
+es_NI ISO-8859-1
+es_PA.UTF-8 UTF-8
+es_PA ISO-8859-1
+es_PE.UTF-8 UTF-8
+es_PE ISO-8859-1
+es_PR.UTF-8 UTF-8
+es_PR ISO-8859-1
+es_PY.UTF-8 UTF-8
+es_PY ISO-8859-1
+es_SV.UTF-8 UTF-8
+es_SV ISO-8859-1
+es_US.UTF-8 UTF-8
+es_US ISO-8859-1
+es_UY.UTF-8 UTF-8
+es_UY ISO-8859-1
+es_VE.UTF-8 UTF-8
+es_VE ISO-8859-1
+et_EE.UTF-8 UTF-8
+et_EE ISO-8859-1
+et_EE.ISO-8859-15 ISO-8859-15
+eu_ES.UTF-8 UTF-8
+eu_ES ISO-8859-1
+eu_ES@euro ISO-8859-15
+fa_IR UTF-8
+ff_SN UTF-8
+fi_FI.UTF-8 UTF-8
+fi_FI ISO-8859-1
+fi_FI@euro ISO-8859-15
+fil_PH UTF-8
+fo_FO.UTF-8 UTF-8
+fo_FO ISO-8859-1
+fr_BE.UTF-8 UTF-8
+fr_BE ISO-8859-1
+fr_BE@euro ISO-8859-15
+fr_CA.UTF-8 UTF-8
+fr_CA ISO-8859-1
+fr_CH.UTF-8 UTF-8
+fr_CH ISO-8859-1
+fr_FR.UTF-8 UTF-8
+fr_FR ISO-8859-1
+fr_FR@euro ISO-8859-15
+fr_LU.UTF-8 UTF-8
+fr_LU ISO-8859-1
+fr_LU@euro ISO-8859-15
+fur_IT UTF-8
+fy_NL UTF-8
+fy_DE UTF-8
+ga_IE.UTF-8 UTF-8
+ga_IE ISO-8859-1
+ga_IE@euro ISO-8859-15
+gd_GB.UTF-8 UTF-8
+gd_GB ISO-8859-15
+gez_ER UTF-8
+gez_ER@abegede UTF-8
+gez_ET UTF-8
+gez_ET@abegede UTF-8
+gl_ES.UTF-8 UTF-8
+gl_ES ISO-8859-1
+gl_ES@euro ISO-8859-15
+gu_IN UTF-8
+gv_GB.UTF-8 UTF-8
+gv_GB ISO-8859-1
+ha_NG UTF-8
+hak_TW UTF-8
+he_IL.UTF-8 UTF-8
+he_IL ISO-8859-8
+hi_IN UTF-8
+hif_FJ UTF-8
+hne_IN UTF-8
+hr_HR.UTF-8 UTF-8
+hr_HR ISO-8859-2
+hsb_DE ISO-8859-2
+hsb_DE.UTF-8 UTF-8
+ht_HT UTF-8
+hu_HU.UTF-8 UTF-8
+hu_HU ISO-8859-2
+hy_AM UTF-8
+hy_AM.ARMSCII-8 ARMSCII-8
+ia_FR UTF-8
+id_ID.UTF-8 UTF-8
+id_ID ISO-8859-1
+ig_NG UTF-8
+ik_CA UTF-8
+is_IS.UTF-8 UTF-8
+is_IS ISO-8859-1
+it_CH.UTF-8 UTF-8
+it_CH ISO-8859-1
+it_IT.UTF-8 UTF-8
+it_IT ISO-8859-1
+it_IT@euro ISO-8859-15
+iu_CA UTF-8
+ja_JP.EUC-JP EUC-JP
+ja_JP.UTF-8 UTF-8
+ka_GE.UTF-8 UTF-8
+ka_GE GEORGIAN-PS
+kab_DZ UTF-8
+kk_KZ.UTF-8 UTF-8
+kk_KZ PT154
+kl_GL.UTF-8 UTF-8
+kl_GL ISO-8859-1
+km_KH UTF-8
+kn_IN UTF-8
+ko_KR.EUC-KR EUC-KR
+ko_KR.UTF-8 UTF-8
+kok_IN UTF-8
+ks_IN UTF-8
+ks_IN@devanagari UTF-8
+ku_TR.UTF-8 UTF-8
+ku_TR ISO-8859-9
+kw_GB.UTF-8 UTF-8
+kw_GB ISO-8859-1
+ky_KG UTF-8
+lb_LU UTF-8
+lg_UG.UTF-8 UTF-8
+lg_UG ISO-8859-10
+li_BE UTF-8
+li_NL UTF-8
+lij_IT UTF-8
+ln_CD UTF-8
+lo_LA UTF-8
+lt_LT.UTF-8 UTF-8
+lt_LT ISO-8859-13
+lv_LV.UTF-8 UTF-8
+lv_LV ISO-8859-13
+lzh_TW UTF-8
+mag_IN UTF-8
+mai_IN UTF-8
+mai_NP UTF-8
+mfe_MU UTF-8
+mg_MG.UTF-8 UTF-8
+mg_MG ISO-8859-15
+mhr_RU UTF-8
+mi_NZ.UTF-8 UTF-8
+mi_NZ ISO-8859-13
+miq_NI UTF-8
+mjw_IN UTF-8
+mk_MK.UTF-8 UTF-8
+mk_MK ISO-8859-5
+ml_IN UTF-8
+mn_MN UTF-8
+mni_IN UTF-8
+mr_IN UTF-8
+ms_MY.UTF-8 UTF-8
+ms_MY ISO-8859-1
+mt_MT.UTF-8 UTF-8
+mt_MT ISO-8859-3
+my_MM UTF-8
+nan_TW UTF-8
+nan_TW@latin UTF-8
+nb_NO.UTF-8 UTF-8
+nb_NO ISO-8859-1
+nds_DE UTF-8
+nds_NL UTF-8
+ne_NP UTF-8
+nhn_MX UTF-8
+niu_NU UTF-8
+niu_NZ UTF-8
+nl_AW UTF-8
+nl_BE.UTF-8 UTF-8
+nl_BE ISO-8859-1
+nl_BE@euro ISO-8859-15
+nl_NL.UTF-8 UTF-8
+nl_NL ISO-8859-1
+nl_NL@euro ISO-8859-15
+nn_NO.UTF-8 UTF-8
+nn_NO ISO-8859-1
+nr_ZA UTF-8
+nso_ZA UTF-8
+oc_FR.UTF-8 UTF-8
+oc_FR ISO-8859-1
+om_ET UTF-8
+om_KE.UTF-8 UTF-8
+om_KE ISO-8859-1
+or_IN UTF-8
+os_RU UTF-8
+pa_IN UTF-8
+pa_PK UTF-8
+pap_AW UTF-8
+pap_CW UTF-8
+pl_PL.UTF-8 UTF-8
+pl_PL ISO-8859-2
+ps_AF UTF-8
+pt_BR.UTF-8 UTF-8
+pt_BR ISO-8859-1
+pt_PT.UTF-8 UTF-8
+pt_PT ISO-8859-1
+pt_PT@euro ISO-8859-15
+quz_PE UTF-8
+raj_IN UTF-8
+ro_RO.UTF-8 UTF-8
+ro_RO ISO-8859-2
+ru_RU.KOI8-R KOI8-R
+ru_RU.UTF-8 UTF-8
+ru_RU ISO-8859-5
+ru_UA.UTF-8 UTF-8
+ru_UA KOI8-U
+rw_RW UTF-8
+sa_IN UTF-8
+sat_IN UTF-8
+sc_IT UTF-8
+sd_IN UTF-8
+sd_IN@devanagari UTF-8
+se_NO UTF-8
+sgs_LT UTF-8
+shn_MM UTF-8
+shs_CA UTF-8
+si_LK UTF-8
+sid_ET UTF-8
+sk_SK.UTF-8 UTF-8
+sk_SK ISO-8859-2
+sl_SI.UTF-8 UTF-8
+sl_SI ISO-8859-2
+sm_WS UTF-8
+so_DJ.UTF-8 UTF-8
+so_DJ ISO-8859-1
+so_ET UTF-8
+so_KE.UTF-8 UTF-8
+so_KE ISO-8859-1
+so_SO.UTF-8 UTF-8
+so_SO ISO-8859-1
+sq_AL.UTF-8 UTF-8
+sq_AL ISO-8859-1
+sq_MK UTF-8
+sr_ME UTF-8
+sr_RS UTF-8
+sr_RS@latin UTF-8
+ss_ZA UTF-8
+st_ZA.UTF-8 UTF-8
+st_ZA ISO-8859-1
+sv_FI.UTF-8 UTF-8
+sv_FI ISO-8859-1
+sv_FI@euro ISO-8859-15
+sv_SE.UTF-8 UTF-8
+sv_SE ISO-8859-1
+sw_KE UTF-8
+sw_TZ UTF-8
+szl_PL UTF-8
+ta_IN UTF-8
+ta_LK UTF-8
+tcy_IN.UTF-8 UTF-8
+te_IN UTF-8
+tg_TJ.UTF-8 UTF-8
+tg_TJ KOI8-T
+th_TH.UTF-8 UTF-8
+th_TH TIS-620
+the_NP UTF-8
+ti_ER UTF-8
+ti_ET UTF-8
+tig_ER UTF-8
+tk_TM UTF-8
+tl_PH.UTF-8 UTF-8
+tl_PH ISO-8859-1
+tn_ZA UTF-8
+to_TO UTF-8
+tpi_PG UTF-8
+tr_CY.UTF-8 UTF-8
+tr_CY ISO-8859-9
+tr_TR.UTF-8 UTF-8
+tr_TR ISO-8859-9
+ts_ZA UTF-8
+tt_RU UTF-8
+tt_RU@iqtelif UTF-8
+ug_CN UTF-8
+uk_UA.UTF-8 UTF-8
+uk_UA KOI8-U
+unm_US UTF-8
+ur_IN UTF-8
+ur_PK UTF-8
+uz_UZ.UTF-8 UTF-8
+uz_UZ ISO-8859-1
+uz_UZ@cyrillic UTF-8
+ve_ZA UTF-8
+vi_VN UTF-8
+wa_BE ISO-8859-1
+wa_BE@euro ISO-8859-15
+wa_BE.UTF-8 UTF-8
+wae_CH UTF-8
+wal_ET UTF-8
+wo_SN UTF-8
+xh_ZA.UTF-8 UTF-8
+xh_ZA ISO-8859-1
+yi_US.UTF-8 UTF-8
+yi_US CP1255
+yo_NG UTF-8
+yue_HK UTF-8
+yuw_PG UTF-8
+zh_CN.GB18030 GB18030
+zh_CN.GBK GBK
+zh_CN.UTF-8 UTF-8
+zh_CN GB2312
+zh_HK.UTF-8 UTF-8
+zh_HK BIG5-HKSCS
+zh_SG.UTF-8 UTF-8
+zh_SG.GBK GBK
+zh_SG GB2312
+zh_TW.EUC-TW EUC-TW
+zh_TW.UTF-8 UTF-8
+zh_TW BIG5
+zu_ZA.UTF-8 UTF-8
+zu_ZA ISO-8859-1
diff --git a/gnu/installer/aux-files/logo.txt b/gnu/installer/aux-files/logo.txt
new file mode 100644
index 0000000000..52418d88c1
--- /dev/null
+++ b/gnu/installer/aux-files/logo.txt
@@ -0,0 +1,19 @@
+ ░░░ ░░░
+ ░░▒▒░░░░░░░░░ ░░░░░░░░░▒▒░░
+ ░░▒▒▒▒▒░░░░░░░ ░░░░░░░▒▒▒▒▒░
+ ░▒▒▒░░▒▒▒▒▒ ░░░░░░░▒▒░
+ ░▒▒▒▒░ ░░░░░░
+ ▒▒▒▒▒ ░░░░░░
+ ▒▒▒▒▒ ░░░░░
+ ░▒▒▒▒▒ ░░░░░
+ ▒▒▒▒▒ ░░░░░
+ ▒▒▒▒▒ ░░░░░
+ ░▒▒▒▒▒░░░░░
+ ▒▒▒▒▒▒░░░
+ ▒▒▒▒▒▒░
+ _____ _ _ _ _ _____ _
+ / ____| \ | | | | | / ____| (_)
+| | __| \| | | | | | | __ _ _ ___ __
+| | |_ | . ' | | | | | | |_ | | | | \ \/ /
+| |__| | |\ | |__| | | |__| | |_| | |> <
+ \_____|_| \_|\____/ \_____|\__,_|_/_/\_\
diff --git a/gnu/installer/connman.scm b/gnu/installer/connman.scm
new file mode 100644
index 0000000000..740df7424a
--- /dev/null
+++ b/gnu/installer/connman.scm
@@ -0,0 +1,400 @@
+;;; 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 connman)
+ #:use-module (gnu installer utils)
+ #:use-module (guix records)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 popen)
+ #:use-module (ice-9 regex)
+ #:use-module (srfi srfi-11)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:export (<technology>
+ technology
+ technology?
+ technology-name
+ technology-type
+ technology-powered?
+ technology-connected?
+
+ <service>
+ service
+ service?
+ service-name
+ service-type
+ service-path
+ service-strength
+ service-state
+
+ &connman-error
+ connman-error?
+ connman-error-command
+ connman-error-output
+ connman-error-status
+
+ &connman-connection-error
+ connman-connection-error?
+ connman-connection-error-service
+ connman-connection-error-output
+
+ &connman-password-error
+ connman-password-error?
+
+ &connman-already-connected-error
+ connman-already-connected-error?
+
+ connman-state
+ connman-technologies
+ connman-enable-technology
+ connman-disable-technology
+ connman-scan-technology
+ connman-services
+ connman-connect
+ connman-disconnect
+ connman-online?
+ connman-connect-with-auth))
+
+;;; Commentary:
+;;;
+;;; This module provides procedures for talking with the connman daemon.
+;;; The best approach would have been using connman dbus interface.
+;;; However, as Guile dbus bindings are not available yet, the console client
+;;; "connmanctl" is used to talk with the daemon.
+;;;
+
+
+;;;
+;;; Technology record.
+;;;
+
+;; The <technology> record encapsulates the "Technology" object of connman.
+;; Technology type will be typically "ethernet", "wifi" or "bluetooth".
+
+(define-record-type* <technology>
+ technology make-technology
+ technology?
+ (name technology-name) ; string
+ (type technology-type) ; string
+ (powered? technology-powered?) ; boolean
+ (connected? technology-connected?)) ; boolean
+
+
+;;;
+;;; Service record.
+;;;
+
+;; The <service> record encapsulates the "Service" object of connman.
+;; Service type is the same as the technology it is associated to, path is a
+;; unique identifier given by connman, strength describes the signal quality
+;; if applicable. Finally, state is "idle", "failure", "association",
+;; "configuration", "ready", "disconnect" or "online".
+
+(define-record-type* <service>
+ service make-service
+ service?
+ (name service-name) ; string
+ (type service-type) ; string
+ (path service-path) ; string
+ (strength service-strength) ; integer
+ (state service-state)) ; string
+
+
+;;;
+;;; Condition types.
+;;;
+
+(define-condition-type &connman-error &error
+ connman-error?
+ (command connman-error-command)
+ (output connman-error-output)
+ (status connman-error-status))
+
+(define-condition-type &connman-connection-error &error
+ connman-connection-error?
+ (service connman-connection-error-service)
+ (output connman-connection-error-output))
+
+(define-condition-type &connman-password-error &connman-connection-error
+ connman-password-error?)
+
+(define-condition-type &connman-already-connected-error
+ &connman-connection-error connman-already-connected-error?)
+
+
+;;;
+;;; Procedures.
+;;;
+
+(define (connman-run command env arguments)
+ "Run the given COMMAND, with the specified ENV and ARGUMENTS. The error
+output is discarded and &connman-error condition is raised if the command
+returns a non zero exit code."
+ (let* ((command `("env" ,env ,command ,@arguments "2>" "/dev/null"))
+ (command-string (string-join command " "))
+ (pipe (open-input-pipe command-string))
+ (output (read-lines pipe))
+ (ret (close-pipe pipe)))
+ (case (status:exit-val ret)
+ ((0) output)
+ (else (raise (condition (&connman-error
+ (command command)
+ (output output)
+ (status ret))))))))
+
+(define (connman . arguments)
+ "Run connmanctl with the specified ARGUMENTS. Set the LANG environment
+variable to C because the command output will be parsed and we don't want it
+to be translated."
+ (connman-run "connmanctl" "LANG=C" arguments))
+
+(define (parse-keys keys)
+ "Parse the given list of strings KEYS, under the following format:
+
+ '((\"KEY = VALUE\") (\"KEY2 = VALUE2\") ...)
+
+Return the corresponding association list of '((KEY . VALUE) (KEY2 . VALUE2)
+...) elements."
+ (let ((key-regex (make-regexp "([^ ]+) = ([^$]+)")))
+ (map (lambda (key)
+ (let ((match-key (regexp-exec key-regex key)))
+ (cons (match:substring match-key 1)
+ (match:substring match-key 2))))
+ keys)))
+
+(define (connman-state)
+ "Return the state of connman. The nominal states are 'offline, 'idle,
+'ready, 'oneline. If an unexpected state is read, 'unknown is
+returned. Finally, an error is raised if the comman output could not be
+parsed, usually because the connman daemon is not responding."
+ (let* ((output (connman "state"))
+ (state-keys (parse-keys output)))
+ (let ((state (assoc-ref state-keys "State")))
+ (if state
+ (cond ((string=? state "offline") 'offline)
+ ((string=? state "idle") 'idle)
+ ((string=? state "ready") 'ready)
+ ((string=? state "online") 'online)
+ (else 'unknown))
+ (raise (condition
+ (&message
+ (message "Could not determine the state of connman."))))))))
+
+(define (split-technology-list technologies)
+ "Parse the given strings list TECHNOLOGIES, under the following format:
+
+ '((\"/net/connman/technology/xxx\")
+ (\"KEY = VALUE\")
+ ...
+ (\"/net/connman/technology/yyy\")
+ (\"KEY2 = VALUE2\")
+ ...)
+ Return the corresponding '(((\"KEY = VALUE\") ...) ((\"KEY2 = VALUE2\") ...))
+list so that each keys of a given technology are gathered in a separate list."
+ (let loop ((result '())
+ (cur-list '())
+ (input (reverse technologies)))
+ (if (null? input)
+ result
+ (let ((item (car input)))
+ (if (string-match "/net/connman/technology" item)
+ (loop (cons cur-list result) '() (cdr input))
+ (loop result (cons item cur-list) (cdr input)))))))
+
+(define (string->boolean string)
+ (equal? string "True"))
+
+(define (connman-technologies)
+ "Return a list of available <technology> records."
+
+ (define (technology-output->technology output)
+ (let ((keys (parse-keys output)))
+ (technology
+ (name (assoc-ref keys "Name"))
+ (type (assoc-ref keys "Type"))
+ (powered? (string->boolean (assoc-ref keys "Powered")))
+ (connected? (string->boolean (assoc-ref keys "Connected"))))))
+
+ (let* ((output (connman "technologies"))
+ (technologies (split-technology-list output)))
+ (map technology-output->technology technologies)))
+
+(define (connman-enable-technology technology)
+ "Enable the given TECHNOLOGY."
+ (let ((type (technology-type technology)))
+ (connman "enable" type)))
+
+(define (connman-disable-technology technology)
+ "Disable the given TECHNOLOGY."
+ (let ((type (technology-type technology)))
+ (connman "disable" type)))
+
+(define (connman-scan-technology technology)
+ "Run a scan for the given TECHNOLOGY."
+ (let ((type (technology-type technology)))
+ (connman "scan" type)))
+
+(define (connman-services)
+ "Return a list of available <services> records."
+
+ (define (service-output->service path output)
+ (let* ((service-keys
+ (match output
+ ((_ . rest) rest)))
+ (keys (parse-keys service-keys)))
+ (service
+ (name (assoc-ref keys "Name"))
+ (type (assoc-ref keys "Type"))
+ (path path)
+ (strength (and=> (assoc-ref keys "Strength") string->number))
+ (state (assoc-ref keys "State")))))
+
+ (let* ((out (connman "services"))
+ (out-filtered (delete "" out))
+ (services-path (map (lambda (service)
+ (match (string-split service #\ )
+ ((_ ... path) path)))
+ out-filtered))
+ (services-output (map (lambda (service)
+ (connman "services" service))
+ services-path)))
+ (map service-output->service services-path services-output)))
+
+(define (connman-connect service)
+ "Connect to the given SERVICE."
+ (let ((path (service-path service)))
+ (connman "connect" path)))
+
+(define (connman-disconnect service)
+ "Disconnect from the given SERVICE."
+ (let ((path (service-path service)))
+ (connman "disconnect" path)))
+
+(define (connman-online?)
+ (let ((state (connman-state)))
+ (eq? state 'online)))
+
+(define (connman-connect-with-auth service password-proc)
+ "Connect to the given SERVICE with the password returned by calling
+PASSWORD-PROC. This is only possible in the interactive mode of connmanctl
+because authentication is done by communicating with an agent.
+
+As the open-pipe procedure of Guile do not allow to read from stderr, we have
+to merge stdout and stderr using bash redirection. Then error messages are
+extracted from connmanctl output using a regexp. This makes the whole
+procedure even more unreliable.
+
+Raise &connman-connection-error if an error occured during connection. Raise
+&connman-password-error if the given password is incorrect."
+
+ (define connman-error-regexp (make-regexp "Error[ ]*([^\n]+)\n"))
+
+ (define (match-connman-error str)
+ (let ((match-error (regexp-exec connman-error-regexp str)))
+ (and match-error (match:substring match-error 1))))
+
+ (define* (read-regexps-or-error port regexps error-handler)
+ "Read characters from port until an error is detected, or one of the given
+REGEXPS is matched. If an error is detected, call ERROR-HANDLER with the error
+string as argument. Raise an error if the eof is reached before one of the
+regexps is matched."
+ (let loop ((res ""))
+ (let ((char (read-char port)))
+ (cond
+ ((eof-object? char)
+ (raise (condition
+ (&message
+ (message "Unable to find expected regexp.")))))
+ ((match-connman-error res)
+ =>
+ (lambda (match)
+ (error-handler match)))
+ ((or-map (lambda (regexp)
+ (and (regexp-exec regexp res) regexp))
+ regexps)
+ =>
+ (lambda (match)
+ match))
+ (else
+ (loop (string-append res (string char))))))))
+
+ (define* (read-regexp-or-error port regexp error-handler)
+ "Same as READ-REGEXPS-OR-ERROR above, but with a single REGEXP."
+ (read-regexps-or-error port (list regexp) error-handler))
+
+ (define (connman-error->condition path error)
+ (cond
+ ((string-match "Already connected" error)
+ (condition (&connman-already-connected-error
+ (service path)
+ (output error))))
+ (else
+ (condition (&connman-connection-error
+ (service path)
+ (output error))))))
+
+ (define (run-connection-sequence pipe)
+ "Run the connection sequence using PIPE as an opened port to an
+interactive connmanctl process."
+ (let* ((path (service-path service))
+ (error-handler (lambda (error)
+ (raise
+ (connman-error->condition path error)))))
+ ;; Start the agent.
+ (format pipe "agent on\n")
+ (read-regexp-or-error pipe (make-regexp "Agent registered") error-handler)
+
+ ;; Let's try to connect to the service. If the service does not require
+ ;; a password, the connection might succeed right after this call.
+ ;; Otherwise, connmanctl will prompt us for a password.
+ (format pipe "connect ~a\n" path)
+ (let* ((connected-regexp (make-regexp (format #f "Connected ~a" path)))
+ (passphrase-regexp (make-regexp "\nPassphrase\\?[ ]*"))
+ (regexps (list connected-regexp passphrase-regexp))
+ (result (read-regexps-or-error pipe regexps error-handler)))
+
+ ;; A password is required.
+ (when (eq? result passphrase-regexp)
+ (format pipe "~a~%" (password-proc))
+
+ ;; Now, we have to wait for the connection to succeed. If an error
+ ;; occurs, it is most likely because the password is incorrect.
+ ;; In that case, we escape from an eventual retry loop that would
+ ;; add complexity to this procedure, and raise a
+ ;; &connman-password-error condition.
+ (read-regexp-or-error pipe connected-regexp
+ (lambda (error)
+ ;; Escape from retry loop.
+ (format pipe "no\n")
+ (raise
+ (condition (&connman-password-error
+ (service path)
+ (output error))))))))))
+
+ ;; XXX: Find a better way to read stderr, like with the "subprocess"
+ ;; procedure of racket that return input ports piped on the process stdin and
+ ;; stderr.
+ (let ((pipe (open-pipe "connmanctl 2>&1" OPEN_BOTH)))
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (run-connection-sequence pipe)
+ #t)
+ (lambda ()
+ (format pipe "quit\n")
+ (close-pipe pipe)))))
diff --git a/gnu/installer/final.scm b/gnu/installer/final.scm
new file mode 100644
index 0000000000..e1c62f5ce0
--- /dev/null
+++ b/gnu/installer/final.scm
@@ -0,0 +1,36 @@
+;;; 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 final)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu services herd)
+ #:use-module (guix build utils)
+ #:export (install-system))
+
+(define (install-system)
+ "Start COW-STORE service on target directory and launch guix install command
+in a subshell."
+ (let ((install-command
+ (format #f "guix system init ~a ~a"
+ (%installer-configuration-file)
+ (%installer-target-dir))))
+ (mkdir-p (%installer-target-dir))
+ (start-service 'cow-store (list (%installer-target-dir)))
+ (false-if-exception (run-shell-command install-command))))
diff --git a/gnu/installer/hostname.scm b/gnu/installer/hostname.scm
new file mode 100644
index 0000000000..b8e823d0a8
--- /dev/null
+++ b/gnu/installer/hostname.scm
@@ -0,0 +1,23 @@
+;;; 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 hostname)
+ #:export (hostname->configuration))
+
+(define (hostname->configuration hostname)
+ `((host-name ,hostname)))
diff --git a/gnu/installer/keymap.scm b/gnu/installer/keymap.scm
new file mode 100644
index 0000000000..d66b376d9c
--- /dev/null
+++ b/gnu/installer/keymap.scm
@@ -0,0 +1,172 @@
+;;; 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 keymap)
+ #:use-module (guix records)
+ #:use-module (sxml match)
+ #:use-module (sxml simple)
+ #:use-module (ice-9 binary-ports)
+ #:use-module (ice-9 ftw)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 regex)
+ #:export (<x11-keymap-model>
+ x11-keymap-model
+ make-x11-keymap-model
+ x11-keymap-model?
+ x11-keymap-model-name
+ x11-keymap-model-description
+
+ <x11-keymap-layout>
+ x11-keymap-layout
+ make-x11-keymap-layout
+ x11-keymap-layout?
+ x11-keymap-layout-name
+ x11-keymap-layout-description
+ x11-keymap-layout-variants
+
+ <x11-keymap-variant>
+ x11-keymap-variant
+ make-x11-keymap-variant
+ x11-keymap-variant?
+ x11-keymap-variant-name
+ x11-keymap-variant-description
+
+ default-keyboard-model
+ xkb-rules->models+layouts
+ kmscon-update-keymap))
+
+(define-record-type* <x11-keymap-model>
+ x11-keymap-model make-x11-keymap-model
+ x11-keymap-model?
+ (name x11-keymap-model-name) ;string
+ (description x11-keymap-model-description)) ;string
+
+(define-record-type* <x11-keymap-layout>
+ x11-keymap-layout make-x11-keymap-layout
+ x11-keymap-layout?
+ (name x11-keymap-layout-name) ;string
+ (description x11-keymap-layout-description) ;string
+ (variants x11-keymap-layout-variants)) ;list of <x11-keymap-variant>
+
+(define-record-type* <x11-keymap-variant>
+ x11-keymap-variant make-x11-keymap-variant
+ x11-keymap-variant?
+ (name x11-keymap-variant-name) ;string
+ (description x11-keymap-variant-description)) ;string
+
+;; Assume all modern keyboards have this model.
+(define default-keyboard-model (make-parameter "pc105"))
+
+(define (xkb-rules->models+layouts file)
+ "Parse FILE and return two values, the list of supported X11-KEYMAP-MODEL
+and X11-KEYMAP-LAYOUT records. FILE is an XML file from the X Keyboard
+Configuration Database, describing possible XKB configurations."
+ (define (model m)
+ (sxml-match m
+ [(model
+ (configItem
+ (name ,name)
+ (description ,description)
+ . ,rest))
+ (x11-keymap-model
+ (name name)
+ (description description))]))
+
+ (define (variant v)
+ (sxml-match v
+ [(variant
+ ;; According to xbd-rules DTD, the definition of a
+ ;; configItem is: <!ELEMENT configItem
+ ;; (name,shortDescription*,description*,vendor?,
+ ;; countryList?,languageList?,hwList?)>
+ ;;
+ ;; shortDescription and description are optional elements
+ ;; but sxml-match does not support default values for
+ ;; elements (only attributes). So to avoid writing as many
+ ;; patterns as existing possibilities, gather all the
+ ;; remaining elements but name in REST-VARIANT.
+ (configItem
+ (name ,name)
+ . ,rest-variant))
+ (x11-keymap-variant
+ (name name)
+ (description (car
+ (assoc-ref rest-variant 'description))))]))
+
+ (define (layout l)
+ (sxml-match l
+ [(layout
+ (configItem
+ (name ,name)
+ . ,rest-layout)
+ (variantList ,[variant -> v] ...))
+ (x11-keymap-layout
+ (name name)
+ (description (car
+ (assoc-ref rest-layout 'description)))
+ (variants (list v ...)))]
+ [(layout
+ (configItem
+ (name ,name)
+ . ,rest-layout))
+ (x11-keymap-layout
+ (name name)
+ (description (car
+ (assoc-ref rest-layout 'description)))
+ (variants '()))]))
+
+ (let ((sxml (call-with-input-file file
+ (lambda (port)
+ (xml->sxml port #:trim-whitespace? #t)))))
+ (match
+ (sxml-match sxml
+ [(*TOP*
+ ,pi
+ (xkbConfigRegistry
+ (@ . ,ignored)
+ (modelList ,[model -> m] ...)
+ (layoutList ,[layout -> l] ...)
+ . ,rest))
+ (list
+ (list m ...)
+ (list l ...))])
+ ((models layouts)
+ (values models layouts)))))
+
+(define (kmscon-update-keymap model layout variant)
+ "Update kmscon keymap with the provided MODEL, LAYOUT and VARIANT."
+ (and=>
+ (getenv "KEYMAP_UPDATE")
+ (lambda (keymap-file)
+ (unless (file-exists? keymap-file)
+ (error "Unable to locate keymap update file"))
+
+ ;; See file gnu/packages/patches/kmscon-runtime-keymap-switch.patch.
+ ;; This dirty hack makes possible to update kmscon keymap at runtime by
+ ;; writing an X11 keyboard model, layout and variant to a named pipe
+ ;; referred by KEYMAP_UPDATE environment variable.
+ (call-with-output-file keymap-file
+ (lambda (port)
+ (format port model)
+ (put-u8 port 0)
+
+ (format port layout)
+ (put-u8 port 0)
+
+ (format port variant)
+ (put-u8 port 0))))))
diff --git a/gnu/installer/locale.scm b/gnu/installer/locale.scm
new file mode 100644
index 0000000000..2b45b2200a
--- /dev/null
+++ b/gnu/installer/locale.scm
@@ -0,0 +1,210 @@
+;;; 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 locale)
+ #:use-module (gnu installer utils)
+ #:use-module (guix records)
+ #:use-module (json)
+ #:use-module (srfi srfi-1)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 regex)
+ #:export (locale-language
+ locale-territory
+ locale-codeset
+ locale-modifier
+
+ locale->locale-string
+ supported-locales->locales
+
+ iso639->iso639-languages
+ language-code->language-name
+
+ iso3166->iso3166-territories
+ territory-code->territory-name
+
+ locale->configuration))
+
+
+;;;
+;;; Locale.
+;;;
+
+;; A glibc locale string has the following format:
+;; language[_territory[.codeset][@modifier]].
+(define locale-regexp "^([^_@]+)(_([^\\.@]+))?(\\.([^@]+))?(@([^$]+))?$")
+
+;; LOCALE will be better expressed in a (guix record) that in an association
+;; list. However, loading large files containing records does not scale
+;; well. The same thing goes for ISO639 and ISO3166 association lists used
+;; later in this module.
+(define (locale-language assoc)
+ (assoc-ref assoc 'language))
+(define (locale-territory assoc)
+ (assoc-ref assoc 'territory))
+(define (locale-codeset assoc)
+ (assoc-ref assoc 'codeset))
+(define (locale-modifier assoc)
+ (assoc-ref assoc 'modifier))
+
+(define (locale-string->locale string)
+ "Return the locale association list built from the parsing of STRING."
+ (let ((matches (string-match locale-regexp string)))
+ `((language . ,(match:substring matches 1))
+ (territory . ,(match:substring matches 3))
+ (codeset . ,(match:substring matches 5))
+ (modifier . ,(match:substring matches 7)))))
+
+(define (locale->locale-string locale)
+ "Reverse operation of locale-string->locale."
+ (let ((language (locale-language locale))
+ (territory (locale-territory locale))
+ (codeset (locale-codeset locale))
+ (modifier (locale-modifier locale)))
+ (apply string-append
+ `(,language
+ ,@(if territory
+ `("_" ,territory)
+ '())
+ ,@(if codeset
+ `("." ,codeset)
+ '())
+ ,@(if modifier
+ `("@" ,modifier)
+ '())))))
+
+(define (supported-locales->locales supported-locales)
+ "Parse the SUPPORTED-LOCALES file from the glibc and return the matching
+list of LOCALE association lists."
+ (call-with-input-file supported-locales
+ (lambda (port)
+ (let ((lines (read-lines port)))
+ (map (lambda (line)
+ (match (string-split line #\ )
+ ((locale-string codeset)
+ (let ((line-locale (locale-string->locale locale-string)))
+ (assoc-set! line-locale 'codeset codeset)))))
+ lines)))))
+
+
+;;;
+;;; Language.
+;;;
+
+(define (iso639-language-alpha2 assoc)
+ (assoc-ref assoc 'alpha2))
+
+(define (iso639-language-alpha3 assoc)
+ (assoc-ref assoc 'alpha3))
+
+(define (iso639-language-name assoc)
+ (assoc-ref assoc 'name))
+
+(define (supported-locale? locales alpha2 alpha3)
+ "Find a locale in LOCALES whose alpha2 field matches ALPHA-2 or alpha3 field
+matches ALPHA-3. The ISO639 standard specifies that ALPHA-2 is optional. Thus,
+if ALPHA-2 is #f, only consider ALPHA-3. Return #f if not matching locale was
+found."
+ (find (lambda (locale)
+ (let ((language (locale-language locale)))
+ (or (and=> alpha2
+ (lambda (code)
+ (string=? language code)))
+ (string=? language alpha3))))
+ locales))
+
+(define (iso639->iso639-languages locales iso639-3 iso639-5)
+ "Return a list of ISO639 association lists created from the parsing of
+ISO639-3 and ISO639-5 files."
+ (call-with-input-file iso639-3
+ (lambda (port-iso639-3)
+ (call-with-input-file iso639-5
+ (lambda (port-iso639-5)
+ (filter-map
+ (lambda (hash)
+ (let ((alpha2 (hash-ref hash "alpha_2"))
+ (alpha3 (hash-ref hash "alpha_3"))
+ (name (hash-ref hash "name")))
+ (and (supported-locale? locales alpha2 alpha3)
+ `((alpha2 . ,alpha2)
+ (alpha3 . ,alpha3)
+ (name . ,name)))))
+ (append
+ (hash-ref (json->scm port-iso639-3) "639-3")
+ (hash-ref (json->scm port-iso639-5) "639-5"))))))))
+
+(define (language-code->language-name languages language-code)
+ "Using LANGUAGES as a list of ISO639 association lists, return the language
+name corresponding to the given LANGUAGE-CODE."
+ (let ((iso639-language
+ (find (lambda (language)
+ (or
+ (and=> (iso639-language-alpha2 language)
+ (lambda (alpha2)
+ (string=? alpha2 language-code)))
+ (string=? (iso639-language-alpha3 language)
+ language-code)))
+ languages)))
+ (iso639-language-name iso639-language)))
+
+
+;;;
+;;; Territory.
+;;;
+
+(define (iso3166-territory-alpha2 assoc)
+ (assoc-ref assoc 'alpha2))
+
+(define (iso3166-territory-alpha3 assoc)
+ (assoc-ref assoc 'alpha3))
+
+(define (iso3166-territory-name assoc)
+ (assoc-ref assoc 'name))
+
+(define (iso3166->iso3166-territories iso3166)
+ "Return a list of ISO3166 association lists created from the parsing of
+ISO3166 file."
+ (call-with-input-file iso3166
+ (lambda (port)
+ (map (lambda (hash)
+ `((alpha2 . ,(hash-ref hash "alpha_2"))
+ (alpha3 . ,(hash-ref hash "alpha_3"))
+ (name . ,(hash-ref hash "name"))))
+ (hash-ref (json->scm port) "3166-1")))))
+
+(define (territory-code->territory-name territories territory-code)
+ "Using TERRITORIES as a list of ISO3166 association lists return the
+territory name corresponding to the given TERRITORY-CODE."
+ (let ((iso3166-territory
+ (find (lambda (territory)
+ (or
+ (and=> (iso3166-territory-alpha2 territory)
+ (lambda (alpha2)
+ (string=? alpha2 territory-code)))
+ (string=? (iso3166-territory-alpha3 territory)
+ territory-code)))
+ territories)))
+ (iso3166-territory-name iso3166-territory)))
+
+
+;;;
+;;; Configuration formatter.
+;;;
+
+(define (locale->configuration locale)
+ "Return the configuration field for LOCALE."
+ `((locale ,locale)))
diff --git a/gnu/installer/newt.scm b/gnu/installer/newt.scm
new file mode 100644
index 0000000000..6c44b4acf6
--- /dev/null
+++ b/gnu/installer/newt.scm
@@ -0,0 +1,128 @@
+;;; 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 newt)
+ #:use-module (gnu installer record)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt ethernet)
+ #:use-module (gnu installer newt final)
+ #:use-module (gnu installer newt hostname)
+ #:use-module (gnu installer newt keymap)
+ #:use-module (gnu installer newt locale)
+ #:use-module (gnu installer newt menu)
+ #:use-module (gnu installer newt network)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer newt partition)
+ #:use-module (gnu installer newt services)
+ #:use-module (gnu installer newt timezone)
+ #:use-module (gnu installer newt user)
+ #:use-module (gnu installer newt utils)
+ #:use-module (gnu installer newt welcome)
+ #:use-module (gnu installer newt wifi)
+ #:use-module (guix config)
+ #:use-module (guix discovery)
+ #:use-module (guix i18n)
+ #:use-module (srfi srfi-26)
+ #:use-module (newt)
+ #:export (newt-installer))
+
+(define (init)
+ (newt-init)
+ (clear-screen)
+ (set-screen-size!))
+
+(define (exit)
+ (newt-finish)
+ (clear-screen))
+
+(define (exit-error file key args)
+ (newt-set-color COLORSET-ROOT "white" "red")
+ (let ((width (nearest-exact-integer
+ (* (screen-columns) 0.8)))
+ (height (nearest-exact-integer
+ (* (screen-rows) 0.7))))
+ (run-file-textbox-page
+ #:info-text (format #f (G_ "The installer has encountered an unexpected \
+problem. The backtrace is displayed below. Please report it by email to \
+<~a>.") %guix-bug-report-address)
+ #:title (G_ "Unexpected problem")
+ #:file file
+ #:exit-button? #f
+ #:info-textbox-width width
+ #:file-textbox-width width
+ #:file-textbox-height height))
+ (newt-set-color COLORSET-ROOT "white" "blue")
+ (newt-finish)
+ (clear-screen))
+
+(define (final-page result prev-steps)
+ (run-final-page result prev-steps))
+
+(define* (locale-page #:key
+ supported-locales
+ iso639-languages
+ iso3166-territories)
+ (run-locale-page
+ #:supported-locales supported-locales
+ #:iso639-languages iso639-languages
+ #:iso3166-territories iso3166-territories))
+
+(define (timezone-page zonetab)
+ (run-timezone-page zonetab))
+
+(define (welcome-page logo)
+ (run-welcome-page logo))
+
+(define (menu-page steps)
+ (run-menu-page steps))
+
+(define* (keymap-page layouts)
+ (run-keymap-page layouts))
+
+(define (network-page)
+ (run-network-page))
+
+(define (hostname-page)
+ (run-hostname-page))
+
+(define (user-page)
+ (run-user-page))
+
+(define (partition-page)
+ (run-partioning-page))
+
+(define (services-page)
+ (run-services-page))
+
+(define newt-installer
+ (installer
+ (name 'newt)
+ (init init)
+ (exit exit)
+ (exit-error exit-error)
+ (final-page final-page)
+ (keymap-page keymap-page)
+ (locale-page locale-page)
+ (menu-page menu-page)
+ (network-page network-page)
+ (timezone-page timezone-page)
+ (hostname-page hostname-page)
+ (user-page user-page)
+ (partition-page partition-page)
+ (services-page services-page)
+ (welcome-page welcome-page)))
diff --git a/gnu/installer/newt/ethernet.scm b/gnu/installer/newt/ethernet.scm
new file mode 100644
index 0000000000..d1f357243b
--- /dev/null
+++ b/gnu/installer/newt/ethernet.scm
@@ -0,0 +1,81 @@
+;;; 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 newt ethernet)
+ #:use-module (gnu installer connman)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer newt utils)
+ #:use-module (gnu installer newt page)
+ #:use-module (guix i18n)
+ #:use-module (ice-9 format)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (newt)
+ #:export (run-ethernet-page))
+
+(define (ethernet-services)
+ "Return all the connman services of ethernet type."
+ (let ((services (connman-services)))
+ (filter (lambda (service)
+ (and (string=? (service-type service) "ethernet")
+ (not (string-null? (service-name service)))))
+ services)))
+
+(define (ethernet-service->text service)
+ "Return a string describing the given ethernet SERVICE."
+ (let* ((name (service-name service))
+ (path (service-path service))
+ (full-name (string-append name "-" path))
+ (state (service-state service))
+ (connected? (or (string=? state "online")
+ (string=? state "ready"))))
+ (format #f "~c ~a~%"
+ (if connected? #\* #\ )
+ full-name)))
+
+(define (connect-ethernet-service service)
+ "Connect to the given ethernet SERVICE. Display a connecting page while the
+connection is pending."
+ (let* ((service-name (service-name service))
+ (form (draw-connecting-page service-name)))
+ (connman-connect service)
+ (destroy-form-and-pop form)
+ service))
+
+(define (run-ethernet-page)
+ (let ((services (ethernet-services)))
+ (if (null? services)
+ (begin
+ (run-error-page
+ (G_ "No ethernet service available, please try again.")
+ (G_ "No service"))
+ (raise
+ (condition
+ (&installer-step-abort))))
+ (run-listbox-selection-page
+ #:info-text (G_ "Please select an ethernet network.")
+ #:title (G_ "Ethernet connection")
+ #:listbox-items services
+ #:listbox-item->text ethernet-service->text
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort))))
+ #:listbox-callback-procedure connect-ethernet-service))))
diff --git a/gnu/installer/newt/final.scm b/gnu/installer/newt/final.scm
new file mode 100644
index 0000000000..645c1e8689
--- /dev/null
+++ b/gnu/installer/newt/final.scm
@@ -0,0 +1,86 @@
+;;; 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 newt final)
+ #:use-module (gnu installer final)
+ #:use-module (gnu installer parted)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer newt utils)
+ #:use-module (guix i18n)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (newt)
+ #:export (run-final-page))
+
+(define (run-config-display-page)
+ (let ((width (%configuration-file-width))
+ (height (nearest-exact-integer
+ (/ (screen-rows) 2))))
+ (run-file-textbox-page
+ #:info-text (G_ "We're now ready to proceed with the installation! \
+A system configuration file has been generated, it is displayed below. \
+The new system will be created from this file once you've pressed OK. \
+This will take a few minutes.")
+ #:title (G_ "Configuration file")
+ #:file (%installer-configuration-file)
+ #:info-textbox-width width
+ #:file-textbox-width width
+ #:file-textbox-height height
+ #:exit-button-callback-procedure
+ (lambda ()
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define (run-install-success-page)
+ (message-window
+ (G_ "Installation complete")
+ (G_ "Reboot")
+ (G_ "Congratulations! Installation is now complete. \
+You may remove the device containing the installation image and \
+press the button to reboot.")))
+
+(define (run-install-failed-page)
+ (choice-window
+ (G_ "Installation failed")
+ (G_ "Restart installer")
+ (G_ "Retry system install")
+ (G_ "The final system installation step failed. You can retry the \
+last step, or restart the installer.")))
+
+(define (run-install-shell)
+ (clear-screen)
+ (newt-suspend)
+ (let ((install-ok? (install-system)))
+ (newt-resume)
+ install-ok?))
+
+(define (run-final-page result prev-steps)
+ (let* ((configuration (format-configuration prev-steps result))
+ (user-partitions (result-step result 'partition))
+ (install-ok?
+ (with-mounted-partitions
+ user-partitions
+ (configuration->file configuration)
+ (run-config-display-page)
+ (run-install-shell))))
+ (if install-ok?
+ (run-install-success-page)
+ (run-install-failed-page))))
diff --git a/gnu/installer/newt/hostname.scm b/gnu/installer/newt/hostname.scm
new file mode 100644
index 0000000000..7783fa6360
--- /dev/null
+++ b/gnu/installer/newt/hostname.scm
@@ -0,0 +1,26 @@
+;;; 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 newt hostname)
+ #:use-module (gnu installer newt page)
+ #:use-module (guix i18n)
+ #:export (run-hostname-page))
+
+(define (run-hostname-page)
+ (run-input-page (G_ "Please enter the system hostname.")
+ (G_ "Hostname")))
diff --git a/gnu/installer/newt/keymap.scm b/gnu/installer/newt/keymap.scm
new file mode 100644
index 0000000000..6211af2bc5
--- /dev/null
+++ b/gnu/installer/newt/keymap.scm
@@ -0,0 +1,122 @@
+;;; 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 newt keymap)
+ #:use-module (gnu installer keymap)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer newt page)
+ #:use-module (guix i18n)
+ #:use-module (guix records)
+ #:use-module (newt)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:export (run-keymap-page))
+
+(define (run-layout-page layouts layout->text)
+ (let ((title (G_ "Layout")))
+ (run-listbox-selection-page
+ #:title title
+ #:info-text (G_ "Please choose your keyboard layout.")
+ #:listbox-items layouts
+ #:listbox-item->text layout->text
+ #:sort-listbox-items? #f
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define (run-variant-page variants variant->text)
+ (let ((title (G_ "Variant")))
+ (run-listbox-selection-page
+ #:title title
+ #:info-text (G_ "Please choose a variant for your keyboard layout.")
+ #:listbox-items variants
+ #:listbox-item->text variant->text
+ #:sort-listbox-items? #f
+ #:button-text (G_ "Back")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define (sort-layouts layouts)
+ "Sort LAYOUTS list by putting the US layout ahead and return it."
+ (call-with-values
+ (lambda ()
+ (partition
+ (lambda (layout)
+ (let ((name (x11-keymap-layout-name layout)))
+ (string=? name "us")))
+ layouts))
+ (cut append <> <>)))
+
+(define (sort-variants variants)
+ "Sort VARIANTS list by putting the internation variant ahead and return it."
+ (call-with-values
+ (lambda ()
+ (partition
+ (lambda (variant)
+ (let ((name (x11-keymap-variant-name variant)))
+ (string=? name "altgr-intl")))
+ variants))
+ (cut append <> <>)))
+
+(define* (run-keymap-page layouts)
+ "Run a page asking the user to select a keyboard layout and variant. LAYOUTS
+is a list of supported X11-KEYMAP-LAYOUT. Return a list of two elements, the
+names of the selected keyboard layout and variant."
+ (define keymap-steps
+ (list
+ (installer-step
+ (id 'layout)
+ (compute
+ (lambda _
+ (run-layout-page
+ (sort-layouts layouts)
+ (lambda (layout)
+ (x11-keymap-layout-description layout))))))
+ ;; Propose the user to select a variant among those supported by the
+ ;; previously selected layout.
+ (installer-step
+ (id 'variant)
+ (compute
+ (lambda (result _)
+ (let* ((layout (result-step result 'layout))
+ (variants (x11-keymap-layout-variants layout)))
+ ;; Return #f if the layout does not have any variant.
+ (and (not (null? variants))
+ (run-variant-page
+ (sort-variants variants)
+ (lambda (variant)
+ (x11-keymap-variant-description
+ variant))))))))))
+
+ (define (format-result result)
+ (let ((layout (x11-keymap-layout-name
+ (result-step result 'layout)))
+ (variant (and=> (result-step result 'variant)
+ (lambda (variant)
+ (x11-keymap-variant-name variant)))))
+ (list layout (or variant ""))))
+ (format-result
+ (run-installer-steps #:steps keymap-steps)))
diff --git a/gnu/installer/newt/locale.scm b/gnu/installer/newt/locale.scm
new file mode 100644
index 0000000000..4fa07df81e
--- /dev/null
+++ b/gnu/installer/newt/locale.scm
@@ -0,0 +1,217 @@
+;;; 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 newt locale)
+ #:use-module (gnu installer locale)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer newt page)
+ #:use-module (guix i18n)
+ #:use-module (newt)
+ #: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)
+ #:export (run-locale-page))
+
+(define (run-language-page languages language->text)
+ (let ((title (G_ "Locale language")))
+ (run-listbox-selection-page
+ #:title title
+ #:info-text (G_ "Choose the locale's language to be used for the \
+installation process. A locale is a regional variant of your language \
+encompassing number, date and currency format, among other details.
+
+Based on the language you choose, you will possibly be asked to \
+select a locale's territory, codeset and modifier in the next \
+steps. The locale will also be used as the default one for the \
+installed system.")
+ #:info-textbox-width 70
+ #:listbox-items languages
+ #:listbox-item->text language->text
+ #:sort-listbox-items? #f
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define (run-territory-page territories territory->text)
+ (let ((title (G_ "Locale location")))
+ (run-listbox-selection-page
+ #:title title
+ #:info-text (G_ "Choose your locale's location. This is a shortlist of \
+locations based on the language you selected.")
+ #:listbox-items territories
+ #:listbox-item->text territory->text
+ #:button-text (G_ "Back")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define (run-codeset-page codesets)
+ (let ((title (G_ "Locale codeset")))
+ (run-listbox-selection-page
+ #:title title
+ #:info-text (G_ "Choose your locale's codeset. If UTF-8 is available, \
+ it should be preferred.")
+ #:listbox-items codesets
+ #:listbox-item->text identity
+ #:listbox-default-item "UTF-8"
+ #:button-text (G_ "Back")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define (run-modifier-page modifiers modifier->text)
+ (let ((title (G_ "Locale modifier")))
+ (run-listbox-selection-page
+ #:title title
+ #:info-text (G_ "Choose your locale's modifier. The most frequent \
+modifier is euro. It indicates that you want to use Euro as the currency \
+symbol.")
+ #:listbox-items modifiers
+ #:listbox-item->text modifier->text
+ #:button-text (G_ "Back")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort)))))))
+
+(define* (run-locale-page #:key
+ supported-locales
+ iso639-languages
+ iso3166-territories)
+ "Run a page asking the user to select a locale language and possibly
+territory, codeset and modifier. Use SUPPORTED-LOCALES as the list of glibc
+available locales. ISO639-LANGUAGES is an association list associating a
+locale code to a locale name. ISO3166-TERRITORIES is an association list
+associating a territory code with a territory name. The formated locale, under
+glibc format is returned."
+
+ (define (break-on-locale-found locales)
+ "Raise the &installer-step-break condition if LOCALES contains exactly one
+element."
+ (and (= (length locales) 1)
+ (raise
+ (condition (&installer-step-break)))))
+
+ (define (filter-locales locales result)
+ "Filter the list of locale records LOCALES using the RESULT returned by
+the installer-steps defined below."
+ (filter
+ (lambda (locale)
+ (and-map identity
+ `(,(string=? (locale-language locale)
+ (result-step result 'language))
+ ,@(if (result-step-done? result 'territory)
+ (list (equal? (locale-territory locale)
+ (result-step result 'territory)))
+ '())
+ ,@(if (result-step-done? result 'codeset)
+ (list (equal? (locale-codeset locale)
+ (result-step result 'codeset)))
+ '())
+ ,@(if (result-step-done? result 'modifier)
+ (list (equal? (locale-modifier locale)
+ (result-step result 'modifier)))
+ '()))))
+ locales))
+
+ (define (result->locale-string locales result)
+ "Supposing that LOCALES contains exactly one locale record, turn it into a
+glibc locale string and return it."
+ (match (filter-locales locales result)
+ ((locale)
+ (locale->locale-string locale))))
+
+ (define (sort-languages languages)
+ "Extract some languages from LANGUAGES list and place them ahead."
+ (let* ((first-languages '("en"))
+ (other-languages (lset-difference equal?
+ languages
+ first-languages)))
+ `(,@first-languages ,@other-languages)))
+
+ (define locale-steps
+ (list
+ (installer-step
+ (id 'language)
+ (compute
+ (lambda _
+ (run-language-page
+ (sort-languages
+ (delete-duplicates (map locale-language supported-locales)))
+ (cut language-code->language-name iso639-languages <>)))))
+ (installer-step
+ (id 'territory)
+ (compute
+ (lambda (result _)
+ (let ((locales (filter-locales supported-locales result)))
+ ;; Stop the process if the language returned by the previous step
+ ;; is matching one and only one supported locale.
+ (break-on-locale-found locales)
+
+ ;; Otherwise, ask the user to select a territory among those
+ ;; supported by the previously selected language.
+ (run-territory-page
+ (delete-duplicates (map locale-territory locales))
+ (lambda (territory-code)
+ (if territory-code
+ (territory-code->territory-name iso3166-territories
+ territory-code)
+ (G_ "No location"))))))))
+ (installer-step
+ (id 'codeset)
+ (compute
+ (lambda (result _)
+ (let ((locales (filter-locales supported-locales result)))
+ ;; Same as above but we now have a language and a territory to
+ ;; narrow down the search of a locale.
+ (break-on-locale-found locales)
+
+ ;; Otherwise, ask for a codeset.
+ (run-codeset-page
+ (delete-duplicates (map locale-codeset locales)))))))
+ (installer-step
+ (id 'modifier)
+ (compute
+ (lambda (result _)
+ (let ((locales (filter-locales supported-locales result)))
+ ;; Same thing with a language, a territory and a codeset this time.
+ (break-on-locale-found locales)
+
+ ;; Otherwise, ask for a modifier.
+ (run-modifier-page
+ (delete-duplicates (map locale-modifier locales))
+ (lambda (modifier)
+ (or modifier (G_ "No modifier"))))))))))
+
+ ;; If run-installer-steps returns locally, it means that the user had to go
+ ;; through all steps (language, territory, codeset and modifier) to select a
+ ;; locale. In that case, like if we exited by raising &installer-step-break
+ ;; condition, turn the result into a glibc locale string and return it.
+ (result->locale-string
+ supported-locales
+ (run-installer-steps #:steps locale-steps)))
diff --git a/gnu/installer/newt/menu.scm b/gnu/installer/newt/menu.scm
new file mode 100644
index 0000000000..161266a94a
--- /dev/null
+++ b/gnu/installer/newt/menu.scm
@@ -0,0 +1,44 @@
+;;; 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 newt menu)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer newt page)
+ #:use-module (guix i18n)
+ #:use-module (newt)
+ #:export (run-menu-page))
+
+(define (run-menu-page steps)
+ "Run a menu page, asking the user to select where to resume the install
+process from."
+ (define (steps->items steps)
+ (filter (lambda (step)
+ (installer-step-description step))
+ steps))
+
+ (run-listbox-selection-page
+ #:info-text (G_ "Choose where you want to resume the install.\
+You can also abort the installation by pressing the Abort button.")
+ #:title (G_ "Installation menu")
+ #:listbox-items (steps->items steps)
+ #:listbox-item->text installer-step-description
+ #:sort-listbox-items? #f
+ #:button-text (G_ "Abort")
+ #:button-callback-procedure (lambda ()
+ (newt-finish)
+ (primitive-exit 1))))
diff --git a/gnu/installer/newt/network.scm b/gnu/installer/newt/network.scm
new file mode 100644
index 0000000000..f263b7df9d
--- /dev/null
+++ b/gnu/installer/newt/network.scm
@@ -0,0 +1,173 @@
+;;; 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 newt network)
+ #:use-module (gnu installer connman)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt ethernet)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer newt wifi)
+ #:use-module (guix i18n)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-11)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (newt)
+ #:export (run-network-page))
+
+;; Maximum length of a technology name.
+(define technology-name-max-length (make-parameter 20))
+
+(define (technology->text technology)
+ "Return a string describing the given TECHNOLOGY."
+ (let* ((name (technology-name technology))
+ (padded-name (string-pad-right name
+ (technology-name-max-length))))
+ (format #f "~a~%" padded-name)))
+
+(define (run-technology-page)
+ "Run a page to ask the user which technology shall be used to access
+Internet and return the selected technology. For now, only technologies with
+\"ethernet\" or \"wifi\" types are supported."
+ (define (technology-items)
+ (filter (lambda (technology)
+ (let ((type (technology-type technology)))
+ (or
+ (string=? type "ethernet")
+ (string=? type "wifi"))))
+ (connman-technologies)))
+
+ (let ((items (technology-items)))
+ (if (null? items)
+ (case (choice-window
+ (G_ "Internet access")
+ (G_ "Continue")
+ (G_ "Exit")
+ (G_ "The install process requires an internet access, but no \
+network device were found. Do you want to continue anyway?"))
+ ((1) (raise
+ (condition
+ (&installer-step-break))))
+ ((2) (raise
+ (condition
+ (&installer-step-abort)))))
+ (run-listbox-selection-page
+ #:info-text (G_ "The install process requires an internet access.\
+ Please select a network device.")
+ #:title (G_ "Internet access")
+ #:listbox-items items
+ #:listbox-item->text technology->text
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort))))))))
+
+(define (find-technology-by-type technologies type)
+ "Find and return a technology with the given TYPE in TECHNOLOGIES list."
+ (find (lambda (technology)
+ (string=? (technology-type technology)
+ type))
+ technologies))
+
+(define (wait-technology-powered technology)
+ "Wait and display a progress bar until the given TECHNOLOGY is powered."
+ (let ((name (technology-name technology))
+ (full-value 5))
+ (run-scale-page
+ #:title (G_ "Powering technology")
+ #:info-text (format #f "Waiting for technology ~a to be powered." name)
+ #:scale-full-value full-value
+ #:scale-update-proc
+ (lambda (value)
+ (let* ((technologies (connman-technologies))
+ (type (technology-type technology))
+ (updated-technology
+ (find-technology-by-type technologies type))
+ (technology-powered? updated-technology))
+ (sleep 1)
+ (if technology-powered?
+ full-value
+ (+ value 1)))))))
+
+(define (wait-service-online)
+ "Display a newt scale until connman detects an Internet access. Do
+FULL-VALUE tentatives, spaced by 1 second."
+ (let* ((full-value 5))
+ (run-scale-page
+ #:title (G_ "Checking connectivity")
+ #:info-text (G_ "Waiting internet access is established.")
+ #:scale-full-value full-value
+ #:scale-update-proc
+ (lambda (value)
+ (sleep 1)
+ (if (connman-online?)
+ full-value
+ (+ value 1))))
+ (unless (connman-online?)
+ (run-error-page
+ (G_ "The selected network does not provide an Internet \
+access, please try again.")
+ (G_ "Connection error"))
+ (raise
+ (condition
+ (&installer-step-abort))))))
+
+(define (run-network-page)
+ "Run a page to allow the user to configure connman so that it can access the
+Internet."
+ (define network-steps
+ (list
+ ;; Ask the user to choose between ethernet and wifi technologies.
+ (installer-step
+ (id 'select-technology)
+ (compute
+ (lambda _
+ (run-technology-page))))
+ ;; Enable the previously selected technology.
+ (installer-step
+ (id 'power-technology)
+ (compute
+ (lambda (result _)
+ (let ((technology (result-step result 'select-technology)))
+ (connman-enable-technology technology)
+ (wait-technology-powered technology)))))
+ ;; Propose the user to connect to one of the service available for the
+ ;; previously selected technology.
+ (installer-step
+ (id 'connect-service)
+ (compute
+ (lambda (result _)
+ (let* ((technology (result-step result 'select-technology))
+ (type (technology-type technology)))
+ (cond
+ ((string=? "wifi" type)
+ (run-wifi-page))
+ ((string=? "ethernet" type)
+ (run-ethernet-page)))))))
+ ;; Wait for connman status to switch to 'online, which means it can
+ ;; access Internet.
+ (installer-step
+ (id 'wait-online)
+ (compute (lambda _
+ (wait-service-online))))))
+ (run-installer-steps
+ #:steps network-steps
+ #:rewind-strategy 'start))
diff --git a/gnu/installer/newt/page.scm b/gnu/installer/newt/page.scm
new file mode 100644
index 0000000000..edf0b8c999
--- /dev/null
+++ b/gnu/installer/newt/page.scm
@@ -0,0 +1,530 @@
+;;; 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 newt page)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt utils)
+ #:use-module (guix i18n)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 receive)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:use-module (newt)
+ #:export (draw-info-page
+ draw-connecting-page
+ run-input-page
+ run-error-page
+ run-listbox-selection-page
+ run-scale-page
+ run-checkbox-tree-page
+ run-file-textbox-page))
+
+;;; Commentary:
+;;;
+;;; Some helpers around guile-newt to draw or run generic pages. The
+;;; difference between 'draw' and 'run' terms comes from newt library. A page
+;;; is drawn when the form it contains does not expect any user
+;;; interaction. In that case, it is necessary to call (newt-refresh) to force
+;;; the page to be displayed. When a form is 'run', it is blocked waiting for
+;;; any action from the user (press a button, input some text, ...).
+;;;
+;;; Code:
+
+(define (draw-info-page text title)
+ "Draw an informative page with the given TEXT as content. Set the title of
+this page to TITLE."
+ (let* ((text-box
+ (make-reflowed-textbox -1 -1 text 40
+ #:flags FLAG-BORDER))
+ (grid (make-grid 1 1))
+ (form (make-form)))
+ (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
+ (add-component-to-form form text-box)
+ (make-wrapped-grid-window grid title)
+ (draw-form form)
+ ;; This call is imperative, otherwise the form won't be displayed. See the
+ ;; explanation in the above commentary.
+ (newt-refresh)
+ form))
+
+(define (draw-connecting-page service-name)
+ "Draw a page to indicate a connection in in progress."
+ (draw-info-page
+ (format #f (G_ "Connecting to ~a, please wait.") service-name)
+ (G_ "Connection in progress")))
+
+(define* (run-input-page text title
+ #:key
+ (allow-empty-input? #f)
+ (default-text #f)
+ (input-field-width 40))
+ "Run a page to prompt user for an input. The given TEXT will be displayed
+above the input field. The page title is set to TITLE. Unless
+allow-empty-input? is set to #t, an error page will be displayed if the user
+enters an empty input."
+ (let* ((text-box
+ (make-reflowed-textbox -1 -1 text
+ input-field-width
+ #:flags FLAG-BORDER))
+ (grid (make-grid 1 3))
+ (input-entry (make-entry -1 -1 20))
+ (ok-button (make-button -1 -1 (G_ "OK")))
+ (form (make-form)))
+
+ (when default-text
+ (set-entry-text input-entry default-text))
+
+ (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
+ (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT input-entry
+ #:pad-top 1)
+ (set-grid-field grid 0 2 GRID-ELEMENT-COMPONENT ok-button
+ #:pad-top 1)
+
+ (add-components-to-form form text-box input-entry ok-button)
+ (make-wrapped-grid-window grid title)
+ (let ((error-page (lambda ()
+ (run-error-page (G_ "Please enter a non empty input.")
+ (G_ "Empty input")))))
+ (let loop ()
+ (receive (exit-reason argument)
+ (run-form form)
+ (let ((input (entry-value input-entry)))
+ (if (and (not allow-empty-input?)
+ (eq? exit-reason 'exit-component)
+ (string=? input ""))
+ (begin
+ ;; Display the error page.
+ (error-page)
+ ;; Set the focus back to the input input field.
+ (set-current-component form input-entry)
+ (loop))
+ (begin
+ (destroy-form-and-pop form)
+ input))))))))
+
+(define (run-error-page text title)
+ "Run a page to inform the user of an error. The page contains the given TEXT
+to explain the error and an \"OK\" button to acknowledge the error. The title
+of the page is set to TITLE."
+ (let* ((text-box
+ (make-reflowed-textbox -1 -1 text 40
+ #:flags FLAG-BORDER))
+ (grid (make-grid 1 2))
+ (ok-button (make-button -1 -1 "OK"))
+ (form (make-form)))
+
+ (set-grid-field grid 0 0 GRID-ELEMENT-COMPONENT text-box)
+ (set-grid-field grid 0 1 GRID-ELEMENT-COMPONENT ok-button
+ #:pad-top 1)
+
+ ;; Set the background color to red to indicate something went wrong.
+ (newt-set-color COLORSET-ROOT "white" "red")
+ (add-components-to-form form text-box ok-button)
+ (make-wrapped-grid-window grid title)
+ (run-form form)
+ ;; Restore the background to its original color.
+ (newt-set-color COLORSET-ROOT "white" "blue")
+ (destroy-form-and-pop form)))
+
+(define* (run-listbox-selection-page #:key
+ info-text
+ title
+ (info-textbox-width 50)
+ listbox-items
+ listbox-item->text
+ (listbox-height 20)
+ (listbox-default-item #f)
+ (listbox-allow-multiple? #f)
+ (sort-listbox-items? #t)
+ (allow-delete? #f)
+ (skip-item-procedure?
+ (const #f))
+ button-text
+ (button-callback-procedure
+ (const #t))
+ (button2-text #f)
+ (button2-callback-procedure
+ (const #t))
+ (listbox-callback-procedure
+ identity)
+ (hotkey-callback-procedure
+ (const #t)))
+ "Run a page asking the user to select an item in a listbox. The page
+contains, stacked vertically from the top to the bottom, an informative text
+set to INFO-TEXT, a listbox and a button. The listbox will be filled with
+LISTBOX-ITEMS converted to text by applying the procedure LISTBOX-ITEM->TEXT
+on every item. The selected item from LISTBOX-ITEMS is returned. The button
+text is set to BUTTON-TEXT and the procedure BUTTON-CALLBACK-PROCEDURE called
+when it is pressed. The procedure LISTBOX-CALLBACK-PROCEDURE is called when an
+item from the listbox is selected (by pressing the <ENTER> key).
+
+INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
+displayed. LISTBOX-HEIGHT is the height of the listbox.
+
+If LISTBOX-DEFAULT-ITEM is set to the value of one of the items in
+LISTBOX-ITEMS, it will be selected by default. Otherwise, the first element of
+the listbox is selected.
+
+If LISTBOX-ALLOW-MULTIPLE? is set to #t, multiple items from the listbox can
+be selected (using the <SPACE> key). It that case, a list containing the
+selected items will be returned.
+
+If SORT-LISTBOX-ITEMS? is set to #t, the listbox items are sorted using
+'string<=' procedure (after being converted to text).
+
+If ALLOW-DELETE? is #t, the form will return if the <DELETE> key is pressed,
+otherwise nothing will happend.
+
+Each time the listbox current item changes, call SKIP-ITEM-PROCEDURE? with the
+current listbox item as argument. If it returns #t, skip the element and jump
+to the next/previous one depending on the previous item, otherwise do
+nothing."
+
+ (define (fill-listbox listbox items)
+ "Append the given ITEMS to LISTBOX, once they have been converted to text
+with LISTBOX-ITEM->TEXT. Each item appended to the LISTBOX is given a key by
+newt. Save this key by returning an association list under the form:
+
+ ((NEWT-LISTBOX-KEY . ITEM) ...)
+
+where NEWT-LISTBOX-KEY is the key returned by APPEND-ENTRY-TO-LISTBOX, when
+ITEM was inserted into LISTBOX."
+ (map (lambda (item)
+ (let* ((text (listbox-item->text item))
+ (key (append-entry-to-listbox listbox text)))
+ (cons key item)))
+ items))
+
+ (define (sort-listbox-items listbox-items)
+ "Return LISTBOX-ITEMS sorted using the 'string<=' procedure on the text
+corresponding to each item in the list."
+ (let* ((items (map (lambda (item)
+ (cons item (listbox-item->text item)))
+ listbox-items))
+ (sorted-items
+ (sort items (lambda (a b)
+ (let ((text-a (cdr a))
+ (text-b (cdr b)))
+ (string<= text-a text-b))))))
+ (map car sorted-items)))
+
+ ;; Store the last selected listbox item's key.
+ (define last-listbox-key (make-parameter #f))
+
+ (define (previous-key keys key)
+ (let ((index (list-index (cut eq? key <>) keys)))
+ (and index
+ (> index 0)
+ (list-ref keys (- index 1)))))
+
+ (define (next-key keys key)
+ (let ((index (list-index (cut eq? key <>) keys)))
+ (and index
+ (< index (- (length keys) 1))
+ (list-ref keys (+ index 1)))))
+
+ (define (set-default-item listbox listbox-keys default-item)
+ "Set the default item of LISTBOX to DEFAULT-ITEM. LISTBOX-KEYS is the
+association list returned by the FILL-LISTBOX procedure. It is used because
+the current listbox item has to be selected by key."
+ (for-each (match-lambda
+ ((key . item)
+ (when (equal? item default-item)
+ (set-current-listbox-entry-by-key listbox key))))
+ listbox-keys))
+
+ (let* ((listbox (make-listbox
+ -1 -1
+ listbox-height
+ (logior FLAG-SCROLL FLAG-BORDER FLAG-RETURNEXIT
+ (if listbox-allow-multiple?
+ FLAG-MULTIPLE
+ 0))))
+ (form (make-form))
+ (info-textbox
+ (make-reflowed-textbox -1 -1 info-text
+ info-textbox-width
+ #:flags FLAG-BORDER))
+ (button (make-button -1 -1 button-text))
+ (button2 (and button2-text
+ (make-button -1 -1 button2-text)))
+ (grid (vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT info-textbox
+ GRID-ELEMENT-COMPONENT listbox
+ GRID-ELEMENT-SUBGRID
+ (apply
+ horizontal-stacked-grid
+ GRID-ELEMENT-COMPONENT button
+ `(,@(if button2
+ (list GRID-ELEMENT-COMPONENT button2)
+ '())))))
+ (sorted-items (if sort-listbox-items?
+ (sort-listbox-items listbox-items)
+ listbox-items))
+ (keys (fill-listbox listbox sorted-items)))
+
+ ;; On every listbox element change, check if we need to skip it. If yes,
+ ;; depending on the 'last-listbox-key', jump forward or backward. If no,
+ ;; do nothing.
+ (add-component-callback
+ listbox
+ (lambda (component)
+ (let* ((current-key (current-listbox-entry listbox))
+ (listbox-keys (map car keys))
+ (last-key (last-listbox-key))
+ (item (assoc-ref keys current-key))
+ (prev-key (previous-key listbox-keys current-key))
+ (next-key (next-key listbox-keys current-key)))
+ ;; Update last-listbox-key before a potential call to
+ ;; set-current-listbox-entry-by-key, because it will immediately
+ ;; cause this callback to be called for the new entry.
+ (last-listbox-key current-key)
+ (when (skip-item-procedure? item)
+ (when (eq? prev-key last-key)
+ (if next-key
+ (set-current-listbox-entry-by-key listbox next-key)
+ (set-current-listbox-entry-by-key listbox prev-key)))
+ (when (eq? next-key last-key)
+ (if prev-key
+ (set-current-listbox-entry-by-key listbox prev-key)
+ (set-current-listbox-entry-by-key listbox next-key)))))))
+
+ (when listbox-default-item
+ (set-default-item listbox keys listbox-default-item))
+
+ (when allow-delete?
+ (form-add-hotkey form KEY-DELETE))
+
+ (add-form-to-grid grid form #t)
+ (make-wrapped-grid-window grid title)
+
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (case exit-reason
+ ((exit-component)
+ (cond
+ ((components=? argument button)
+ (button-callback-procedure))
+ ((and button2
+ (components=? argument button2))
+ (button2-callback-procedure))
+ ((components=? argument listbox)
+ (if listbox-allow-multiple?
+ (let* ((entries (listbox-selection listbox))
+ (items (map (lambda (entry)
+ (assoc-ref keys entry))
+ entries)))
+ (listbox-callback-procedure items))
+ (let* ((entry (current-listbox-entry listbox))
+ (item (assoc-ref keys entry)))
+ (listbox-callback-procedure item))))))
+ ((exit-hotkey)
+ (let* ((entry (current-listbox-entry listbox))
+ (item (assoc-ref keys entry)))
+ (hotkey-callback-procedure argument item)))))
+ (lambda ()
+ (destroy-form-and-pop form))))))
+
+(define* (run-scale-page #:key
+ title
+ info-text
+ (info-textbox-width 50)
+ (scale-width 40)
+ (scale-full-value 100)
+ scale-update-proc
+ (max-scale-update 5))
+ "Run a page with a progress bar (called 'scale' in newt). The given
+INFO-TEXT is displayed in a textbox above the scale. The width of the textbox
+is set to INFO-TEXTBOX-WIDTH. The width of the scale is set to
+SCALE-WIDTH. SCALE-FULL-VALUE indicates the value that correspond to 100% of
+the scale.
+
+The procedure SCALE-UPDATE-PROC shall return a new scale
+value. SCALE-UPDATE-PROC will be called until the returned value is superior
+or equal to SCALE-FULL-VALUE, but no more than MAX-SCALE-UPDATE times. An
+error is raised if the MAX-SCALE-UPDATE limit is reached."
+ (let* ((info-textbox
+ (make-reflowed-textbox -1 -1 info-text
+ info-textbox-width
+ #:flags FLAG-BORDER))
+ (scale (make-scale -1 -1 scale-width scale-full-value))
+ (grid (vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT info-textbox
+ GRID-ELEMENT-COMPONENT scale))
+ (form (make-form)))
+
+ (add-form-to-grid grid form #t)
+ (make-wrapped-grid-window grid title)
+
+ (draw-form form)
+ ;; This call is imperative, otherwise the form won't be displayed. See the
+ ;; explanation in the above commentary.
+ (newt-refresh)
+
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (let loop ((i max-scale-update)
+ (last-value 0))
+ (let ((value (scale-update-proc last-value)))
+ (set-scale-value scale value)
+ ;; Same as above.
+ (newt-refresh)
+ (unless (>= value scale-full-value)
+ (if (> i 0)
+ (loop (- i 1) value)
+ (error "Max scale updates reached."))))))
+ (lambda ()
+ (destroy-form-and-pop form)))))
+
+(define* (run-checkbox-tree-page #:key
+ info-text
+ title
+ items
+ item->text
+ (info-textbox-width 50)
+ (checkbox-tree-height 10)
+ (ok-button-callback-procedure
+ (const #t))
+ (exit-button-callback-procedure
+ (const #t)))
+ "Run a page allowing the user to select one or multiple items among ITEMS in
+a checkbox list. The page contains vertically stacked from the top to the
+bottom, an informative text set to INFO-TEXT, the checkbox list and two
+buttons, 'Ok' and 'Exit'. The page title's is set to TITLE. ITEMS are
+converted to text using ITEM->TEXT before being displayed in the checkbox
+list.
+
+INFO-TEXTBOX-WIDTH is the width of the textbox where INFO-TEXT will be
+displayed. CHECKBOX-TREE-HEIGHT is the height of the checkbox list.
+
+OK-BUTTON-CALLBACK-PROCEDURE is called when the 'Ok' button is pressed.
+EXIT-BUTTON-CALLBACK-PROCEDURE is called when the 'Exit' button is
+pressed.
+
+This procedure returns the list of checked items in the checkbox list among
+ITEMS when 'Ok' is pressed."
+ (define (fill-checkbox-tree checkbox-tree items)
+ (map
+ (lambda (item)
+ (let* ((item-text (item->text item))
+ (key (add-entry-to-checkboxtree checkbox-tree item-text 0)))
+ (cons key item)))
+ items))
+
+ (let* ((checkbox-tree
+ (make-checkboxtree -1 -1
+ checkbox-tree-height
+ FLAG-BORDER))
+ (info-textbox
+ (make-reflowed-textbox -1 -1 info-text
+ info-textbox-width
+ #:flags FLAG-BORDER))
+ (ok-button (make-button -1 -1 (G_ "OK")))
+ (exit-button (make-button -1 -1 (G_ "Exit")))
+ (grid (vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT info-textbox
+ GRID-ELEMENT-COMPONENT checkbox-tree
+ GRID-ELEMENT-SUBGRID
+ (horizontal-stacked-grid
+ GRID-ELEMENT-COMPONENT ok-button
+ GRID-ELEMENT-COMPONENT exit-button)))
+ (keys (fill-checkbox-tree checkbox-tree items))
+ (form (make-form)))
+
+ (add-form-to-grid grid form #t)
+ (make-wrapped-grid-window grid title)
+
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (case exit-reason
+ ((exit-component)
+ (cond
+ ((components=? argument ok-button)
+ (let* ((entries (current-checkbox-selection checkbox-tree))
+ (current-items (map (lambda (entry)
+ (assoc-ref keys entry))
+ entries)))
+ (ok-button-callback-procedure)
+ current-items))
+ ((components=? argument exit-button)
+ (exit-button-callback-procedure))))))
+ (lambda ()
+ (destroy-form-and-pop form))))))
+
+(define* (run-file-textbox-page #:key
+ info-text
+ title
+ file
+ (info-textbox-width 50)
+ (file-textbox-width 50)
+ (file-textbox-height 30)
+ (exit-button? #t)
+ (ok-button-callback-procedure
+ (const #t))
+ (exit-button-callback-procedure
+ (const #t)))
+ (let* ((info-textbox
+ (make-reflowed-textbox -1 -1 info-text
+ info-textbox-width
+ #:flags FLAG-BORDER))
+ (file-text (read-all file))
+ (file-textbox
+ (make-textbox -1 -1
+ file-textbox-width
+ file-textbox-height
+ (logior FLAG-SCROLL FLAG-BORDER)))
+ (ok-button (make-button -1 -1 (G_ "OK")))
+ (exit-button (make-button -1 -1 (G_ "Exit")))
+ (grid (vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT info-textbox
+ GRID-ELEMENT-COMPONENT file-textbox
+ GRID-ELEMENT-SUBGRID
+ (apply
+ horizontal-stacked-grid
+ GRID-ELEMENT-COMPONENT ok-button
+ `(,@(if exit-button?
+ (list GRID-ELEMENT-COMPONENT exit-button)
+ '())))))
+ (form (make-form)))
+
+ (set-textbox-text file-textbox file-text)
+ (add-form-to-grid grid form #t)
+ (make-wrapped-grid-window grid title)
+
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (case exit-reason
+ ((exit-component)
+ (cond
+ ((components=? argument ok-button)
+ (ok-button-callback-procedure))
+ ((and exit-button?
+ (components=? argument exit-button))
+ (exit-button-callback-procedure))))))
+ (lambda ()
+ (destroy-form-and-pop form))))))
diff --git a/gnu/installer/newt/partition.scm b/gnu/installer/newt/partition.scm
new file mode 100644
index 0000000000..d4c91edc66
--- /dev/null
+++ b/gnu/installer/newt/partition.scm
@@ -0,0 +1,766 @@
+;;; 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 newt partition)
+ #:use-module (gnu installer parted)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer newt utils)
+ #:use-module (guix i18n)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (newt)
+ #:use-module (parted)
+ #:export (run-partioning-page))
+
+(define (button-exit-action)
+ "Raise the &installer-step-abort condition."
+ (raise
+ (condition
+ (&installer-step-abort))))
+
+(define (run-scheme-page)
+ "Run a page asking the user for a partitioning scheme."
+ (let* ((items
+ '((root . "Everything is one partition")
+ (root-home . "Separate /home partition")))
+ (result (run-listbox-selection-page
+ #:info-text (G_ "Please select a partitioning scheme.")
+ #:title (G_ "Partition scheme")
+ #:listbox-items items
+ #:listbox-item->text cdr
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure button-exit-action)))
+ (car result)))
+
+(define (draw-formatting-page)
+ "Draw a page to indicate partitions are being formated."
+ (draw-info-page
+ (format #f (G_ "Partition formatting is in progress, please wait."))
+ (G_ "Preparing partitions")))
+
+(define (run-device-page devices)
+ "Run a page asking the user to select a device among those in the given
+DEVICES list."
+ (define (device-items)
+ (map (lambda (device)
+ `(,device . ,(device-description device)))
+ devices))
+
+ (let* ((result (run-listbox-selection-page
+ #:info-text (G_ "Please select a disk.")
+ #:title (G_ "Disk")
+ #:listbox-items (device-items)
+ #:listbox-item->text cdr
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure button-exit-action))
+ (device (car result)))
+ device))
+
+(define (run-label-page button-text button-callback)
+ "Run a page asking the user to select a partition table label."
+ (run-listbox-selection-page
+ #:info-text (G_ "Select a new partition table type. \
+Be careful, all data on the disk will be lost.")
+ #:title (G_ "Partition table")
+ #:listbox-items '("msdos" "gpt")
+ #:listbox-item->text identity
+ #:button-text button-text
+ #:button-callback-procedure button-callback))
+
+(define (run-type-page partition)
+ "Run a page asking the user to select a partition type."
+ (let* ((disk (partition-disk partition))
+ (partitions (disk-partitions disk))
+ (other-extended-partitions?
+ (any extended-partition? partitions))
+ (items
+ `(normal ,@(if other-extended-partitions?
+ '()
+ '(extended)))))
+ (run-listbox-selection-page
+ #:info-text (G_ "Please select a partition type.")
+ #:title (G_ "Partition type")
+ #:listbox-items items
+ #:listbox-item->text symbol->string
+ #:sort-listbox-items? #f
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure button-exit-action)))
+
+(define (run-fs-type-page)
+ "Run a page asking the user to select a file-system type."
+ (run-listbox-selection-page
+ #:info-text (G_ "Please select the file-system type for this partition.")
+ #:title (G_ "File-system type")
+ #:listbox-items '(ext4 btrfs fat32 swap)
+ #:listbox-item->text user-fs-type-name
+ #:sort-listbox-items? #f
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure button-exit-action))
+
+(define (inform-can-create-partition? user-partition)
+ "Return #t if it is possible to create USER-PARTITION. This is determined by
+calling CAN-CREATE-PARTITION? procedure. If an exception is raised, catch it
+an inform the user with an appropriate error-page and return #f."
+ (guard (c ((max-primary-exceeded? c)
+ (run-error-page
+ (G_ "Primary partitions count exceeded.")
+ (G_ "Creation error"))
+ #f)
+ ((extended-creation-error? c)
+ (run-error-page
+ (G_ "Extended partition creation error.")
+ (G_ "Creation error"))
+ #f)
+ ((logical-creation-error? c)
+ (run-error-page
+ (G_ "Logical partition creation error.")
+ (G_ "Creation error"))
+ #f))
+ (can-create-partition? user-partition)))
+
+(define (prompt-luks-passwords user-partitions)
+ "Prompt for the luks passwords of the encrypted partitions in
+USER-PARTITIONS list. Return this list with password fields filled-in."
+ (map (lambda (user-part)
+ (let* ((crypt-label (user-partition-crypt-label user-part))
+ (file-name (user-partition-file-name user-part))
+ (password-page
+ (lambda ()
+ (run-input-page
+ (format #f (G_ "Please enter the password for the \
+encryption of partition ~a (label: ~a).") file-name crypt-label)
+ (G_ "Password required"))))
+ (password-confirm-page
+ (lambda ()
+ (run-input-page
+ (format #f (G_ "Please confirm the password for the \
+encryption of partition ~a (label: ~a).") file-name crypt-label)
+ (G_ "Password confirmation required")))))
+ (if crypt-label
+ (let loop ()
+ (let ((password (password-page))
+ (confirmation (password-confirm-page)))
+ (if (string=? password confirmation)
+ (user-partition
+ (inherit user-part)
+ (crypt-password password))
+ (begin
+ (run-error-page
+ (G_ "Password mismatch, please try again.")
+ (G_ "Password error"))
+ (loop)))))
+ user-part)))
+ user-partitions))
+
+(define* (run-partition-page target-user-partition
+ #:key
+ (default-item #f))
+ "Run a page allowing the user to edit the given TARGET-USER-PARTITION
+record. If the argument DEFAULT-ITEM is passed, use it to select the current
+listbox item. This is used to avoid the focus to switch back to the first
+listbox entry while calling this procedure recursively."
+
+ (define (numeric-size device size)
+ "Parse the given SIZE on DEVICE and return it."
+ (call-with-values
+ (lambda ()
+ (unit-parse size device))
+ (lambda (value range)
+ value)))
+
+ (define (numeric-size-range device size)
+ "Parse the given SIZE on DEVICE and return the associated RANGE."
+ (call-with-values
+ (lambda ()
+ (unit-parse size device))
+ (lambda (value range)
+ range)))
+
+ (define* (fill-user-partition-geom user-part
+ #:key
+ device (size #f) start end)
+ "Return the given USER-PART with the START, END and SIZE fields set to the
+eponym arguments. Use UNIT-FORMAT-CUSTOM to format START and END arguments as
+sectors on DEVICE."
+ (user-partition
+ (inherit user-part)
+ (size size)
+ (start (unit-format-custom device start UNIT-SECTOR))
+ (end (unit-format-custom device end UNIT-SECTOR))))
+
+ (define (apply-user-partition-changes user-part)
+ "Set the name, file-system type and boot flag on the partition specified
+by USER-PART, if it is applicable for the partition type."
+ (let* ((partition (user-partition-parted-object user-part))
+ (disk (partition-disk partition))
+ (disk-type (disk-disk-type disk))
+ (device (disk-device disk))
+ (has-name? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-PARTITION-NAME))
+ (name (user-partition-name user-part))
+ (fs-type (filesystem-type-get
+ (user-fs-type-name
+ (user-partition-fs-type user-part))))
+ (bootable? (user-partition-bootable? user-part))
+ (esp? (user-partition-esp? user-part))
+ (flag-bootable?
+ (partition-is-flag-available? partition PARTITION-FLAG-BOOT))
+ (flag-esp?
+ (partition-is-flag-available? partition PARTITION-FLAG-ESP)))
+ (when (and has-name? name)
+ (partition-set-name partition name))
+ (partition-set-system partition fs-type)
+ (when flag-bootable?
+ (partition-set-flag partition
+ PARTITION-FLAG-BOOT
+ (if bootable? 1 0)))
+ (when flag-esp?
+ (partition-set-flag partition
+ PARTITION-FLAG-ESP
+ (if esp? 1 0)))
+ #t))
+
+ (define (listbox-action listbox-item)
+ (let* ((item (car listbox-item))
+ (partition (user-partition-parted-object
+ target-user-partition))
+ (disk (partition-disk partition))
+ (device (disk-device disk)))
+ (list
+ item
+ (case item
+ ((name)
+ (let* ((old-name (user-partition-name target-user-partition))
+ (name
+ (run-input-page (G_ "Please enter the partition gpt name.")
+ (G_ "Partition name")
+ #:default-text old-name)))
+ (user-partition
+ (inherit target-user-partition)
+ (name name))))
+ ((type)
+ (let ((new-type (run-type-page partition)))
+ (user-partition
+ (inherit target-user-partition)
+ (type new-type))))
+ ((bootable)
+ (user-partition
+ (inherit target-user-partition)
+ (bootable? (not (user-partition-bootable?
+ target-user-partition)))))
+ ((esp?)
+ (let ((new-esp? (not (user-partition-esp?
+ target-user-partition))))
+ (user-partition
+ (inherit target-user-partition)
+ (esp? new-esp?)
+ (mount-point (if new-esp?
+ (default-esp-mount-point)
+ "")))))
+ ((crypt-label)
+ (let* ((label (user-partition-crypt-label
+ target-user-partition))
+ (new-label
+ (and (not label)
+ (run-input-page
+ (G_ "Please enter the encrypted label")
+ (G_ "Encryption label")))))
+ (user-partition
+ (inherit target-user-partition)
+ (need-formatting? #t)
+ (crypt-label new-label))))
+ ((need-formatting?)
+ (user-partition
+ (inherit target-user-partition)
+ (need-formatting?
+ (not (user-partition-need-formatting?
+ target-user-partition)))))
+ ((size)
+ (let* ((old-size (user-partition-size target-user-partition))
+ (max-size-value (partition-length partition))
+ (max-size (unit-format device max-size-value))
+ (start (partition-start partition))
+ (size (run-input-page
+ (format #f (G_ "Please enter the size of the partition.\
+ The maximum size is ~a.") max-size)
+ (G_ "Partition size")
+ #:default-text (or old-size max-size)))
+ (size-percentage (read-percentage size))
+ (size-value (if size-percentage
+ (nearest-exact-integer
+ (/ (* max-size-value size-percentage)
+ 100))
+ (numeric-size device size)))
+ (end (and size-value
+ (+ start size-value)))
+ (size-range (numeric-size-range device size))
+ (size-range-ok? (and size-range
+ (< (+ start
+ (geometry-start size-range))
+ (partition-end partition)))))
+ (cond
+ ((and size-percentage (> size-percentage 100))
+ (run-error-page
+ (G_ "The percentage can not be superior to 100.")
+ (G_ "Size error"))
+ target-user-partition)
+ ((not size-value)
+ (run-error-page
+ (G_ "The requested size is incorrectly formatted, or too large.")
+ (G_ "Size error"))
+ target-user-partition)
+ ((not (or size-percentage size-range-ok?))
+ (run-error-page
+ (G_ "The request size is superior to the maximum size.")
+ (G_ "Size error"))
+ target-user-partition)
+ (else
+ (fill-user-partition-geom target-user-partition
+ #:device device
+ #:size size
+ #:start start
+ #:end end)))))
+ ((fs-type)
+ (let ((fs-type (run-fs-type-page)))
+ (user-partition
+ (inherit target-user-partition)
+ (fs-type fs-type))))
+ ((mount-point)
+ (let* ((old-mount (or (user-partition-mount-point
+ target-user-partition)
+ ""))
+ (mount
+ (run-input-page
+ (G_ "Please enter the desired mounting point for this \
+partition. Leave this field empty if you don't want to set a mounting point.")
+ (G_ "Mounting point")
+ #:default-text old-mount
+ #:allow-empty-input? #t)))
+ (user-partition
+ (inherit target-user-partition)
+ (mount-point (and (not (string=? mount ""))
+ mount)))))))))
+
+ (define (button-action)
+ (let* ((partition (user-partition-parted-object
+ target-user-partition))
+ (prev-part (partition-prev partition))
+ (disk (partition-disk partition))
+ (device (disk-device disk))
+ (creation? (freespace-partition? partition))
+ (start (partition-start partition))
+ (end (partition-end partition))
+ (new-user-partition
+ (if (user-partition-start target-user-partition)
+ target-user-partition
+ (fill-user-partition-geom target-user-partition
+ #:device device
+ #:start start
+ #:end end))))
+ ;; It the backend PARTITION has free-space type, it means we are
+ ;; creating a new partition, otherwise, we are editing an already
+ ;; existing PARTITION.
+ (if creation?
+ (let* ((ok-create-partition?
+ (inform-can-create-partition? new-user-partition))
+ (new-partition
+ (and ok-create-partition?
+ (mkpart disk
+ new-user-partition
+ #:previous-partition prev-part))))
+ (and new-partition
+ (user-partition
+ (inherit new-user-partition)
+ (need-formatting? #t)
+ (file-name (partition-get-path new-partition))
+ (disk-file-name (device-path device))
+ (parted-object new-partition))))
+ (and (apply-user-partition-changes new-user-partition)
+ new-user-partition))))
+
+ (let* ((items (user-partition-description target-user-partition))
+ (partition (user-partition-parted-object
+ target-user-partition))
+ (disk (partition-disk partition))
+ (device (disk-device disk))
+ (file-name (device-path device))
+ (number-str (partition-print-number partition))
+ (type (user-partition-type target-user-partition))
+ (type-str (symbol->string type))
+ (start (unit-format device (partition-start partition)))
+ (creation? (freespace-partition? partition))
+ (default-item (and default-item
+ (find (lambda (item)
+ (eq? (car item) default-item))
+ items)))
+ (result
+ (run-listbox-selection-page
+ #:info-text
+ (if creation?
+ (G_ (format #f "Creating ~a partition starting at ~a of ~a."
+ type-str start file-name))
+ (G_ (format #f "You are currently editing partition ~a."
+ number-str)))
+ #:title (if creation?
+ (G_ "Partition creation")
+ (G_ "Partition edit"))
+ #:listbox-items items
+ #:listbox-item->text cdr
+ #:sort-listbox-items? #f
+ #:listbox-default-item default-item
+ #:button-text (G_ "OK")
+ #:listbox-callback-procedure listbox-action
+ #:button-callback-procedure button-action)))
+ (match result
+ ((item new-user-partition)
+ (run-partition-page new-user-partition
+ #:default-item item))
+ (else result))))
+
+(define* (run-disk-page disks
+ #:optional (user-partitions '())
+ #:key (guided? #f))
+ "Run a page allowing to edit the partition tables of the given DISKS. If
+specified, USER-PARTITIONS is a list of <user-partition> records associated to
+the partitions on DISKS."
+
+ (define (other-logical-partitions? partitions)
+ "Return #t if at least one of the partition in PARTITIONS list is a
+logical partition, return #f otherwise."
+ (any logical-partition? partitions))
+
+ (define (other-non-logical-partitions? partitions)
+ "Return #t is at least one of the partitions in PARTITIONS list is not a
+logical partition, return #f otherwise."
+ (let ((non-logical-partitions
+ (remove logical-partition? partitions)))
+ (or (any normal-partition? non-logical-partitions)
+ (any freespace-partition? non-logical-partitions))))
+
+ (define (add-tree-symbols partitions descriptions)
+ "Concatenate tree symbols to the given DESCRIPTIONS list and return
+it. The PARTITIONS list is the list of partitions described in
+DESCRIPTIONS. The tree symbols are used to indicate the partition's disk and
+for logical partitions, the extended partition which includes them."
+ (match descriptions
+ (() '())
+ ((description . rest-descriptions)
+ (match partitions
+ ((partition . rest-partitions)
+ (if (null? rest-descriptions)
+ (list (if (logical-partition? partition)
+ (string-append " ┗━ " description)
+ (string-append "┗━ " description)))
+ (cons (cond
+ ((extended-partition? partition)
+ (if (other-non-logical-partitions? rest-partitions)
+ (string-append "┣┳ " description)
+ (string-append "┗┳ " description)))
+ ((logical-partition? partition)
+ (if (other-logical-partitions? rest-partitions)
+ (if (other-non-logical-partitions? rest-partitions)
+ (string-append "┃┣━ " description)
+ (string-append " ┣━ " description))
+ (if (other-non-logical-partitions? rest-partitions)
+ (string-append "┃┗━ " description)
+ (string-append " ┗━ " description))))
+ (else
+ (string-append "┣━ " description)))
+ (add-tree-symbols rest-partitions
+ rest-descriptions))))))))
+
+ (define (skip-item? item)
+ (eq? (car item) 'skip))
+
+ (define (disk-items)
+ "Return the list of strings describing DISKS."
+ (let loop ((disks disks))
+ (match disks
+ (() '())
+ ((disk . rest)
+ (let* ((device (disk-device disk))
+ (partitions (disk-partitions disk))
+ (partitions*
+ (filter-map
+ (lambda (partition)
+ (and (not (metadata-partition? partition))
+ (not (small-freespace-partition? device
+ partition))
+ partition))
+ partitions))
+ (descriptions (add-tree-symbols
+ partitions*
+ (partitions-descriptions partitions*
+ user-partitions)))
+ (partition-items (map cons partitions* descriptions)))
+ (append
+ `((,disk . ,(device-description device disk))
+ ,@partition-items
+ ,@(if (null? rest)
+ '()
+ '((skip . ""))))
+ (loop rest)))))))
+
+ (define (remove-user-partition-by-partition user-partitions partition)
+ "Return the USER-PARTITIONS list with the record with the given PARTITION
+object removed. If PARTITION is an extended partition, also remove all logical
+partitions from USER-PARTITIONS."
+ (remove (lambda (p)
+ (let ((cur-partition (user-partition-parted-object p)))
+ (or (equal? cur-partition partition)
+ (and (extended-partition? partition)
+ (logical-partition? cur-partition)))))
+ user-partitions))
+
+ (define (remove-user-partition-by-disk user-partitions disk)
+ "Return the USER-PARTITIONS list with the <user-partition> records located
+on given DISK removed."
+ (remove (lambda (p)
+ (let* ((partition (user-partition-parted-object p))
+ (cur-disk (partition-disk partition)))
+ (equal? cur-disk disk)))
+ user-partitions))
+
+ (define (update-user-partitions user-partitions new-user-partition)
+ "Update or insert NEW-USER-PARTITION record in USER-PARTITIONS list
+depending if one of the <user-partition> record in USER-PARTITIONS has the
+same PARTITION object as NEW-USER-PARTITION."
+ (let* ((partition (user-partition-parted-object new-user-partition))
+ (user-partitions*
+ (remove-user-partition-by-partition user-partitions
+ partition)))
+ (cons new-user-partition user-partitions*)))
+
+ (define (button-ok-action)
+ "Commit the modifications to all DISKS and return #t."
+ (for-each (lambda (disk)
+ (disk-commit disk))
+ disks)
+ #t)
+
+ (define (listbox-action listbox-item)
+ "A disk or a partition has been selected. If it's a disk, ask for a label
+to create a new partition table. If it is a partition, propose the user to
+edit it."
+ (let ((item (car listbox-item)))
+ (cond
+ ((disk? item)
+ (let ((label (run-label-page (G_ "Back") (const #f))))
+ (if label
+ (let* ((device (disk-device item))
+ (new-disk (mklabel device label))
+ (commit-new-disk (disk-commit new-disk))
+ (other-disks (remove (lambda (disk)
+ (equal? disk item))
+ disks))
+ (new-user-partitions
+ (remove-user-partition-by-disk user-partitions item)))
+ (disk-destroy item)
+ `((disks . ,(cons new-disk other-disks))
+ (user-partitions . ,new-user-partitions)))
+ `((disks . ,disks)
+ (user-partitions . ,user-partitions)))))
+ ((partition? item)
+ (let* ((partition item)
+ (disk (partition-disk partition))
+ (device (disk-device disk))
+ (existing-user-partition
+ (find-user-partition-by-parted-object user-partitions
+ partition))
+ (edit-user-partition
+ (or existing-user-partition
+ (partition->user-partition partition))))
+ `((disks . ,disks)
+ (user-partitions . ,user-partitions)
+ (edit-user-partition . ,edit-user-partition)))))))
+
+ (define (hotkey-action key listbox-item)
+ "The DELETE key has been pressed on a disk or a partition item."
+ (let ((item (car listbox-item))
+ (default-result
+ `((disks . ,disks)
+ (user-partitions . ,user-partitions))))
+ (cond
+ ((disk? item)
+ (let* ((device (disk-device item))
+ (file-name (device-path device))
+ (info-text
+ (format #f (G_ "Are you sure you want to delete everything on disk ~a?")
+ file-name))
+ (result (choice-window (G_ "Delete disk")
+ (G_ "OK")
+ (G_ "Exit")
+ info-text)))
+ (case result
+ ((1)
+ (disk-delete-all item)
+ `((disks . ,disks)
+ (user-partitions
+ . ,(remove-user-partition-by-disk user-partitions item))))
+ (else
+ default-result))))
+ ((partition? item)
+ (if (freespace-partition? item)
+ (run-error-page (G_ "You cannot delete a free space area.")
+ (G_ "Delete partition"))
+ (let* ((disk (partition-disk item))
+ (number-str (partition-print-number item))
+ (info-text
+ (format #f (G_ "Are you sure you want to delete partition ~a?")
+ number-str))
+ (result (choice-window (G_ "Delete partition")
+ (G_ "OK")
+ (G_ "Exit")
+ info-text)))
+ (case result
+ ((1)
+ (let ((new-user-partitions
+ (remove-user-partition-by-partition user-partitions
+ item)))
+ (disk-delete-partition disk item)
+ `((disks . ,disks)
+ (user-partitions . ,new-user-partitions))))
+ (else
+ default-result))))))))
+
+ (let* ((info-text (G_ "You can change a disk's partition table by \
+selecting it and pressing ENTER. You can also edit a partition by selecting it \
+and pressing ENTER, or remove it by pressing DELETE. To create a new \
+partition, select a free space area and press ENTER.
+
+At least one partition must have its mounting point set to '/'."))
+ (guided-info-text (format #f (G_ "This is the proposed \
+partitioning. It is still possible to edit it or to go back to install menu \
+by pressing the Exit button.~%~%")))
+ (result
+ (run-listbox-selection-page
+ #:info-text (if guided?
+ (string-append guided-info-text info-text)
+ info-text)
+
+ #:title (if guided?
+ (G_ "Guided partitioning")
+ (G_ "Manual partitioning"))
+ #:info-textbox-width 70
+ #:listbox-items (disk-items)
+ #:listbox-item->text cdr
+ #:sort-listbox-items? #f
+ #:skip-item-procedure? skip-item?
+ #:allow-delete? #t
+ #:button-text (G_ "OK")
+ #:button-callback-procedure button-ok-action
+ #:button2-text (G_ "Exit")
+ #:button2-callback-procedure button-exit-action
+ #:listbox-callback-procedure listbox-action
+ #:hotkey-callback-procedure hotkey-action)))
+ (if (eq? result #t)
+ (let ((user-partitions-ok?
+ (guard
+ (c ((no-root-mount-point? c)
+ (run-error-page
+ (G_ "No root mount point found.")
+ (G_ "Missing mount point"))
+ #f))
+ (check-user-partitions user-partitions))))
+ (if user-partitions-ok?
+ (begin
+ (for-each (cut disk-destroy <>) disks)
+ user-partitions)
+ (run-disk-page disks user-partitions
+ #:guided? guided?)))
+ (let* ((result-disks (assoc-ref result 'disks))
+ (result-user-partitions (assoc-ref result
+ 'user-partitions))
+ (edit-user-partition (assoc-ref result
+ 'edit-user-partition))
+ (can-create-partition?
+ (and edit-user-partition
+ (inform-can-create-partition? edit-user-partition)))
+ (new-user-partition (and edit-user-partition
+ can-create-partition?
+ (run-partition-page
+ edit-user-partition)))
+ (new-user-partitions
+ (if new-user-partition
+ (update-user-partitions result-user-partitions
+ new-user-partition)
+ result-user-partitions)))
+ (run-disk-page result-disks new-user-partitions
+ #:guided? guided?)))))
+
+(define (run-partioning-page)
+ "Run a page asking the user for a partitioning method."
+ (define (run-page devices)
+ (let* ((items
+ '((entire . "Guided - using the entire disk")
+ (entire-encrypted . "Guided - using the entire disk with encryption")
+ (manual . "Manual")))
+ (result (run-listbox-selection-page
+ #:info-text (G_ "Please select a partitioning method.")
+ #:title (G_ "Partitioning method")
+ #:listbox-items items
+ #:listbox-item->text cdr
+ #:button-text (G_ "Exit")
+ #:button-callback-procedure button-exit-action))
+ (method (car result)))
+ (cond
+ ((or (eq? method 'entire)
+ (eq? method 'entire-encrypted))
+ (let* ((device (run-device-page devices))
+ (disk-type (disk-probe device))
+ (disk (if disk-type
+ (disk-new device)
+ (let* ((label (run-label-page
+ (G_ "Exit")
+ button-exit-action))
+ (disk (mklabel device label)))
+ (disk-commit disk)
+ disk)))
+ (scheme (symbol-append method '- (run-scheme-page)))
+ (user-partitions (append
+ (auto-partition disk #:scheme scheme)
+ (create-special-user-partitions
+ (disk-partitions disk)))))
+ (run-disk-page (list disk) user-partitions
+ #:guided? #t)))
+ ((eq? method 'manual)
+ (let* ((disks (filter-map disk-new devices))
+ (user-partitions (append-map
+ create-special-user-partitions
+ (map disk-partitions disks)))
+ (result-user-partitions (run-disk-page disks
+ user-partitions)))
+ result-user-partitions)))))
+
+ (init-parted)
+ (let* ((non-install-devices (non-install-devices))
+ (user-partitions (run-page non-install-devices))
+ (user-partitions-with-pass (prompt-luks-passwords
+ user-partitions))
+ (form (draw-formatting-page)))
+ ;; Make sure the disks are not in use before proceeding to formatting.
+ (free-parted non-install-devices)
+ (format-user-partitions user-partitions-with-pass)
+ (destroy-form-and-pop form)
+ user-partitions))
diff --git a/gnu/installer/newt/services.scm b/gnu/installer/newt/services.scm
new file mode 100644
index 0000000000..6bcb6244ae
--- /dev/null
+++ b/gnu/installer/newt/services.scm
@@ -0,0 +1,48 @@
+;;; 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 newt services)
+ #:use-module (gnu installer services)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer newt utils)
+ #:use-module (guix i18n)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (newt)
+ #:export (run-services-page))
+
+(define (run-desktop-environments-cbt-page)
+ "Run a page allowing the user to choose between various desktop
+environments."
+ (run-checkbox-tree-page
+ #:info-text (G_ "Please select the desktop(s) environment(s) you wish to \
+install. If you select multiple desktops environments, we will be able to \
+choose the one to use on the log-in screen with F1.")
+ #:title (G_ "Desktop environment")
+ #:items %desktop-environments
+ #:item->text desktop-environment-name
+ #:checkbox-tree-height 5
+ #:exit-button-callback-procedure
+ (lambda ()
+ (raise
+ (condition
+ (&installer-step-abort))))))
+
+(define (run-services-page)
+ (run-desktop-environments-cbt-page))
diff --git a/gnu/installer/newt/timezone.scm b/gnu/installer/newt/timezone.scm
new file mode 100644
index 0000000000..6c96ee55b1
--- /dev/null
+++ b/gnu/installer/newt/timezone.scm
@@ -0,0 +1,83 @@
+;;; 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 newt timezone)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer timezone)
+ #:use-module (gnu installer newt page)
+ #: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)
+ #:use-module (newt)
+ #:export (run-timezone-page))
+
+;; Heigth of the listbox displaying timezones.
+(define timezone-listbox-heigth (make-parameter 20))
+
+;; Information textbox width.
+(define info-textbox-width (make-parameter 40))
+
+(define (fill-timezones listbox timezones)
+ "Fill the given LISTBOX with TIMEZONES. Return an association list
+correlating listbox keys with timezones."
+ (map (lambda (timezone)
+ (let ((key (append-entry-to-listbox listbox timezone)))
+ (cons key timezone)))
+ timezones))
+
+(define (run-timezone-page zonetab)
+ "Run a page displaying available timezones, grouped by regions. The user is
+invited to select a timezone. The selected timezone, under Posix format is
+returned."
+ (define (all-but-last list)
+ (reverse (cdr (reverse list))))
+
+ (define (run-page timezone-tree)
+ (define (loop path)
+ (let ((timezones (locate-childrens timezone-tree path)))
+ (run-listbox-selection-page
+ #:title (G_ "Timezone")
+ #:info-text (G_ "Please select a timezone.")
+ #:listbox-items timezones
+ #:listbox-item->text identity
+ #:button-text (if (null? path)
+ (G_ "Exit")
+ (G_ "Back"))
+ #:button-callback-procedure
+ (if (null? path)
+ (lambda _
+ (raise
+ (condition
+ (&installer-step-abort))))
+ (lambda _
+ (loop (all-but-last path))))
+ #:listbox-callback-procedure
+ (lambda (timezone)
+ (let* ((timezone* (append path (list timezone)))
+ (tz (timezone->posix-tz timezone*)))
+ (if (timezone-has-child? timezone-tree timezone*)
+ (loop timezone*)
+ tz))))))
+ (loop '()))
+
+ (let ((timezone-tree (zonetab->timezone-tree zonetab)))
+ (run-page timezone-tree)))
diff --git a/gnu/installer/newt/user.scm b/gnu/installer/newt/user.scm
new file mode 100644
index 0000000000..59b1913cfc
--- /dev/null
+++ b/gnu/installer/newt/user.scm
@@ -0,0 +1,175 @@
+;;; 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 newt user)
+ #:use-module (gnu installer user)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu installer newt utils)
+ #:use-module (guix i18n)
+ #:use-module (newt)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 receive)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:export (run-user-page))
+
+(define (run-user-add-page)
+ (define (pad-label label)
+ (string-pad-right label 20))
+
+ (let* ((label-name
+ (make-label -1 -1 (pad-label (G_ "Name"))))
+ (label-home-directory
+ (make-label -1 -1 (pad-label (G_ "Home directory"))))
+ (entry-width 30)
+ (entry-name (make-entry -1 -1 entry-width))
+ (entry-home-directory (make-entry -1 -1 entry-width))
+ (entry-grid (make-grid 2 2))
+ (button-grid (make-grid 1 1))
+ (ok-button (make-button -1 -1 (G_ "OK")))
+ (grid (make-grid 1 2))
+ (title (G_ "User creation"))
+ (set-entry-grid-field
+ (cut set-grid-field entry-grid <> <> GRID-ELEMENT-COMPONENT <>))
+ (form (make-form)))
+
+ (set-entry-grid-field 0 0 label-name)
+ (set-entry-grid-field 1 0 entry-name)
+ (set-entry-grid-field 0 1 label-home-directory)
+ (set-entry-grid-field 1 1 entry-home-directory)
+
+ (set-grid-field button-grid 0 0 GRID-ELEMENT-COMPONENT ok-button)
+
+ (add-component-callback
+ entry-name
+ (lambda (component)
+ (set-entry-text entry-home-directory
+ (string-append "/home/" (entry-value entry-name)))))
+
+ (add-components-to-form form
+ label-name label-home-directory
+ entry-name entry-home-directory
+ ok-button)
+
+ (make-wrapped-grid-window (vertically-stacked-grid
+ GRID-ELEMENT-SUBGRID entry-grid
+ GRID-ELEMENT-SUBGRID button-grid)
+ title)
+ (let ((error-page
+ (lambda ()
+ (run-error-page (G_ "Empty inputs are not allowed.")
+ (G_ "Empty input")))))
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (when (eq? exit-reason 'exit-component)
+ (cond
+ ((components=? argument ok-button)
+ (let ((name (entry-value entry-name))
+ (home-directory (entry-value entry-home-directory)))
+ (if (or (string=? name "")
+ (string=? home-directory ""))
+ (begin
+ (error-page)
+ (run-user-add-page))
+ (user
+ (name name)
+ (home-directory home-directory))))))))
+ (lambda ()
+ (destroy-form-and-pop form)))))))
+
+(define (run-user-page)
+ (define (run users)
+ (let* ((listbox (make-listbox
+ -1 -1 10
+ (logior FLAG-SCROLL FLAG-BORDER)))
+ (info-textbox
+ (make-reflowed-textbox
+ -1 -1
+ (G_ "Please add at least one user to system\
+ using the 'Add' button.")
+ 40 #:flags FLAG-BORDER))
+ (add-button (make-compact-button -1 -1 (G_ "Add")))
+ (del-button (make-compact-button -1 -1 (G_ "Delete")))
+ (listbox-button-grid
+ (apply
+ vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT add-button
+ `(,@(if (null? users)
+ '()
+ (list GRID-ELEMENT-COMPONENT del-button)))))
+ (ok-button (make-button -1 -1 (G_ "OK")))
+ (exit-button (make-button -1 -1 (G_ "Exit")))
+ (title "User creation")
+ (grid
+ (vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT info-textbox
+ GRID-ELEMENT-SUBGRID (horizontal-stacked-grid
+ GRID-ELEMENT-COMPONENT listbox
+ GRID-ELEMENT-SUBGRID listbox-button-grid)
+ GRID-ELEMENT-SUBGRID (horizontal-stacked-grid
+ GRID-ELEMENT-COMPONENT ok-button
+ GRID-ELEMENT-COMPONENT exit-button)))
+ (sorted-users (sort users (lambda (a b)
+ (string<= (user-name a)
+ (user-name b)))))
+ (listbox-elements
+ (map
+ (lambda (user)
+ `((key . ,(append-entry-to-listbox listbox
+ (user-name user)))
+ (user . ,user)))
+ sorted-users))
+ (form (make-form)))
+
+
+ (add-form-to-grid grid form #t)
+ (make-wrapped-grid-window grid title)
+ (if (null? users)
+ (set-current-component form add-button)
+ (set-current-component form ok-button))
+
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (when (eq? exit-reason 'exit-component)
+ (cond
+ ((components=? argument add-button)
+ (run (cons (run-user-add-page) users)))
+ ((components=? argument del-button)
+ (let* ((current-user-key (current-listbox-entry listbox))
+ (users
+ (map (cut assoc-ref <> 'user)
+ (remove (lambda (element)
+ (equal? (assoc-ref element 'key)
+ current-user-key))
+ listbox-elements))))
+ (run users)))
+ ((components=? argument ok-button)
+ (when (null? users)
+ (run-error-page (G_ "Please create at least one user.")
+ (G_ "No user"))
+ (run users))
+ users))))
+ (lambda ()
+ (destroy-form-and-pop form))))))
+ (run '()))
diff --git a/gnu/installer/newt/utils.scm b/gnu/installer/newt/utils.scm
new file mode 100644
index 0000000000..1c2ce4e628
--- /dev/null
+++ b/gnu/installer/newt/utils.scm
@@ -0,0 +1,43 @@
+;;; 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 newt utils)
+ #:use-module (ice-9 receive)
+ #:use-module (newt)
+ #:export (screen-columns
+ screen-rows
+
+ destroy-form-and-pop
+ set-screen-size!))
+
+;; Number of columns and rows of the terminal.
+(define screen-columns (make-parameter 0))
+(define screen-rows (make-parameter 0))
+
+(define (destroy-form-and-pop form)
+ "Destory the given FORM and pop the current window."
+ (destroy-form form)
+ (pop-window))
+
+(define (set-screen-size!)
+ "Set the parameters 'screen-columns' and 'screen-rows' to the number of
+columns and rows respectively of the current terminal."
+ (receive (columns rows)
+ (screen-size)
+ (screen-columns columns)
+ (screen-rows rows)))
diff --git a/gnu/installer/newt/welcome.scm b/gnu/installer/newt/welcome.scm
new file mode 100644
index 0000000000..eec98e291a
--- /dev/null
+++ b/gnu/installer/newt/welcome.scm
@@ -0,0 +1,118 @@
+;;; 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
+
+;;;
+;;; 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 newt welcome)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt utils)
+ #:use-module (guix build syscalls)
+ #:use-module (guix i18n)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 receive)
+ #:use-module (newt)
+ #:export (run-welcome-page))
+
+;; Expected width and height for the logo.
+(define logo-width (make-parameter 43))
+(define logo-height (make-parameter 19))
+
+(define info-textbox-width (make-parameter 70))
+(define options-listbox-height (make-parameter 5))
+
+(define* (run-menu-page title info-text logo
+ #:key
+ listbox-items
+ listbox-item->text)
+ "Run a page with the given TITLE, to ask the user to choose between
+LISTBOX-ITEMS displayed in a listbox. The listbox items are converted to text
+using LISTBOX-ITEM->TEXT procedure. Display the textual LOGO in the center of
+the page. Contrary to other pages, we cannot resort to grid layouts, because
+we want this page to occupy all the screen space available."
+ (define (fill-listbox listbox items)
+ (map (lambda (item)
+ (let* ((text (listbox-item->text item))
+ (key (append-entry-to-listbox listbox text)))
+ (cons key item)))
+ items))
+
+ (let* ((logo-textbox
+ (make-textbox -1 -1 (logo-width) (logo-height) 0))
+ (info-textbox
+ (make-reflowed-textbox -1 -1
+ info-text
+ (info-textbox-width)))
+ (options-listbox
+ (make-listbox -1 -1
+ (options-listbox-height)
+ (logior FLAG-BORDER FLAG-RETURNEXIT)))
+ (keys (fill-listbox options-listbox listbox-items))
+ (grid (vertically-stacked-grid
+ GRID-ELEMENT-COMPONENT logo-textbox
+ GRID-ELEMENT-COMPONENT info-textbox
+ GRID-ELEMENT-COMPONENT options-listbox))
+ (form (make-form)))
+
+ (set-textbox-text logo-textbox (read-all logo))
+
+ (add-form-to-grid grid form #t)
+ (make-wrapped-grid-window grid title)
+
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (when (eq? exit-reason 'exit-component)
+ (cond
+ ((components=? argument options-listbox)
+ (let* ((entry (current-listbox-entry options-listbox))
+ (item (assoc-ref keys entry)))
+ (match item
+ ((text . proc)
+ (proc))))))))
+ (lambda ()
+ (destroy-form-and-pop form))))))
+
+(define (run-welcome-page logo)
+ "Run a welcome page with the given textual LOGO displayed at the center of
+the page. Ask the user to choose between manual installation, graphical
+installation and reboot."
+ (run-menu-page
+ (G_ "GNU GuixSD install")
+ (G_ "Welcome to GNU GuixSD installer!
+
+Please note that the present graphical installer is still under heavy \
+development, so you might want to prefer using the shell based process. \
+The documentation is accessible at any time by pressing CTRL-ALT-F2.")
+ logo
+ #:listbox-items
+ `((,(G_ "Graphical install using a terminal based interface")
+ .
+ ,(const #t))
+ (,(G_ "Install using the shell based process")
+ .
+ ,(lambda ()
+ ;; Switch to TTY3, where a root shell is available for shell based
+ ;; install. The other root TTY's would have been ok too.
+ (system* "chvt" "3")
+ (run-welcome-page logo)))
+ (,(G_ "Reboot")
+ .
+ ,(lambda ()
+ (newt-finish)
+ (reboot))))
+ #:listbox-item->text car))
diff --git a/gnu/installer/newt/wifi.scm b/gnu/installer/newt/wifi.scm
new file mode 100644
index 0000000000..59e40e327e
--- /dev/null
+++ b/gnu/installer/newt/wifi.scm
@@ -0,0 +1,243 @@
+;;; 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 newt wifi)
+ #:use-module (gnu installer connman)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer newt utils)
+ #:use-module (gnu installer newt page)
+ #:use-module (guix i18n)
+ #:use-module (guix records)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 popen)
+ #:use-module (ice-9 receive)
+ #:use-module (ice-9 regex)
+ #:use-module (ice-9 rdelim)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:use-module (newt)
+ #:export (run-wifi-page))
+
+;; This record associates a connman service to its key the listbox.
+(define-record-type* <service-item>
+ service-item make-service-item
+ service-item?
+ (service service-item-service) ; connman <service>
+ (key service-item-key)) ; newt listbox-key
+
+(define (strength->string strength)
+ "Convert STRENGTH as an integer percentage into a text printable strength
+bar using unicode characters. Taken from NetworkManager's
+nmc_wifi_strength_bars."
+ (let ((quarter #\x2582)
+ (half #\x2584)
+ (three-quarter #\x2586)
+ (full #\x2588))
+ (cond
+ ((> strength 80)
+ ;; ▂▄▆█
+ (string quarter half three-quarter full))
+ ((> strength 55)
+ ;; ▂▄▆_
+ (string quarter half three-quarter #\_))
+ ((> strength 30)
+ ;; ▂▄__
+ (string quarter half #\_ #\_))
+ ((> strength 5)
+ ;; ▂___
+ (string quarter #\_ #\_ #\_))
+ (else
+ ;; ____
+ (string quarter #\_ #\_ #\_ #\_)))))
+
+(define (force-wifi-scan)
+ "Force a wifi scan. Raise a condition if no wifi technology is available."
+ (let* ((technologies (connman-technologies))
+ (wifi-technology
+ (find (lambda (technology)
+ (string=? (technology-type technology) "wifi"))
+ technologies)))
+ (if wifi-technology
+ (connman-scan-technology wifi-technology)
+ (raise (condition
+ (&message
+ (message (G_ "Unable to find a wifi technology"))))))))
+
+(define (draw-scanning-page)
+ "Draw a page to indicate a wifi scan in in progress."
+ (draw-info-page (G_ "Scanning wifi for available networks, please wait.")
+ (G_ "Scan in progress")))
+
+(define (run-wifi-password-page)
+ "Run a page prompting user for a password and return it."
+ (run-input-page (G_ "Please enter the wifi password.")
+ (G_ "Password required")))
+
+(define (run-wrong-password-page service-name)
+ "Run a page to inform user of a wrong password input."
+ (run-error-page
+ (format #f (G_ "The password you entered for ~a is incorrect.")
+ service-name)
+ (G_ "Wrong password")))
+
+(define (run-unknown-error-page service-name)
+ "Run a page to inform user that a connection error happened."
+ (run-error-page
+ (format #f
+ (G_ "An error occured while trying to connect to ~a, please retry.")
+ service-name)
+ (G_ "Connection error")))
+
+(define (password-callback)
+ (run-wifi-password-page))
+
+(define (connect-wifi-service listbox service-items)
+ "Connect to the wifi service selected in LISTBOX. SERVICE-ITEMS is the list
+of <service-item> records present in LISTBOX."
+ (let* ((listbox-key (current-listbox-entry listbox))
+ (item (find (lambda (item)
+ (eq? (service-item-key item) listbox-key))
+ service-items))
+ (service (service-item-service item))
+ (service-name (service-name service))
+ (form (draw-connecting-page service-name)))
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (guard (c ((connman-password-error? c)
+ (run-wrong-password-page service-name)
+ #f)
+ ((connman-already-connected-error? c)
+ #t)
+ ((connman-connection-error? c)
+ (run-unknown-error-page service-name)
+ #f))
+ (connman-connect-with-auth service password-callback)))
+ (lambda ()
+ (destroy-form-and-pop form)))))
+
+(define (run-wifi-scan-page)
+ "Force a wifi scan and draw a page during the operation."
+ (let ((form (draw-scanning-page)))
+ (force-wifi-scan)
+ (destroy-form-and-pop form)))
+
+(define (wifi-services)
+ "Return all the connman services of wifi type."
+ (let ((services (connman-services)))
+ (filter (lambda (service)
+ (and (string=? (service-type service) "wifi")
+ (not (string-null? (service-name service)))))
+ services)))
+
+(define* (fill-wifi-services listbox wifi-services)
+ "Append all the services in WIFI-SERVICES to the given LISTBOX."
+ (clear-listbox listbox)
+ (map (lambda (service)
+ (let* ((text (service->text service))
+ (key (append-entry-to-listbox listbox text)))
+ (service-item
+ (service service)
+ (key key))))
+ wifi-services))
+
+;; Maximum length of a wifi service name.
+(define service-name-max-length (make-parameter 20))
+
+;; Heigth of the listbox displaying wifi services.
+(define wifi-listbox-heigth (make-parameter 20))
+
+;; Information textbox width.
+(define info-textbox-width (make-parameter 40))
+
+(define (service->text service)
+ "Return a string composed of the name and the strength of the given
+SERVICE. A '*' preceding the service name indicates that it is connected."
+ (let* ((name (service-name service))
+ (padded-name (string-pad-right name
+ (service-name-max-length)))
+ (strength (service-strength service))
+ (strength-string (strength->string strength))
+ (state (service-state service))
+ (connected? (or (string=? state "online")
+ (string=? state "ready"))))
+ (format #f "~c ~a ~a~%"
+ (if connected? #\* #\ )
+ padded-name
+ strength-string)))
+
+(define (run-wifi-page)
+ "Run a page displaying available wifi networks in a listbox. Connect to the
+network when the corresponding listbox entry is selected. A button allow to
+force a wifi scan."
+ (let* ((listbox (make-listbox
+ -1 -1
+ (wifi-listbox-heigth)
+ (logior FLAG-SCROLL FLAG-BORDER FLAG-RETURNEXIT)))
+ (form (make-form))
+ (buttons-grid (make-grid 1 1))
+ (middle-grid (make-grid 2 1))
+ (info-text (G_ "Please select a wifi network."))
+ (info-textbox
+ (make-reflowed-textbox -1 -1 info-text
+ (info-textbox-width)
+ #:flags FLAG-BORDER))
+ (exit-button (make-button -1 -1 (G_ "Exit")))
+ (scan-button (make-button -1 -1 (G_ "Scan")))
+ (services (wifi-services))
+ (service-items '()))
+
+ (if (null? services)
+ (append-entry-to-listbox listbox (G_ "No wifi detected"))
+ (set! service-items (fill-wifi-services listbox services)))
+
+ (set-grid-field middle-grid 0 0 GRID-ELEMENT-COMPONENT listbox)
+ (set-grid-field middle-grid 1 0 GRID-ELEMENT-COMPONENT scan-button
+ #:anchor ANCHOR-TOP
+ #:pad-left 2)
+ (set-grid-field buttons-grid 0 0 GRID-ELEMENT-COMPONENT exit-button)
+
+ (add-components-to-form form
+ info-textbox
+ listbox scan-button
+ exit-button)
+ (make-wrapped-grid-window
+ (basic-window-grid info-textbox middle-grid buttons-grid)
+ (G_ "Wifi"))
+
+ (receive (exit-reason argument)
+ (run-form form)
+ (dynamic-wind
+ (const #t)
+ (lambda ()
+ (when (eq? exit-reason 'exit-component)
+ (cond
+ ((components=? argument scan-button)
+ (run-wifi-scan-page)
+ (run-wifi-page))
+ ((components=? argument exit-button)
+ (raise
+ (condition
+ (&installer-step-abort))))
+ ((components=? argument listbox)
+ (let ((result (connect-wifi-service listbox service-items)))
+ (unless result
+ (run-wifi-page)))))))
+ (lambda ()
+ (destroy-form-and-pop form))))))
diff --git a/gnu/installer/parted.scm b/gnu/installer/parted.scm
new file mode 100644
index 0000000000..187311e633
--- /dev/null
+++ b/gnu/installer/parted.scm
@@ -0,0 +1,1312 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2018, 2019 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 parted)
+ #:use-module (gnu installer steps)
+ #:use-module (gnu installer utils)
+ #:use-module (gnu installer newt page)
+ #:use-module (gnu system uuid)
+ #:use-module ((gnu build file-systems)
+ #:select (read-partition-uuid
+ read-luks-partition-uuid))
+ #:use-module (guix build syscalls)
+ #:use-module (guix build utils)
+ #:use-module (guix records)
+ #:use-module (guix utils)
+ #:use-module (guix i18n)
+ #:use-module (parted)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 regex)
+ #:use-module (rnrs io ports)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-34)
+ #:use-module (srfi srfi-35)
+ #:export (<user-partition>
+ user-partition
+ make-user-partition
+ user-partition?
+ user-partition-name
+ user-partition-type
+ user-partition-file-name
+ user-partition-disk-file-name
+ user-partition-crypt-label
+ user-partition-crypt-password
+ user-partition-fs-type
+ user-partition-bootable?
+ user-partition-esp?
+ user-partition-bios-grub?
+ user-partition-size
+ user-partition-start
+ user-partition-end
+ user-partition-mount-point
+ user-partition-need-formatting?
+ user-partition-parted-object
+
+ find-esp-partition
+ data-partition?
+ metadata-partition?
+ freespace-partition?
+ small-freespace-partition?
+ normal-partition?
+ extended-partition?
+ logical-partition?
+ esp-partition?
+ boot-partition?
+ default-esp-mount-point
+
+ with-delay-device-in-use?
+ force-device-sync
+ non-install-devices
+ partition-user-type
+ user-fs-type-name
+ partition-filesystem-user-type
+ partition-get-flags
+ partition->user-partition
+ create-special-user-partitions
+ find-user-partition-by-parted-object
+
+ device-description
+ partition-end-formatted
+ partition-print-number
+ partition-description
+ partitions-descriptions
+ user-partition-description
+
+ &max-primary-exceeded
+ max-primary-exceeded?
+ &extended-creation-error
+ extended-creation-error?
+ &logical-creation-error
+ logical-creation-error?
+
+ can-create-partition?
+ mklabel
+ mkpart
+ rmpart
+
+ create-adjacent-partitions
+ auto-partition
+
+ &no-root-mount-point
+ no-root-mount-point?
+
+ check-user-partitions
+ set-user-partitions-file-name
+ format-user-partitions
+ mount-user-partitions
+ umount-user-partitions
+ with-mounted-partitions
+ user-partitions->file-systems
+ user-partitions->configuration
+
+ init-parted
+ free-parted))
+
+
+;;;
+;;; Partition record.
+;;;
+
+(define-record-type* <user-partition>
+ user-partition make-user-partition
+ user-partition?
+ (name user-partition-name ;string
+ (default #f))
+ (type user-partition-type
+ (default 'normal)) ; 'normal | 'logical | 'extended
+ (file-name user-partition-file-name
+ (default #f))
+ (disk-file-name user-partition-disk-file-name
+ (default #f))
+ (crypt-label user-partition-crypt-label
+ (default #f))
+ (crypt-password user-partition-crypt-password
+ (default #f))
+ (fs-type user-partition-fs-type
+ (default 'ext4))
+ (bootable? user-partition-bootable?
+ (default #f))
+ (esp? user-partition-esp?
+ (default #f))
+ (bios-grub? user-partition-bios-grub?
+ (default #f))
+ (size user-partition-size
+ (default #f))
+ (start user-partition-start ;start as string (e.g. '11MB')
+ (default #f))
+ (end user-partition-end ;same as start
+ (default #f))
+ (mount-point user-partition-mount-point ;string
+ (default #f))
+ (need-formatting? user-partition-need-formatting? ; boolean
+ (default #f))
+ (parted-object user-partition-parted-object ; <partition> from parted
+ (default #f)))
+
+
+;;
+;; Utilities.
+;;
+
+(define (find-esp-partition partitions)
+ "Find and return the ESP partition among PARTITIONS."
+ (find esp-partition? partitions))
+
+(define (data-partition? partition)
+ "Return #t if PARTITION is a partition dedicated to data (by opposition to
+freespace, metadata and protected partition types), return #f otherwise."
+ (let ((type (partition-type partition)))
+ (not (any (lambda (flag)
+ (member flag type))
+ '(free-space metadata protected)))))
+
+(define (metadata-partition? partition)
+ "Return #t if PARTITION is a metadata partition, #f otherwise."
+ (let ((type (partition-type partition)))
+ (member 'metadata type)))
+
+(define (freespace-partition? partition)
+ "Return #t if PARTITION is a free-space partition, #f otherwise."
+ (let ((type (partition-type partition)))
+ (member 'free-space type)))
+
+(define* (small-freespace-partition? device
+ partition
+ #:key (max-size MEBIBYTE-SIZE))
+ "Return #t is PARTITION is a free-space partition with less a size strictly
+inferior to MAX-SIZE, #f otherwise."
+ (let ((size (partition-length partition))
+ (max-sector-size (/ max-size
+ (device-sector-size device))))
+ (< size max-sector-size)))
+
+(define (normal-partition? partition)
+ "return #t if partition is a normal partition, #f otherwise."
+ (let ((type (partition-type partition)))
+ (member 'normal type)))
+
+(define (extended-partition? partition)
+ "return #t if partition is an extended partition, #f otherwise."
+ (let ((type (partition-type partition)))
+ (member 'extended type)))
+
+(define (logical-partition? partition)
+ "Return #t if PARTITION is a logical partition, #f otherwise."
+ (let ((type (partition-type partition)))
+ (member 'logical type)))
+
+(define (partition-user-type partition)
+ "Return the type of PARTITION, to be stored in the TYPE field of
+<user-partition> record. It can be 'normal, 'extended or 'logical."
+ (cond ((normal-partition? partition)
+ 'normal)
+ ((extended-partition? partition)
+ 'extended)
+ ((logical-partition? partition)
+ 'logical)
+ (else #f)))
+
+(define (esp-partition? partition)
+ "Return #t if partition has the ESP flag, return #f otherwise."
+ (let* ((disk (partition-disk partition))
+ (disk-type (disk-disk-type disk))
+ (has-extended? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-EXTENDED)))
+ (and (data-partition? partition)
+ (not has-extended?)
+ (partition-is-flag-available? partition PARTITION-FLAG-ESP)
+ (partition-get-flag partition PARTITION-FLAG-ESP))))
+
+(define (boot-partition? partition)
+ "Return #t if partition has the boot flag, return #f otherwise."
+ (and (data-partition? partition)
+ (partition-is-flag-available? partition PARTITION-FLAG-BOOT)
+ (partition-get-flag partition PARTITION-FLAG-BOOT)))
+
+
+;; The default mount point for ESP partitions.
+(define default-esp-mount-point
+ (make-parameter "/boot/efi"))
+
+(define (efi-installation?)
+ "Return #t if an EFI installation should be performed, #f otherwise."
+ (file-exists? "/sys/firmware/efi"))
+
+(define (user-fs-type-name fs-type)
+ "Return the name of FS-TYPE as specified by libparted."
+ (case fs-type
+ ((ext4) "ext4")
+ ((btrfs) "btrfs")
+ ((fat32) "fat32")
+ ((swap) "linux-swap")))
+
+(define (user-fs-type->mount-type fs-type)
+ "Return the mount type of FS-TYPE."
+ (case fs-type
+ ((ext4) "ext4")
+ ((btrfs) "btrfs")
+ ((fat32) "vfat")))
+
+(define (partition-filesystem-user-type partition)
+ "Return the filesystem type of PARTITION, to be stored in the FS-TYPE field
+of <user-partition> record."
+ (let ((fs-type (partition-fs-type partition)))
+ (and fs-type
+ (let ((name (filesystem-type-name fs-type)))
+ (cond
+ ((string=? name "ext4") 'ext4)
+ ((string=? name "btrfs") 'btrfs)
+ ((string=? name "fat32") 'fat32)
+ ((or (string=? name "swsusp")
+ (string=? name "linux-swap(v0)")
+ (string=? name "linux-swap(v1)"))
+ 'swap)
+ (else
+ (error (format #f "Unhandled ~a fs-type~%" name))))))))
+
+(define (partition-get-flags partition)
+ "Return the list of flags supported by the given PARTITION."
+ (filter-map (lambda (flag)
+ (and (partition-get-flag partition flag)
+ flag))
+ (partition-flags partition)))
+
+(define (partition->user-partition partition)
+ "Convert PARTITION into a <user-partition> record and return it."
+ (let* ((disk (partition-disk partition))
+ (device (disk-device disk))
+ (disk-type (disk-disk-type disk))
+ (has-name? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-PARTITION-NAME))
+ (name (and has-name?
+ (data-partition? partition)
+ (partition-get-name partition))))
+ (user-partition
+ (name (and (and name
+ (not (string=? name "")))
+ name))
+ (type (or (partition-user-type partition)
+ 'normal))
+ (file-name (partition-get-path partition))
+ (disk-file-name (device-path device))
+ (fs-type (or (partition-filesystem-user-type partition)
+ 'ext4))
+ (mount-point (and (esp-partition? partition)
+ (default-esp-mount-point)))
+ (bootable? (boot-partition? partition))
+ (esp? (esp-partition? partition))
+ (parted-object partition))))
+
+(define (create-special-user-partitions partitions)
+ "Return a list with a <user-partition> record describing the ESP partition
+found in PARTITIONS, if any."
+ (filter-map (lambda (partition)
+ (and (esp-partition? partition)
+ (partition->user-partition partition)))
+ partitions))
+
+(define (find-user-partition-by-parted-object user-partitions
+ partition)
+ "Find and return the <user-partition> record in USER-PARTITIONS list which
+PARTED-OBJECT field equals PARTITION, return #f if not found."
+ (find (lambda (user-partition)
+ (equal? (user-partition-parted-object user-partition)
+ partition))
+ user-partitions))
+
+
+;;
+;; Devices
+;;
+
+(define (with-delay-device-in-use? file-name)
+ "Call DEVICE-IN-USE? with a few retries, as the first re-read will often
+fail. See rereadpt function in wipefs.c of util-linux for an explanation."
+ ;; Kernel always return EINVAL for BLKRRPART on loopdevices.
+ (and (not (string-match "/dev/loop*" file-name))
+ (let loop ((try 4))
+ (usleep 250000)
+ (let ((in-use? (device-in-use? file-name)))
+ (if (and in-use? (> try 0))
+ (loop (- try 1))
+ in-use?)))))
+
+(define* (force-device-sync device)
+ "Force a flushing of the given DEVICE."
+ (device-open device)
+ (device-sync device)
+ (device-close device))
+
+(define (non-install-devices)
+ "Return all the available devices, except the busy one, allegedly the
+install device. DEVICE-IS-BUSY? is a parted call, checking if the device is
+mounted. The install image uses an overlayfs so the install device does not
+appear as mounted and won't be considered as busy. So use also DEVICE-IN-USE?
+from (guix build syscalls) module, who will try to re-read the device's
+partition table to determine whether or not it is already used (like sfdisk
+from util-linux)."
+ (remove (lambda (device)
+ (let ((file-name (device-path device)))
+ (or (device-is-busy? device)
+ (with-delay-device-in-use? file-name))))
+ (devices)))
+
+
+;;
+;; Disk and partition printing.
+;;
+
+(define* (device-description device #:optional disk)
+ "Return a string describing the given DEVICE."
+ (let* ((type (device-type device))
+ (file-name (device-path device))
+ (model (device-model device))
+ (type-str (device-type->string type))
+ (disk-type (if disk
+ (disk-disk-type disk)
+ (disk-probe device)))
+ (length (device-length device))
+ (sector-size (device-sector-size device))
+ (end (unit-format-custom-byte device
+ (* length sector-size)
+ UNIT-GIGABYTE)))
+ (string-join
+ `(,@(if (string=? model "")
+ `(,type-str)
+ `(,model ,(string-append "(" type-str ")")))
+ ,file-name
+ ,end
+ ,@(if disk-type
+ `(,(disk-type-name disk-type))
+ '()))
+ " ")))
+
+(define (partition-end-formatted device partition)
+ "Return as a string the end of PARTITION with the relevant unit."
+ (unit-format-byte
+ device
+ (-
+ (* (+ (partition-end partition) 1)
+ (device-sector-size device))
+ 1)))
+
+(define (partition-print-number partition)
+ "Convert the given partition NUMBER to string."
+ (let ((number (partition-number partition)))
+ (number->string number)))
+
+(define (partition-description partition user-partition)
+ "Return a string describing the given PARTITION, located on the DISK of
+DEVICE."
+
+ (define (partition-print-type partition)
+ "Return the type of PARTITION as a string."
+ (if (freespace-partition? partition)
+ (G_ "Free space")
+ (let ((type (partition-type partition)))
+ (match type
+ ((type-symbol)
+ (symbol->string type-symbol))))))
+
+ (define (partition-print-flags partition)
+ "Return the flags of PARTITION as a string of comma separated flags."
+ (string-join
+ (filter-map
+ (lambda (flag)
+ (and (partition-get-flag partition flag)
+ (partition-flag-get-name flag)))
+ (partition-flags partition))
+ ","))
+
+ (define (maybe-string-pad string length)
+ "Returned a string formatted by padding STRING of LENGTH characters to the
+right. If STRING is #f use an empty string."
+ (if (and string (not (string=? string "")))
+ (string-pad-right string length)
+ ""))
+
+ (let* ((disk (partition-disk partition))
+ (device (disk-device disk))
+ (disk-type (disk-disk-type disk))
+ (has-name? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-PARTITION-NAME))
+ (has-extended? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-EXTENDED))
+ (part-type (partition-print-type partition))
+ (number (and (not (freespace-partition? partition))
+ (partition-print-number partition)))
+ (name (and has-name?
+ (if (freespace-partition? partition)
+ (G_ "Free space")
+ (partition-get-name partition))))
+ (start (unit-format device
+ (partition-start partition)))
+ (end (partition-end-formatted device partition))
+ (size (unit-format device (partition-length partition)))
+ (fs-type (partition-fs-type partition))
+ (fs-type-name (and fs-type
+ (filesystem-type-name fs-type)))
+ (crypt-label (and user-partition
+ (user-partition-crypt-label user-partition)))
+ (flags (and (not (freespace-partition? partition))
+ (partition-print-flags partition)))
+ (mount-point (and user-partition
+ (user-partition-mount-point user-partition))))
+ `(,(or number "")
+ ,@(if has-extended?
+ (list part-type)
+ '())
+ ,size
+ ,(or fs-type-name "")
+ ,(or flags "")
+ ,(or mount-point "")
+ ,(or crypt-label "")
+ ,(maybe-string-pad name 30))))
+
+(define (partitions-descriptions partitions user-partitions)
+ "Return a list of strings describing all the partitions found on
+DEVICE. METADATA partitions are not described. The strings are padded to the
+right so that they can be displayed as a table."
+
+ (define (max-length-column lists column-index)
+ "Return the maximum length of the string at position COLUMN-INDEX in the
+list of string lists LISTS."
+ (apply max
+ (map (lambda (list)
+ (string-length
+ (list-ref list column-index)))
+ lists)))
+
+ (define (pad-descriptions descriptions)
+ "Return a padded version of the list of string lists DESCRIPTIONS. The
+strings are padded to the length of the longer string in a same column, as
+determined by MAX-LENGTH-COLUMN procedure."
+ (let* ((description-length (length (car descriptions)))
+ (paddings (map (lambda (index)
+ (max-length-column descriptions index))
+ (iota description-length))))
+ (map (lambda (description)
+ (map string-pad-right description paddings))
+ descriptions)))
+
+ (let* ((descriptions
+ (map
+ (lambda (partition)
+ (let ((user-partition
+ (find-user-partition-by-parted-object user-partitions
+ partition)))
+ (partition-description partition user-partition)))
+ partitions))
+ (padded-descriptions (if (null? partitions)
+ '()
+ (pad-descriptions descriptions))))
+ (map (cut string-join <> " ") padded-descriptions)))
+
+(define (user-partition-description user-partition)
+ "Return a string describing the given USER-PARTITION record."
+ (let* ((partition (user-partition-parted-object user-partition))
+ (disk (partition-disk partition))
+ (disk-type (disk-disk-type disk))
+ (device (disk-device disk))
+ (has-name? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-PARTITION-NAME))
+ (has-extended? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-EXTENDED))
+ (name (user-partition-name user-partition))
+ (type (user-partition-type user-partition))
+ (type-name (symbol->string type))
+ (fs-type (user-partition-fs-type user-partition))
+ (fs-type-name (user-fs-type-name fs-type))
+ (bootable? (user-partition-bootable? user-partition))
+ (esp? (user-partition-esp? user-partition))
+ (need-formatting? (user-partition-need-formatting? user-partition))
+ (crypt-label (user-partition-crypt-label user-partition))
+ (size (user-partition-size user-partition))
+ (mount-point (user-partition-mount-point user-partition)))
+ `(,@(if has-name?
+ `((name . ,(string-append "Name: " (or name "None"))))
+ '())
+ ,@(if (and has-extended?
+ (freespace-partition? partition)
+ (not (eq? type 'logical)))
+ `((type . ,(string-append "Type: " type-name)))
+ '())
+ ,@(if (eq? type 'extended)
+ '()
+ `((fs-type . ,(string-append "Filesystem type: " fs-type-name))))
+ ,@(if (or (eq? type 'extended)
+ (eq? fs-type 'swap)
+ (not has-extended?))
+ '()
+ `((bootable . ,(string-append "Bootable flag: "
+ (if bootable? "On" "Off")))))
+ ,@(if (and (not has-extended?)
+ (not (eq? fs-type 'swap)))
+ `((esp? . ,(string-append "ESP flag: "
+ (if esp? "On" "Off"))))
+ '())
+ ,@(if (freespace-partition? partition)
+ (let ((size-formatted
+ (or size (unit-format device
+ (partition-length partition)))))
+ `((size . ,(string-append "Size : " size-formatted))))
+ '())
+ ,@(if (or (eq? type 'extended)
+ (eq? fs-type 'swap))
+ '()
+ `((crypt-label
+ . ,(string-append
+ "Encryption: "
+ (if crypt-label
+ (format #f "Yes (label ~a)" crypt-label)
+ "No")))))
+ ,@(if (or (freespace-partition? partition)
+ (eq? fs-type 'swap))
+ '()
+ `((need-formatting?
+ . ,(string-append "Format the partition? : "
+ (if need-formatting? "Yes" "No")))))
+ ,@(if (or (eq? type 'extended)
+ (eq? fs-type 'swap))
+ '()
+ `((mount-point
+ . ,(string-append "Mount point : "
+ (or mount-point
+ (and esp? (default-esp-mount-point))
+ "None"))))))))
+
+
+;;
+;; Partition table creation.
+;;
+
+(define (mklabel device type-name)
+ "Create a partition table on DEVICE. TYPE-NAME is the type of the partition
+table, \"msdos\" or \"gpt\"."
+ (let ((type (disk-type-get type-name)))
+ (disk-new-fresh device type)))
+
+
+;;
+;; Partition creation.
+;;
+
+;; The maximum count of primary partitions is exceeded.
+(define-condition-type &max-primary-exceeded &condition
+ max-primary-exceeded?)
+
+;; It is not possible to create an extended partition.
+(define-condition-type &extended-creation-error &condition
+ extended-creation-error?)
+
+;; It is not possible to create a logical partition.
+(define-condition-type &logical-creation-error &condition
+ logical-creation-error?)
+
+(define (can-create-primary? disk)
+ "Return #t if it is possible to create a primary partition on DISK, return
+#f otherwise."
+ (let ((max-primary (disk-get-max-primary-partition-count disk)))
+ (find (lambda (number)
+ (not (disk-get-partition disk number)))
+ (iota max-primary 1))))
+
+(define (can-create-extended? disk)
+ "Return #t if it is possible to create an extended partition on DISK, return
+#f otherwise."
+ (let* ((disk-type (disk-disk-type disk))
+ (has-extended? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-EXTENDED)))
+ (and (can-create-primary? disk)
+ has-extended?
+ (not (disk-extended-partition disk)))))
+
+(define (can-create-logical? disk)
+ "Return #t is it is possible to create a logical partition on DISK, return
+#f otherwise."
+ (let* ((disk-type (disk-disk-type disk))
+ (has-extended? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-EXTENDED)))
+ (and has-extended?
+ (disk-extended-partition disk))))
+
+(define (can-create-partition? user-part)
+ "Return #t if it is possible to create the given USER-PART record, return #f
+otherwise."
+ (let* ((type (user-partition-type user-part))
+ (partition (user-partition-parted-object user-part))
+ (disk (partition-disk partition)))
+ (case type
+ ((normal)
+ (or (can-create-primary? disk)
+ (raise
+ (condition (&max-primary-exceeded)))))
+ ((extended)
+ (or (can-create-extended? disk)
+ (raise
+ (condition (&extended-creation-error)))))
+ ((logical)
+ (or (can-create-logical? disk)
+ (raise
+ (condition (&logical-creation-error))))))))
+
+(define* (mkpart disk user-partition
+ #:key (previous-partition #f))
+ "Create the given USER-PARTITION on DISK. The PREVIOUS-PARTITION argument as
+to be set to the partition preceeding USER-PARTITION if any."
+
+ (define (parse-start-end start end)
+ "Parse start and end strings as positions on DEVICE expressed with a unit,
+like '100GB' or '12.2KiB'. Return a list of 4 elements, the start sector, its
+range (1 unit large area centered on start sector), the end sector and its
+range."
+ (let ((device (disk-device disk)))
+ (call-with-values
+ (lambda ()
+ (unit-parse start device))
+ (lambda (start-sector start-range)
+ (call-with-values
+ (lambda ()
+ (unit-parse end device))
+ (lambda (end-sector end-range)
+ (list start-sector start-range
+ end-sector end-range)))))))
+
+ (define* (extend-ranges! start-range end-range
+ #:key (offset 0))
+ "Try to extend START-RANGE by 1 MEBIBYTE to the right and END-RANGE by 1
+MEBIBYTE to the left. This way, if the disk is aligned on 2048 sectors of
+512KB (like frequently), we will have a chance for the
+'optimal-align-constraint' to succeed. Do not extend ranges if that would
+cause them to cross."
+ (let* ((device (disk-device disk))
+ (start-range-end (geometry-end start-range))
+ (end-range-start (geometry-start end-range))
+ (mebibyte-sector-size (/ MEBIBYTE-SIZE
+ (device-sector-size device)))
+ (new-start-range-end
+ (+ start-range-end mebibyte-sector-size offset))
+ (new-end-range-start
+ (- end-range-start mebibyte-sector-size offset)))
+ (when (< new-start-range-end new-end-range-start)
+ (geometry-set-end start-range new-start-range-end)
+ (geometry-set-start end-range new-end-range-start))))
+
+ (match (parse-start-end (user-partition-start user-partition)
+ (user-partition-end user-partition))
+ ((start-sector start-range end-sector end-range)
+ (let* ((prev-end (if previous-partition
+ (partition-end previous-partition)
+ 0))
+ (start-distance (- start-sector prev-end))
+ (type (user-partition-type user-partition))
+ ;; There should be at least 2 unallocated sectors in front of each
+ ;; logical partition, otherwise parted will fail badly:
+ ;; https://gparted.org/h2-fix-msdos-pt.php#apply-action-fail.
+ (start-offset (if previous-partition
+ (- 3 start-distance)
+ 0))
+ (start-sector* (if (and (eq? type 'logical)
+ (< start-distance 3))
+ (+ start-sector start-offset)
+ start-sector)))
+ ;; This is a hackery but parted almost always fails to create optimally
+ ;; aligned partitions (unless specifiying percentages) because, the
+ ;; default range of 1MB centered on the start sector is not enough when
+ ;; the optimal alignment is 2048 sectors of 512KB.
+ (extend-ranges! start-range end-range #:offset start-offset)
+
+ (let* ((device (disk-device disk))
+ (disk-type (disk-disk-type disk))
+ (length (device-length device))
+ (name (user-partition-name user-partition))
+ (filesystem-type
+ (filesystem-type-get
+ (user-fs-type-name
+ (user-partition-fs-type user-partition))))
+ (flags `(,@(if (user-partition-bootable? user-partition)
+ `(,PARTITION-FLAG-BOOT)
+ '())
+ ,@(if (user-partition-esp? user-partition)
+ `(,PARTITION-FLAG-ESP)
+ '())
+ ,@(if (user-partition-bios-grub? user-partition)
+ `(,PARTITION-FLAG-BIOS-GRUB)
+ '())))
+ (has-name? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-PARTITION-NAME))
+ (partition-type (partition-type->int type))
+ (partition (partition-new disk
+ #:type partition-type
+ #:filesystem-type filesystem-type
+ #:start start-sector*
+ #:end end-sector))
+ (user-constraint (constraint-new
+ #:start-align 'any
+ #:end-align 'any
+ #:start-range start-range
+ #:end-range end-range
+ #:min-size 1
+ #:max-size length))
+ (dev-constraint
+ (device-get-optimal-aligned-constraint device))
+ (final-constraint (constraint-intersect user-constraint
+ dev-constraint))
+ (no-constraint (constraint-any device))
+ ;; Try to create a partition with an optimal alignment
+ ;; constraint. If it fails, fallback to creating a partition with
+ ;; no specific constraint.
+ (partition-ok?
+ (or (disk-add-partition disk partition final-constraint)
+ (disk-add-partition disk partition no-constraint))))
+ ;; Set the partition name if supported.
+ (when (and partition-ok? has-name? name)
+ (partition-set-name partition name))
+
+ ;; Set flags is required.
+ (for-each (lambda (flag)
+ (and (partition-is-flag-available? partition flag)
+ (partition-set-flag partition flag 1)))
+ flags)
+
+ (and partition-ok?
+ (partition-set-system partition filesystem-type)
+ partition))))))
+
+
+;;
+;; Partition destruction.
+;;
+
+(define (rmpart disk number)
+ "Remove the partition with the given NUMBER on DISK."
+ (let ((partition (disk-get-partition disk number)))
+ (disk-remove-partition disk partition)))
+
+
+;;
+;; Auto partitionning.
+;;
+
+(define* (create-adjacent-partitions disk partitions
+ #:key (last-partition-end 0))
+ "Create the given PARTITIONS on DISK. LAST-PARTITION-END is the sector from
+which we want to start creating partitions. The START and END of each created
+partition are computed from its SIZE value and the position of the last
+partition."
+ (let ((device (disk-device disk)))
+ (let loop ((partitions partitions)
+ (remaining-space (- (device-length device)
+ last-partition-end))
+ (start last-partition-end))
+ (match partitions
+ (() '())
+ ((partition . rest)
+ (let* ((size (user-partition-size partition))
+ (percentage-size (and (string? size)
+ (read-percentage size)))
+ (sector-size (device-sector-size device))
+ (partition-size (if percentage-size
+ (exact->inexact
+ (* (/ percentage-size 100)
+ remaining-space))
+ size))
+ (end-partition (min (- (device-length device) 1)
+ (nearest-exact-integer
+ (+ start partition-size 1))))
+ (name (user-partition-name partition))
+ (type (user-partition-type partition))
+ (fs-type (user-partition-fs-type partition))
+ (start-formatted (unit-format-custom device
+ start
+ UNIT-SECTOR))
+ (end-formatted (unit-format-custom device
+ end-partition
+ UNIT-SECTOR))
+ (new-user-partition (user-partition
+ (inherit partition)
+ (start start-formatted)
+ (end end-formatted)))
+ (new-partition
+ (mkpart disk new-user-partition)))
+ (if new-partition
+ (cons (user-partition
+ (inherit new-user-partition)
+ (file-name (partition-get-path new-partition))
+ (disk-file-name (device-path device))
+ (parted-object new-partition))
+ (loop rest
+ (if (eq? type 'extended)
+ remaining-space
+ (- remaining-space
+ (partition-length new-partition)))
+ (if (eq? type 'extended)
+ (+ start 1)
+ (+ (partition-end new-partition) 1))))
+ (error
+ (format #f "Unable to create partition ~a~%" name)))))))))
+
+(define (force-user-partitions-formatting user-partitions)
+ "Set the NEED-FORMATING? fields to #t on all <user-partition> records of
+USER-PARTITIONS list and return the updated list."
+ (map (lambda (p)
+ (user-partition
+ (inherit p)
+ (need-formatting? #t)))
+ user-partitions))
+
+(define* (auto-partition disk
+ #:key
+ (scheme 'entire-root))
+ "Automatically create partitions on DISK. All the previous
+partitions (except the ESP on a GPT disk, if present) are wiped. SCHEME is the
+desired partitioning scheme. It can be 'entire-root or
+'entire-root-home. 'entire-root will create a swap partition and a root
+partition occupying all the remaining space. 'entire-root-home will create a
+swap partition, a root partition and a home partition."
+ (let* ((device (disk-device disk))
+ (disk-type (disk-disk-type disk))
+ (has-extended? (disk-type-check-feature
+ disk-type
+ DISK-TYPE-FEATURE-EXTENDED))
+ (partitions (filter data-partition? (disk-partitions disk)))
+ (esp-partition (find-esp-partition partitions))
+ ;; According to
+ ;; https://wiki.archlinux.org/index.php/EFI_system_partition, the ESP
+ ;; size should be at least 550MiB.
+ (new-esp-size (nearest-exact-integer
+ (/ (* 550 MEBIBYTE-SIZE)
+ (device-sector-size device))))
+ (end-esp-partition (and esp-partition
+ (partition-end esp-partition)))
+ (non-boot-partitions (remove esp-partition? partitions))
+ (bios-grub-size (/ (* 3 MEBIBYTE-SIZE)
+ (device-sector-size device)))
+ (five-percent-disk (nearest-exact-integer
+ (* 0.05 (device-length device))))
+ (default-swap-size (nearest-exact-integer
+ (/ (* 4 GIGABYTE-SIZE)
+ (device-sector-size device))))
+ ;; Use a 4GB size for the swap if it represents less than 5% of the
+ ;; disk space. Otherwise, set the swap size to 5% of the disk space.
+ (swap-size (min default-swap-size five-percent-disk)))
+
+ (if has-extended?
+ ;; msdos - remove everything.
+ (disk-delete-all disk)
+ ;; gpt - remove everything but esp if it exists.
+ (for-each
+ (lambda (partition)
+ (and (data-partition? partition)
+ (disk-remove-partition disk partition)))
+ non-boot-partitions))
+
+ (let* ((start-partition
+ (and (not has-extended?)
+ (not esp-partition)
+ (if (efi-installation?)
+ (user-partition
+ (fs-type 'fat32)
+ (esp? #t)
+ (size new-esp-size)
+ (mount-point (default-esp-mount-point)))
+ (user-partition
+ (fs-type 'ext4)
+ (bootable? #t)
+ (bios-grub? #t)
+ (size bios-grub-size)))))
+ (new-partitions
+ (cond
+ ((or (eq? scheme 'entire-root)
+ (eq? scheme 'entire-encrypted-root))
+ (let ((encrypted? (eq? scheme 'entire-encrypted-root)))
+ `(,@(if start-partition
+ `(,start-partition)
+ '())
+ ,@(if encrypted?
+ '()
+ `(,(user-partition
+ (fs-type 'swap)
+ (size swap-size))))
+ ,(user-partition
+ (fs-type 'ext4)
+ (bootable? has-extended?)
+ (crypt-label (and encrypted? "cryptroot"))
+ (size "100%")
+ (mount-point "/")))))
+ ((or (eq? scheme 'entire-root-home)
+ (eq? scheme 'entire-encrypted-root-home))
+ (let ((encrypted? (eq? scheme 'entire-encrypted-root-home)))
+ `(,@(if start-partition
+ `(,start-partition)
+ '())
+ ,(user-partition
+ (fs-type 'ext4)
+ (bootable? has-extended?)
+ (crypt-label (and encrypted? "cryptroot"))
+ (size "33%")
+ (mount-point "/"))
+ ,@(if has-extended?
+ `(,(user-partition
+ (type 'extended)
+ (size "100%")))
+ '())
+ ,@(if encrypted?
+ '()
+ `(,(user-partition
+ (type (if has-extended?
+ 'logical
+ 'normal))
+ (fs-type 'swap)
+ (size swap-size))))
+ ,(user-partition
+ (type (if has-extended?
+ 'logical
+ 'normal))
+ (fs-type 'ext4)
+ (crypt-label (and encrypted? "crypthome"))
+ (size "100%")
+ (mount-point "/home")))))))
+ (new-partitions* (force-user-partitions-formatting
+ new-partitions)))
+ (create-adjacent-partitions disk
+ new-partitions*
+ #:last-partition-end
+ (or end-esp-partition 0)))))
+
+
+;;
+;; Convert user-partitions.
+;;
+
+;; No root mount point found.
+(define-condition-type &no-root-mount-point &condition
+ no-root-mount-point?)
+
+(define (check-user-partitions user-partitions)
+ "Return #t if the USER-PARTITIONS lists contains one <user-partition> record
+with a mount-point set to '/', raise &no-root-mount-point condition
+otherwise."
+ (let ((mount-points
+ (map user-partition-mount-point user-partitions)))
+ (or (member "/" mount-points)
+ (raise
+ (condition (&no-root-mount-point))))))
+
+(define (set-user-partitions-file-name user-partitions)
+ "Set the partition file-name of <user-partition> records in USER-PARTITIONS
+list and return the updated list."
+ (map (lambda (p)
+ (let* ((partition (user-partition-parted-object p))
+ (file-name (partition-get-path partition)))
+ (user-partition
+ (inherit p)
+ (file-name file-name))))
+ user-partitions))
+
+(define-syntax-rule (with-null-output-ports exp ...)
+ "Evaluate EXP with both the output port and the error port pointing to the
+bit bucket."
+ (with-output-to-port (%make-void-port "w")
+ (lambda ()
+ (with-error-to-port (%make-void-port "w")
+ (lambda () exp ...)))))
+
+(define (create-ext4-file-system partition)
+ "Create an ext4 file-system for PARTITION file-name."
+ (with-null-output-ports
+ (invoke "mkfs.ext4" "-F" partition)))
+
+(define (create-fat32-file-system partition)
+ "Create an ext4 file-system for PARTITION file-name."
+ (with-null-output-ports
+ (invoke "mkfs.fat" "-F32" partition)))
+
+(define (create-swap-partition partition)
+ "Set up swap area on PARTITION file-name."
+ (with-null-output-ports
+ (invoke "mkswap" "-f" partition)))
+
+(define (call-with-luks-key-file password proc)
+ "Write PASSWORD in a temporary file and pass it to PROC as argument."
+ (call-with-temporary-output-file
+ (lambda (file port)
+ (put-string port password)
+ (close port)
+ (proc file))))
+
+(define (user-partition-upper-file-name user-partition)
+ "Return the file-name of the virtual block device corresponding to
+USER-PARTITION if it is encrypted, or the plain file-name otherwise."
+ (let ((crypt-label (user-partition-crypt-label user-partition))
+ (file-name (user-partition-file-name user-partition)))
+ (if crypt-label
+ (string-append "/dev/mapper/" crypt-label)
+ file-name)))
+
+(define (luks-format-and-open user-partition)
+ "Format and open the encrypted partition pointed by USER-PARTITION."
+ (let* ((file-name (user-partition-file-name user-partition))
+ (label (user-partition-crypt-label user-partition))
+ (password (user-partition-crypt-password user-partition)))
+ (call-with-luks-key-file
+ password
+ (lambda (key-file)
+ (system* "cryptsetup" "-q" "luksFormat" file-name key-file)
+ (system* "cryptsetup" "open" "--type" "luks"
+ "--key-file" key-file file-name label)))))
+
+(define (luks-close user-partition)
+ "Close the encrypted partition pointed by USER-PARTITION."
+ (let ((label (user-partition-crypt-label user-partition)))
+ (system* "cryptsetup" "close" label)))
+
+(define (format-user-partitions user-partitions)
+ "Format the <user-partition> records in USER-PARTITIONS list with
+NEED-FORMATING? field set to #t."
+ (for-each
+ (lambda (user-partition)
+ (let* ((need-formatting?
+ (user-partition-need-formatting? user-partition))
+ (type (user-partition-type user-partition))
+ (crypt-label (user-partition-crypt-label user-partition))
+ (file-name (user-partition-upper-file-name user-partition))
+ (fs-type (user-partition-fs-type user-partition)))
+ (when crypt-label
+ (luks-format-and-open user-partition))
+
+ (case fs-type
+ ((ext4)
+ (and need-formatting?
+ (not (eq? type 'extended))
+ (create-ext4-file-system file-name)))
+ ((fat32)
+ (and need-formatting?
+ (not (eq? type 'extended))
+ (create-fat32-file-system file-name)))
+ ((swap)
+ (create-swap-partition file-name))
+ (else
+ ;; TODO: Add support for other file-system types.
+ #t))))
+ user-partitions))
+
+(define (sort-partitions user-partitions)
+ "Sort USER-PARTITIONS by mount-points, so that the more nested mount-point
+comes last. This is useful to mount/umount partitions in a coherent order."
+ (sort user-partitions
+ (lambda (a b)
+ (let ((mount-point-a (user-partition-mount-point a))
+ (mount-point-b (user-partition-mount-point b)))
+ (string-prefix? mount-point-a mount-point-b)))))
+
+(define (mount-user-partitions user-partitions)
+ "Mount the <user-partition> records in USER-PARTITIONS list on their
+respective mount-points."
+ (let* ((mount-partitions (filter user-partition-mount-point user-partitions))
+ (sorted-partitions (sort-partitions mount-partitions)))
+ (for-each (lambda (user-partition)
+ (let* ((mount-point
+ (user-partition-mount-point user-partition))
+ (target
+ (string-append (%installer-target-dir)
+ mount-point))
+ (fs-type
+ (user-partition-fs-type user-partition))
+ (crypt-label
+ (user-partition-crypt-label user-partition))
+ (mount-type
+ (user-fs-type->mount-type fs-type))
+ (file-name
+ (user-partition-upper-file-name user-partition)))
+ (mkdir-p target)
+ (mount file-name target mount-type)))
+ sorted-partitions)))
+
+(define (umount-user-partitions user-partitions)
+ "Unmount all the <user-partition> records in USER-PARTITIONS list."
+ (let* ((mount-partitions (filter user-partition-mount-point user-partitions))
+ (sorted-partitions (sort-partitions mount-partitions)))
+ (for-each (lambda (user-partition)
+ (let* ((mount-point
+ (user-partition-mount-point user-partition))
+ (crypt-label
+ (user-partition-crypt-label user-partition))
+ (target
+ (string-append (%installer-target-dir)
+ mount-point)))
+ (umount target)
+ (when crypt-label
+ (luks-close user-partition))))
+ (reverse sorted-partitions))))
+
+(define (find-swap-user-partitions user-partitions)
+ "Return the subset of <user-partition> records in USER-PARTITIONS list with
+the FS-TYPE field set to 'swap, return the empty list if none found."
+ (filter (lambda (user-partition)
+ (let ((fs-type (user-partition-fs-type user-partition)))
+ (eq? fs-type 'swap)))
+ user-partitions))
+
+(define (start-swapping user-partitions)
+ "Start swaping on <user-partition> records with FS-TYPE equal to 'swap."
+ (let* ((swap-user-partitions (find-swap-user-partitions user-partitions))
+ (swap-devices (map user-partition-file-name swap-user-partitions)))
+ (for-each swapon swap-devices)))
+
+(define (stop-swapping user-partitions)
+ "Stop swaping on <user-partition> records with FS-TYPE equal to 'swap."
+ (let* ((swap-user-partitions (find-swap-user-partitions user-partitions))
+ (swap-devices (map user-partition-file-name swap-user-partitions)))
+ (for-each swapoff swap-devices)))
+
+(define-syntax-rule (with-mounted-partitions user-partitions exp ...)
+ "Mount USER-PARTITIONS and start swapping within the dynamic extent of EXP."
+ (dynamic-wind
+ (lambda ()
+ (mount-user-partitions user-partitions)
+ (start-swapping user-partitions))
+ (lambda ()
+ exp ...)
+ (lambda ()
+ (umount-user-partitions user-partitions)
+ (stop-swapping user-partitions)
+ #f)))
+
+(define (user-partition->file-system user-partition)
+ "Convert the given USER-PARTITION record in a FILE-SYSTEM record from
+(gnu system file-systems) module and return it."
+ (let* ((mount-point (user-partition-mount-point user-partition))
+ (fs-type (user-partition-fs-type user-partition))
+ (crypt-label (user-partition-crypt-label user-partition))
+ (mount-type (user-fs-type->mount-type fs-type))
+ (file-name (user-partition-file-name user-partition))
+ (upper-file-name (user-partition-upper-file-name user-partition))
+ ;; Only compute uuid if partition is not encrypted.
+ (uuid (or crypt-label
+ (uuid->string (read-partition-uuid file-name) fs-type))))
+ `(file-system
+ (mount-point ,mount-point)
+ (device ,@(if crypt-label
+ `(,upper-file-name)
+ `((uuid ,uuid (quote ,fs-type)))))
+ (type ,mount-type)
+ ,@(if crypt-label
+ '((dependencies mapped-devices))
+ '()))))
+
+(define (user-partitions->file-systems user-partitions)
+ "Convert the given USER-PARTITIONS list of <user-partition> records into a
+list of <file-system> records."
+ (filter-map
+ (lambda (user-partition)
+ (let ((mount-point
+ (user-partition-mount-point user-partition)))
+ (and mount-point
+ (user-partition->file-system user-partition))))
+ user-partitions))
+
+(define (user-partition->mapped-device user-partition)
+ "Convert the given USER-PARTITION record into a MAPPED-DEVICE record
+from (gnu system mapped-devices) and return it."
+ (let ((label (user-partition-crypt-label user-partition))
+ (file-name (user-partition-file-name user-partition)))
+ `(mapped-device
+ (source (uuid ,(uuid->string
+ (read-luks-partition-uuid file-name)
+ 'luks)))
+ (target ,label)
+ (type luks-device-mapping))))
+
+(define (bootloader-configuration user-partitions)
+ "Return the bootloader configuration field for USER-PARTITIONS."
+ (let* ((root-partition
+ (find (lambda (user-partition)
+ (let ((mount-point
+ (user-partition-mount-point user-partition)))
+ (and mount-point
+ (string=? mount-point "/"))))
+ user-partitions))
+ (root-partition-disk (user-partition-disk-file-name root-partition)))
+ `((bootloader-configuration
+ ,@(if (efi-installation?)
+ `((bootloader grub-efi-bootloader)
+ (target ,(default-esp-mount-point)))
+ `((bootloader grub-bootloader)
+ (target ,root-partition-disk)))))))
+
+(define (user-partitions->configuration user-partitions)
+ "Return the configuration field for USER-PARTITIONS."
+ (let* ((swap-user-partitions (find-swap-user-partitions user-partitions))
+ (swap-devices (map user-partition-file-name swap-user-partitions))
+ (encrypted-partitions
+ (filter user-partition-crypt-label user-partitions)))
+ `(,@(if (null? swap-devices)
+ '()
+ `((swap-devices (list ,@swap-devices))))
+ (bootloader ,@(bootloader-configuration user-partitions))
+ ,@(if (null? encrypted-partitions)
+ '()
+ `((mapped-devices
+ (list ,@(map user-partition->mapped-device
+ encrypted-partitions)))))
+ (file-systems (cons*
+ ,@(user-partitions->file-systems user-partitions)
+ %base-file-systems)))))
+
+
+;;
+;; Initialization.
+;;
+
+(define (init-parted)
+ "Initialize libparted support."
+ (probe-all-devices)
+ (exception-set-handler (lambda (exception)
+ EXCEPTION-OPTION-UNHANDLED)))
+
+(define (free-parted devices)
+ "Deallocate memory used for DEVICES in parted, force sync them and wait for
+the devices not to be used before returning."
+ ;; XXX: Formatting and further operations on disk partition table may fail
+ ;; because the partition table changes are not synced, or because the device
+ ;; is still in use, even if parted should have finished editing
+ ;; partitions. This is not well understood, but syncing devices and waiting
+ ;; them to stop returning EBUSY to BLKRRPART ioctl seems to be enough. The
+ ;; same kind of issue is described here:
+ ;; https://mail.gnome.org/archives/commits-list/2013-March/msg18423.html.
+ (let ((device-file-names (map device-path devices)))
+ (for-each force-device-sync devices)
+ (free-all-devices)
+ (for-each (lambda (file-name)
+ (let ((in-use? (with-delay-device-in-use? file-name)))
+ (and in-use?
+ (error
+ (format #f (G_ "Device ~a is still in use.")
+ file-name)))))
+ device-file-names)))
diff --git a/gnu/installer/record.scm b/gnu/installer/record.scm
new file mode 100644
index 0000000000..edf73b6215
--- /dev/null
+++ b/gnu/installer/record.scm
@@ -0,0 +1,84 @@
+;;; 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 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-timezone-page
+ installer-hostname-page
+ installer-user-page
+ installer-partition-page
+ installer-services-page
+ installer-welcome-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) -> (list layout variant)
+ (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 (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))
diff --git a/gnu/installer/services.scm b/gnu/installer/services.scm
new file mode 100644
index 0000000000..ed44b87682
--- /dev/null
+++ b/gnu/installer/services.scm
@@ -0,0 +1,59 @@
+;;; 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 services)
+ #:use-module (guix records)
+ #:export (<desktop-environment>
+ desktop-environment
+ make-desktop-environment
+ desktop-environment-name
+ desktop-environment-snippet
+
+ %desktop-environments
+ desktop-environments->configuration))
+
+(define-record-type* <desktop-environment>
+ desktop-environment make-desktop-environment
+ desktop-environment?
+ (name desktop-environment-name) ;string
+ (snippet desktop-environment-snippet)) ;symbol
+
+;; This is the list of desktop environments supported as services.
+(define %desktop-environments
+ (list
+ (desktop-environment
+ (name "GNOME")
+ (snippet '(gnome-desktop-service)))
+ (desktop-environment
+ (name "Xfce")
+ (snippet '(xfce-desktop-service)))
+ (desktop-environment
+ (name "MATE")
+ (snippet '(mate-desktop-service)))
+ (desktop-environment
+ (name "Enlightenment")
+ (snippet '(service enlightenment-desktop-service-type)))))
+
+(define (desktop-environments->configuration desktop-environments)
+ "Return the configuration field for DESKTOP-ENVIRONMENTS."
+ (let ((snippets
+ (map desktop-environment-snippet desktop-environments)))
+ `(,@(if (null? snippets)
+ '()
+ `((services (cons* ,@snippets
+ %desktop-services)))))))
diff --git a/gnu/installer/steps.scm b/gnu/installer/steps.scm
new file mode 100644
index 0000000000..3f0bdad4f7
--- /dev/null
+++ b/gnu/installer/steps.scm
@@ -0,0 +1,237 @@
+;;; 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 steps)
+ #:use-module (guix records)
+ #:use-module (guix build 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
+ %configuration-file-width
+ 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))
+ (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
+sequencially. 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 (list todo-steps done-steps)
+ (((todo . rest-todo) (prev-done ... last-done))
+ (if (eq? (installer-step-id todo)
+ (installer-step-id step))
+ (run result
+ #:todo-steps todo-steps
+ #:done-steps done-steps)
+ (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)))
+ (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))))))))
+
+ (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 %configuration-file-width (make-parameter 79))
+
+(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 desktop))))
+ `(,@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)
+ (format port ";; This is an operating system configuration generated~%")
+ (format port ";; by the graphical installer.~%")
+ (newline port)
+ (for-each (lambda (part)
+ (if (null? part)
+ (newline port)
+ (pretty-print part port)))
+ configuration)
+ (flush-output-port port))))
diff --git a/gnu/installer/timezone.scm b/gnu/installer/timezone.scm
new file mode 100644
index 0000000000..32bc2ed6bb
--- /dev/null
+++ b/gnu/installer/timezone.scm
@@ -0,0 +1,127 @@
+;;; 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-childrens
+ 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-childrens tree path)
+ "Return the childrens 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-childrens <> 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-childrens 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)))
diff --git a/gnu/installer/user.scm b/gnu/installer/user.scm
new file mode 100644
index 0000000000..1f8d40a011
--- /dev/null
+++ b/gnu/installer/user.scm
@@ -0,0 +1,50 @@
+;;; 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)
+ #:export (<user>
+ user
+ make-user
+ user-name
+ user-group
+ user-home-directory
+
+ users->configuration))
+
+(define-record-type* <user>
+ user make-user
+ user?
+ (name user-name)
+ (group user-group
+ (default "users"))
+ (home-directory user-home-directory))
+
+(define (users->configuration users)
+ "Return the configuration field for USERS."
+ `((users (cons*
+ ,@(map (lambda (user)
+ `(user-account
+ (name ,(user-name user))
+ (group ,(user-group user))
+ (home-directory ,(user-home-directory user))
+ (supplementary-groups
+ (quote ("wheel" "netdev"
+ "audio" "video")))))
+ users)
+ %base-user-accounts))))
diff --git a/gnu/installer/utils.scm b/gnu/installer/utils.scm
new file mode 100644
index 0000000000..e91f90a84d
--- /dev/null
+++ b/gnu/installer/utils.scm
@@ -0,0 +1,63 @@
+;;; 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 utils)
+ #:use-module (guix utils)
+ #:use-module (guix build utils)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 regex)
+ #:use-module (ice-9 textual-ports)
+ #:export (read-lines
+ read-all
+ nearest-exact-integer
+ read-percentage
+ run-shell-command))
+
+(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-shell-command command)
+ (call-with-temporary-output-file
+ (lambda (file port)
+ (format port "~a~%" command)
+ ;; (format port "exit~%")
+ (close port)
+ (invoke "bash" "--init-file" file))))