aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2013-2020, 2022, 2023 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2016 Alex Griffin <a@ajgrf.com>
;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
;;; Copyright © 2020, 2023 Efraim Flashner <efraim@flashner.co.il>
;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; Copyright © 2024 Giacomo Leidi <goodoldpaul@autistici.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu system shadow)
  #:use-module ((guix diagnostics) #:select (formatted-message))
  #:use-module (guix records)
  #:use-module (guix gexp)
  #:use-module (guix store)
  #:use-module (guix modules)
  #:use-module (guix sets)
  #:use-module (guix ui)
  #:use-module ((gnu build accounts)
                #:select (%subordinate-id-count
                          %subordinate-id-max
                          %subordinate-id-min))
  #:use-module (gnu system accounts)
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:use-module ((gnu system file-systems)
                #:select (%tty-gid))
  #:use-module ((gnu packages admin)
                #:select (shadow))
  #:use-module (gnu packages bash)
  #: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)

  ;; Re-export these bindings for backward compatibility.
  #:re-export (user-account
               user-account?
               user-account-name
               user-account-password
               user-account-uid
               user-account-group
               user-account-supplementary-groups
               user-account-comment
               user-account-home-directory
               user-account-create-home-directory?
               user-account-shell
               user-account-system?

               user-group
               user-group?
               user-group-name
               user-group-password
               user-group-id
               user-group-system?

               user-extra-groups
               user-extra-groups?
               user-extra-groups-user
               user-extra-groups-groups)

  #:export (%default-bashrc
            %default-bash-profile
            %default-zprofile
            %default-xdefaults
            %default-gdbinit
            %default-nanorc
            %default-dotguile
            %default-skeleton-home-config
            default-skeletons
            skeleton-directory
            %base-groups
            %base-user-accounts

            account-service-type
            account-service

            subids-configuration
            subids-configuration?
            subids-configuration-add-root?
            subids-configuration-subgids
            subids-configuration-subuids

            subids-extension
            subids-extension?
            subids-extension-subgids
            subids-extension-subuids

            subids-service-type))

;;; Commentary:
;;;
;;; Utilities for configuring the Shadow tool suite ('login', 'passwd', etc.)
;;;
;;; Code:

;; Change the default shell used by new <user-account> records.
(default-shell (file-append bash "/bin/bash"))

(define %base-groups
  ;; Default set of groups.
  (let-syntax ((system-group (syntax-rules ()
                               ((_ args ...)
                                (user-group (system? #t) args ...)))))
    (list (system-group (name "root") (id 0))
          (system-group (name "wheel"))           ; root-like users
          (system-group (name "users"))           ; normal users
          (system-group (name "nogroup"))         ; for daemons etc.

          ;; The following groups are conventionally used by things like udev to
          ;; control access to hardware devices.
          (system-group (name "tty") (id %tty-gid))
          (system-group (name "dialout"))
          (system-group (name "kmem"))
          (system-group (name "input"))           ; input devices, from udev
          (system-group (name "video"))
          (system-group (name "audio"))
          (system-group (name "netdev"))          ; used in avahi-dbus.conf
          (system-group (name "lp"))
          (system-group (name "disk"))
          (system-group (name "floppy"))
          (system-group (name "cdrom"))
          (system-group (name "tape"))
          (system-group (name "kvm")))))          ; for /dev/kvm

(define %base-user-accounts
  ;; List of standard user accounts.  Note that "root" is a special case, so
  ;; it's not listed here.
  (list (user-account
         (name "nobody")
         (uid 65534)
         (group "nogroup")
         (shell (file-append shadow "/sbin/nologin"))
         (home-directory "/nonexistent")
         (create-home-directory? #f)
         (system? #t))))

(define %default-bashrc
  (plain-file "bashrc" "\
# Bash initialization for interactive non-login shells and
# for remote shells (info \"(bash) Bash Startup Files\").

# Export 'SHELL' to child processes.  Programs such as 'screen'
# honor it and otherwise use /bin/sh.
export SHELL

if [[ $- != *i* ]]
then
    # We are being invoked from a non-interactive shell.  If this
    # is an SSH session (as in \"ssh host command\"), source
    # /etc/profile so we get PATH and other essential variables.
    [[ -n \"$SSH_CLIENT\" ]] && source /etc/profile

    # Don't do anything else.
    return
fi

# Source the system-wide file.
[ -f /etc/bashrc ] && source /etc/bashrc

alias ls='ls -p --color=auto'
alias ll='ls -l'
alias grep='grep --color=auto'
alias ip='ip -color=auto'\n"))

(define %default-bash-profile
  (plain-file "bash_profile" "\
# Set up Guix Home profile
if [ -f ~/.profile ]; then . ~/.profile; fi

# Honor per-interactive-shell startup file
if [ -f ~/.bashrc ]; then . ~/.bashrc; fi

# Merge search-paths from multiple profiles, the order matters.
eval \"$(guix package --search-paths \\
-p $HOME/.config/guix/current \\
-p $HOME/.guix-home/profile \\
-p $HOME/.guix-profile \\
-p /run/current-system/profile)\"

# Prepend setuid programs.
export PATH=/run/setuid-programs:$PATH
"))

(define %default-zprofile
  (plain-file "zprofile" "\
# Set up the system, user profile, and related variables.
source /etc/profile
# Set up the home environment profile.
source ~/.profile
"))

(define %default-xdefaults
  (plain-file "Xdefaults" "\
XTerm*utf8: always
XTerm*metaSendsEscape: true\n"))

(define %default-gdbinit
  (plain-file "gdbinit"
              "# Tell GDB where to look for separate debugging files.
guile
(use-modules (gdb))
(execute (string-append \"set debug-file-directory \"
                        (string-join
                          (filter file-exists?
                                  (append
                                    (if (getenv \"GDB_DEBUG_FILE_DIRECTORY\")
                                      (list (getenv \"GDB_DEBUG_FILE_DIRECTORY\"))
                                      '())
                                    (list \"~/.guix-home/profile/lib/debug\"
                                          \"~/.guix-profile/lib/debug\"
                                          \"/run/current-system/profile/lib/debug\")))
                          \":\")))
end

# Authorize extensions found in the store, such as the
# pretty-printers of libstdc++.
set auto-load safe-path /gnu/store/*/lib\n"))

(define %default-nanorc
  (plain-file "nanorc"
              "# Include all the syntax highlighting modules.
include /run/current-system/profile/share/nano/*.nanorc\n"))

(define %default-dotguile
  (plain-file "dot-guile"
              "(cond ((false-if-exception (resolve-interface '(ice-9 readline)))
       =>
       (lambda (module)
         ;; Enable completion and input history at the REPL.
         ((module-ref module 'activate-readline))))
      (else
       (display \"Consider installing the 'guile-readline' package for
convenient interactive line editing and input history.\\n\\n\")))

      (unless (getenv \"INSIDE_EMACS\")
        (cond ((false-if-exception (resolve-interface '(ice-9 colorized)))
               =>
               (lambda (module)
                 ;; Enable completion and input history at the REPL.
                 ((module-ref module 'activate-colorized))))
              (else
               (display \"Consider installing the 'guile-colorized' package
for a colorful Guile experience.\\n\\n\"))))\n"))

(define %default-skeleton-home-config
  (plain-file "default-home-config" "\
;; This is a sample Guix Home configuration which can help setup your
;; home directory in the same declarative manner as Guix System.
;; For more information, see the Home Configuration section of the manual.
(define-module (guix-home-config)
  #:use-module (gnu home)
  #:use-module (gnu home services)
  #:use-module (gnu home services shells)
  #:use-module (gnu services)
  #:use-module (gnu system shadow))

(define home-config
  (home-environment
    (services
      (list
        ;; Uncomment the shell you wish to use for your user:
        ;(service home-bash-service-type)
        ;(service home-fish-service-type)
        ;(service home-zsh-service-type)

        (service home-files-service-type
         `((\".guile\" ,%default-dotguile)
           (\".Xdefaults\" ,%default-xdefaults)))

        (service home-xdg-configuration-files-service-type
         `((\"gdb/gdbinit\" ,%default-gdbinit)
           (\"nano/nanorc\" ,%default-nanorc)))))))

home-config"))

(define (default-skeletons)
  "Return the default skeleton files for /etc/skel.  These files are copied by
'useradd' in the home directory of newly created user accounts."

  (let ((profile   %default-bash-profile)
        (bashrc    %default-bashrc)
        (zprofile  %default-zprofile)
        (xdefaults %default-xdefaults)
        (gdbinit   %default-gdbinit))
    `((".bash_profile" ,profile)
      (".bashrc" ,bashrc)
      ;; Zsh sources ~/.zprofile before ~/.zshrc, and it sources ~/.zlogin
      ;; after ~/.zshrc.  To avoid interfering with any customizations a user
      ;; may have made in their ~/.zshrc, put this in .zprofile, not .zlogin.
      (".zprofile" ,zprofile)
      (".nanorc" ,%default-nanorc)
      (".Xdefaults" ,xdefaults)
      (".guile" ,%default-dotguile)
      (".gdbinit" ,gdbinit)
      ("guix-home-config.scm" ,%default-skeleton-home-config))))

(define (skeleton-directory skeletons)
  "Return a directory containing SKELETONS, a list of name/derivation tuples."
  (computed-file "skel"
                 (with-imported-modules '((guix build utils))
                   #~(begin
                       (use-modules (ice-9 match)
                                    (guix build utils))

                       (mkdir #$output)
                       (chdir #$output)

                       ;; Note: copy the skeletons instead of symlinking
                       ;; them like 'file-union' does, because 'useradd'
                       ;; would just copy the symlinks as is.
                       (for-each (match-lambda
                                   ((target source)
                                    (copy-recursively source target)))
                                 '#$skeletons)
                       ;; Make nanorc respect XDG_CONFIG_HOME.
                       (when (file-exists? ".nanorc")
                         (mkdir-p ".config/nano")
                         (rename-file ".nanorc" ".config/nano/nanorc"))
                       (when (file-exists? ".gdbinit")
                         (mkdir-p ".config/gdb")
                         (rename-file ".gdbinit" ".config/gdb/gdbinit"))
                       #t))))

(define (find-duplicates list)
  "Find duplicate entries in @var{list}.
Two entries are considered duplicates, if they are @code{equal?} to each other.
This implementation is made asymptotically faster than @code{delete-duplicates}
through the internal use of hash tables."
  (let loop ((list list)
             ;; We actually modify table in-place, but still allocate it here
             ;; so that we only need one level of indentation.
             (table (make-hash-table)))
    (match list
      (()
       (hash-fold (lambda (key value seed)
                    (if (> value 1)
                        (cons key seed)
                        seed))
                  '()
                  table))
      ((first . rest)
       (hash-set! table first
                  (1+ (hash-ref table first 0)))
       (loop rest table)))))

(define (assert-unique-account-names users)
  (match (find-duplicates (map user-account-name users))
    (() *unspecified*)
    (duplicates
     (warning
      (G_ "the following accounts appear more than once:~{ ~a~}~%")
      duplicates))))

(define (assert-unique-group-names groups)
  (match (find-duplicates (map user-group-name groups))
    (() *unspecified*)
    (duplicates
     (warning
      (G_ "the following groups appear more than once:~{ ~a~}~%")
      duplicates))))

(define (assert-valid-users/groups users groups)
  "Raise an error if USERS refer to groups not listed in GROUPS."
  (let ((groups (list->set (map user-group-name groups))))
    (define (validate-supplementary-group user group)
      (unless (set-contains? groups group)
        (raise (condition
                (&message
                 (message
                  (format #f (G_ "supplementary group '~a' \
of user '~a' is undeclared")
                          group
                          (user-account-name user))))))))

    (for-each (lambda (user)
                (unless (set-contains? groups (user-account-group user))
                  (raise (condition
                          (&message
                           (message
                            (format #f (G_ "primary group '~a' \
of user '~a' is undeclared")
                                    (user-account-group user)
                                    (user-account-name user)))))))

                (for-each (cut validate-supplementary-group user <>)
                          (user-account-supplementary-groups user)))
              users)))


;;;
;;; Accounts Service.
;;;

(define (user-group->gexp group)
  "Turn GROUP, a <user-group> object, into a list-valued gexp suitable for
'active-groups'."
  #~(list #$(user-group-name group)
          #$(user-group-password group)
          #$(user-group-id group)
          #$(user-group-system? group)))

(define (user-account->gexp account)
  "Turn ACCOUNT, a <user-account> object, into a list-valued gexp suitable for
'activate-users'."
  #~`(#$(user-account-name account)
      #$(user-account-uid account)
      #$(user-account-group account)
      #$(user-account-supplementary-groups account)
      #$(user-account-comment account)
      #$(user-account-home-directory account)
      #$(user-account-create-home-directory? account)
      ,#$(user-account-shell account)             ; this one is a gexp
      #$(user-account-password account)
      #$(user-account-system? account)))

(define (account-activation accounts+groups)
  "Return a gexp that activates ACCOUNTS+GROUPS, a list of <user-account> and
<user-group> objects.  Raise an error if a user account refers to a undefined
group."
  (define accounts
    (delete-duplicates (filter user-account? accounts+groups) eq?))

  (define user-specs
    (map user-account->gexp accounts))

  (define groups
    (delete-duplicates (filter user-group? accounts+groups) eq?))

  (define group-specs
    (map user-group->gexp groups))

  (assert-unique-account-names accounts)
  (assert-unique-group-names groups)
  (assert-valid-users/groups accounts groups)

  ;; Add users and user groups.
  (with-imported-modules (source-module-closure '((gnu system accounts)))
    #~(begin
        (use-modules (gnu system accounts))

        (activate-users+groups (map sexp->user-account (list #$@user-specs))
                               (map sexp->user-group (list #$@group-specs))))))

(define (account-shepherd-service accounts+groups)
  "Return a Shepherd service that creates the home directories for the user
accounts among ACCOUNTS+GROUPS."
  (define accounts
    (filter user-account? accounts+groups))

  ;; Create home directories only once 'file-systems' is up.  This makes sure
  ;; they are created in the right place if /home lives on a separate
  ;; partition.
  ;;
  ;; XXX: We arrange for this service to stop right after it's done its job so
  ;; that 'guix system reconfigure' knows that it can reload it fearlessly
  ;; (and thus create new home directories).
  (list (shepherd-service
         (requirement '(file-systems))
         (provision '(user-homes))
         (one-shot? #t)
         (modules '((gnu build activation)
                    (gnu system accounts)))
         (start (with-imported-modules (source-module-closure
                                        '((gnu build activation)
                                          (gnu system accounts)))
                  #~(lambda ()
                      (activate-user-home
                       (map sexp->user-account
                            (list #$@(map user-account->gexp accounts))))
                      #t)))                       ;success
         (documentation "Create user home directories."))))

(define (shells-file shells)
  "Return a file-like object that builds a shell list for use as /etc/shells
based on SHELLS.  /etc/shells is used by xterm, polkit, and other programs."
  (computed-file "shells"
                 #~(begin
                     (use-modules (srfi srfi-1))

                     (define shells
                       (delete-duplicates (list #$@shells)))

                     (call-with-output-file #$output
                       (lambda (port)
                         (display "\
/bin/sh
/run/current-system/profile/bin/sh
/run/current-system/profile/bin/bash\n" port)
                         (for-each (lambda (shell)
                                     (display shell port)
                                     (newline port))
                                   shells))))))
(define (etc-files arguments)
  "Filter out among ARGUMENTS things corresponding to skeletons, and return
the /etc/skel directory for those."
  (let ((skels (filter pair? arguments))
        (users (filter user-account? arguments)))
    `(("skel" ,(skeleton-directory skels))
      ("shells" ,(shells-file (map user-account-shell users))))))

(define account-service-type
  (service-type (name 'account)

                ;; Concatenate <user-account>, <user-group>,
                ;; <user-extra-groups> and skeleton lists.
                (extend (compose merge-extra-groups-data append))
                (compose concatenate)

                (extensions
                 (list (service-extension activation-service-type
                                          account-activation)
                       (service-extension shepherd-root-service-type
                                          account-shepherd-service)
                       ;; Have 'user-processes' depend on 'user-homes' so that
                       ;; daemons start after their home directory has been
                       ;; created.
                       (service-extension user-processes-service-type
                                          (const '(user-homes)))
                       (service-extension etc-service-type
                                          etc-files)))
                (default-value '())
                (description
                 "Ensure the specified user accounts and groups exist, as well
as each account home directory.")))

(define (account-service accounts+groups skeletons)
  "Return a <service> that takes care of user accounts and user groups, with
ACCOUNTS+GROUPS as its initial list of accounts and groups."
  (service account-service-type
           (append skeletons accounts+groups)))


;;;
;;; Subids Service.
;;;

(define* (%root-subid #:optional (start %subordinate-id-min) (count %subordinate-id-count))
  (subid-range
   (name "root")
   (start start)
   (count count)))

(define-record-type* <subids-configuration>
  subids-configuration make-subids-configuration
  subids-configuration?
  this-subids-configuration

  (add-root?   subids-configuration-add-root?                 ; boolean
               (default #t))
  (subgids     subids-configuration-subgids                   ; list of <subid-range>
               (default '()))
  (subuids     subids-configuration-subuids                   ; list of <subid-range>
               (default '())))

(define (subid-range->gexp range)
  "Turn RANGE, a <subid-range> object, into a list-valued gexp suitable for
'activate-subuids+subgids'."
  (define count (subid-range-count range))
  #~`(#$(subid-range-name range)
      #$(subid-range-start range)
      #$(if (and (number? count)
                 (> count 0))
            count
            %subordinate-id-count)))

(define (assert-valid-subids ranges)
  (cond ((>= (fold + 0 (map subid-range-count ranges))
             (- %subordinate-id-max %subordinate-id-min -1))
         (raise
          (formatted-message
           (G_
            "The configured ranges are more than the ~a max allowed.")
           (- %subordinate-id-max %subordinate-id-min -1))))
        ((any (lambda (r)
                (define start (subid-range-start r))
                (and start
                     (< start %subordinate-id-min)))
              ranges)
         (raise
          (formatted-message
           (G_
            "One subid-range starts before the minimum allowed sub id ~a.")
           %subordinate-id-min)))
        ((any (lambda (r)
                (define end (subid-range-end r))
                (and end
                     (> end %subordinate-id-max)))
              ranges)
         (raise
          (formatted-message
           (G_
            "One subid-range ends after the maximum allowed sub id ~a.")
           %subordinate-id-max)))
        ((any (compose null? subid-range-name)
              ranges)
         (raise
          (formatted-message
           (G_
            "One subid-range has a null name."))))
        ((any (compose string-null? subid-range-name)
              ranges)
         (raise
          (formatted-message
           (G_
            "One subid-range has a name equal to the empty string."))))
        (else #t)))

(define (delete-duplicate-ranges ranges)
  (delete-duplicates ranges
                     (lambda args
                       (apply string=? (map subid-range-name ranges)))))

(define (subids-activation config)
  "Return a gexp that activates SUBUIDS+SUBGIDS, a list of <subid-range>
objects."
  (define (add-root-when-missing ranges)
    (define sorted-ranges
      (sort-list ranges subid-range-less))
    (define root-missing?
      (not
       (find (lambda (r)
               (string=? "root"
                         (subid-range-name r)))
             sorted-ranges)))
    (define first-start
      (and (> (length sorted-ranges) 0)
           (subid-range-start (first sorted-ranges))))
    (define first-has-start?
      (number? first-start))
    (define root-start
      (if first-has-start?
          (and
           (> first-start %subordinate-id-min)
           %subordinate-id-min)
          %subordinate-id-min))
    (define root-count
      (if first-has-start?
          (- first-start %subordinate-id-min)
          %subordinate-id-count))
    (if (and root-missing?
             (subids-configuration-add-root? config))
        (append (list (%root-subid root-start root-count))
                sorted-ranges)
        sorted-ranges))

  (define subuids
    (delete-duplicate-ranges (subids-configuration-subuids config)))

  (define subuids-specs
    (map subid-range->gexp (add-root-when-missing subuids)))

  (define subgids
    (delete-duplicate-ranges (subids-configuration-subgids config)))

  (define subgids-specs
    (map subid-range->gexp (add-root-when-missing subgids)))

  (assert-valid-subids subgids)
  (assert-valid-subids subuids)

  ;; Add subuids and subgids.
  (with-imported-modules (source-module-closure '((gnu system accounts)))
    #~(begin
        (use-modules (gnu system accounts))

        (activate-subuids+subgids (map sexp->subid-range (list #$@subuids-specs))
                                  (map sexp->subid-range (list #$@subgids-specs))))))

(define-record-type* <subids-extension>
  subids-extension make-subids-extension
  subids-extension?
  this-subids-extension

  (subgids   subids-extension-subgids                   ; list of <subid-range>
             (default '()))
  (subuids   subids-extension-subuids                   ; list of <subid-range>
             (default '())))

(define append-subid-ranges
  (lambda args
    (delete-duplicate-ranges
     (apply append args))))

(define (subids-extension-merge a b)
  (subids-extension
   (subgids (append-subid-ranges
             (subids-extension-subgids a)
             (subids-extension-subgids b)))
   (subuids (append-subid-ranges
             (subids-extension-subuids a)
             (subids-extension-subuids b)))))

(define subids-service-type
  (service-type (name 'subids)
                ;; Concatenate <subid-range> lists.
                (compose (lambda (args)
                           (fold subids-extension-merge
                                 (subids-extension)
                                 args)))
                (extend
                 (lambda (config extension)
                   (subids-configuration
                    (inherit config)
                    (subgids
                     (append-subid-ranges
                      (subids-configuration-subgids config)
                      (subids-extension-subgids extension)))
                    (subuids
                     (append-subid-ranges
                      (subids-configuration-subuids config)
                      (subids-extension-subuids extension))))))
                (extensions
                 (list (service-extension activation-service-type
                                          subids-activation)))
                (default-value
                  (subids-configuration))
                (description
                 "Ensure the specified sub UIDs and sub GIDs exist in
/etc/subuid and /etc/subgid.")))

;;; shadow.scm ends here
-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 preceding 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 hack. Parted almost always fails to create optimally ;; aligned partitions (unless specifying 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-constraint-ok? (disk-add-partition disk partition final-constraint)) (partition-no-contraint-ok? (or partition-constraint-ok? (disk-add-partition disk partition no-constraint))) (partition-ok? (or partition-constraint-ok? partition-no-contraint-ok?))) (installer-log-line "Creating partition:") (installer-log-line "~/type: ~a" partition-type) (installer-log-line "~/filesystem-type: ~a" (filesystem-type-name filesystem-type)) (installer-log-line "~/flags: ~a" flags) (installer-log-line "~/start: ~a" start-sector*) (installer-log-line "~/end: ~a" end-sector) (installer-log-line "~/start-range: [~a, ~a]" (geometry-start start-range) (geometry-end start-range)) (installer-log-line "~/end-range: [~a, ~a]" (geometry-start end-range) (geometry-end end-range)) (installer-log-line "~/constraint: ~a" partition-constraint-ok?) (installer-log-line "~/no-constraint: ~a" partition-no-contraint-ok?) ;; Set the partition name if supported. (when (and partition-ok? has-name? name) (partition-set-name partition name)) ;; Both partition-set-system and partition-set-flag calls can affect ;; the partition type. Their order is important, see: ;; https://issues.guix.gnu.org/55549. (partition-set-system partition filesystem-type) ;; Set flags if required. (for-each (lambda (flag) (and (partition-is-flag-available? partition flag) (partition-set-flag partition flag 1))) flags) (and partition-ok? 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-FORMATTING? 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. Return the complete list of partitions on DISK, including the ESP when it exists." (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))) ;; Remove everything but esp if it exists. (for-each (lambda (partition) (and (data-partition? partition) ;; Do not remove logical partitions ourselves, since ;; disk-remove-partition* will remove all the logical partitions ;; residing on an extended partition, which would lead to a ;; double-remove and ensuing SEGFAULT. (not (logical-partition? partition)) (disk-remove-partition* disk partition))) non-boot-partitions) (let* ((start-partition (cond ((target-hurd?) #f) ((efi-installation?) (and (not esp-partition) (user-partition (fs-type 'fat32) (esp? #t) (size new-esp-size) (mount-point (default-esp-mount-point))))) (else (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 (or encrypted? (target-hurd?)) '() `(,(user-partition (fs-type 'swap) (size swap-size)))) ,(user-partition (fs-type (if (target-hurd?) 'ext2 '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 (if (target-hurd?) 'ext2 '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 (if (target-hurd?) 'ext2 'ext4)) (crypt-label (and encrypted? "crypthome")) (size "100%") (mount-point "/home"))))))) (new-partitions* (force-user-partitions-formatting new-partitions))) (append (if esp-partition (list (partition->user-partition esp-partition)) '()) (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?) ;; Cannot not read the partition UUID. (define-condition-type &cannot-read-uuid &condition cannot-read-uuid? (partition cannot-read-uuid-partition)) (define (check-user-partitions user-partitions) "Check the following statements: The USER-PARTITIONS list contains one <user-partition> record with a mount-point set to '/'. Raise &no-root-mount-point condition otherwise. All the USER-PARTITIONS with a mount point and that will not be formatted have a valid UUID. Raise a &cannot-read-uuid condition specifying the faulty partition otherwise. Return #t if all the statements are valid." (define (check-root) (let ((mount-points (map user-partition-mount-point user-partitions))) (or (member "/" mount-points) (raise (condition (&no-root-mount-point)))))) (define (check-uuid) (let ((mount-partitions (filter user-partition-mount-point user-partitions))) (every (lambda (user-partition) (let ((file-name (user-partition-file-name user-partition)) (need-formatting? (user-partition-need-formatting? user-partition))) (or need-formatting? (read-partition-uuid/retry file-name) (raise (condition (&cannot-read-uuid (partition file-name))))))) mount-partitions))) (and (check-root) (check-uuid) #t)) (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 (create-btrfs-file-system partition) "Create a btrfs file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.btrfs" "-f" partition)) (define (create-ext2-file-system partition) "Create an ext2 file-system for PARTITION file-name, when TARGET-HURD?, for the Hurd." (apply (%run-command-in-installer) `("mkfs.ext2" ,@(if (target-hurd?) '("-o" "hurd") '()) "-F" ,partition))) (define (create-ext4-file-system partition) "Create an ext4 file-system for PARTITION file-name." ;; Enable the 'large_dir' feature so users can have a store of several TiBs. ;; Failing to do that, the directory index (enabled by 'dir_index') can fill ;; up and adding new files would fail with ENOSPC despite there being plenty ;; of free space and inodes: ;; <https://blog.merovius.de/posts/2013-10-20-ext4-mysterious-no-space-left-on/>. ((%run-command-in-installer) "mkfs.ext4" "-F" partition "-O" "large_dir")) (define (create-fat16-file-system partition) "Create a fat16 file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.fat" "-F16" partition)) (define (create-fat32-file-system partition) "Create a fat32 file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.fat" "-F32" partition)) (define (create-jfs-file-system partition) "Create a JFS file-system for PARTITION file-name." ((%run-command-in-installer) "jfs_mkfs" "-f" partition)) (define (create-ntfs-file-system partition) "Create a JFS file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.ntfs" "-F" "-f" partition)) (define (create-xfs-file-system partition) "Create an XFS file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.xfs" "-f" partition)) (define (create-swap-partition partition) "Set up swap area on PARTITION file-name." ((%run-command-in-installer) "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 (secret-content (user-partition-crypt-password user-partition)))) (call-with-luks-key-file password (lambda (key-file) (installer-log-line "formatting and opening LUKS entry ~s at ~s" label file-name) ((%run-command-in-installer) "cryptsetup" "-q" "luksFormat" file-name key-file) ((%run-command-in-installer) "cryptsetup" "open" "--type" "luks" "--key-file" key-file file-name label))))) (define (luks-ensure-open user-partition) "Ensure partition pointed by USER-PARTITION is opened." (unless (file-exists? (user-partition-upper-file-name user-partition)) (let* ((file-name (user-partition-file-name user-partition)) (label (user-partition-crypt-label user-partition)) (password (secret-content (user-partition-crypt-password user-partition)))) (call-with-luks-key-file password (lambda (key-file) (installer-log-line "opening LUKS entry ~s at ~s" label file-name) ((%run-command-in-installer) "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))) (installer-log-line "closing LUKS entry ~s" label) ((%run-command-in-installer) "cryptsetup" "close" label))) (define (format-user-partitions user-partitions) "Format the <user-partition> records in USER-PARTITIONS list with NEED-FORMATTING? 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 ((btrfs) (and need-formatting? (not (eq? type 'extended)) (create-btrfs-file-system file-name))) ((ext2) (and need-formatting? (not (eq? type 'extended)) (create-ext2-file-system file-name))) ((ext4) (and need-formatting? (not (eq? type 'extended)) (create-ext4-file-system file-name))) ((fat16) (and need-formatting? (not (eq? type 'extended)) (create-fat16-file-system file-name))) ((fat32) (and need-formatting? (not (eq? type 'extended)) (create-fat32-file-system file-name))) ((jfs) (and need-formatting? (not (eq? type 'extended)) (create-jfs-file-system file-name))) ((ntfs) (and need-formatting? (not (eq? type 'extended)) (create-ntfs-file-system file-name))) ((xfs) (and need-formatting? (not (eq? type 'extended)) (create-xfs-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))) (when crypt-label (luks-ensure-open user-partition)) (mkdir-p target) (installer-log-line "mounting ~s on ~s" file-name 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))) (installer-log-line "unmounting ~s" target) (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 swapping 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 swapping 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 (root-user-partition? partition) "Return true if PARTITION is the root partition." (let ((mount-point (user-partition-mount-point partition))) (and mount-point (string=? mount-point "/")))) (define (bootloader-configuration user-partitions) "Return the bootloader configuration field for USER-PARTITIONS." (let ((root-partition (find root-user-partition? user-partitions))) (match user-partitions (() (if (target-hurd?) '(bootloader-configuration (bootloader grub-minimal-bootloader) (targets "/dev/sdaX")) '())) (_ (let ((root-partition-disk (user-partition-disk-file-name root-partition))) `((bootloader-configuration ,@(if (efi-installation?) `((bootloader grub-efi-bootloader) (targets (list ,(default-esp-mount-point)))) `((bootloader ,(if (target-hurd?) 'grub-minimal-bootloader 'grub-bootloader)) (targets (list ,root-partition-disk)))) ;; XXX: Assume we defined the 'keyboard-layout' field of ;; <operating-system> right above. (keyboard-layout keyboard-layout)))))))) (define (user-partition-missing-modules user-partitions) "Return the list of kernel modules missing from the default set of kernel modules to access USER-PARTITIONS." (let ((devices (filter user-partition-crypt-label user-partitions)) (root (find root-user-partition? user-partitions))) (delete-duplicates (append-map (lambda (device) (catch 'system-error (lambda () (missing-modules device %base-initrd-modules)) (const '()))) (delete-duplicates (map user-partition-file-name (filter identity (cons root devices)))))))) (define (initrd-configuration user-partitions) "Return an 'initrd-modules' field with everything needed for USER-PARTITIONS, or return nothing." (if (target-hurd?) '((initrd #f) (initrd-modules '())) (match (user-partition-missing-modules user-partitions) (() '()) ((modules ...) `((initrd-modules (append ',modules %base-initrd-modules))))))) (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 (if (target-hurd?) '() (map user-partition-file-name swap-user-partitions))) (encrypted-partitions (filter user-partition-crypt-label user-partitions))) `((bootloader ,@(bootloader-configuration user-partitions)) ,@(initrd-configuration user-partitions) ,@(if (null? swap-devices) '() (let* ((uuids (map (lambda (file) (uuid->string (read-partition-uuid file))) swap-devices))) `((swap-devices (list ,@(map (lambda (uuid) `(swap-space (target (uuid ,uuid)))) uuids)))))) ,@(if (null? encrypted-partitions) '() `((mapped-devices (list ,@(map user-partition->mapped-device encrypted-partitions))))) ,(vertical-space 1) ,(let-syntax ((G_ (syntax-rules () ((_ str) str)))) (comment (G_ "\ ;; The list of file systems that get \"mounted\". The unique ;; file system identifiers there (\"UUIDs\") can be obtained ;; by running 'blkid' in a terminal.\n"))) (file-systems (cons* ,@(user-partitions->file-systems user-partitions) %base-file-systems))))) ;; ;; Initialization. ;; (define (init-parted) "Initialize libparted support." (probe-all-devices!) ;; Remove all logical devices, otherwise "device-is-busy?" will report true ;; on all devices containaing active logical volumes. (remove-logical-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) (for-each (lambda (file-name) (let/time ((time in-use? (with-delay-device-in-use? file-name))) (if in-use? (error (format #f (G_ "Device ~a is still in use.") file-name)) (installer-log-line "Syncing ~a took ~a seconds." file-name (time-second time))))) device-file-names)))