1875 lines
76 KiB
Scheme
1875 lines
76 KiB
Scheme
;;; GNU Guix --- Functional package management for GNU
|
||
;;; Copyright © 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
|
||
;;; Copyright © 2015, 2016 Alex Kost <alezost@gmail.com>
|
||
;;; Copyright © 2015, 2016 Mark H Weaver <mhw@netris.org>
|
||
;;; Copyright © 2015 Sou Bunnbu <iyzsong@gmail.com>
|
||
;;; Copyright © 2016, 2017 Leo Famulari <leo@famulari.name>
|
||
;;; Copyright © 2016 David Craven <david@craven.ch>
|
||
;;; Copyright © 2016 Ricardo Wurmus <rekado@elephly.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/>.
|
||
|
||
(define-module (gnu services base)
|
||
#:use-module (guix store)
|
||
#:use-module (gnu services)
|
||
#:use-module (gnu services shepherd)
|
||
#:use-module (gnu services networking)
|
||
#:use-module (gnu system pam)
|
||
#:use-module (gnu system shadow) ; 'user-account', etc.
|
||
#:use-module (gnu system file-systems) ; 'file-system', etc.
|
||
#:use-module (gnu system mapped-devices)
|
||
#:use-module ((gnu system linux-initrd)
|
||
#:select (file-system-packages))
|
||
#:use-module (gnu packages admin)
|
||
#:use-module ((gnu packages linux)
|
||
#:select (alsa-utils crda eudev e2fsprogs fuse gpm kbd lvm2 rng-tools))
|
||
#:use-module ((gnu packages base)
|
||
#:select (canonical-package glibc))
|
||
#:use-module (gnu packages bash)
|
||
#:use-module (gnu packages package-management)
|
||
#:use-module (gnu packages linux)
|
||
#:use-module (gnu packages lsof)
|
||
#:use-module (gnu packages terminals)
|
||
#:use-module ((gnu build file-systems)
|
||
#:select (mount-flags->bit-mask))
|
||
#:use-module (guix gexp)
|
||
#:use-module (guix records)
|
||
#:use-module (srfi srfi-1)
|
||
#:use-module (srfi srfi-26)
|
||
#:use-module (ice-9 match)
|
||
#:use-module (ice-9 format)
|
||
#:export (fstab-service-type
|
||
root-file-system-service
|
||
file-system-service-type
|
||
user-unmount-service
|
||
swap-service
|
||
user-processes-service
|
||
session-environment-service
|
||
session-environment-service-type
|
||
host-name-service
|
||
console-keymap-service
|
||
%default-console-font
|
||
console-font-service-type
|
||
console-font-service
|
||
|
||
udev-configuration
|
||
udev-configuration?
|
||
udev-configuration-rules
|
||
udev-service-type
|
||
udev-service
|
||
udev-rule
|
||
|
||
login-configuration
|
||
login-configuration?
|
||
login-service-type
|
||
login-service
|
||
|
||
agetty-configuration
|
||
agetty-configuration?
|
||
agetty-service
|
||
agetty-service-type
|
||
|
||
mingetty-configuration
|
||
mingetty-configuration?
|
||
mingetty-service
|
||
mingetty-service-type
|
||
|
||
%nscd-default-caches
|
||
%nscd-default-configuration
|
||
|
||
nscd-configuration
|
||
nscd-configuration?
|
||
|
||
nscd-cache
|
||
nscd-cache?
|
||
|
||
nscd-service-type
|
||
nscd-service
|
||
|
||
syslog-configuration
|
||
syslog-configuration?
|
||
syslog-service
|
||
syslog-service-type
|
||
%default-syslog.conf
|
||
|
||
%default-authorized-guix-keys
|
||
guix-configuration
|
||
guix-configuration?
|
||
|
||
guix-configuration-guix
|
||
guix-configuration-build-group
|
||
guix-configuration-build-accounts
|
||
guix-configuration-authorize-key?
|
||
guix-configuration-authorized-keys
|
||
guix-configuration-use-substitutes?
|
||
guix-configuration-substitute-urls
|
||
guix-configuration-extra-options
|
||
guix-configuration-log-file
|
||
guix-configuration-lsof
|
||
|
||
guix-service
|
||
guix-service-type
|
||
guix-publish-configuration
|
||
guix-publish-configuration?
|
||
guix-publish-configuration-guix
|
||
guix-publish-configuration-port
|
||
guix-publish-configuration-host
|
||
guix-publish-configuration-compression-level
|
||
guix-publish-configuration-nar-path
|
||
guix-publish-configuration-cache
|
||
guix-publish-configuration-ttl
|
||
guix-publish-service
|
||
guix-publish-service-type
|
||
|
||
gpm-configuration
|
||
gpm-configuration?
|
||
gpm-service-type
|
||
gpm-service
|
||
|
||
urandom-seed-service-type
|
||
urandom-seed-service
|
||
|
||
rngd-configuration
|
||
rngd-configuration?
|
||
rngd-service-type
|
||
rngd-service
|
||
|
||
kmscon-configuration
|
||
kmscon-configuration?
|
||
kmscon-service-type
|
||
|
||
pam-limits-service-type
|
||
pam-limits-service
|
||
|
||
%base-services))
|
||
|
||
;;; Commentary:
|
||
;;;
|
||
;;; Base system services---i.e., services that 99% of the users will want to
|
||
;;; use.
|
||
;;;
|
||
;;; Code:
|
||
|
||
|
||
;;;
|
||
;;; File systems.
|
||
;;;
|
||
|
||
(define (file-system->fstab-entry file-system)
|
||
"Return a @file{/etc/fstab} entry for @var{file-system}."
|
||
(string-append (case (file-system-title file-system)
|
||
((label)
|
||
(string-append "LABEL=" (file-system-device file-system)))
|
||
((uuid)
|
||
(string-append
|
||
"UUID="
|
||
(uuid->string (file-system-device file-system))))
|
||
(else
|
||
(file-system-device file-system)))
|
||
"\t"
|
||
(file-system-mount-point file-system) "\t"
|
||
(file-system-type file-system) "\t"
|
||
(or (file-system-options file-system) "defaults") "\t"
|
||
|
||
;; XXX: Omit the 'fs_freq' and 'fs_passno' fields because we
|
||
;; don't have anything sensible to put in there.
|
||
))
|
||
|
||
(define (file-systems->fstab file-systems)
|
||
"Return a @file{/etc} entry for an @file{fstab} describing
|
||
@var{file-systems}."
|
||
`(("fstab" ,(plain-file "fstab"
|
||
(string-append
|
||
"\
|
||
# This file was generated from your GuixSD configuration. Any changes
|
||
# will be lost upon reboot or reconfiguration.\n\n"
|
||
(string-join (map file-system->fstab-entry
|
||
file-systems)
|
||
"\n")
|
||
"\n")))))
|
||
|
||
(define fstab-service-type
|
||
;; The /etc/fstab service.
|
||
(service-type (name 'fstab)
|
||
(extensions
|
||
(list (service-extension etc-service-type
|
||
file-systems->fstab)))
|
||
(compose concatenate)
|
||
(extend append)))
|
||
|
||
(define %root-file-system-shepherd-service
|
||
(shepherd-service
|
||
(documentation "Take care of the root file system.")
|
||
(provision '(root-file-system))
|
||
(start #~(const #t))
|
||
(stop #~(lambda _
|
||
;; Return #f if successfully stopped.
|
||
(sync)
|
||
|
||
(call-with-blocked-asyncs
|
||
(lambda ()
|
||
(let ((null (%make-void-port "w")))
|
||
;; Close 'shepherd.log'.
|
||
(display "closing log\n")
|
||
((@ (shepherd comm) stop-logging))
|
||
|
||
;; Redirect the default output ports..
|
||
(set-current-output-port null)
|
||
(set-current-error-port null)
|
||
|
||
;; Close /dev/console.
|
||
(for-each close-fdes '(0 1 2))
|
||
|
||
;; At this point, there are no open files left, so the
|
||
;; root file system can be re-mounted read-only.
|
||
(mount #f "/" #f
|
||
(logior MS_REMOUNT MS_RDONLY)
|
||
#:update-mtab? #f)
|
||
|
||
#f)))))
|
||
(respawn? #f)))
|
||
|
||
(define root-file-system-service-type
|
||
(shepherd-service-type 'root-file-system
|
||
(const %root-file-system-shepherd-service)))
|
||
|
||
(define (root-file-system-service)
|
||
"Return a service whose sole purpose is to re-mount read-only the root file
|
||
system upon shutdown (aka. cleanly \"umounting\" root.)
|
||
|
||
This service must be the root of the service dependency graph so that its
|
||
'stop' action is invoked when shepherd is the only process left."
|
||
(service root-file-system-service-type #f))
|
||
|
||
(define (file-system->shepherd-service-name file-system)
|
||
"Return the symbol that denotes the service mounting and unmounting
|
||
FILE-SYSTEM."
|
||
(symbol-append 'file-system-
|
||
(string->symbol (file-system-mount-point file-system))))
|
||
|
||
(define (mapped-device->shepherd-service-name md)
|
||
"Return the symbol that denotes the shepherd service of MD, a <mapped-device>."
|
||
(symbol-append 'device-mapping-
|
||
(string->symbol (mapped-device-target md))))
|
||
|
||
(define dependency->shepherd-service-name
|
||
(match-lambda
|
||
((? mapped-device? md)
|
||
(mapped-device->shepherd-service-name md))
|
||
((? file-system? fs)
|
||
(file-system->shepherd-service-name fs))))
|
||
|
||
(define (file-system-shepherd-service file-system)
|
||
"Return the shepherd service for @var{file-system}, or @code{#f} if
|
||
@var{file-system} is not auto-mounted upon boot."
|
||
(let ((target (file-system-mount-point file-system))
|
||
(device (file-system-device file-system))
|
||
(type (file-system-type file-system))
|
||
(title (file-system-title file-system))
|
||
(flags (file-system-flags file-system))
|
||
(options (file-system-options file-system))
|
||
(check? (file-system-check? file-system))
|
||
(create? (file-system-create-mount-point? file-system))
|
||
(dependencies (file-system-dependencies file-system))
|
||
(packages (file-system-packages (list file-system))))
|
||
(and (file-system-mount? file-system)
|
||
(with-imported-modules '((gnu build file-systems)
|
||
(guix build bournish))
|
||
(shepherd-service
|
||
(provision (list (file-system->shepherd-service-name file-system)))
|
||
(requirement `(root-file-system
|
||
,@(map dependency->shepherd-service-name dependencies)))
|
||
(documentation "Check, mount, and unmount the given file system.")
|
||
(start #~(lambda args
|
||
#$(if create?
|
||
#~(mkdir-p #$target)
|
||
#t)
|
||
|
||
(let (($PATH (getenv "PATH")))
|
||
;; Make sure fsck.ext2 & co. can be found.
|
||
(dynamic-wind
|
||
(lambda ()
|
||
;; Don’t display the PATH settings.
|
||
(with-output-to-port (%make-void-port "w")
|
||
(lambda ()
|
||
(set-path-environment-variable "PATH"
|
||
'("bin" "sbin")
|
||
'#$packages))))
|
||
(lambda ()
|
||
(mount-file-system
|
||
`(#$device #$title #$target #$type #$flags
|
||
#$options #$check?)
|
||
#:root "/"))
|
||
(lambda ()
|
||
(setenv "PATH" $PATH)))
|
||
#t)))
|
||
(stop #~(lambda args
|
||
;; Normally there are no processes left at this point, so
|
||
;; TARGET can be safely unmounted.
|
||
|
||
;; Make sure PID 1 doesn't keep TARGET busy.
|
||
(chdir "/")
|
||
|
||
(umount #$target)
|
||
#f))
|
||
|
||
;; We need an additional module.
|
||
(modules `(((gnu build file-systems)
|
||
#:select (mount-file-system))
|
||
,@%default-modules)))))))
|
||
|
||
(define (file-system-shepherd-services file-systems)
|
||
"Return the list of Shepherd services for FILE-SYSTEMS."
|
||
(let* ((file-systems (filter file-system-mount? file-systems)))
|
||
(define sink
|
||
(shepherd-service
|
||
(provision '(file-systems))
|
||
(requirement (cons* 'root-file-system 'user-file-systems
|
||
(map file-system->shepherd-service-name
|
||
file-systems)))
|
||
(documentation "Target for all the initially-mounted file systems")
|
||
(start #~(const #t))
|
||
(stop #~(const #f))))
|
||
|
||
(cons sink (map file-system-shepherd-service file-systems))))
|
||
|
||
(define file-system-service-type
|
||
(service-type (name 'file-systems)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type
|
||
file-system-shepherd-services)
|
||
(service-extension fstab-service-type
|
||
identity)))
|
||
(compose concatenate)
|
||
(extend append)))
|
||
|
||
(define user-unmount-service-type
|
||
(shepherd-service-type
|
||
'user-file-systems
|
||
(lambda (known-mount-points)
|
||
(shepherd-service
|
||
(documentation "Unmount manually-mounted file systems.")
|
||
(provision '(user-file-systems))
|
||
(start #~(const #t))
|
||
(stop #~(lambda args
|
||
(define (known? mount-point)
|
||
(member mount-point
|
||
(cons* "/proc" "/sys" '#$known-mount-points)))
|
||
|
||
;; Make sure we don't keep the user's mount points busy.
|
||
(chdir "/")
|
||
|
||
(for-each (lambda (mount-point)
|
||
(format #t "unmounting '~a'...~%" mount-point)
|
||
(catch 'system-error
|
||
(lambda ()
|
||
(umount mount-point))
|
||
(lambda args
|
||
(let ((errno (system-error-errno args)))
|
||
(format #t "failed to unmount '~a': ~a~%"
|
||
mount-point (strerror errno))))))
|
||
(filter (negate known?) (mount-points)))
|
||
#f))))))
|
||
|
||
(define (user-unmount-service known-mount-points)
|
||
"Return a service whose sole purpose is to unmount file systems not listed
|
||
in KNOWN-MOUNT-POINTS when it is stopped."
|
||
(service user-unmount-service-type known-mount-points))
|
||
|
||
(define %do-not-kill-file
|
||
;; Name of the file listing PIDs of processes that must survive when halting
|
||
;; the system. Typical example is user-space file systems.
|
||
"/etc/shepherd/do-not-kill")
|
||
|
||
(define user-processes-service-type
|
||
(shepherd-service-type
|
||
'user-processes
|
||
(lambda (grace-delay)
|
||
(shepherd-service
|
||
(documentation "When stopped, terminate all user processes.")
|
||
(provision '(user-processes))
|
||
(requirement '(file-systems))
|
||
(start #~(const #t))
|
||
(stop #~(lambda _
|
||
(define (kill-except omit signal)
|
||
;; Kill all the processes with SIGNAL except those listed
|
||
;; in OMIT and the current process.
|
||
(let ((omit (cons (getpid) omit)))
|
||
(for-each (lambda (pid)
|
||
(unless (memv pid omit)
|
||
(false-if-exception
|
||
(kill pid signal))))
|
||
(processes))))
|
||
|
||
(define omitted-pids
|
||
;; List of PIDs that must not be killed.
|
||
(if (file-exists? #$%do-not-kill-file)
|
||
(map string->number
|
||
(call-with-input-file #$%do-not-kill-file
|
||
(compose string-tokenize
|
||
(@ (ice-9 rdelim) read-string))))
|
||
'()))
|
||
|
||
(define (now)
|
||
(car (gettimeofday)))
|
||
|
||
(define (sleep* n)
|
||
;; Really sleep N seconds.
|
||
;; Work around <http://bugs.gnu.org/19581>.
|
||
(define start (now))
|
||
(let loop ((elapsed 0))
|
||
(when (> n elapsed)
|
||
(sleep (- n elapsed))
|
||
(loop (- (now) start)))))
|
||
|
||
(define lset= (@ (srfi srfi-1) lset=))
|
||
|
||
(display "sending all processes the TERM signal\n")
|
||
|
||
(if (null? omitted-pids)
|
||
(begin
|
||
;; Easy: terminate all of them.
|
||
(kill -1 SIGTERM)
|
||
(sleep* #$grace-delay)
|
||
(kill -1 SIGKILL))
|
||
(begin
|
||
;; Kill them all except OMITTED-PIDS. XXX: We would
|
||
;; like to (kill -1 SIGSTOP) to get a fixed list of
|
||
;; processes, like 'killall5' does, but that seems
|
||
;; unreliable.
|
||
(kill-except omitted-pids SIGTERM)
|
||
(sleep* #$grace-delay)
|
||
(kill-except omitted-pids SIGKILL)
|
||
(delete-file #$%do-not-kill-file)))
|
||
|
||
(let wait ()
|
||
(let ((pids (processes)))
|
||
(unless (lset= = pids (cons 1 omitted-pids))
|
||
(format #t "waiting for process termination\
|
||
(processes left: ~s)~%"
|
||
pids)
|
||
(sleep* 2)
|
||
(wait))))
|
||
|
||
(display "all processes have been terminated\n")
|
||
#f))
|
||
(respawn? #f)))))
|
||
|
||
(define* (user-processes-service #:key (grace-delay 4))
|
||
"Return the service that is responsible for terminating all the processes so
|
||
that the root file system can be re-mounted read-only, just before
|
||
rebooting/halting. Processes still running GRACE-DELAY seconds after SIGTERM
|
||
has been sent are terminated with SIGKILL.
|
||
|
||
The returned service will depend on 'file-systems', meaning that it is
|
||
considered started after all the auto-mount file systems have been mounted.
|
||
|
||
All the services that spawn processes must depend on this one so that they are
|
||
stopped before 'kill' is called."
|
||
(service user-processes-service-type grace-delay))
|
||
|
||
|
||
;;;
|
||
;;; Preserve entropy to seed /dev/urandom on boot.
|
||
;;;
|
||
|
||
(define %random-seed-file
|
||
"/var/lib/random-seed")
|
||
|
||
(define (urandom-seed-shepherd-service _)
|
||
"Return a shepherd service for the /dev/urandom seed."
|
||
(list (shepherd-service
|
||
(documentation "Preserve entropy across reboots for /dev/urandom.")
|
||
(provision '(urandom-seed))
|
||
(requirement '(user-processes))
|
||
(start #~(lambda _
|
||
;; On boot, write random seed into /dev/urandom.
|
||
(when (file-exists? #$%random-seed-file)
|
||
(call-with-input-file #$%random-seed-file
|
||
(lambda (seed)
|
||
(call-with-output-file "/dev/urandom"
|
||
(lambda (urandom)
|
||
(dump-port seed urandom))))))
|
||
;; Immediately refresh the seed in case the system doesn't
|
||
;; shut down cleanly.
|
||
(call-with-input-file "/dev/urandom"
|
||
(lambda (urandom)
|
||
(let ((previous-umask (umask #o077))
|
||
(buf (make-bytevector 512)))
|
||
(mkdir-p (dirname #$%random-seed-file))
|
||
(get-bytevector-n! urandom buf 0 512)
|
||
(call-with-output-file #$%random-seed-file
|
||
(lambda (seed)
|
||
(put-bytevector seed buf)))
|
||
(umask previous-umask))))
|
||
#t))
|
||
(stop #~(lambda _
|
||
;; During shutdown, write from /dev/urandom into random seed.
|
||
(let ((buf (make-bytevector 512)))
|
||
(call-with-input-file "/dev/urandom"
|
||
(lambda (urandom)
|
||
(let ((previous-umask (umask #o077)))
|
||
(get-bytevector-n! urandom buf 0 512)
|
||
(mkdir-p (dirname #$%random-seed-file))
|
||
(call-with-output-file #$%random-seed-file
|
||
(lambda (seed)
|
||
(put-bytevector seed buf)))
|
||
(umask previous-umask))
|
||
#t)))))
|
||
(modules `((rnrs bytevectors)
|
||
(rnrs io ports)
|
||
,@%default-modules)))))
|
||
|
||
(define urandom-seed-service-type
|
||
(service-type (name 'urandom-seed)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type
|
||
urandom-seed-shepherd-service)))))
|
||
|
||
(define (urandom-seed-service)
|
||
(service urandom-seed-service-type #f))
|
||
|
||
|
||
;;;
|
||
;;; Add hardware random number generator to entropy pool.
|
||
;;;
|
||
|
||
(define-record-type* <rngd-configuration>
|
||
rngd-configuration make-rngd-configuration
|
||
rngd-configuration?
|
||
(rng-tools rngd-configuration-rng-tools) ;package
|
||
(device rngd-configuration-device)) ;string
|
||
|
||
(define rngd-service-type
|
||
(shepherd-service-type
|
||
'rngd
|
||
(lambda (config)
|
||
(define rng-tools (rngd-configuration-rng-tools config))
|
||
(define device (rngd-configuration-device config))
|
||
|
||
(define rngd-command
|
||
(list (file-append rng-tools "/sbin/rngd")
|
||
"-f" "-r" device))
|
||
|
||
(shepherd-service
|
||
(documentation "Add TRNG to entropy pool.")
|
||
(requirement '(udev))
|
||
(provision '(trng))
|
||
(start #~(make-forkexec-constructor #$@rngd-command))
|
||
(stop #~(make-kill-destructor))))))
|
||
|
||
(define* (rngd-service #:key
|
||
(rng-tools rng-tools)
|
||
(device "/dev/hwrng"))
|
||
"Return a service that runs the @command{rngd} program from @var{rng-tools}
|
||
to add @var{device} to the kernel's entropy pool. The service will fail if
|
||
@var{device} does not exist."
|
||
(service rngd-service-type
|
||
(rngd-configuration
|
||
(rng-tools rng-tools)
|
||
(device device))))
|
||
|
||
|
||
;;;
|
||
;;; System-wide environment variables.
|
||
;;;
|
||
|
||
(define (environment-variables->environment-file vars)
|
||
"Return a file for pam_env(8) that contains environment variables VARS."
|
||
(apply mixed-text-file "environment"
|
||
(append-map (match-lambda
|
||
((key . value)
|
||
(list key "=" value "\n")))
|
||
vars)))
|
||
|
||
(define session-environment-service-type
|
||
(service-type
|
||
(name 'session-environment)
|
||
(extensions
|
||
(list (service-extension
|
||
etc-service-type
|
||
(lambda (vars)
|
||
(list `("environment"
|
||
,(environment-variables->environment-file vars)))))))
|
||
(compose concatenate)
|
||
(extend append)))
|
||
|
||
(define (session-environment-service vars)
|
||
"Return a service that builds the @file{/etc/environment}, which can be read
|
||
by PAM-aware applications to set environment variables for sessions.
|
||
|
||
VARS should be an association list in which both the keys and the values are
|
||
strings or string-valued gexps."
|
||
(service session-environment-service-type vars))
|
||
|
||
|
||
;;;
|
||
;;; Console & co.
|
||
;;;
|
||
|
||
(define host-name-service-type
|
||
(shepherd-service-type
|
||
'host-name
|
||
(lambda (name)
|
||
(shepherd-service
|
||
(documentation "Initialize the machine's host name.")
|
||
(provision '(host-name))
|
||
(start #~(lambda _
|
||
(sethostname #$name)))
|
||
(respawn? #f)))))
|
||
|
||
(define (host-name-service name)
|
||
"Return a service that sets the host name to @var{name}."
|
||
(service host-name-service-type name))
|
||
|
||
(define (unicode-start tty)
|
||
"Return a gexp to start Unicode support on @var{tty}."
|
||
|
||
;; We have to run 'unicode_start' in a pipe so that when it invokes the
|
||
;; 'tty' command, that command returns TTY.
|
||
#~(begin
|
||
(let ((pid (primitive-fork)))
|
||
(case pid
|
||
((0)
|
||
(close-fdes 0)
|
||
(dup2 (open-fdes #$tty O_RDONLY) 0)
|
||
(close-fdes 1)
|
||
(dup2 (open-fdes #$tty O_WRONLY) 1)
|
||
(execl #$(file-append kbd "/bin/unicode_start")
|
||
"unicode_start"))
|
||
(else
|
||
(zero? (cdr (waitpid pid))))))))
|
||
|
||
(define console-keymap-service-type
|
||
(shepherd-service-type
|
||
'console-keymap
|
||
(lambda (files)
|
||
(shepherd-service
|
||
(documentation (string-append "Load console keymap (loadkeys)."))
|
||
(provision '(console-keymap))
|
||
(start #~(lambda _
|
||
(zero? (system* #$(file-append kbd "/bin/loadkeys")
|
||
#$@files))))
|
||
(respawn? #f)))))
|
||
|
||
(define (console-keymap-service . files)
|
||
"Return a service to load console keymaps from @var{files}."
|
||
(service console-keymap-service-type files))
|
||
|
||
(define %default-console-font
|
||
;; Note: 'LatGrkCyr-8x16' has the advantage of providing three common
|
||
;; scripts as well as glyphs for em dash, quotation marks, and other Unicode
|
||
;; codepoints notably found in the UTF-8 manual.
|
||
"LatGrkCyr-8x16")
|
||
|
||
(define (console-font-shepherd-services tty+font)
|
||
"Return a list of Shepherd services for each pair in TTY+FONT."
|
||
(map (match-lambda
|
||
((tty . font)
|
||
(let ((device (string-append "/dev/" tty)))
|
||
(shepherd-service
|
||
(documentation "Load a Unicode console font.")
|
||
(provision (list (symbol-append 'console-font-
|
||
(string->symbol tty))))
|
||
|
||
;; Start after mingetty has been started on TTY, otherwise the settings
|
||
;; are ignored.
|
||
(requirement (list (symbol-append 'term-
|
||
(string->symbol tty))))
|
||
|
||
(start #~(lambda _
|
||
(and #$(unicode-start device)
|
||
(zero?
|
||
(system* #$(file-append kbd "/bin/setfont")
|
||
"-C" #$device #$font)))))
|
||
(stop #~(const #t))
|
||
(respawn? #f)))))
|
||
tty+font))
|
||
|
||
(define console-font-service-type
|
||
(service-type (name 'console-fonts)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type
|
||
console-font-shepherd-services)))
|
||
(compose concatenate)
|
||
(extend append)))
|
||
|
||
(define* (console-font-service tty #:optional (font "LatGrkCyr-8x16"))
|
||
"This procedure is deprecated in favor of @code{console-font-service-type}.
|
||
|
||
Return a service that sets up Unicode support in @var{tty} and loads
|
||
@var{font} for that tty (fonts are per virtual console in Linux.)"
|
||
(simple-service (symbol-append 'console-font- (string->symbol tty))
|
||
console-font-service-type `((,tty . ,font))))
|
||
|
||
(define %default-motd
|
||
(plain-file "motd" "This is the GNU operating system, welcome!\n\n"))
|
||
|
||
(define-record-type* <login-configuration>
|
||
login-configuration make-login-configuration
|
||
login-configuration?
|
||
(motd login-configuration-motd ;file-like
|
||
(default %default-motd))
|
||
;; Allow empty passwords by default so that first-time users can log in when
|
||
;; the 'root' account has just been created.
|
||
(allow-empty-passwords? login-configuration-allow-empty-passwords?
|
||
(default #t))) ;Boolean
|
||
|
||
(define (login-pam-service config)
|
||
"Return the list of PAM service needed for CONF."
|
||
;; Let 'login' be known to PAM.
|
||
(list (unix-pam-service "login"
|
||
#:allow-empty-passwords?
|
||
(login-configuration-allow-empty-passwords? config)
|
||
#:motd
|
||
(login-configuration-motd config))))
|
||
|
||
(define login-service-type
|
||
(service-type (name 'login)
|
||
(extensions (list (service-extension pam-root-service-type
|
||
login-pam-service)))))
|
||
|
||
(define* (login-service #:optional (config (login-configuration)))
|
||
"Return a service configure login according to @var{config}, which specifies
|
||
the message of the day, among other things."
|
||
(service login-service-type config))
|
||
|
||
(define-record-type* <agetty-configuration>
|
||
agetty-configuration make-agetty-configuration
|
||
agetty-configuration?
|
||
(agetty agetty-configuration-agetty ;<package>
|
||
(default util-linux))
|
||
(tty agetty-configuration-tty) ;string
|
||
(term agetty-term ;string | #f
|
||
(default #f))
|
||
(baud-rate agetty-baud-rate ;string | #f
|
||
(default #f))
|
||
(auto-login agetty-auto-login ;list of strings | #f
|
||
(default #f))
|
||
(login-program agetty-login-program ;gexp
|
||
(default (file-append shadow "/bin/login")))
|
||
(login-pause? agetty-login-pause? ;Boolean
|
||
(default #f))
|
||
(eight-bits? agetty-eight-bits? ;Boolean
|
||
(default #f))
|
||
(no-reset? agetty-no-reset? ;Boolean
|
||
(default #f))
|
||
(remote? agetty-remote? ;Boolean
|
||
(default #f))
|
||
(flow-control? agetty-flow-control? ;Boolean
|
||
(default #f))
|
||
(host agetty-host ;string | #f
|
||
(default #f))
|
||
(no-issue? agetty-no-issue? ;Boolean
|
||
(default #f))
|
||
(init-string agetty-init-string ;string | #f
|
||
(default #f))
|
||
(no-clear? agetty-no-clear? ;Boolean
|
||
(default #f))
|
||
(local-line agetty-local-line ;always | never | auto
|
||
(default #f))
|
||
(extract-baud? agetty-extract-baud? ;Boolean
|
||
(default #f))
|
||
(skip-login? agetty-skip-login? ;Boolean
|
||
(default #f))
|
||
(no-newline? agetty-no-newline? ;Boolean
|
||
(default #f))
|
||
(login-options agetty-login-options ;string | #f
|
||
(default #f))
|
||
(chroot agetty-chroot ;string | #f
|
||
(default #f))
|
||
(hangup? agetty-hangup? ;Boolean
|
||
(default #f))
|
||
(keep-baud? agetty-keep-baud? ;Boolean
|
||
(default #f))
|
||
(timeout agetty-timeout ;integer | #f
|
||
(default #f))
|
||
(detect-case? agetty-detect-case? ;Boolean
|
||
(default #f))
|
||
(wait-cr? agetty-wait-cr? ;Boolean
|
||
(default #f))
|
||
(no-hints? agetty-no-hints? ;Boolean
|
||
(default #f))
|
||
(no-hostname? agetty-no hostname? ;Boolean
|
||
(default #f))
|
||
(long-hostname? agetty-long-hostname? ;Boolean
|
||
(default #f))
|
||
(erase-characters agetty-erase-characters ;string | #f
|
||
(default #f))
|
||
(kill-characters agetty-kill-characters ;string | #f
|
||
(default #f))
|
||
(chdir agetty-chdir ;string | #f
|
||
(default #f))
|
||
(delay agetty-delay ;integer | #f
|
||
(default #f))
|
||
(nice agetty-nice ;integer | #f
|
||
(default #f))
|
||
;; "Escape hatch" for passing arbitrary command-line arguments.
|
||
(extra-options agetty-extra-options ;list of strings
|
||
(default '()))
|
||
;;; XXX Unimplemented for now!
|
||
;;; (issue-file agetty-issue-file ;file-like
|
||
;;; (default #f))
|
||
)
|
||
|
||
(define agetty-shepherd-service
|
||
(match-lambda
|
||
(($ <agetty-configuration> agetty tty term baud-rate auto-login
|
||
login-program login-pause? eight-bits? no-reset? remote? flow-control?
|
||
host no-issue? init-string no-clear? local-line extract-baud?
|
||
skip-login? no-newline? login-options chroot hangup? keep-baud? timeout
|
||
detect-case? wait-cr? no-hints? no-hostname? long-hostname?
|
||
erase-characters kill-characters chdir delay nice extra-options)
|
||
(list
|
||
(shepherd-service
|
||
(documentation "Run agetty on a tty.")
|
||
(provision (list (symbol-append 'term- (string->symbol tty))))
|
||
|
||
;; Since the login prompt shows the host name, wait for the 'host-name'
|
||
;; service to be done. Also wait for udev essentially so that the tty
|
||
;; text is not lost in the middle of kernel messages (see also
|
||
;; mingetty-shepherd-service).
|
||
(requirement '(user-processes host-name udev))
|
||
|
||
(start #~(make-forkexec-constructor
|
||
(list #$(file-append util-linux "/sbin/agetty")
|
||
#$@extra-options
|
||
#$@(if eight-bits?
|
||
#~("--8bits")
|
||
#~())
|
||
#$@(if no-reset?
|
||
#~("--noreset")
|
||
#~())
|
||
#$@(if remote?
|
||
#~("--remote")
|
||
#~())
|
||
#$@(if flow-control?
|
||
#~("--flow-control")
|
||
#~())
|
||
#$@(if host
|
||
#~("--host" #$host)
|
||
#~())
|
||
#$@(if no-issue?
|
||
#~("--noissue")
|
||
#~())
|
||
#$@(if init-string
|
||
#~("--init-string" #$init-string)
|
||
#~())
|
||
#$@(if no-clear?
|
||
#~("--noclear")
|
||
#~())
|
||
;;; FIXME This doesn't work as expected. According to agetty(8), if this option
|
||
;;; is not passed, then the default is 'auto'. However, in my tests, when that
|
||
;;; option is selected, agetty never presents the login prompt, and the
|
||
;;; term-ttyS0 service respawns every few seconds.
|
||
#$@(if local-line
|
||
#~(#$(match local-line
|
||
('auto "--local-line=auto")
|
||
('always "--local-line=always")
|
||
('never "-local-line=never")))
|
||
#~())
|
||
#$@(if extract-baud?
|
||
#~("--extract-baud")
|
||
#~())
|
||
#$@(if skip-login?
|
||
#~("--skip-login")
|
||
#~())
|
||
#$@(if no-newline?
|
||
#~("--nonewline")
|
||
#~())
|
||
#$@(if login-options
|
||
#~("--login-options" #$login-options)
|
||
#~())
|
||
#$@(if chroot
|
||
#~("--chroot" #$chroot)
|
||
#~())
|
||
#$@(if hangup?
|
||
#~("--hangup")
|
||
#~())
|
||
#$@(if keep-baud?
|
||
#~("--keep-baud")
|
||
#~())
|
||
#$@(if timeout
|
||
#~("--timeout" #$(number->string timeout))
|
||
#~())
|
||
#$@(if detect-case?
|
||
#~("--detect-case")
|
||
#~())
|
||
#$@(if wait-cr?
|
||
#~("--wait-cr")
|
||
#~())
|
||
#$@(if no-hints?
|
||
#~("--nohints?")
|
||
#~())
|
||
#$@(if no-hostname?
|
||
#~("--nohostname")
|
||
#~())
|
||
#$@(if long-hostname?
|
||
#~("--long-hostname")
|
||
#~())
|
||
#$@(if erase-characters
|
||
#~("--erase-chars" #$erase-characters)
|
||
#~())
|
||
#$@(if kill-characters
|
||
#~("--kill-chars" #$kill-characters)
|
||
#~())
|
||
#$@(if chdir
|
||
#~("--chdir" #$chdir)
|
||
#~())
|
||
#$@(if delay
|
||
#~("--delay" #$(number->string delay))
|
||
#~())
|
||
#$@(if nice
|
||
#~("--nice" #$(number->string nice))
|
||
#~())
|
||
#$@(if auto-login
|
||
(list "--autologin" auto-login)
|
||
'())
|
||
#$@(if login-program
|
||
#~("--login-program" #$login-program)
|
||
#~())
|
||
#$@(if login-pause?
|
||
#~("--login-pause")
|
||
#~())
|
||
#$tty
|
||
#$@(if baud-rate
|
||
#~(#$baud-rate)
|
||
#~())
|
||
#$@(if term
|
||
#~(#$term)
|
||
#~()))))
|
||
(stop #~(make-kill-destructor)))))))
|
||
|
||
(define agetty-service-type
|
||
(service-type (name 'agetty)
|
||
(extensions (list (service-extension shepherd-root-service-type
|
||
agetty-shepherd-service)))))
|
||
|
||
(define* (agetty-service config)
|
||
"Return a service to run agetty according to @var{config}, which specifies
|
||
the tty to run, among other things."
|
||
(service agetty-service-type config))
|
||
|
||
(define-record-type* <mingetty-configuration>
|
||
mingetty-configuration make-mingetty-configuration
|
||
mingetty-configuration?
|
||
(mingetty mingetty-configuration-mingetty ;<package>
|
||
(default mingetty))
|
||
(tty mingetty-configuration-tty) ;string
|
||
(auto-login mingetty-auto-login ;string | #f
|
||
(default #f))
|
||
(login-program mingetty-login-program ;gexp
|
||
(default #f))
|
||
(login-pause? mingetty-login-pause? ;Boolean
|
||
(default #f)))
|
||
|
||
(define mingetty-shepherd-service
|
||
(match-lambda
|
||
(($ <mingetty-configuration> mingetty tty auto-login login-program
|
||
login-pause?)
|
||
(list
|
||
(shepherd-service
|
||
(documentation "Run mingetty on an tty.")
|
||
(provision (list (symbol-append 'term- (string->symbol tty))))
|
||
|
||
;; Since the login prompt shows the host name, wait for the 'host-name'
|
||
;; service to be done. Also wait for udev essentially so that the tty
|
||
;; text is not lost in the middle of kernel messages (XXX).
|
||
(requirement '(user-processes host-name udev))
|
||
|
||
(start #~(make-forkexec-constructor
|
||
(list #$(file-append mingetty "/sbin/mingetty")
|
||
"--noclear" #$tty
|
||
#$@(if auto-login
|
||
#~("--autologin" #$auto-login)
|
||
#~())
|
||
#$@(if login-program
|
||
#~("--loginprog" #$login-program)
|
||
#~())
|
||
#$@(if login-pause?
|
||
#~("--loginpause")
|
||
#~()))))
|
||
(stop #~(make-kill-destructor)))))))
|
||
|
||
(define mingetty-service-type
|
||
(service-type (name 'mingetty)
|
||
(extensions (list (service-extension shepherd-root-service-type
|
||
mingetty-shepherd-service)))))
|
||
|
||
(define* (mingetty-service config)
|
||
"Return a service to run mingetty according to @var{config}, which specifies
|
||
the tty to run, among other things."
|
||
(service mingetty-service-type config))
|
||
|
||
(define-record-type* <nscd-configuration> nscd-configuration
|
||
make-nscd-configuration
|
||
nscd-configuration?
|
||
(log-file nscd-configuration-log-file ;string
|
||
(default "/var/log/nscd.log"))
|
||
(debug-level nscd-debug-level ;integer
|
||
(default 0))
|
||
;; TODO: See nscd.conf in glibc for other options to add.
|
||
(caches nscd-configuration-caches ;list of <nscd-cache>
|
||
(default %nscd-default-caches))
|
||
(name-services nscd-configuration-name-services ;list of <packages>
|
||
(default '()))
|
||
(glibc nscd-configuration-glibc ;<package>
|
||
(default (canonical-package glibc))))
|
||
|
||
(define-record-type* <nscd-cache> nscd-cache make-nscd-cache
|
||
nscd-cache?
|
||
(database nscd-cache-database) ;symbol
|
||
(positive-time-to-live nscd-cache-positive-time-to-live) ;integer
|
||
(negative-time-to-live nscd-cache-negative-time-to-live
|
||
(default 20)) ;integer
|
||
(suggested-size nscd-cache-suggested-size ;integer ("default module
|
||
;of hash table")
|
||
(default 211))
|
||
(check-files? nscd-cache-check-files? ;Boolean
|
||
(default #t))
|
||
(persistent? nscd-cache-persistent? ;Boolean
|
||
(default #t))
|
||
(shared? nscd-cache-shared? ;Boolean
|
||
(default #t))
|
||
(max-database-size nscd-cache-max-database-size ;integer
|
||
(default (* 32 (expt 2 20))))
|
||
(auto-propagate? nscd-cache-auto-propagate? ;Boolean
|
||
(default #t)))
|
||
|
||
(define %nscd-default-caches
|
||
;; Caches that we want to enable by default. Note that when providing an
|
||
;; empty nscd.conf, all caches are disabled.
|
||
(list (nscd-cache (database 'hosts)
|
||
|
||
;; Aggressively cache the host name cache to improve
|
||
;; privacy and resilience.
|
||
(positive-time-to-live (* 3600 12))
|
||
(negative-time-to-live 20)
|
||
(persistent? #t))
|
||
|
||
(nscd-cache (database 'services)
|
||
|
||
;; Services are unlikely to change, so we can be even more
|
||
;; aggressive.
|
||
(positive-time-to-live (* 3600 24))
|
||
(negative-time-to-live 3600)
|
||
(check-files? #t) ;check /etc/services changes
|
||
(persistent? #t))))
|
||
|
||
(define %nscd-default-configuration
|
||
;; Default nscd configuration.
|
||
(nscd-configuration))
|
||
|
||
(define (nscd.conf-file config)
|
||
"Return the @file{nscd.conf} configuration file for @var{config}, an
|
||
@code{<nscd-configuration>} object."
|
||
(define cache->config
|
||
(match-lambda
|
||
(($ <nscd-cache> (= symbol->string database)
|
||
positive-ttl negative-ttl size check-files?
|
||
persistent? shared? max-size propagate?)
|
||
(string-append "\nenable-cache\t" database "\tyes\n"
|
||
|
||
"positive-time-to-live\t" database "\t"
|
||
(number->string positive-ttl) "\n"
|
||
"negative-time-to-live\t" database "\t"
|
||
(number->string negative-ttl) "\n"
|
||
"suggested-size\t" database "\t"
|
||
(number->string size) "\n"
|
||
"check-files\t" database "\t"
|
||
(if check-files? "yes\n" "no\n")
|
||
"persistent\t" database "\t"
|
||
(if persistent? "yes\n" "no\n")
|
||
"shared\t" database "\t"
|
||
(if shared? "yes\n" "no\n")
|
||
"max-db-size\t" database "\t"
|
||
(number->string max-size) "\n"
|
||
"auto-propagate\t" database "\t"
|
||
(if propagate? "yes\n" "no\n")))))
|
||
|
||
(match config
|
||
(($ <nscd-configuration> log-file debug-level caches)
|
||
(plain-file "nscd.conf"
|
||
(string-append "\
|
||
# Configuration of libc's name service cache daemon (nscd).\n\n"
|
||
(if log-file
|
||
(string-append "logfile\t" log-file)
|
||
"")
|
||
"\n"
|
||
(if debug-level
|
||
(string-append "debug-level\t"
|
||
(number->string debug-level))
|
||
"")
|
||
"\n"
|
||
(string-concatenate
|
||
(map cache->config caches)))))))
|
||
|
||
(define (nscd-shepherd-service config)
|
||
"Return a shepherd service for CONFIG, an <nscd-configuration> object."
|
||
(let ((nscd.conf (nscd.conf-file config))
|
||
(name-services (nscd-configuration-name-services config)))
|
||
(list (shepherd-service
|
||
(documentation "Run libc's name service cache daemon (nscd).")
|
||
(provision '(nscd))
|
||
(requirement '(user-processes))
|
||
(start #~(make-forkexec-constructor
|
||
(list #$(file-append (nscd-configuration-glibc config)
|
||
"/sbin/nscd")
|
||
"-f" #$nscd.conf "--foreground")
|
||
|
||
;; Wait for the PID file. However, the PID file is
|
||
;; written before nscd is actually listening on its
|
||
;; socket (XXX).
|
||
#:pid-file "/var/run/nscd/nscd.pid"
|
||
|
||
#:environment-variables
|
||
(list (string-append "LD_LIBRARY_PATH="
|
||
(string-join
|
||
(map (lambda (dir)
|
||
(string-append dir "/lib"))
|
||
(list #$@name-services))
|
||
":")))))
|
||
(stop #~(make-kill-destructor))))))
|
||
|
||
(define nscd-activation
|
||
;; Actions to take before starting nscd.
|
||
#~(begin
|
||
(use-modules (guix build utils))
|
||
(mkdir-p "/var/run/nscd")
|
||
(mkdir-p "/var/db/nscd") ;for the persistent cache
|
||
|
||
;; In libc 2.25 nscd uses inotify to watch /etc/resolv.conf, but only if
|
||
;; that file exists when it is started. Thus create it here. Note: on
|
||
;; some systems, such as when NetworkManager is used, /etc/resolv.conf
|
||
;; is a symlink, hence 'lstat'.
|
||
(unless (false-if-exception (lstat "/etc/resolv.conf"))
|
||
(call-with-output-file "/etc/resolv.conf"
|
||
(lambda (port)
|
||
(display "# This is a placeholder.\n" port))))))
|
||
|
||
(define nscd-service-type
|
||
(service-type (name 'nscd)
|
||
(extensions
|
||
(list (service-extension activation-service-type
|
||
(const nscd-activation))
|
||
(service-extension shepherd-root-service-type
|
||
nscd-shepherd-service)))
|
||
|
||
;; This can be extended by providing additional name services
|
||
;; such as nss-mdns.
|
||
(compose concatenate)
|
||
(extend (lambda (config name-services)
|
||
(nscd-configuration
|
||
(inherit config)
|
||
(name-services (append
|
||
(nscd-configuration-name-services config)
|
||
name-services)))))))
|
||
|
||
(define* (nscd-service #:optional (config %nscd-default-configuration))
|
||
"Return a service that runs libc's name service cache daemon (nscd) with the
|
||
given @var{config}---an @code{<nscd-configuration>} object. @xref{Name
|
||
Service Switch}, for an example."
|
||
(service nscd-service-type config))
|
||
|
||
|
||
(define-record-type* <syslog-configuration>
|
||
syslog-configuration make-syslog-configuration
|
||
syslog-configuration?
|
||
(syslogd syslog-configuration-syslogd
|
||
(default (file-append inetutils "/libexec/syslogd")))
|
||
(config-file syslog-configuration-config-file
|
||
(default %default-syslog.conf)))
|
||
|
||
(define syslog-service-type
|
||
(shepherd-service-type
|
||
'syslog
|
||
(lambda (config)
|
||
(shepherd-service
|
||
(documentation "Run the syslog daemon (syslogd).")
|
||
(provision '(syslogd))
|
||
(requirement '(user-processes))
|
||
(start #~(make-forkexec-constructor
|
||
(list #$(syslog-configuration-syslogd config)
|
||
"--rcfile" #$(syslog-configuration-config-file config))
|
||
#:pid-file "/var/run/syslog.pid"))
|
||
(stop #~(make-kill-destructor))))))
|
||
|
||
;; Snippet adapted from the GNU inetutils manual.
|
||
(define %default-syslog.conf
|
||
(plain-file "syslog.conf" "
|
||
# Log all error messages, authentication messages of
|
||
# level notice or higher and anything of level err or
|
||
# higher to the console.
|
||
# Don't log private authentication messages!
|
||
*.alert;auth.notice;authpriv.none /dev/console
|
||
|
||
# Log anything (except mail) of level info or higher.
|
||
# Don't log private authentication messages!
|
||
*.info;mail.none;authpriv.none /var/log/messages
|
||
|
||
# Same, in a different place.
|
||
*.info;mail.none;authpriv.none /dev/tty12
|
||
|
||
# The authpriv file has restricted access.
|
||
authpriv.* /var/log/secure
|
||
|
||
# Log all the mail messages in one place.
|
||
mail.* /var/log/maillog
|
||
"))
|
||
|
||
(define* (syslog-service #:optional (config (syslog-configuration)))
|
||
"Return a service that runs @command{syslogd} and takes
|
||
@var{<syslog-configuration>} as a parameter.
|
||
|
||
@xref{syslogd invocation,,, inetutils, GNU Inetutils}, for more
|
||
information on the configuration file syntax."
|
||
(service syslog-service-type config))
|
||
|
||
|
||
(define pam-limits-service-type
|
||
(let ((security-limits
|
||
;; Create /etc/security containing the provided "limits.conf" file.
|
||
(lambda (limits-file)
|
||
`(("security"
|
||
,(computed-file
|
||
"security"
|
||
#~(begin
|
||
(mkdir #$output)
|
||
(stat #$limits-file)
|
||
(symlink #$limits-file
|
||
(string-append #$output "/limits.conf"))))))))
|
||
(pam-extension
|
||
(lambda (pam)
|
||
(let ((pam-limits (pam-entry
|
||
(control "required")
|
||
(module "pam_limits.so")
|
||
(arguments '("conf=/etc/security/limits.conf")))))
|
||
(if (member (pam-service-name pam)
|
||
'("login" "su" "slim"))
|
||
(pam-service
|
||
(inherit pam)
|
||
(session (cons pam-limits
|
||
(pam-service-session pam))))
|
||
pam)))))
|
||
(service-type
|
||
(name 'limits)
|
||
(extensions
|
||
(list (service-extension etc-service-type security-limits)
|
||
(service-extension pam-root-service-type
|
||
(lambda _ (list pam-extension))))))))
|
||
|
||
(define* (pam-limits-service #:optional (limits '()))
|
||
"Return a service that makes selected programs respect the list of
|
||
pam-limits-entry specified in LIMITS via pam_limits.so."
|
||
(service pam-limits-service-type
|
||
(plain-file "limits.conf"
|
||
(string-join (map pam-limits-entry->string limits)
|
||
"\n"))))
|
||
|
||
|
||
;;;
|
||
;;; Guix services.
|
||
;;;
|
||
|
||
(define* (guix-build-accounts count #:key
|
||
(group "guixbuild")
|
||
(first-uid 30001)
|
||
(shadow shadow))
|
||
"Return a list of COUNT user accounts for Guix build users, with UIDs
|
||
starting at FIRST-UID, and under GID."
|
||
(unfold (cut > <> count)
|
||
(lambda (n)
|
||
(user-account
|
||
(name (format #f "guixbuilder~2,'0d" n))
|
||
(system? #t)
|
||
(uid (+ first-uid n -1))
|
||
(group group)
|
||
|
||
;; guix-daemon expects GROUP to be listed as a
|
||
;; supplementary group too:
|
||
;; <http://lists.gnu.org/archive/html/bug-guix/2013-01/msg00239.html>.
|
||
(supplementary-groups (list group "kvm"))
|
||
|
||
(comment (format #f "Guix Build User ~2d" n))
|
||
(home-directory "/var/empty")
|
||
(shell (file-append shadow "/sbin/nologin"))))
|
||
1+
|
||
1))
|
||
|
||
(define (hydra-key-authorization key guix)
|
||
"Return a gexp with code to register KEY, a file containing a 'guix archive'
|
||
public key, with GUIX."
|
||
#~(unless (file-exists? "/etc/guix/acl")
|
||
(let ((pid (primitive-fork)))
|
||
(case pid
|
||
((0)
|
||
(let* ((key #$key)
|
||
(port (open-file key "r0b")))
|
||
(format #t "registering public key '~a'...~%" key)
|
||
(close-port (current-input-port))
|
||
(dup port 0)
|
||
(execl #$(file-append guix "/bin/guix")
|
||
"guix" "archive" "--authorize")
|
||
(exit 1)))
|
||
(else
|
||
(let ((status (cdr (waitpid pid))))
|
||
(unless (zero? status)
|
||
(format (current-error-port) "warning: \
|
||
failed to register hydra.gnu.org public key: ~a~%" status))))))))
|
||
|
||
(define %default-authorized-guix-keys
|
||
;; List of authorized substitute keys.
|
||
(list (file-append guix "/share/guix/hydra.gnu.org.pub")
|
||
(file-append guix "/share/guix/bayfront.guixsd.org.pub")))
|
||
|
||
(define-record-type* <guix-configuration>
|
||
guix-configuration make-guix-configuration
|
||
guix-configuration?
|
||
(guix guix-configuration-guix ;<package>
|
||
(default guix))
|
||
(build-group guix-configuration-build-group ;string
|
||
(default "guixbuild"))
|
||
(build-accounts guix-configuration-build-accounts ;integer
|
||
(default 10))
|
||
(authorize-key? guix-configuration-authorize-key? ;Boolean
|
||
(default #t))
|
||
(authorized-keys guix-configuration-authorized-keys ;list of gexps
|
||
(default %default-authorized-guix-keys))
|
||
(use-substitutes? guix-configuration-use-substitutes? ;Boolean
|
||
(default #t))
|
||
(substitute-urls guix-configuration-substitute-urls ;list of strings
|
||
(default %default-substitute-urls))
|
||
(max-silent-time guix-configuration-max-silent-time ;integer
|
||
(default 0))
|
||
(timeout guix-configuration-timeout ;integer
|
||
(default 0))
|
||
(extra-options guix-configuration-extra-options ;list of strings
|
||
(default '()))
|
||
(log-file guix-configuration-log-file ;string
|
||
(default "/var/log/guix-daemon.log"))
|
||
(lsof guix-configuration-lsof ;<package>
|
||
(default lsof))
|
||
(http-proxy guix-http-proxy ;string | #f
|
||
(default #f))
|
||
(tmpdir guix-tmpdir ;string | #f
|
||
(default #f)))
|
||
|
||
(define %default-guix-configuration
|
||
(guix-configuration))
|
||
|
||
(define (guix-shepherd-service config)
|
||
"Return a <shepherd-service> for the Guix daemon service with CONFIG."
|
||
(match config
|
||
(($ <guix-configuration> guix build-group build-accounts
|
||
authorize-key? keys
|
||
use-substitutes? substitute-urls
|
||
max-silent-time timeout
|
||
extra-options
|
||
log-file lsof http-proxy tmpdir)
|
||
(list (shepherd-service
|
||
(documentation "Run the Guix daemon.")
|
||
(provision '(guix-daemon))
|
||
(requirement '(user-processes))
|
||
(start
|
||
#~(make-forkexec-constructor
|
||
(list #$(file-append guix "/bin/guix-daemon")
|
||
"--build-users-group" #$build-group
|
||
"--max-silent-time" #$(number->string max-silent-time)
|
||
"--timeout" #$(number->string timeout)
|
||
#$@(if use-substitutes?
|
||
'()
|
||
'("--no-substitutes"))
|
||
"--substitute-urls" #$(string-join substitute-urls)
|
||
#$@extra-options)
|
||
|
||
;; Add 'lsof' (for the GC) to the daemon's $PATH.
|
||
#:environment-variables
|
||
(list (string-append "PATH=" #$lsof "/bin")
|
||
#$@(if http-proxy
|
||
(list (string-append "http_proxy=" http-proxy))
|
||
'())
|
||
#$@(if tmpdir
|
||
(list (string-append "TMPDIR=" tmpdir))
|
||
'()))
|
||
|
||
#:log-file #$log-file))
|
||
(stop #~(make-kill-destructor)))))))
|
||
|
||
(define (guix-accounts config)
|
||
"Return the user accounts and user groups for CONFIG."
|
||
(match config
|
||
(($ <guix-configuration> _ build-group build-accounts)
|
||
(cons (user-group
|
||
(name build-group)
|
||
(system? #t)
|
||
|
||
;; Use a fixed GID so that we can create the store with the right
|
||
;; owner.
|
||
(id 30000))
|
||
(guix-build-accounts build-accounts
|
||
#:group build-group)))))
|
||
|
||
(define (guix-activation config)
|
||
"Return the activation gexp for CONFIG."
|
||
(match config
|
||
(($ <guix-configuration> guix build-group build-accounts authorize-key? keys)
|
||
;; Assume that the store has BUILD-GROUP as its group. We could
|
||
;; otherwise call 'chown' here, but the problem is that on a COW unionfs,
|
||
;; chown leads to an entire copy of the tree, which is a bad idea.
|
||
|
||
;; Optionally authorize hydra.gnu.org's key.
|
||
(if authorize-key?
|
||
#~(begin
|
||
#$@(map (cut hydra-key-authorization <> guix) keys))
|
||
#~#f))))
|
||
|
||
(define guix-service-type
|
||
(service-type
|
||
(name 'guix)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type guix-shepherd-service)
|
||
(service-extension account-service-type guix-accounts)
|
||
(service-extension activation-service-type guix-activation)
|
||
(service-extension profile-service-type
|
||
(compose list guix-configuration-guix))))
|
||
(default-value (guix-configuration))))
|
||
|
||
(define* (guix-service #:optional (config %default-guix-configuration))
|
||
"Return a service that runs the Guix build daemon according to
|
||
@var{config}."
|
||
(service guix-service-type config))
|
||
|
||
|
||
(define-record-type* <guix-publish-configuration>
|
||
guix-publish-configuration make-guix-publish-configuration
|
||
guix-publish-configuration?
|
||
(guix guix-publish-configuration-guix ;package
|
||
(default guix))
|
||
(port guix-publish-configuration-port ;number
|
||
(default 80))
|
||
(host guix-publish-configuration-host ;string
|
||
(default "localhost"))
|
||
(compression-level guix-publish-configuration-compression-level ;integer
|
||
(default 3))
|
||
(nar-path guix-publish-configuration-nar-path ;string
|
||
(default "nar"))
|
||
(cache guix-publish-configuration-cache ;#f | string
|
||
(default #f))
|
||
(workers guix-publish-configuration-workers ;#f | integer
|
||
(default #f))
|
||
(ttl guix-publish-configuration-ttl ;#f | integer
|
||
(default #f)))
|
||
|
||
(define guix-publish-shepherd-service
|
||
(match-lambda
|
||
(($ <guix-publish-configuration> guix port host compression
|
||
nar-path cache workers ttl)
|
||
(list (shepherd-service
|
||
(provision '(guix-publish))
|
||
(requirement '(guix-daemon))
|
||
(start #~(make-forkexec-constructor
|
||
(list #$(file-append guix "/bin/guix")
|
||
"publish" "-u" "guix-publish"
|
||
"-p" #$(number->string port)
|
||
"-C" #$(number->string compression)
|
||
(string-append "--nar-path=" #$nar-path)
|
||
(string-append "--listen=" #$host)
|
||
#$@(if workers
|
||
#~((string-append "--workers="
|
||
#$(number->string
|
||
workers)))
|
||
#~())
|
||
#$@(if ttl
|
||
#~((string-append "--ttl="
|
||
#$(number->string ttl)
|
||
"s"))
|
||
#~())
|
||
#$@(if cache
|
||
#~((string-append "--cache=" #$cache))
|
||
#~()))))
|
||
(stop #~(make-kill-destructor)))))))
|
||
|
||
(define %guix-publish-accounts
|
||
(list (user-group (name "guix-publish") (system? #t))
|
||
(user-account
|
||
(name "guix-publish")
|
||
(group "guix-publish")
|
||
(system? #t)
|
||
(comment "guix publish user")
|
||
(home-directory "/var/empty")
|
||
(shell (file-append shadow "/sbin/nologin")))))
|
||
|
||
(define (guix-publish-activation config)
|
||
(let ((cache (guix-publish-configuration-cache config)))
|
||
(if cache
|
||
(with-imported-modules '((guix build utils))
|
||
#~(begin
|
||
(use-modules (guix build utils))
|
||
|
||
(mkdir-p #$cache)
|
||
(let* ((pw (getpw "guix-publish"))
|
||
(uid (passwd:uid pw))
|
||
(gid (passwd:gid pw)))
|
||
(chown #$cache uid gid))))
|
||
#t)))
|
||
|
||
(define guix-publish-service-type
|
||
(service-type (name 'guix-publish)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type
|
||
guix-publish-shepherd-service)
|
||
(service-extension account-service-type
|
||
(const %guix-publish-accounts))
|
||
(service-extension activation-service-type
|
||
guix-publish-activation)))
|
||
(default-value (guix-publish-configuration))))
|
||
|
||
(define* (guix-publish-service #:key (guix guix) (port 80) (host "localhost"))
|
||
"Return a service that runs @command{guix publish} listening on @var{host}
|
||
and @var{port} (@pxref{Invoking guix publish}).
|
||
|
||
This assumes that @file{/etc/guix} already contains a signing key pair as
|
||
created by @command{guix archive --generate-key} (@pxref{Invoking guix
|
||
archive}). If that is not the case, the service will fail to start."
|
||
;; Deprecated.
|
||
(service guix-publish-service-type
|
||
(guix-publish-configuration (guix guix) (port port) (host host))))
|
||
|
||
|
||
;;;
|
||
;;; Udev.
|
||
;;;
|
||
|
||
(define-record-type* <udev-configuration>
|
||
udev-configuration make-udev-configuration
|
||
udev-configuration?
|
||
(udev udev-configuration-udev ;<package>
|
||
(default udev))
|
||
(rules udev-configuration-rules ;list of <package>
|
||
(default '())))
|
||
|
||
(define (udev-rules-union packages)
|
||
"Return the union of the @code{lib/udev/rules.d} directories found in each
|
||
item of @var{packages}."
|
||
(define build
|
||
(with-imported-modules '((guix build union)
|
||
(guix build utils))
|
||
#~(begin
|
||
(use-modules (guix build union)
|
||
(guix build utils)
|
||
(srfi srfi-1)
|
||
(srfi srfi-26))
|
||
|
||
(define %standard-locations
|
||
'("/lib/udev/rules.d" "/libexec/udev/rules.d"))
|
||
|
||
(define (rules-sub-directory directory)
|
||
;; Return the sub-directory of DIRECTORY containing udev rules, or
|
||
;; #f if none was found.
|
||
(find directory-exists?
|
||
(map (cut string-append directory <>) %standard-locations)))
|
||
|
||
(mkdir-p (string-append #$output "/lib/udev"))
|
||
(union-build (string-append #$output "/lib/udev/rules.d")
|
||
(filter-map rules-sub-directory '#$packages)))))
|
||
|
||
(computed-file "udev-rules" build))
|
||
|
||
(define (udev-rule file-name contents)
|
||
"Return a directory with a udev rule file FILE-NAME containing CONTENTS."
|
||
(computed-file file-name
|
||
(with-imported-modules '((guix build utils))
|
||
#~(begin
|
||
(use-modules (guix build utils))
|
||
|
||
(define rules.d
|
||
(string-append #$output "/lib/udev/rules.d"))
|
||
|
||
(mkdir-p rules.d)
|
||
(call-with-output-file
|
||
(string-append rules.d "/" #$file-name)
|
||
(lambda (port)
|
||
(display #$contents port)))))))
|
||
|
||
(define kvm-udev-rule
|
||
;; Return a directory with a udev rule that changes the group of /dev/kvm to
|
||
;; "kvm" and makes it #o660. Apparently QEMU-KVM used to ship this rule,
|
||
;; but now we have to add it by ourselves.
|
||
|
||
;; Build users are part of the "kvm" group, so we can fearlessly make
|
||
;; /dev/kvm 660 (see <http://bugs.gnu.org/18994>, for background.)
|
||
(udev-rule "90-kvm.rules"
|
||
"KERNEL==\"kvm\", GROUP=\"kvm\", MODE=\"0660\"\n"))
|
||
|
||
(define udev-shepherd-service
|
||
;; Return a <shepherd-service> for UDEV with RULES.
|
||
(match-lambda
|
||
(($ <udev-configuration> udev rules)
|
||
(let* ((rules (udev-rules-union (cons* udev kvm-udev-rule rules)))
|
||
(udev.conf (computed-file "udev.conf"
|
||
#~(call-with-output-file #$output
|
||
(lambda (port)
|
||
(format port
|
||
"udev_rules=\"~a/lib/udev/rules.d\"\n"
|
||
#$rules))))))
|
||
(list
|
||
(shepherd-service
|
||
(provision '(udev))
|
||
|
||
;; Udev needs /dev to be a 'devtmpfs' mount so that new device nodes can
|
||
;; be added: see
|
||
;; <http://www.linuxfromscratch.org/lfs/view/development/chapter07/udev.html>.
|
||
(requirement '(root-file-system))
|
||
|
||
(documentation "Populate the /dev directory, dynamically.")
|
||
(start #~(lambda ()
|
||
(define find
|
||
(@ (srfi srfi-1) find))
|
||
|
||
(define udevd
|
||
;; Choose the right 'udevd'.
|
||
(find file-exists?
|
||
(map (lambda (suffix)
|
||
(string-append #$udev suffix))
|
||
'("/libexec/udev/udevd" ;udev
|
||
"/sbin/udevd")))) ;eudev
|
||
|
||
(define (wait-for-udevd)
|
||
;; Wait until someone's listening on udevd's control
|
||
;; socket.
|
||
(let ((sock (socket AF_UNIX SOCK_SEQPACKET 0)))
|
||
(let try ()
|
||
(catch 'system-error
|
||
(lambda ()
|
||
(connect sock PF_UNIX "/run/udev/control")
|
||
(close-port sock))
|
||
(lambda args
|
||
(format #t "waiting for udevd...~%")
|
||
(usleep 500000)
|
||
(try))))))
|
||
|
||
;; Allow udev to find the modules.
|
||
(setenv "LINUX_MODULE_DIRECTORY"
|
||
"/run/booted-system/kernel/lib/modules")
|
||
|
||
;; The first one is for udev, the second one for eudev.
|
||
(setenv "UDEV_CONFIG_FILE" #$udev.conf)
|
||
(setenv "EUDEV_RULES_DIRECTORY"
|
||
#$(file-append rules "/lib/udev/rules.d"))
|
||
|
||
(let ((pid (primitive-fork)))
|
||
(case pid
|
||
((0)
|
||
(exec-command (list udevd)))
|
||
(else
|
||
;; Wait until udevd is up and running. This
|
||
;; appears to be needed so that the events
|
||
;; triggered below are actually handled.
|
||
(wait-for-udevd)
|
||
|
||
;; Trigger device node creation.
|
||
(system* #$(file-append udev "/bin/udevadm")
|
||
"trigger" "--action=add")
|
||
|
||
;; Wait for things to settle down.
|
||
(system* #$(file-append udev "/bin/udevadm")
|
||
"settle")
|
||
pid)))))
|
||
(stop #~(make-kill-destructor))
|
||
|
||
;; When halting the system, 'udev' is actually killed by
|
||
;; 'user-processes', i.e., before its own 'stop' method was called.
|
||
;; Thus, make sure it is not respawned.
|
||
(respawn? #f)))))))
|
||
|
||
(define udev-service-type
|
||
(service-type (name 'udev)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type
|
||
udev-shepherd-service)))
|
||
|
||
(compose concatenate) ;concatenate the list of rules
|
||
(extend (lambda (config rules)
|
||
(match config
|
||
(($ <udev-configuration> udev initial-rules)
|
||
(udev-configuration
|
||
(udev udev)
|
||
(rules (append initial-rules rules)))))))))
|
||
|
||
(define* (udev-service #:key (udev eudev) (rules '()))
|
||
"Run @var{udev}, which populates the @file{/dev} directory dynamically. Get
|
||
extra rules from the packages listed in @var{rules}."
|
||
(service udev-service-type
|
||
(udev-configuration (udev udev) (rules rules))))
|
||
|
||
(define swap-service-type
|
||
(shepherd-service-type
|
||
'swap
|
||
(lambda (device)
|
||
(define requirement
|
||
(if (string-prefix? "/dev/mapper/" device)
|
||
(list (symbol-append 'device-mapping-
|
||
(string->symbol (basename device))))
|
||
'()))
|
||
|
||
(shepherd-service
|
||
(provision (list (symbol-append 'swap- (string->symbol device))))
|
||
(requirement `(udev ,@requirement))
|
||
(documentation "Enable the given swap device.")
|
||
(start #~(lambda ()
|
||
(restart-on-EINTR (swapon #$device))
|
||
#t))
|
||
(stop #~(lambda _
|
||
(restart-on-EINTR (swapoff #$device))
|
||
#f))
|
||
(respawn? #f)))))
|
||
|
||
(define (swap-service device)
|
||
"Return a service that uses @var{device} as a swap device."
|
||
(service swap-service-type device))
|
||
|
||
(define-record-type* <gpm-configuration>
|
||
gpm-configuration make-gpm-configuration gpm-configuration?
|
||
(gpm gpm-configuration-gpm) ;package
|
||
(options gpm-configuration-options)) ;list of strings
|
||
|
||
(define gpm-shepherd-service
|
||
(match-lambda
|
||
(($ <gpm-configuration> gpm options)
|
||
(list (shepherd-service
|
||
(requirement '(udev))
|
||
(provision '(gpm))
|
||
(start #~(lambda ()
|
||
;; 'gpm' runs in the background and sets a PID file.
|
||
;; Note that it requires running as "root".
|
||
(false-if-exception (delete-file "/var/run/gpm.pid"))
|
||
(fork+exec-command (list #$(file-append gpm "/sbin/gpm")
|
||
#$@options))
|
||
|
||
;; Wait for the PID file to appear; declare failure if
|
||
;; it doesn't show up.
|
||
(let loop ((i 3))
|
||
(or (file-exists? "/var/run/gpm.pid")
|
||
(if (zero? i)
|
||
#f
|
||
(begin
|
||
(sleep 1)
|
||
(loop (1- i))))))))
|
||
|
||
(stop #~(lambda (_)
|
||
;; Return #f if successfully stopped.
|
||
(not (zero? (system* #$(file-append gpm "/sbin/gpm")
|
||
"-k"))))))))))
|
||
|
||
(define gpm-service-type
|
||
(service-type (name 'gpm)
|
||
(extensions
|
||
(list (service-extension shepherd-root-service-type
|
||
gpm-shepherd-service)))))
|
||
|
||
(define* (gpm-service #:key (gpm gpm)
|
||
(options '("-m" "/dev/input/mice" "-t" "ps2")))
|
||
"Run @var{gpm}, the general-purpose mouse daemon, with the given
|
||
command-line @var{options}. GPM allows users to use the mouse in the console,
|
||
notably to select, copy, and paste text. The default value of @var{options}
|
||
uses the @code{ps2} protocol, which works for both USB and PS/2 mice.
|
||
|
||
This service is not part of @var{%base-services}."
|
||
;; To test in QEMU, use "-usbdevice mouse" and then, in the monitor, use
|
||
;; "info mice" and "mouse_set X" to use the right mouse.
|
||
(service gpm-service-type
|
||
(gpm-configuration (gpm gpm) (options options))))
|
||
|
||
(define-record-type* <kmscon-configuration>
|
||
kmscon-configuration make-kmscon-configuration
|
||
kmscon-configuration?
|
||
(kmscon kmscon-configuration-kmscon
|
||
(default kmscon))
|
||
(virtual-terminal kmscon-configuration-virtual-terminal)
|
||
(login-program kmscon-configuration-login-program
|
||
(default (file-append shadow "/bin/login")))
|
||
(login-arguments kmscon-configuration-login-arguments
|
||
(default '("-p")))
|
||
(hardware-acceleration? kmscon-configuration-hardware-acceleration?
|
||
(default #f))) ; #t causes failure
|
||
|
||
(define kmscon-service-type
|
||
(shepherd-service-type
|
||
'kmscon
|
||
(lambda (config)
|
||
(let ((kmscon (kmscon-configuration-kmscon config))
|
||
(virtual-terminal (kmscon-configuration-virtual-terminal config))
|
||
(login-program (kmscon-configuration-login-program config))
|
||
(login-arguments (kmscon-configuration-login-arguments config))
|
||
(hardware-acceleration? (kmscon-configuration-hardware-acceleration? config)))
|
||
|
||
(define kmscon-command
|
||
#~(list
|
||
#$(file-append kmscon "/bin/kmscon") "--login"
|
||
"--vt" #$virtual-terminal
|
||
#$@(if hardware-acceleration? '("--hwaccel") '())
|
||
"--" #$login-program #$@login-arguments))
|
||
|
||
(shepherd-service
|
||
(documentation "kmscon virtual terminal")
|
||
(requirement '(user-processes udev dbus-system))
|
||
(provision (list (symbol-append 'term- (string->symbol virtual-terminal))))
|
||
(start #~(make-forkexec-constructor #$kmscon-command))
|
||
(stop #~(make-kill-destructor)))))))
|
||
|
||
|
||
(define %base-services
|
||
;; Convenience variable holding the basic services.
|
||
(list (login-service)
|
||
|
||
(service console-font-service-type
|
||
(map (lambda (tty)
|
||
(cons tty %default-console-font))
|
||
'("tty1" "tty2" "tty3" "tty4" "tty5" "tty6")))
|
||
|
||
(mingetty-service (mingetty-configuration
|
||
(tty "tty1")))
|
||
(mingetty-service (mingetty-configuration
|
||
(tty "tty2")))
|
||
(mingetty-service (mingetty-configuration
|
||
(tty "tty3")))
|
||
(mingetty-service (mingetty-configuration
|
||
(tty "tty4")))
|
||
(mingetty-service (mingetty-configuration
|
||
(tty "tty5")))
|
||
(mingetty-service (mingetty-configuration
|
||
(tty "tty6")))
|
||
|
||
(service static-networking-service-type
|
||
(list (static-networking (interface "lo")
|
||
(ip "127.0.0.1")
|
||
(provision '(loopback)))))
|
||
(syslog-service)
|
||
(urandom-seed-service)
|
||
(guix-service)
|
||
(nscd-service)
|
||
|
||
;; The LVM2 rules are needed as soon as LVM2 or the device-mapper is
|
||
;; used, so enable them by default. The FUSE and ALSA rules are
|
||
;; less critical, but handy.
|
||
(udev-service #:rules (list lvm2 fuse alsa-utils crda))
|
||
|
||
(service special-files-service-type
|
||
`(("/bin/sh" ,(file-append (canonical-package bash)
|
||
"/bin/sh"))))))
|
||
|
||
;;; base.scm ends here
|