# GNU Guix --- Functional package management for GNU
# Copyright © 2018, 2019, 2020, 2023 Ludovic Courtès <ludo@gnu.org>
# Copyright © 2020 Eric Bavier <bavier@posteo.net>
#
# 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/>.

#
# Test the 'guix pack --relocatable' using the external store, if any.
#

guix pack --version

# 'guix pack --relocatable' requires a C compiler and libc.a, which our
# bootstrap binaries don't provide.  To make the test relatively inexpensive,
# run it on the user's global store if possible, on the grounds that binaries
# may already be there or can be built or downloaded inexpensively.

storedir="`guile -c '(use-modules (guix config))(display %storedir)'`"
localstatedir="`guile -c '(use-modules (guix config))(display %localstatedir)'`"
NIX_STORE_DIR="$storedir"
GUIX_DAEMON_SOCKET="$localstatedir/guix/daemon-socket/socket"
export NIX_STORE_DIR GUIX_DAEMON_SOCKET

if ! guile -c '(use-modules (guix)) (exit (false-if-exception (open-connection)))'
then
    exit 77
fi

# Attempt to run the given command in a namespace where the store is
# invisible.  This makes sure the presence of the store does not hide
# problems.
run_without_store ()
{
    if unshare -r true		# Are user namespaces supported?
    then
	# Run that relocatable executable in a user namespace where we "erase"
	# the store by mounting an empty file system on top of it.  That way,
	# we exercise the wrapper code that creates the user namespace and
	# bind-mounts the store.
	unshare -mrf sh -c 'mount -t tmpfs -o ro none "$NIX_STORE_DIR"; '"$*"
    else
	# Run the relocatable program in the current namespaces.  This is a
	# weak test because we're going to access store items from the host
	# store.
	sh -c "$*"
    fi
}

# Wait for the given file to show up.  Error out if it doesn't show up in a
# timely fashion.
wait_for_file ()
{
    i=0
    while ! test -f "$1" && test $i -lt 20
    do
	sleep 0.3
	i=`expr $i + 1`
    done
    test -f "$1"
}

test_directory="`mktemp -d`"
export test_directory
trap 'chmod -Rf +w "$test_directory"; rm -rf "$test_directory"' EXIT

if unshare -r true
then
    # Test the 'userns' execution engine.
    tarball="`guix pack -R -S /Bin=bin sed`"
    (cd "$test_directory"; tar xvf "$tarball")

    chmod +w "$test_directory"
    run_without_store "$test_directory/Bin/sed" --version > "$test_directory/output"
    grep 'GNU sed' "$test_directory/output"

    # Same with an explicit engine.
    run_without_store GUIX_EXECUTION_ENGINE="userns" \
		      "$test_directory/Bin/sed" --version > "$test_directory/output"
    grep 'GNU sed' "$test_directory/output"

    # Check whether the exit code is preserved.
    run_without_store "$test_directory/Bin/sed" --does-not-exist && false

    chmod -Rf +w "$test_directory"; rm -rf "$test_directory"/*
else
    echo "'userns' execution tests skipped" >&2
fi

case "`uname -m`" in
    x86_64|i?86)
	# Try '-RR' and PRoot.
	tarball="`guix pack -RR -S /Bin=bin sed`"
	tar tvf "$tarball" | grep /bin/proot
	(cd "$test_directory"; tar xf "$tarball")
	chmod +w "$test_directory"
	run_without_store GUIX_EXECUTION_ENGINE="proot" \
	"$test_directory/Bin/sed" --version > "$test_directory/output"
	grep 'GNU sed' "$test_directory/output"

	# Now with fakechroot.
	run_without_store GUIX_EXECUTION_ENGINE="fakechroot" \
	"$test_directory/Bin/sed" --version > "$test_directory/output"
	grep 'GNU sed' "$test_directory/output"
	unset GUIX_EXECUTION_ENGINE

	chmod -Rf +w "$test_directory"; rm -rf "$test_directory"/*

	if unshare -r true
	then
	    # Check whether the store contains everything it should.  Check
	    # once when erasing $STORE_PARENT ("/gnu") and once when erasing
	    # $NIX_STORE_DIR ("/gnu/store").
	    tarball="`guix pack -RR -S /bin=bin bash-minimal`"
	    (cd "$test_directory"; tar xf "$tarball")

	    STORE_PARENT="`dirname $NIX_STORE_DIR`"
	    export STORE_PARENT

	    for engine in userns proot fakechroot
	    do
		for i in $(guix gc -R $(guix build bash-minimal | grep -v -e '-doc$'))
		do
		    unshare -mrf sh -c "mount -t tmpfs none \"$NIX_STORE_DIR\"; GUIX_EXECUTION_ENGINE=$engine $test_directory/bin/sh -c 'echo $NIX_STORE_DIR/*'" | grep $(basename $i)
		    unshare -mrf sh -c "mount -t tmpfs none \"$STORE_PARENT\";  GUIX_EXECUTION_ENGINE=$engine $test_directory/bin/sh -c 'echo $NIX_STORE_DIR/*'" | grep $(basename $i)
		done
	    done

	    chmod -Rf +w "$test_directory"; rm -rf "$test_directory"/*
	fi
	;;
    *)
	echo "skipping PRoot and Fakechroot tests" >&2
	;;
esac

if unshare -r true
then
    # Check what happens if the wrapped binary forks and leaves child
    # processes behind, like a daemon.  The root file system should remain
    # available to those child processes.  See <https://bugs.gnu.org/44261>.
    cat > "$test_directory/manifest.scm" <<EOF
(use-modules (guix))

(define daemon
  (program-file "daemon"
                #~(begin
                    (use-modules (ice-9 match)
                                 (ice-9 ftw))

                    (call-with-output-file "parent-store"
                      (lambda (port)
                        (write (scandir (ungexp (%store-prefix)))
                               port)))

                    (match (primitive-fork)
                      (0 (sigaction SIGHUP (const #t))
                         (call-with-output-file "pid"
                           (lambda (port)
                             (display (getpid) port)))
                         (pause)
                         (call-with-output-file "child-store"
                           (lambda (port)
                             (write (scandir (ungexp (%store-prefix)))
                                    port))))
                      (_ #t)))))

(define package
  (computed-file "package"
                 #~(let ((out (ungexp output)))
                     (mkdir out)
                     (mkdir (string-append out "/bin"))
                     (symlink (ungexp daemon)
                              (string-append out "/bin/daemon")))))

(manifest (list (manifest-entry
                  (name "daemon")
                  (version "0")
                  (item package))))
EOF

    tarball="$(guix pack -S /bin=bin -R -m "$test_directory/manifest.scm")"
    (cd "$test_directory"; tar xf "$tarball")

    # Run '/bin/daemon', which forks, then wait for the child, send it SIGHUP
    # so that it dumps its view of the store, and make sure the child and
    # parent both see the same store contents.
    chmod +w "$test_directory"
    (cd "$test_directory"; run_without_store ./bin/daemon)
    wait_for_file "$test_directory/pid"
    kill -HUP $(cat "$test_directory/pid")
    wait_for_file "$test_directory/child-store"
    diff -u "$test_directory/parent-store" "$test_directory/child-store"

    chmod -Rf +w "$test_directory"; rm -rf "$test_directory"/*
fi

# Ensure '-R' works with outputs other than "out".
tarball="`guix pack -R -S /share=share groff:doc`"
(cd "$test_directory"; tar xf "$tarball")
test -d "$test_directory/share/doc/groff/html"
chmod -Rf +w "$test_directory"; rm -rf "$test_directory"/*

# Ensure '-R' applies to propagated inputs.  Failing to do that, it would fail
# with a profile collision error in this case because 'python-scipy'
# propagates 'python-numpy'.  See <https://bugs.gnu.org/42510>.
guix pack -RR python-numpy python-scipy --no-grafts -n

# Check that packages that mix executable and support files (e.g. git) in the
# "binary" directories still work after wrapped.
cat >"$test_directory/manifest.scm" <<'EOF'
(use-modules (guix) (guix profiles) (guix search-paths)
             (gnu packages bootstrap))
(manifest
 (list (manifest-entry
        (name "test") (version "0")
        (item (file-union "test"
                          `(("bin/hello"
                             ,(program-file
                               "hello"
                               #~(begin
                                   (add-to-load-path (getenv "HELLO_EXEC_PATH"))
                                   (display (load-from-path "msg"))(newline))
                               #:guile %bootstrap-guile))
                            ("libexec/hello/msg"
                             ,(plain-file "msg" "42")))))
        (search-paths
         (list (search-path-specification
                (variable "HELLO_EXEC_PATH")
                (files '("libexec/hello"))
                (separator #f)))))))
EOF
tarball="`guix pack -RR -S /opt= -m $test_directory/manifest.scm`"
(cd "$test_directory"; tar xvf "$tarball")
chmod +w "$test_directory"
( export GUIX_PROFILE=$test_directory/opt
  . $GUIX_PROFILE/etc/profile
  run_without_store "$test_directory/opt/bin/hello" > "$test_directory/output" )
cat "$test_directory/output"
test "`cat $test_directory/output`" = "42"
(string-append (string-join '#$(log-rotation-files rotation) ",") " {" #$(string-join (log-rotation-options rotation) "\n " 'prefix) (if post (string-append "\n postrotate\n " post "\n endscript\n") "") "\n}\n"))) (define (log-rotations->/etc-entries rotations) "Return the list of /etc entries for ROTATIONS, a list of <log-rotation>." (define (frequency-file frequency rotations) (computed-file (string-append "rottlog." (symbol->string frequency)) #~(call-with-output-file #$output (lambda (port) (for-each (lambda (str) (display str port)) (list #$@(map log-rotation->config rotations))))))) (let* ((frequencies (delete-duplicates (map log-rotation-frequency rotations))) (table (fold (lambda (rotation table) (vhash-consq (log-rotation-frequency rotation) rotation table)) vlist-null rotations))) (map (lambda (frequency) `(,(symbol->string frequency) ,(frequency-file frequency (vhash-foldq* cons '() frequency table)))) frequencies))) (define (default-jobs rottlog) (list #~(job '(next-hour '(0)) ;midnight #$(file-append rottlog "/sbin/rottlog")) #~(job '(next-hour '(12)) ;noon #$(file-append rottlog "/sbin/rottlog")))) (define-record-type* <rottlog-configuration> rottlog-configuration make-rottlog-configuration rottlog-configuration? (rottlog rottlog-configuration-rottlog ;file-like (default rottlog)) (rc-file rottlog-configuration-rc-file ;file-like (default (file-append rottlog "/etc/rc"))) (rotations rottlog-configuration-rotations ;list of <log-rotation> (default %default-rotations)) (jobs rottlog-configuration-jobs ;list of <mcron-job> (default #f))) (define (rottlog-etc config) `(("rottlog" ,(file-union "rottlog" (cons `("rc" ,(rottlog-configuration-rc-file config)) (log-rotations->/etc-entries (rottlog-configuration-rotations config))))))) (define (rottlog-jobs-or-default config) (or (rottlog-configuration-jobs config) (default-jobs (rottlog-configuration-rottlog config)))) ;; TODO: Deprecated; remove sometime after 2025-06-15. (define-deprecated rottlog-service-type log-rotation-service-type (service-type (name 'rottlog) (description "Periodically rotate log files using GNU@tie{}Rottlog and GNU@tie{}mcron. Old log files are removed or compressed according to the configuration. This service is deprecated and slated for removal after 2025-06-15.") (extensions (list (service-extension etc-service-type rottlog-etc) (service-extension mcron-service-type rottlog-jobs-or-default) ;; Add Rottlog to the global profile so users can access ;; the documentation. (service-extension profile-service-type (compose list rottlog-configuration-rottlog)))) (compose concatenate) (extend (lambda (config rotations) (rottlog-configuration (inherit config) (rotations (append (rottlog-configuration-rotations config) rotations))))) (default-value (rottlog-configuration)))) ;;; ;;; Build log removal. ;;; (define-record-type* <log-cleanup-configuration> log-cleanup-configuration make-log-cleanup-configuration log-cleanup-configuration? (directory log-cleanup-configuration-directory) ;string (expiry log-cleanup-configuration-expiry ;integer (seconds) (default (* 6 30 24 3600))) (schedule log-cleanup-configuration-schedule ;string or gexp (default "30 12 01,08,15,22 * *"))) (define (log-cleanup-program directory expiry) (program-file "delete-old-logs" (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (let* ((now (car (gettimeofday))) (logs (find-files #$directory (lambda (file stat) (> (- now (stat:mtime stat)) #$expiry))))) (format #t "deleting ~a log files from '~a'...~%" (length logs) #$directory) (for-each delete-file logs)))))) (define (log-cleanup-shepherd-services configuration) (match-record configuration <log-cleanup-configuration> (directory expiry schedule) (let ((program (log-cleanup-program directory expiry))) (list (shepherd-service (provision '(log-cleanup)) (requirement '(user-processes)) (modules '((shepherd service timer))) (start #~(make-timer-constructor #$(if (string? schedule) #~(cron-string->calendar-event #$schedule) schedule) (command '(#$program)))) (stop #~(make-timer-destructor)) (actions (list (shepherd-action (name 'trigger) (documentation "Trigger log cleanup.") (procedure #~trigger-timer))))))))) (define log-cleanup-service-type (service-type (name 'log-cleanup) (extensions (list (service-extension shepherd-root-service-type log-cleanup-shepherd-services))) (description "Periodically delete old log files."))) ;;; ;;; File databases. ;;; (define %default-file-database-update-schedule ;; Default mcron schedule for the periodic 'updatedb' job: once every ;; Sunday. "10 23 * * 0") (define %default-file-database-excluded-directories ;; Regexps of directories excluded from the 'locate' database. (list (%store-prefix) "/tmp" "/var/tmp" "/var/cache" ".*/\\.cache" "/run/udev")) (define (string-or-gexp? obj) (or (string? obj) (gexp? obj))) (define string-list? (match-lambda (((? string?) ...) #t) (_ #f))) (define-configuration/no-serialization file-database-configuration (package (file-like (let-system (system target) ;; Unless we're cross-compiling, avoid pulling a second copy ;; of findutils. (if target findutils (canonical-package findutils)))) "The GNU@tie{}Findutils package from which the @command{updatedb} command is taken.") (schedule (string-or-gexp %default-file-database-update-schedule) "String or G-exp denoting an mcron schedule for the periodic @command{updatedb} job (@pxref{Guile Syntax,,, mcron, GNU@tie{}mcron}).") (excluded-directories (string-list %default-file-database-excluded-directories) "List of regular expressions of directories to ignore when building the file database. By default, this includes @file{/tmp} and @file{/gnu/store}; the latter should instead be indexed by @command{guix locate} (@pxref{Invoking guix locate}). This list is passed to the @option{--prunepaths} option of @command{updatedb} (@pxref{Invoking updatedb,,, find, GNU@tie{}Findutils}).")) (define (file-database-mcron-jobs configuration) (match-record configuration <file-database-configuration> (package schedule excluded-directories) (let ((updatedb (program-file "updatedb" #~(begin ;; 'updatedb' is a shell script that expects various ;; commands in $PATH. (setenv "PATH" (string-append #$package "/bin:" #$(canonical-package coreutils) "/bin:" #$(canonical-package sed) "/bin")) (execl #$(file-append package "/bin/updatedb") "updatedb" #$(string-append "--prunepaths=" (string-join excluded-directories))))))) (list #~(job #$schedule #$updatedb))))) (define file-database-service-type (service-type (name 'file-database) (extensions (list (service-extension mcron-service-type file-database-mcron-jobs))) (description "Periodically update the file database used by the @command{locate} command, which lets you search for files by name. The database is created by running the @command{updatedb} command.") (default-value (file-database-configuration)))) (define %default-package-database-update-schedule ;; Default mcron schedule for the periodic 'guix locate --update' job: once ;; every Monday. "10 23 * * 1") (define-configuration/no-serialization package-database-configuration (package (file-like guix) "The Guix package to use.") (schedule (string-or-gexp %default-package-database-update-schedule) "String or G-exp denoting an mcron schedule for the periodic @command{guix locate --update} job (@pxref{Guile Syntax,,, mcron, GNU@tie{}mcron}).") (method (symbol 'store) "Indexing method for @command{guix locate}. The default value, @code{'store}, yields a more complete database but is relatively expensive in terms of CPU and input/output.") (channels (gexp #~%default-channels) "G-exp denoting the channels to use when updating the database (@pxref{Channels}).")) (define (package-database-mcron-jobs configuration) (match-record configuration <package-database-configuration> (package schedule method channels) (let ((channels (scheme-file "channels.scm" channels))) (list #~(job #$schedule ;; XXX: The whole thing's running as "root" just because it ;; needs write access to /var/cache/guix/locate. (string-append #$(file-append package "/bin/guix") " time-machine -C " #$channels " -- locate --update --method=" #$(symbol->string method))))))) (define package-database-service-type (service-type (name 'package-database) (extensions (list (service-extension mcron-service-type package-database-mcron-jobs))) (description "Periodically update the package database used by the @code{guix locate} command, which lets you search for packages that provide a given file.") (default-value (package-database-configuration)))) ;;; ;;; Unattended upgrade. ;;; (define-record-type* <unattended-upgrade-configuration> unattended-upgrade-configuration make-unattended-upgrade-configuration unattended-upgrade-configuration? (operating-system-file unattended-upgrade-operating-system-file (default "/run/current-system/configuration.scm")) (operating-system-expression unattended-upgrade-operating-system-expression (default #f)) (schedule unattended-upgrade-configuration-schedule (default "30 01 * * 0")) (channels unattended-upgrade-configuration-channels (default #~%default-channels)) (reboot? unattended-upgrade-configuration-reboot? (default #f)) (services-to-restart unattended-upgrade-configuration-services-to-restart (default '(unattended-upgrade))) (system-expiration unattended-upgrade-system-expiration (default (* 3 30 24 3600))) (maximum-duration unattended-upgrade-maximum-duration (default 3600)) (log-file unattended-upgrade-configuration-log-file (default %unattended-upgrade-log-file))) (define %unattended-upgrade-log-file "/var/log/unattended-upgrade.log") (define (unattended-upgrade-shepherd-services config) (define channels (scheme-file "channels.scm" (unattended-upgrade-configuration-channels config))) (define log (unattended-upgrade-configuration-log-file config)) (define schedule (unattended-upgrade-configuration-schedule config)) (define services (unattended-upgrade-configuration-services-to-restart config)) (define reboot? (unattended-upgrade-configuration-reboot? config)) (define expiration (unattended-upgrade-system-expiration config)) (define config-file (unattended-upgrade-operating-system-file config)) (define expression (unattended-upgrade-operating-system-expression config)) (define arguments (if expression #~(list "-e" (object->string '#$expression)) #~(list #$config-file))) (define code (with-imported-modules (source-module-closure '((guix build utils) (gnu services herd))) #~(begin (use-modules (guix build utils) (gnu services herd) (srfi srfi-34)) (setvbuf (current-output-port) 'line) (setvbuf (current-error-port) 'line) ;; 'guix time-machine' needs X.509 certificates to authenticate the ;; Git host. (setenv "SSL_CERT_DIR" #$(file-append nss-certs "/etc/ssl/certs")) (format #t "starting upgrade...~%") (guard (c ((invoke-error? c) (report-invoke-error c))) (apply invoke #$(file-append guix "/bin/guix") "time-machine" "-C" #$channels "--" "system" "reconfigure" #$arguments) ;; 'guix system delete-generations' fails when there's no ;; matching generation. Thus, catch 'invoke-error?'. (guard (c ((invoke-error? c) (report-invoke-error c))) (invoke #$(file-append guix "/bin/guix") "system" "delete-generations" #$(string-append (number->string expiration) "s"))) (unless #$reboot? ;; Rebooting effectively restarts services anyway and execution ;; would be halted here if mcron is restarted. (format #t "restarting services...~%") (for-each restart-service '#$services)) ;; XXX: If this service has been restarted, this is not reached. (format #t "upgrade complete~%") ;; Stopping the root shepherd service triggers a reboot. (when #$reboot? (format #t "rebooting system~%") (force-output) ;ensure the entire log is written. (stop-service 'root)))))) (define upgrade (program-file "unattended-upgrade" code)) (list (shepherd-service (provision '(unattended-upgrade)) (requirement '(user-processes networking)) (modules '((shepherd service timer))) (start #~(make-timer-constructor #$(if (string? schedule) #~(cron-string->calendar-event #$schedule) schedule) (command '(#$upgrade)) #:log-file #$log ;; Make sure the upgrade doesn't take too long. #:max-duration #$(unattended-upgrade-maximum-duration config) ;; Wait for the previous attempt to terminate before trying ;; again. #:wait-for-termination? #t)) (stop #~(make-timer-destructor)) (actions (list (shepherd-action (name 'trigger) (documentation "Trigger unattended system upgrade.") (procedure #~trigger-timer))))))) (define unattended-upgrade-service-type (service-type (name 'unattended-upgrade) (extensions (list (service-extension shepherd-root-service-type unattended-upgrade-shepherd-services))) (description "Periodically upgrade the system from the current configuration.") (default-value (unattended-upgrade-configuration)))) ;;; ;;; Resize file system. ;;; (define-record-type* <resize-file-system-configuration> resize-file-system-configuration make-resize-file-system-configuration resize-file-system-configuration? (file-system resize-file-system-file-system) (cloud-utils resize-file-system-cloud-utils (default cloud-utils)) (e2fsprogs resize-file-system-e2fsprogs (default e2fsprogs)) (btrfs-progs resize-file-system-btrfs-progs (default btrfs-progs)) (bcachefs-tools resize-file-system-bcachefs-tools (default bcachefs-tools))) (define (resize-file-system-shepherd-service config) "Returns a <shepherd-service> for resize-file-system-service for CONFIG." (match-record config <resize-file-system-configuration> (file-system cloud-utils e2fsprogs btrfs-progs bcachefs-tools) (let ((fs-spec (file-system->spec file-system))) (shepherd-service (documentation "Resize a file system. Intended for Guix Systems that are booted from a system image flashed onto a larger medium.") ;; XXX: This could be extended with file-system info. (provision '(resize-file-system)) (requirement '(user-processes)) (one-shot? #t) (respawn? #f) (modules '((guix build utils) (gnu build file-systems) (gnu system file-systems) (ice-9 control) (ice-9 match) (ice-9 ftw) (ice-9 rdelim) (srfi srfi-34))) (start (with-imported-modules (source-module-closure '((guix build utils) (gnu build file-systems) (gnu system file-systems))) #~(lambda _ (use-modules (guix build utils) (gnu build file-systems) (gnu system file-systems) (ice-9 control) (ice-9 match) (ice-9 ftw) (ice-9 rdelim) (srfi srfi-34)) (define file-system (spec->file-system '#$fs-spec)) ;; Shepherd recommends the start constructor takes <1 ;; minute, canonicalize-device-spec will hang for up to ;; max-trials seconds (20 seconds) if an invalid device is ;; connected. Revisit this if max-trials increases. (define device (canonicalize-device-spec (file-system-device file-system))) (define grow-partition-command (let* ((sysfs-device (string-append "/sys/class/block/" (basename device))) (partition-number (with-input-from-file (string-append sysfs-device "/partition") read-line)) (parent (string-append "/dev/" (basename (dirname (readlink sysfs-device)))))) (list #$(file-append cloud-utils "/bin/growpart") parent partition-number))) (define grow-filesystem-command (match (file-system-type file-system) ((or "ext2" "ext3" "ext4") (list #$(file-append e2fsprogs "/sbin/resize2fs") device)) ("btrfs" (list #$(file-append btrfs-progs "/bin/btrfs") "filesystem" "resize" device)) ("bcachefs" (list #$(file-append bcachefs-tools "/sbin/bcachefs") "device" "resize" device)) (e (error "Unsupported filesystem type" e)))) (let/ec return (guard (c ((and (invoke-error? c) ;; growpart NOCHANGE exits with 1. It is ;; unlikely the partition was resized ;; while the file system was not. Just ;; exit. (equal? (invoke-error-exit-status c) 1)) (format (current-error-port) "The device ~a is already resized.~%" device) ;; Must return something or Shepherd considers ;; the service perpetually starting. (return 0))) (apply invoke grow-partition-command)) (apply invoke grow-filesystem-command))))))))) (define resize-file-system-service-type (service-type (name 'resize-file-system) (description "Resize a partition and the underlying file system during boot.") (extensions (list (service-extension shepherd-root-service-type (compose list resize-file-system-shepherd-service)))))) ;;; admin.scm ends here