s6-linux-init + Shepherd boot configuration

Warning: this is a personal boot configuration for one host. It hard-codes a disk UUID, a user, Unix groups, the Guix store layout, and specific Shepherd/Guix generations. It is not a portable installer. Borrow individual services at your own risk and audit them first.

AI assistance: produced with opencode, model poolside/laguna-s-2.1 No warranty is provided.

License: all prose and documentation in this repository are CC0 1.0 Universal (no rights reserved). The Scheme, shell, and s6-linux-init scripts are licensed GPL-3.0-or-later. The same license as GNU Shepherd and Guile, keeping derivatives compatible with the rest of the stack. SPDX-License-Identifier: CC0-1.0 OR GPL-3.0-or-later.

Source: you can view in gh:shepherd-void

1. Introduction

This is the only documentation this boot stack needs. It describes, end to end, how a single Void Linux root with a Guix-installed Shepherd 0.10.5 (and a Guile 3.0.9) comes up, and how to add a new service to it.1 Why not just use Guix System’s operating-system /services? Because owning dracut -> s6 -> Shepherd explicitly makes every failure mode legible and avoids Guix SD’s implicit service graph. This repo is that explicit graph.

A machine has one service manager, not three. Here it is #4, with s6 reduced to the smallest thing that makes that possible.

guix build shepherd resolves to the store path of Shepherd; the symlink keeps /usr/local/bin/shepherd stable across profile generations. guix build See the Guix manual for details on profile generation stability.

2. The three layers

+--------------------------------------------+
|         Layer 1: dracut initramfs          |
|  hardware discovery, storage, mount root   |
+--------------------------------------------+
                     |
                     |
                     v
+--------------------------------------------+
|           Layer 2: s6-linux-init           |
|    mount @gnu, @log, mkdir control dirs    |
+--------------------------------------------+
                     |
                     |
                     v
+--------------------------------------------+
|        Layer 3: GNU Shepherd 0.10.5        |
| root-rw, pseudo-filesystems, cgroups, udev |
+--------------------------------------------+

S6’s only job is Layer 2: bring the store and runtime writable, then hand control to Shepherd with (exec). Shepherd then runs the whole graph; s6 does not manage services.

#!/bin/sh -e
# s6 stage 0 -> Shepherd handoff.  Regenerate with:
#   s6-linux-init-maker -b /etc/s6-linux-init/skel <more options>
DISK_UUID=abcd-...

# --- 1. mount what Shepherd/store need (idempotent) ---
mountpoint -q /gnu     || mount -t btrfs -o subvol=@/@gnu,noatime /dev/disk/by-uuid/$DISK_UUID /gnu \
                       || mount -t btrfs -o subvol=@gnu,noatime       /dev/disk/by-uuid/$DISK_UUID /gnu
mountpoint -q /var/log || mount -t btrfs -o subvol=@log,noatime       /dev/disk/by-uuid/$DISK_UUID /var/log
mountpoint -q /var/tmp || mount -t btrfs -o subvol=@tmp,noatime       /dev/disk/by-uuid/$DISK_UUID /var/tmp
mountpoint -q /var/cache || mount -t btrfs -o subvol=@cache,noatime   /dev/disk/by-uuid/$DISK_UUID /var/cache

# --- 2. exec Shepherd (PID 1) ---
SHEPHERD=/usr/local/bin/shepherd    # or /gnu/current-guix-profile/bin/shepherd
[ -x "$SHEPHERD" ] || exec /sbin/agetty --noclear -a root tty1 linux
mkdir -p /run/shepherd
"$SHEPHERD" -c /etc/shepherd/init.scm -s /run/shepherd/socket \
            -l /var/log/shepherd.log -I

The generator writes current/scripts/rc.init from skel/rc.init. Edit the skel copy and re-run s6-linux-init-maker. never patch current directly.

3. Shepherd’s graph

Shepherd does not have a “boot script”. It has a dependency graph and a target symbol that means “everything that must be up”; starting that symbol resolves and runs the whole graph in parallel.

The targets live in services.d/28-boot-stages.scm 2 Shepherd has no runlevel. A stage here is a service whose #:start is (const #t) i.e. a named barrier whose only purpose is its #:requirement, so the DAG is readable from one file. See info:(shepherd)Service Types and services.d/28-boot-stages.scm.

;; 28-boot-stages.scm --- named barriers for the boot graph.

(define boot-critical
  '(root-rw pseudo-filesystems runtime-directories
    static-device-nodes kernel-modules cgroups udev udev-settle
    file-systems tmpfs swap sysctl dmesg guix-daemon))

(define boot-ready
  '(boot-critical hostname log-files syslog random-seed
    console-setup timezone hwclock))

(define interactive
  '(boot-ready agetty-tty1 agetty-tty2 agetty-tty3
    agetty-tty4 agetty-tty5 agetty-tty6))

(define fully-online
  '(interactive loopback dbus polkit elogind seatd acpid
    network-manager zram openssh chrony))

Boot DAG (what actually gates the gettys):

                          root-rw
                       ───┬──────┬──────────────┐
         pseudo-filesystems cgroups    runtime-dirs
       (proc sys run dev ┌──cgroup2  (mkdir /run, /run/user
        dev-pts dev-shm  └──cgroup-v1 runtime-dirs)
        securityfs mqueue              │
        efivarfs)                      │
                       udev ─ udev-settle ─────────┤
                          │ static-device-nodes    │
                   file-systems ─ swap ────────────┤
                          │ hostname               │
                   log-files ─ syslog ─────────────┤
                 kernel-modules ─ random-seed ─────┤
                          │                        │
                      boot-ready ─ agetty-tty1..6 ─┤
                          │                        │
                interactive ─ loopback ─ dbus ─────┤
                          │            polkit  │   │
                 fully-online ─ network-manager ───┘

                   seatd · acpid · zram · ssh · chrony

The solid lines are #:requirement edges. Gettys depend on boot-ready, not on network/D-Bus/desktop, so a slow link or GUI never blanks the login prompt. udev is eudev from Void (Shepherd just supervises it); device nodes are pre-seeded by 05-static-devnodes.scm for udev’s very first run. Udev (ArchWiki) covers the rules format.

4. Add a service (template + worked example)

Every service is a (service '(provides) #:requirement '(...) #:start ...). That is the configuration: declare what it is, what it waits on, and what it does, and never a serial start ladder.

Checklist:

  1. Add the file (zero-padded NN sorts before 28-boot-stages).
  2. Pick real provides (a list of symbols; not a string, not a list-in-a-list) and the minimal #:requirement.
  3. For daemons: argv list + #:log-file + make-kill-destructor + #:respawn? #t.
  4. For one-shots: use a pure Guile (lambda _ (zero? (system* ...))) or ~run-or-ignored~ + #:one-shot? #t + #:stop (const #t).
  5. Wire it into a stage: edit the #:requirement of interactive or fully-online’s stage in 28-boot-stages if it should be part of one.3 The stage #:requirement list is the “what must be up before X is considered up” contract. Add your symbol there to make it part of an ordered phase; leave it out and it only starts if something else pulls it in.
  6. Reload: herd load init.scm. Shepherd restarts only what changed; already-running services are untouched.

4.1. Template for daemon(ssh, etc.)

;; NN-name.scm --- one line.

;; Why it exists: <sentence>.  Why that dependency list: <edges>.
(define name
  (service '(name alias)
    #:documentation "Human sentence."
    #:requirement '(the-services-it-cannot-run-without)
    #:start  (make-forkexec-constructor '("name" "args" …)
                  #:log-file "/var/log/name.log")
    #:stop   (make-kill-destructor)
    #:respawn? #t))            ; one-shot? #t for transient steps

(register-services (list name))

4.2. Template for One-Shots (mkdir, swapon, sysctl, etc.):

;;; NN-name.scm --- one-shot setup task.

(define name
  (service '(name)
    #:documentation "Human sentence."
    #:requirement '(the-services-it-cannot-run-without)
    #:start (lambda _
              ;; Pure Guile: no shell scripts, no make-system-constructor
              (zero? (system* "command" "arg1" "arg2")))
    #:stop (const #t)
    #:one-shot? #t))

(register-services (list name))

4.3. example: syncthing as a system service (4 Syncthing is a fine fully-online citizen: network-bound, not login-critical, restart-on-crash via #:respawn? #t. The example intentionally uses the system instance; for a desktop session move it to user-shepherd, set HOME and XDG_CONFIG_HOME, and require seatd. ):

;;; 31-syncthing-example.scm --- Syncthing daemon.

;; Example service for the "how to add a service" guide in readme.org.
;; It is kept under examples/, NOT services.d/, so it is registered only when
;; you deliberately copy it in:
;;
;;   cp examples/31-syncthing-example.scm /etc/shepherd/services.d/
;;   # then add `syncthing' to the `fully-online' stage in 28-boot-stages.scm
;;   herd load init.scm
;;
;; Syncthing is a good fully-online citizen: network-bound, not login-critical,
;; and restart-on-crash via #:respawn?.  Its systemd unit ties it to
;; `network-online.target` + `local-fs.target`, which here map to
;; `network-manager` + `file-systems`.

(define syncthing
  (service '(syncthing)
    #:documentation "Syncthing file synchronizer (system instance)."
    #:requirement '(network-manager file-systems)
    #:start (make-forkexec-constructor
             '("/usr/bin/syncthing" "-no-browser" "-gui-address=0.0.0.0:8384")
             #:log-file "/var/log/syncthing.log")
    #:stop (make-kill-destructor)
    #:respawn? #t))

(register-services (list syncthing))

To enable: cp the file into /etc/shepherd/services.d/, add syncthing to the fully-online stage requirements in 28-boot-stages.scm, then herd load init.scm. A new syncthing service appears in herd status with no Shepherd restart.5 syncthing/README · its systemd unit network-online.target + local-fs.target maps exactly to network-manager + file-systems here.

Syncthing ships a systemd unit that does the same network-online + filesystem dance; this is the manual port. For a per-user GUI session, put the same service under user-shepherd/services.d/ with GUIX_PROFILE set.

5. Runtime model (what you observe at boot)

  • /etc/shepherd/init.scm loads services.d/*.scm, then (start-in-the-background '(fully-online)).
  • Shepherd resolves requirements breadth-first and starts them concurrently where the graph allows.6 start-in-the-background (from services.d/init.scm) spawns a fiber that calls start-in-parallel: each node’s #:requirement starts first, and siblings with no edge between them start concurrently. Shepherd manual §“Dependency resolution”. You see interleaved Starting service X / Service X has been started. lines in /var/log/shepherd.log.
  • A service whose #:start throws is marked failed; dependents are skipped / retried, logged, and the boot continues on other branches. one bad daemon never hangs the whole boot.7 Failures are per-service. Shepherd logs Exception caught while starting X to /var/log/shepherd.log, marks the service failed, and continues other branches rather than aborting the boot. That is intentional: a one-shot random-seed failing must not stop udev.
  • boot-ready is the barrier behind which gettys wait; fully-online is everything else that is allowed to lag.
  • On shutdown, services run their #:stop thunks in reverse-dependency order (e.g., seedrng save, hwclock --systohc, cleaning /tmp).
$ herd status                   # on the running machine
root                            (running)   uptime 00:00:47
udev                            (running)   uptime 00:00:46
syslog                          (running)   uptime 00:00:45
boot-ready                      (stopped)   start it to unblock login
agetty-tty1                     (running)   uptime 00:00:44
fully-online                    (stopped)   start it to bring the rest
$ tail -f /var/log/shepherd.log  # the spinner feeds this
2026-... Starting service udev...
2026-... Service udev has been started.

Shepherd also exposes herd = the CLI. =herd help, herd doc <service>, and [[info:(shepherd)Invoking shepherd][info:(shepherd)]] cover every flag.

6. Layout

shepherd/
├── readme.org                    <- you are here
├── init.scm                      <- Shepherd config entry point
├── services.d/
│   ├── 00-utilities.scm          <- shared helpers (mounted?, read-rc-conf-var)
│   ├── 01-root.scm               <- root-rw remount
│   ├── 02-pseudo-fs.scm          <- proc/sys/run/dev/+
│   ├── 02-tmpfs.scm              <- mount /tmp as tmpfs
│   ├── 03-runtime.scm            <- early runtime directories
│   ├── 03-console.scm            <- KEYMAP/FONT from /etc/rc.conf
│   ├── 04-cgroups.scm            <- unified | hybrid | legacy
│   ├── 05-static-devnodes.scm
│   ├── 06-kernel-modules.scm
│   ├── 07-udev.scm               <- udev + udev-settle
│   ├── 08-file-systems.scm       <- mount -a
│   ├── 09-hostname.scm
│   ├── 10-sysctl.scm
│   ├── 10-timezone.scm           <- /etc/localtime symlink
│   ├── 11-hwclock.scm            <- RTC sync on boot/shutdown
│   ├── 12-dmesg.scm
│   ├── 13-random-seed.scm        <- seedrng (now saves on shutdown)
│   ├── 14-log-files.scm          <- utmp/wtmp + socklog spool
│   ├── 15-syslog.scm             <- socklog -> /dev/log
│   ├── 16-guix-daemon.scm
│   ├── 17-getty.scm              <- agetty-tty1..6 (--skip-login, --noissue)
│   ├── 18-loopback.scm
│   ├── 19-dbus.scm               <- dbus + dbus-runtime dir
│   ├── 20-network.scm            <- NetworkManager
│   ├── 21-polkit.scm
│   ├── 22-elogind.scm            <- D-Bus-activated, one-shot pin
│   ├── 23-seatd.scm
│   ├── 24-acpid.scm
│   ├── 25-zram.scm               <- zram swap (pure Guile sysfs writes)
│   ├── 26-openssh.scm
│   ├── 27-chrony.scm
│   └── 28-boot-stages.scm        <- boot-critical -> boot-ready -> interactive -> fully-online
├── s6-linux-init/
│   └── skel/rc.init              <- stage-0 handoff (edit here)
├── user-shepherd
│   ├── init.scm
│   └── services.d
│       ├── 00-utilities.scm
│       ├── 01-runtime-dirs.scm
│       ├── 02-dbus.scm
│       ├── 03-pipewire.scm
│       ├── 04-gpg-agent.scm
│       ├── 05-sway.scm
│       ├── 06-graphical-session.scm
│       ├── 07-portals.scm
│       ├── 08-dunst.scm
│       ├── 09-swayidle.scm
│       ├── 10-sway-audio-idle-inhibit.scm
│       ├── 11-wlsunset.scm
│       ├── 12-clipman.scm
│       └── 13-azotebg.scm

7. User Shepherd (Graphical Session)

While the system Shepherd manages the machine, the User Shepherd manages your graphical session (Sway, PipeWire, D-Bus, etc.). It runs as your regular user, supervising Wayland compositors and desktop daemons without needing systemd --user.

7.1. Session Flow

  1. init.scm sets XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS.
  2. 01-runtime-dirs ensures the runtime directory exists with strict 0700 permissions.
  3. 02-dbus, 03-pipewire, and 04-gpg-agent start in parallel (no GUI dependency).
  4. 05-sway starts the compositor (depends on dbus and runtime-dirs).
  5. 06-graphical-session barrier resolves once Sway is running.
  6. ALL GUI services (dunst, portals, swayidle, clipman, etc.) start in parallel.

7.2. Installation & Bring Up

# 1. Copy user Shepherd config and services to your home directory
mkdir -p ~/.config/shepherd/services.d
cp user-shepherd/init.scm ~/.config/shepherd/init.scm
cp user-shepherd/services.d/*.scm ~/.config/shepherd/services.d/

# 2. Ensure you are in the 'seatd' group (required for Sway to access input/DRM)
sudo usermod -aG _seatd $USER
# (Log out and log back in for group changes to take effect)

# 3. Start the user Shepherd daemon
shepherd -s /run/shepherd/$(id -u).socket \
    -c ~/.config/shepherd/init.scm -I

To start User Shepherd automatically on TTY login, add this to your ~/.bash_profile or ~/.profile: if [ -z "$WAYLAND_DISPLAY" ] && [ "$XDG_VTNR" -eq 1 ]; then exec shepherd -I; fi

7.3. Managing the Session

Because every component is a first-class Shepherd service, you can manage them individually without restarting the whole session:

# Restart just the compositor (apps using DBus/PipeWire will survive!)
herd restart sway

# Check audio stack status
herd status pipewire wireplumber pipewire-pulse

# View compositor logs
tail -f ~/.local/state/shepherd/sway.log

8. Install & bring up (manual)

# 1. Shepherd config    ->    where rc.init expects it
cp services.d/*.scm           /etc/shepherd/services.d/
cp init.scm                  /etc/shepherd/init.scm

# 2. Shepherd binary visible to rc.init (one of:)
ln -s $(guix build shepherd)/bin/shepherd /usr/local/bin/shepherd       # Guix
# or:  ln -s $(which shepherd) /usr/local/bin/shepherd                  # Void

# 3. s6 stage 0 from the skel (regenerates current/scripts/rc.init)
s6-linux-init-maker -b /etc/s6-linux-init/skel \
  -u shepherd -g shepherd \
  -s /run/s6-svscan \
  -m /etc/s6-linux-init/monitor

# 4. Boot it (or reboot)
exec /etc/s6-linux-init/current/scripts/rc.init default

guix build shepherd resolves to the store path of Shepherd; the symlink keeps /usr/local/bin/shepherd stable across profile generations. guix build

9. Appendix: configuration reference (source of truth)

This appendix embeds the actual Scheme configuration so this readme, carries the source of truth alongside the prose above. Each file leads with a header comment describing its role, and services carry a #:documentation string

9.1. System Shepherd entry point

(define-module (shepherd config) #:declarative? #f)
(use-modules ((ice-9 ftw) #:select (scandir))
             (srfi srfi-1))

;; Where this configuration lives.  Default to the standard location; the
;; s6 ~rc.init~ script points Shepherd here via ~-c /etc/shepherd/init.scm~.
(define %services-dir
  (or (getenv "SHEPHERD_SERVICES_DIR")
      "/etc/shepherd/services.d"))

;; 1. Load every *.scm (alphabetically) from the services directory.
;;    Each file registers its own services; ~#:requirement~ may reference a
;;    service defined in an earlier-loaded file (dependencies are resolved
;;    lazily at start time, so load order only needs shared helpers first --
;;    which is why 00-utilities.scm sorts before everything else).  The stage
;;    targets (boot-ready, interactive, fully-online, ...) live in
;;    ~28-boot-stages.scm~.
(define (load-services dir)
  (let ((files (scandir dir
                  (lambda (f)
                    (and (string-suffix? ".scm" f)
                         (not (string-prefix? "." f)))))))
    ;; Guard: warn about old-style (unnumbered) files that shadow numbered
    ;; ones.  Digits sort before letters in ASCII (0x30 < 0x61), so
    ;; 15-syslog.scm loads BEFORE socklog.scm.  The unnumbered file then
    ;; silently overrides the canonical definition -- this was the root cause
    ;; of the socklog 127 / respawn-too-fast cascades on the Void host.
    (let ((stale (filter (lambda (f)
                           (not (char-numeric? (string-ref f 0))))
                         files)))
      (when (pair? stale)
        (format #t "WARNING: old-style (unnumbered) service files shadow numbered files:\n  ~a\nThese load AFTER the numbered files and silently override them.\nRemove them: rm ~a/*.scm (the unnumbered ones)\n\n"
                (string-join stale ", ") dir)))
    (for-each
     (lambda (file)
       (load (string-append dir "/" file)))
     files)))

(load-services %services-dir)

;; 2. Self-check policy.
;;    Shepherd 0.10.x exposes no public "list all services" API from a config
;;    file (the registry is channel-based and private), so a static
;;    "missing-requirements" scan cannot be done here inline.  Instead:
;;      * graph integrity (no cycles, no dangling requirements) is verified by
;;        the off-line validator (~validate-guile.scm~), run by the author;
;;      * at boot, ~start-in-the-background~ resolves the ~fully-online~ graph
;;        and Shepherd throws/logs an ~unresolved requirement' (or
;;        ~Unregistered service') to /var/log/shepherd.log for any symbol that
;;        is required but never provided -- i.e. boot still fails fast and
;;        loud, just one layer inside the daemon.
;;    This keeps init.scm free of version-coupled internal accessors.

;; 3. Kick off boot by resolving the ~fully-online~ target's graph.  Placed at
;;    the end of init.scm on purpose: Shepherd evaluates this file once at
;;    startup (launched from s6 stage 0) and starts the machine.  ~start-in-the
;;    background' returns immediately so the control socket is served right
;;    away; already-running services are left untouched on a later
;;    ~herd load init.scm~.  (shepherd/service) exports start-in-the-background,
;;    NOT a bare ~start'.
(unless (equal? (getenv "SHEPHERD_NO_AUTOBOOT") "1")
  (start-in-the-background '(fully-online)))

9.2. System service definitions

;;; 00-utilities.scm --- shared helpers (loaded first; registers nothing).
;; Because ~scandir~ in init.scm loads every *.scm file, these helpers are
;; available to every service below.  They encode the small, repeated pieces
;; (mount-option formatting, idempotent mountpoint checks) so that individual
;; service files read like declarative data, not shell.

(use-modules (srfi srfi-1) ; filter
             (shepherd service))

(define (opts->string opts)
  "Render ~('nosuid', 'nodev', ...)~ as ~-o nosuid,nodev~.
The empty / ~('default')~ list renders as ''."
  (if (or (null? opts) (equal? opts '("defaults")))
      ""
      (string-append " -o " (string-join opts ","))))

(define (mountpoint-q? target)
  "Return #t when TARGET is already a mount point."
  (zero? (system (string-append "mountpoint -q " target))))

(define (ensure-dir dir)
  "Create DIR and parents."
  (zero? (system* "mkdir" "-p" dir)))

(define (run-or-ignored command . args)
  "Run the primitive COMMAND (an absolute path) with ARGS, treating absence or
a non-zero exit as success -- i.e. the Scheme equivalent of the shell idiom
~(command ... 2>/dev/null || true)' used for best-effort one-shots such as
swapon or seedrng.  Returns a truthy value so Shepherd records success."
  (catch 'system-error
    (lambda () (apply system* command args))
    (lambda _ #t)))

(define (mounted? dir)
  (catch 'system-error
    (lambda ()
      (let ((st-dir (stat dir))
            (st-parent (stat (dirname dir))))
        (not (= (stat:dev st-dir) (stat:dev st-parent)))))
    (lambda args
      ;; If the directory doesn't exist yet, it's not mounted.
      #f)))

(define* (pseudo-fs-service name
                            #:key
                            source
                            target
                            type
                            (options '())
                            (requirement '()))
  (service
   (list name)
   #:documentation
   (format #f "Mount pseudo-filesystem ~a on ~a~a."
           type target
           (if (null? options) ""
               (format #f " (options: ~{~a~^,~})" options)))
   #:requirement requirement
   #:start
   (lambda _
     ;; Build the mount command arguments purely as a Scheme list.
     (let ((args (append `("-t" ,type)
                         (if (null? options)
                             '()
                             `("-o" ,(string-join options ",")))
                         `(,source ,target))))
       ;; Attempt to mount. If it fails, check if it's already mounted.
       ;; This makes the service perfectly idempotent without shell scripts.
       (or (zero? (apply system* "mount" args))
           (mounted? target))))
   #:stop (const #t)
   #:one-shot? #t))

;; A descriptive, non-working target service grouping several requirements.
;; Used purely to give the boot graph readable named layers.
(define* (stage name documentation requirements)
  ;; If name is already a list, use it; otherwise, wrap the symbol in a list.
  (service (if (list? name) name (list name))
           #:documentation documentation
           #:requirement requirements
           #:start (const #t)
           #:stop (const #t)
           #:one-shot? #t))

(define (read-rc-conf-var var)
  "Read a variable from /etc/rc.conf (e.g., KEYMAP='us')."
  (catch 'system-error
    (lambda ()
      (call-with-input-file "/etc/rc.conf"
        (lambda (port)
          (let loop ((line (read-line port)))
            (cond
             ((eof-object? line) #f)
             ((string-prefix? (string-append var "=") (string-trim-both line))
              (let* ((val (substring line (+ (string-length var) 1)))
                     (val (string-trim-both val)))
                ;; Strip surrounding quotes if present
                (if (and (>= (string-length val) 2)
                         (char=? (string-ref val 0) #\")
                         (char=? (string-ref val (- (string-length val) 1)) #\"))
                    (substring val 1 (- (string-length val) 1))
                    val)))
             (else (loop (read-line port))))))))
    (lambda args #f)))
;;; 01-root.scm --- root filesystem remount.

;; ~root-rw~ is intentionally the earliest service: it has NO requirements.
;; The previous config made it depend on ~hostname~, but root-rw -> hostname
;; is not semantically justified (the hostname is set from /etc which lives
;; on root, and nothing on root-rw depends on the hostname).  Instead,
;; pseudo-filesystem services depend on ~root-rw~, expressing the real edge.

(define root-rw
  (service '(root-rw root-filesystem)
           #:documentation "Remount the root file system read/write."
           #:requirement '()
           #:start (lambda _
                     (setenv "LIBMOUNT_FORCE_MOUNT2" "always")
                     (or (zero? (system* "mount" "-o" "remount,rw" "/"))
                         #t))
           #:stop (const #t) ;; Let the kernel handle ro remount on poweroff
           #:one-shot? #t))

(register-services (list root-rw))
;;; 02-pseudo-fs.scm --- independent pseudo-filesystem services.

;; Replaces the old monolithic ~pseudo-filesystems~ service.  Each pseudo
;; filesystem is its own service so they mount in parallel and so a service
;; can depend on exactly the one it needs (e.g. agetty needs dev-pts).
;;
;; Dependency graph (all under root-rw, all parallelizable):
;;   root-rw
;;    ├── proc
;;    ├── sys    └── securityfs
;;    ├── run
;;    └── dev ── dev-pts / dev-shm   (+ mqueue, efivarfs under sys/dev)

(define pseudo-fs-proc
  (pseudo-fs-service 'proc
                     #:source "proc" #:target "/proc" #:type "proc"
                     #:options '("nosuid" "nodev" "noexec")
                     #:requirement '(root-rw)))

(define pseudo-fs-sys
  (pseudo-fs-service 'sys
                     #:source "sysfs" #:target "/sys" #:type "sysfs"
                     #:options '("nosuid" "nodev" "noexec")
                     #:requirement '(root-rw)))

(define pseudo-fs-run
  (pseudo-fs-service 'run
                     #:source "run" #:target "/run" #:type "tmpfs"
                     #:options '("mode=0755" "nosuid" "nodev" "size=32M")
                     #:requirement '(root-rw)))

(define pseudo-fs-dev
  (pseudo-fs-service 'dev
                     #:source "devtmpfs" #:target "/dev" #:type "devtmpfs"
                     #:options '("nosuid" "mode=755")
                     #:requirement '(root-rw)))

(define pseudo-fs-dev-pts
  (pseudo-fs-service 'dev-pts
                     #:source "devpts" #:target "/dev/pts" #:type "devpts"
                     #:options '("mode=0620" "gid=5" "nosuid" "noexec")
                     #:requirement '(dev)))

(define pseudo-fs-dev-shm
  (pseudo-fs-service 'dev-shm
                     #:source "shm" #:target "/dev/shm" #:type "tmpfs"
                     #:options '("mode=1777" "nosuid" "nodev")
                     #:requirement '(dev)))

(define pseudo-fs-securityfs
  (pseudo-fs-service 'securityfs
                     #:source "securityfs" #:target "/sys/kernel/security" #:type "securityfs"
                     #:options '("nosuid" "nodev" "noexec")
                     #:requirement '(sys)))

(define pseudo-fs-mqueue
  (service '(mqueue)
           #:documentation "Mount /dev/mqueue."
           #:requirement '(dev)
           #:start (lambda _
                     (system* "mkdir" "-p" "/dev/mqueue")
                     (zero? (system* "mount" "-t" "mqueue" "none" "/dev/mqueue")))
           #:stop (const #t)
           #:one-shot? #t))

(define pseudo-fs-efivarfs
  (service '(efivarfs)
           #:documentation "Mount efivarfs (UEFI/FAT EFI only)."
           #:requirement '(sys)
           #:start (lambda _
                     (if (file-exists? "/sys/firmware/efi/efivars")
                         (begin
                           (system* "mkdir" "-p" "/sys/firmware/efi/efivars")
                           (zero? (system* "mount" "-t" "efivarfs" "efivarfs" "/sys/firmware/efi/efivars")))
                         #t)) ;; Safe no-op on BIOS
           #:stop (const #t)
           #:one-shot? #t))

;; Descriptive overlay target used by the boot stages.
(define pseudo-filesystems
  (stage 'pseudo-filesystems
         "All early pseudo-filesystems are mounted."
         '(proc sys run dev dev-pts dev-shm securityfs mqueue efivarfs)))

(register-services
 (list pseudo-fs-proc pseudo-fs-sys pseudo-fs-run pseudo-fs-dev
       pseudo-fs-dev-pts pseudo-fs-dev-shm pseudo-fs-securityfs
       pseudo-fs-mqueue pseudo-fs-efivarfs
       pseudo-filesystems))
;;; 06-tmpfs.scm --- mount /tmp as tmpfs.

(define tmpfs-tmp
  (service '(tmpfs tmp)
           #:documentation "Mount /tmp as a tmpfs filesystem."
           #:requirement '(root-rw)
           #:start (lambda _
                     ;; Ensure the mount point exists
                     (system* "mkdir" "-p" "/tmp")
                     ;; Mount with strict security and 50% RAM limit
                     (and (zero? (system* "mount" "-t" "tmpfs"
                                          "-o" "mode=1777,nosuid,nodev,size=50%"
                                          "tmpfs" "/tmp"))
                          ;; Belt-and-suspenders: enforce permissions
                          (zero? (system* "chmod" "1777" "/tmp"))))
           #:stop (lambda _ (zero? (system* "umount" "/tmp")))
           #:one-shot? #t))

(register-services (list tmpfs-tmp))
(define console-setup
  (service '(console-setup)
           #:documentation "Set console font and keyboard map from /etc/rc.conf."
           #:requirement '(dev)
           #:start (lambda _
                     (let ((keymap (read-rc-conf-var "KEYMAP"))
                           (font (read-rc-conf-var "FONT")))
                       (when keymap
                         (system* "loadkeys" "-q" "-u" keymap))
                       (when font
                         (system* "setfont" font)))
                     #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list console-setup))
;;; 03-runtime.scm --- runtime directory creation (mounts live elsewhere).

;; Mounting and runtime-directory creation are separate responsibilities, so
;; this service only creates the well-known /run and /var/log runtime spots
;; that several daemons depend on.  It performs no mount.

(define runtime-directories
  (service '(runtime-directories)
           #:documentation "Create early runtime directories under /run, /var/log."
           #:requirement '(run root-rw)
           #:start (lambda _
                     (apply system* "mkdir" "-p"
                            '("/run/runit" "/run/udev" "/run/systemd" "/run/user" "/run/lock"
                              "/run/shepherd" "/var/log/shepherd" "/var/log/socklog"))
                     #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list runtime-directories))
;;; 04-cgroups.scm --- cgroup hierarchy mounting.

;; The old setup shipped a large shell block switching on CGROUP_MODE in the
;; middle of a ~while read~ loop over /proc/cgroups.  Here the mode decision
;; and the controller enumeration live in Scheme; only each individual ~mount~
;; is a single small shell command guarded by an idempotency check.

(use-modules (srfi srfi-1)   ; filter, filter-map
             (shepherd service))

;; mkdir-p is NOT exported by (ice-9 ftw) in Guile 3.0.9 from Guix store.
;; Use system* "mkdir" "-p" instead — this is the robust, portable approach.
(define (ensure-dir dir)
  (zero? (system* "mkdir" "-p" dir)))

(define %cgroup-mode
  ;; Mirrors Void's /etc/rc.conf CGROUP_MODE (default: unified; this box).
  (or (getenv "CGROUP_MODE") "unified"))

(define (mountpoint-q? target)
  (zero? (system (string-append "mountpoint -q " target))))

(define (enabled-cgroup-v1-controllers)
  "Return the names of cgroup v1 controllers flagged ~enabled=1~ in
/proc/cgroups.  Returns the empty list when the file is absent."
  (catch 'done
    (lambda ()
      (let ((port (false-if-exception (open-input-file "/proc/cgroups"))))
        (if port
            (let ((lines '()))
              (do ((line (get-line port) (get-line port)))
                  ((eof-object? line) (set! lines (reverse lines)))
                (set! lines (cons line lines)))
              (close-input-port port)
              (filter-map
               (lambda (line)
                 (let ((f (filter (lambda (s) (not (string=? s "")))
                                  (string-split line char-whitespace))))
                   (and (>= (length f) 4)
                        (string=? (cadddr f) "1")
                        (car f))))
               lines))
            (throw 'done '()))))
    (lambda _ '())))

(define (mount-node type source target opts)
  "Mount a cgroup node, idempotently: skip when already a mount point."
  (unless (mountpoint-q? target)
    (ensure-dir target)
    (system (string-append "mount -t " type
                           (if (null? opts) ""
                               (string-append " -o " (string-join opts ",")))
                           " " source " " target))))

(define (cgroup2-boot)
  "Mount the cgroup v2 hierarchy on unified and hybrid modes (no-op legacy)."
  (when (not (string=? %cgroup-mode "legacy"))
    (mount-node "cgroup2" "cgroup2" "/sys/fs/cgroup" '("nsdelegate"))))

(define (cgroup-v1-boot)
  "Mount cgroup v1 tmpfs + per-controller hierarchies (legacy/hybrid only)."
  (when (member %cgroup-mode '("legacy" "hybrid"))
    (unless (mountpoint-q? "/sys/fs/cgroup")
      (ensure-dir "/sys/fs/cgroup")
      (system "mount -t tmpfs cgroup_root /sys/fs/cgroup"))
    (for-each
     (lambda (controller)
       (mount-node "cgroup" controller
                   (string-append "/sys/fs/cgroup/" controller) '()))
     (enabled-cgroup-v1-controllers))
    ;; systemd-tracking cgroup, for nested systemd instances.
    (mount-node "cgroup" "none,name=systemd" "/sys/fs/cgroup/systemd" '())))

(define cgroup2
  (service '(cgroup2 cgroup-hierarchy)
           #:documentation "Mount the cgroup v2 hierarchy at /sys/fs/cgroup."
           #:requirement '(sys runtime-directories)
           #:start (lambda _ (cgroup2-boot) #t)
           #:stop (const #t)
           #:one-shot? #t))

(define cgroup-v1
  (service '(cgroup-v1)
           #:documentation
           "Mount cgroup v1 hierarchies (legacy/hybrid only; no-op on unified)."
           #:requirement '(sys runtime-directories cgroup2)
           #:start (lambda _ (cgroup-v1-boot) #t)
           #:stop (const #t)
           #:one-shot? #t))

;; Descriptive overlay: the single ~cgroups~ name referenced by boot-ready.
(define cgroups
  (stage 'cgroups "cgroup hierarchies are in place." '(cgroup2 cgroup-v1)))

(register-services (list cgroup2 cgroup-v1 cgroups))
;;; 05-static-devnodes.scm --- load modules backing static /dev nodes.

(use-modules (ice-9 popen)  ;; open-pipe* close-pipe
             (ice-9 rdelim) ;; read-line
             (srfi srfi-13) ;; string things
             )

(define (load-static-devnodes)
  (let ((port (open-pipe* OPEN_READ "kmod" "static-nodes" "-f" "devname")))
    (catch 'system-error
      (lambda ()
        (let loop ((line (read-line port)))
          (unless (eof-object? line)
            (let ((parts (string-split line #\space)))
              (unless (null? parts)
                (let ((mod (string-trim-both (car parts))))
                  (unless (string-null? mod)
                    (system* "modprobe" "-bq" mod)))))
            (loop (read-line port)))))
      (lambda args #f))
      (catch 'system-error
        (lambda () (close-pipe port))
        (lambda args #f))))

(define static-device-nodes
  (service '(static-device-nodes)
           #:documentation "Load device drivers backing the static nodes in devtmpfs."
           #:requirement '(runtime-directories)
           #:start (lambda _ (load-static-devnodes) #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list static-device-nodes))
;;; 06-kernel-modules.scm --- load modules from modules-load.d + /etc/modules.

(use-modules (ice-9 rdelim) ;; read-line
             (srfi srfi-13)  ;; string things
             )

(define (load-modules-from-file file)
  (when (file-exists? file)
    (call-with-input-file file
      (lambda (port)
        (let loop ((line (read-line port)))
          (unless (eof-object? line)
            (let ((trimmed (string-trim-both line)))
              (unless (or (string-null? trimmed)
                          (string-prefix? "#" trimmed))
                (system* "modprobe" trimmed)))
            (loop (read-line port))))))))

(define kernel-modules
  (service '(kernel-modules)
           #:documentation "Load kernel modules listed by modules-load and /etc/modules."
           #:requirement '(proc runtime-directories)
           #:start (lambda _
                     (system* "modules-load")
                     (load-modules-from-file "/etc/modules")
                     #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list kernel-modules))
;;; 07-udev.scm --- device event daemon + initial enumeration.

;; udevd itself is a long-running daemon managed by Shepherd (respawn).
;; Triggering rules and settling the queue are a separate one-shot, so the
;; boot graph can express "udev is running" vs "udev finished the initial
;; device pass".

(define udev
  (service '(udev)
           #:documentation "udev device event-management daemon (udevd)."
           #:requirement '(root-rw dev cgroup2
                           static-device-nodes kernel-modules runtime-directories)
           #:start (make-forkexec-constructor
                    '("/usr/sbin/udevd" "--debug")
                    #:log-file "/var/log/udevd.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(define udev-settle
  (service '(udev-settle)
           #:documentation "Trigger udev rules and wait for the queue to settle."
           #:requirement '(udev)
           #:start (lambda _
                     (system* "udevadm" "trigger" "--action=add" "--type=subsystems")
                     (system* "udevadm" "trigger" "--action=add" "--type=devices")
                     (zero? (system* "udevadm" "settle")))
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list udev udev-settle))
;;; 08-file-systems.scm --- mount local filesystems from /etc/fstab.

;; ~mount -a~ is idempotent: already-mounted entries are skipped, so the
;; btrfs subvolumes that s6 stage 0 already mounted (home, /gnu, /var/log...)
;; are left alone.  We never hard-fail boot on a single mount error (matches
;; Void's behavior for optional/media devices).

(define file-systems
  (service '(file-systems)
           #:documentation "Mount all non-network filesystems from /etc/fstab."
           ;; We depend on tmpfs so /tmp is ready before apps try to use it
           #:requirement '(root-rw tmpfs run dev udev-settle)
           #:start (lambda _
                     (zero? (system* "mount" "-a" "-t" "nosysfs,nonfs,nonfs4,nosmbfs,nocifs")))
           #:stop (lambda _
                    (zero? (system* "umount" "-a" "-t" "nosysfs,nonfs,nonfs4,nosmbfs,nocifs")))
           #:one-shot? #t))

(register-services (list file-systems))
;;; 09-hostname.scm --- set the kernel hostname from /etc/hostname.

(use-modules (ice-9 rdelim) ;; read-line
             (srfi srfi-13)  ;; string things
             )

(define hostname
  (service '(hostname)
           #:documentation "Set the system hostname from /etc/hostname."
           #:requirement '(root-rw)
           #:start (lambda _
                     (let ((h (catch 'system-error
                                (lambda ()
                                  (let ((line (call-with-input-file "/etc/hostname" read-line)))
                                    (if (or (eof-object? line) (string-null? (string-trim-both line)))
                                        "machine"
                                        (string-trim-both line))))
                                (lambda args "machine"))))
                       (call-with-output-file "/proc/sys/kernel/hostname"
                         (lambda (port) (display h port)))
                       (system* "hostname" h))
                     #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list hostname))
;; 10-hwclock.scm --- hwclock set to utc

(define hwclock
  (service '(hwclock)
           #:documentation "Sync hardware clock with system time."
           #:requirement '(root-rw)
           #:start (lambda _
                     ;; Read hardware clock and set system time.
                     ;; Use --utc if your BIOS is set to UTC (recommended),
                     ;; or --localtime if your BIOS is set to local time.
                     (system* "hwclock" "--systz" "--utc")
                     #t)
           #:stop (lambda _
                    ;; Write system time to hardware clock on shutdown.
                    (system* "hwclock" "--systohc" "--utc")
                    #t)
           #:one-shot? #t))

(register-services (list hwclock))
;;; 10-sysctl.scm --- apply sysctl settings.
;; sysctl is a boot-critical concern (networking knobs, fs limits), but it
;; does not gate login, so it is kept out of the ~boot-ready~ barrier and may
;; run in parallel with the TTYs being brought up.

(use-modules (ice-9 ftw)     ;; scandir, file-is-directory?
             (srfi srfi-13)  ;; string things
             )


(define sysctl
  (service '(sysctl)
           #:documentation "Load /etc/sysctl.conf and /etc/sysctl.d/*.conf."
           #:requirement '(root-rw sys file-systems)
            #:start (lambda _
                      (define (conf-dir? dir)
                        (and (false-if-exception (file-exists? dir))
                             (false-if-exception (file-is-directory? dir))))
                      (for-each (lambda (dir)
                                  (when (conf-dir? dir)
                                    (for-each (lambda (f)
                                                (system* "sysctl" "-p" (string-append dir "/" f)))
                                              (scandir dir (lambda (f) (string-suffix? ".conf" f))))))
                                '("/etc/sysctl.d" "/run/sysctl.d" "/usr/lib/sysctl.d"))
                      (when (false-if-exception (file-exists? "/etc/sysctl.conf"))
                        (system* "sysctl" "-p" "/etc/sysctl.conf"))
                      #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list sysctl))
;;; 10-timezone.scm --- set timezone

(use-modules (ice-9 rdelim) ;; readline
             (srfi srfi-13) ;; string
             )

(define timezone
  (service '(timezone)
           #:documentation "Set system timezone from /etc/rc.conf or /etc/timezone."
           #:requirement '(root-rw)
           #:start (lambda _
                     (let ((tz (or (read-rc-conf-var "TIMEZONE")
                                   (and (file-exists? "/etc/timezone")
                                        (call-with-input-file "/etc/timezone"
                                          (lambda (p) (string-trim-both (read-line p)))))
                                   "UTC")))
                       (let ((target (string-append "/usr/share/zoneinfo/" tz)))
                         (when (file-exists? target)
                           (false-if-exception (delete-file "/etc/localtime"))
                           (symlink target "/etc/localtime"))))
                     #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list timezone))
;;; 11-swap.scm --- activate swap areas from /etc/fstab.

;; ~swapon~/~swapoff~ are primitive binaries; the only shell-like logic was the
;; ~|| true~ "don't fail boot if there is no swap".  That is composition, so it
;; lives in Scheme via ~run-or-ignored~.  No ~/bin/sh -c~ needed.

(define (swapon-all)
  (run-or-ignored "/usr/sbin/swapon" "-a"))

(define (swapoff-all)
  (run-or-ignored "/usr/sbin/swapoff" "-a"))

(define swap
  (service '(swap)
           #:documentation "Activate swap areas (~swapon -a~)."
           #:requirement '(file-systems)
           #:start swapon-all
           #:stop swapoff-all
           #:one-shot? #t))

(register-services (list swap))
;;; 12-dmesg.scm --- snapshot kernel ring buffer into /var/log/dmesg.log.
(use-modules  (ice-9 popen)  ;; open-pipe* close-pipe
              (ice-9 rdelim) ;; read-line
              (srfi srfi-13) ;; string things
              )

(define dmesg
  (service '(dmesg)
           #:documentation "Save the kernel ring buffer to /var/log/dmesg.log."
           #:requirement '(root-rw file-systems)
            #:start (lambda _
                      (with-output-to-file "/var/log/dmesg.log"
                        (lambda () (system* "dmesg")))
                      (let ((restrict? (catch 'system-error
                                         (lambda ()
                                           (let ((port (open-pipe* OPEN_READ "sysctl" "-n" "kernel.dmesg_restrict")))
                                             (let ((val (read-line port)))
                                               (close-pipe port)
                                               (string=? (string-trim-both val) "1"))))
                                         (lambda args #f))))
                        (let ((mode (if restrict? "0600" "0644")))
                          (false-if-exception
                            (system* "chmod" mode "/var/log/dmesg.log"))))
                      #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list dmesg))
;;; 13-random-seed.scm --- credit the kernel CSPRNG early.

;; Mirrors Void's 05-misc.sh ~seedrng~.  Best-effort: if seedrng is absent or
;; fails, do not block boot.  Run purely in Scheme (no shell): the only logic
;; here is "try the primitive and tolerate failure", which is composition --
;; the binary itself is the primitive.

(define (seedrng-thunk)
  "Credit the kernel CSPRNG.  seedrng exits immediately after seeding."
  (run-or-ignored "/usr/sbin/seedrng"))

(define random-seed
  (service '(random-seed)
           #:documentation "Credit the kernel CSPRNG with seedrng."
           #:requirement '(root-rw file-systems)
           #:start seedrng-thunk
           #:stop (lambda _
                    (system* "seedrng" "save")
                    #t)
           #:one-shot? #t))

(register-services (list random-seed))
;;; 14-log-files.scm --- utmp/wtmp/btmp/lastlog + socklog spool + dirs.

;; based on runit/core-services/99-cleanup.sh
;; Creates the accounting and log scaffolding.  Per-service logs are written by
;; Shepherd to their own ~#:log-file~ (and Shepherd auto-creates the parent
;; dir on first write), so here we only lay out the system-wide log/utmp
;; files and the socklog spool directories with their syslog ~config~ files.
;;
;; socklog reads its per-facility ~config~ using svlogd directives:
;;   s<size>  rotate at <size> bytes
;;   n<count> keep <count> rotated files
;;   -*       clear the accept list
;;   +pat     accept messages matching pat

(use-modules (srfi srfi-13) ;; string things
             )

(define log-files
  (service '(log-files utmp)
           #:documentation "Create utmp/wtmp/btmp/lastlog and the socklog spool."
           #:requirement '(root-rw file-systems runtime-directories)
           #:start (lambda _
                     ;; 1. Create the utmp/wtmp/btmp/lastlog files
                     (for-each (lambda (args) (apply system* args))
                               '(("install" "-m0664" "-o" "root" "-g" "utmp" "/dev/null" "/run/utmp")
                                 ("install" "-m0664" "-o" "root" "-g" "utmp" "/dev/null" "/var/log/wtmp")
                                 ("install" "-m0600" "-o" "root" "-g" "utmp" "/dev/null" "/var/log/btmp")
                                 ("install" "-m0600" "-o" "root" "-g" "utmp" "/dev/null" "/var/log/lastlog")))

                      ;; 2. Boot marker in /var/log/wtmp (so `last` shows the boot).
                      ;;    Void's `halt -B' idiom is unsafe here: on this host
                      ;;    /bin/halt is a `herd` proxy that *reboots* (and now
                      ;;    that /usr/local/bin is on PATH at boot, `halt -B`
                      ;;    would actually trigger a reboot during boot).
                      ;;    Writing a wtmp BOOT_TIME record needs a proper utmp
                      ;;    writer (pututxline/utmpdump); until one is wired in,
                      ;;    skip it rather than risk a boot-time reboot.
                      ;;    (See services.d/29-power.scm for the real shutdown.)

                     ;; 3. socklog spool setup
                     (system* "mkdir" "-p" "/var/log/socklog")
                     (system* "chown" "root:socklog" "/var/log/socklog")
                     (system* "chmod" "2750" "/var/log/socklog")
                     (for-each (lambda (facility)
                                 (let ((dir (string-append "/var/log/socklog/" facility)))
                                   (system* "mkdir" "-p" dir)
                                   (call-with-output-file (string-append dir "/config")
                                     (lambda (port)
                                       (display "s1000000\nn10\n" port)))
                                   (system* "chown" "root:socklog" dir)
                                   (system* "chmod" "0750" dir)))
                               '("messages" "daemon" "cron"))
                     #t)
           #:stop (lambda _
                    ;; Shutdown cleanup (replaces Void's 99-cleanup.sh)
                    (for-each (lambda (args)
                                (unless (file-exists? (last args))
                                  (apply system* args)))
                              '(("install" "-m0664" "-o" "root" "-g" "utmp" "/dev/null" "/var/log/wtmp")
                                ("install" "-m0600" "-o" "root" "-g" "utmp" "/dev/null" "/var/log/btmp")
                                ("install" "-m0600" "-o" "root" "-g" "utmp" "/dev/null" "/var/log/lastlog")))
                    (system* "install" "-dm1777" "/tmp/.X11-unix" "/tmp/.ICE-unix")
                    (for-each (lambda (f) (false-if-exception (delete-file f)))
                              '("/etc/nologin" "/forcefsck" "/forcequotacheck" "/fastboot"))
                    #t)
           #:one-shot? #t))

(register-services (list log-files))
;;; 15-syslog.scm --- system logger via Void ~socklog' listening on /dev/log.

;; Void ships no syslog by default; the ~socklog~ package (from the
;; ~socklog-void~ configuration) provides it.  socklog runs in the foreground
;; by default (it is meant to be supervised), and writes the syslog messages
;; it receives on /dev/log into /var/log/socklog/<facility>/current.  The
;; spool directories and their ~config~ files are created by ~log-files~.

(define syslog
  (service '(syslog socklog)
           #:documentation "socklog system logger listening on /dev/log."
           #:requirement '(log-files runtime-directories)
           #:start (make-forkexec-constructor
                    '("/usr/bin/socklog" "-U" "unix" "/dev/log")
                    #:log-file "/var/log/socklog/socklog.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list syslog))
;;; 16-guix-daemon.scm --- the Guix build daemon.

;; guix-daemon only needs the store mounted (/gnu, in /etc/fstab via
;; file-systems) and the guixbuild users to exist; it does not need the
;; network.  It must NOT be in the boot-critical set: on foreign distros
;; (e.g. Void), the Guix store libraries are in /gnu/store/* and the
;; dynamic loader can resolve them, but a garbage-collected dependency
;; (e.g. libgpg-error) will cause exit 127 which triggers a respawn loop
;; that wastes CPU and clutters the boot log.  Since Shepherd itself is the
;; only Guix component needed at boot, guix-daemon is moved to fully-online
;; and made non-respawning so a failure is logged but does not cascade.

(define guix-daemon
  (service '(guix-daemon)
           #:documentation "Guix build daemon. Not required for Shepherd-only use."
           #:requirement '(file-systems)
           #:start (make-forkexec-constructor
                     '("/var/guix/profiles/per-user/root/current-guix/bin/guix-daemon"
                       "--build-users-group=guixbuild"
                       "--enable-substitutes=yes")
                     #:log-file "/var/log/guix-daemon.log")
           #:stop (make-kill-destructor)
           #:respawn? #f))  ;; Do NOT respawn — a 127 means a missing store dep, not a crash.

(register-services (list guix-daemon))
;;; 17-getty.scm --- virtual terminals on tty1..tty6.

;; Agetty is the interactive tier.  Every getty depends on the boot barrier
;; ~boot-ready~ (defined in 28-boot-stages.scm), so a TTY is never offered
;; before the machine is actually ready to log in.

(define (agetty-binary)
  "Resolve the agetty executable (Void ships it under /usr/sbin)."
  (or (and (file-exists? "/usr/sbin/agetty") "/usr/sbin/agetty")
      (and (file-exists? "/sbin/agetty") "/sbin/agetty")
      (and (file-exists? "/usr/bin/agetty") "/usr/bin/agetty")
      "/usr/sbin/agetty"))

(define* (make-agetty tty #:optional (extra-args '()))
  "Return a respawning agetty service for TTY."
  (service
   (list (string->symbol (string-append "agetty-" tty)))
   #:documentation (string-append "Getty on " tty)
   #:requirement '(boot-ready)
   #:start (make-forkexec-constructor
            `(,(agetty-binary) 
              "--noclear" 
              "--noissue"       ; <-- Don't wait to display /etc/issue
              "--local-line"    ; <-- Tell it the line is local (no modem/carrier wait)
              ,tty 
              "linux" 
              ,@extra-args)
            #:log-file (string-append "/var/log/agetty-" tty ".log"))
   #:stop (make-kill-destructor)
   #:respawn? #t))

(define agetty-tty1 (make-agetty "tty1" '("--autologin" "komeil")))
(define agetty-tty2 (make-agetty "tty2"))
(define agetty-tty3 (make-agetty "tty3"))
(define agetty-tty4 (make-agetty "tty4"))
(define agetty-tty5 (make-agetty "tty5"))
(define agetty-tty6 (make-agetty "tty6"))

(register-services
 (list agetty-tty1 agetty-tty2 agetty-tty3 agetty-tty4 agetty-tty5 agetty-tty6))
;;; 18-loopback.scm --- bring up the loopback interface.

;; lo has no real dependencies beyond the ip tool and the dev pseudo-filesystem,
;; so it can come up in parallel with dbus / nm.

(define loopback
  (service '(loopback)
           #:documentation "Bring the loopback interface up."
           #:requirement '(dev file-systems)
           #:start (lambda _ (zero? (system* "ip" "link" "set" "up" "dev" "lo")))
           #:stop (lambda _ (zero? (system* "ip" "link" "set" "down" "dev" "lo")))
           #:one-shot? #t))

(register-services (list loopback))
;;; 19-dbus.scm --- system D-Bus message bus.

;; The runtime directory (/run/dbus, owned by the dbus user, uid/gid 22 on
;; Void) is a runtime-directory concern, so it is a separate one-shot service
;; (~dbus-runtime~) that depends on ~runtime-directories~.  The daemon itself
;; is then started from a plain argv list -- no ~/bin/sh -c~ wrapper, and no
;; conditional ~install || install~ chain.

(define dbus-runtime
  (service '(dbus-runtime)
           #:documentation "Create and chown /run/dbus for the D-Bus system bus (dbus:22)."
           #:requirement '(runtime-directories)
           #:start (lambda _
                     (zero? (system* "install" "-d" "-m755" "-g" "22" "-o" "22" "/run/dbus")))
           #:stop (const #t)
           #:one-shot? #t))

(define dbus
  (service '(dbus)
           #:documentation "D-Bus system message bus."
           #:requirement '(dbus-runtime dev)
           #:start (make-forkexec-constructor
                    '("/usr/sbin/dbus-daemon" "--system" "--nofork" "--nopidfile")
                    #:log-file "/var/log/dbus.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list dbus-runtime dbus))
;;; 20-network.scm --- NetworkManager (primary) and dhcpcd (fallback).

;; NetworkManager is the primary network stack (required by fully-online).
;; ~dhcpcd~ is a fallback: it is NOT pulled in by the boot graph, so the two
;; never fight over the same interface.  If NetworkManager is unavailable or
;; you need it to manage only one link, start dhcpcd manually:
;;   herd start dhcpcd

(define network-manager
  (service '(network-manager networking)
           #:documentation "NetworkManager (primary network stack)."
           #:requirement '(dbus udev file-systems loopback)
           #:start (make-forkexec-constructor
                    '("/usr/sbin/NetworkManager" "--no-daemon")
                    #:log-file "/var/log/NetworkManager.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(define dhcpcd
  (service '(dhcpcd)
           #:documentation "dhcpcd (fallback; enable manually if NetworkManager is absent)."
           #:requirement '(loopback file-systems)
           #:start (make-forkexec-constructor
                    '("/sbin/dhcpcd" "-B" "-M" "-q")
                    #:log-file "/var/log/dhcpcd.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list network-manager dhcpcd))
;;; 21-polkit.scm --- authority manager.

(define polkit
  (service '(polkit)
    #:documentation "polkit authority manager."
    #:requirement '(dbus)
    #:start (make-forkexec-constructor
             '("/usr/lib/polkit-1/polkitd" "--no-debug")
             #:log-file "/var/log/polkit.log")
    #:stop (make-kill-destructor)
    #:respawn? #t))

(register-services (list polkit))
;;; 22-elogind.scm --- systemd-logind replacement for sessions/seats.

;; On Void, elogind is *activated on demand* by the system D-Bus daemon
;; through /usr/share/dbus-1/system-services/org.freedesktop.login1.service
;; (the .service file Exec=es elogind-wrapper).  Launching elogind.wrapper
;; from a Shepherd service would race that activation and make elogind
;; reclaim org.freedesktop.login1 -- i.e. exactly the bug this used to carry.
;;
;; So this is a purely declarative place-holder: it pins elogind behind
;; ~dbus' (so it cannot be ordered before the bus) and exports the ~elogind~
;; / ~logind' symbols other services may depend on.  The actual daemon is
;; bus-activated, lazily, on the first Login1 call (first graphical or
;; console login handled by PAM).

(define elogind
  (service '(elogind logind)
           #:documentation
           "elogind is D-Bus-activated on Void; this service pins ordering and exports the symbol."
           #:requirement '(dbus)
           #:start (const #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list elogind))
;;; 23-seatd.scm --- seat management for Wayland (sway).

;; Void's seatd group is ~_seatd~. seatd must start after udev so device
;; nodes exist, and after /run is writable.

(define seatd
  (service '(seatd)
           #:documentation "seatd seat-management daemon (for sway/etc.)"
           #:requirement '(udev udev-settle run runtime-directories)
           #:start (make-forkexec-constructor
                    '("/usr/sbin/seatd" "-g" "_seatd")
                    #:log-file "/var/log/seatd.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list seatd))
;;; 24-acpid.scm --- ACPI event daemon.

;; acpid reads events from /proc/acpi/event (needs sysfs + dev).  It blocks
;; nothing critical, so it stays in fully-online, parallel to dbus/NM.

(define acpid
  (service '(acpid)
    #:documentation "ACPI event daemon."
    #:requirement '(sys udev)
    #:start (make-forkexec-constructor
             '("/usr/sbin/acpid" "--foreground")
             #:log-file "/var/log/acpid.log")
    #:stop (make-kill-destructor)
    #:respawn? #t))

(register-services (list acpid))
;;; 25-zram.scm --- compressed swap on a zram block device.

;; Replaces the old inline shell.  The swap activation is a separate one-shot
;; so the boot graph is explicit; zram only needs the kmod (handled by
;; static-device-nodes/kernel-modules) and the block layer.

(define zram
  (service '(zram swap-zram)
           #:documentation "Set up a zram swap device."
           #:requirement '(static-device-nodes)
           #:start (lambda _
                     (system* "modprobe" "zram")
                     (call-with-output-file "/sys/block/zram0/comp_algorithm"
                       (lambda (p) (display "lz4\n" p)))
                     (call-with-output-file "/sys/block/zram0/disksize"
                       (lambda (p)
                         (display (* 8 1024 1024 1024) p)  ;; 8GB -> B
                         (newline p)))
                     (system* "mkswap" "/dev/zram0")
                     (system* "swapon" "/dev/zram0")
                     #t)
           #:stop (lambda _
                    (system* "swapoff" "/dev/zram0")
                    (call-with-output-file "/sys/block/zram0/reset"
                      (lambda (p) (display "1\n" p)))
                    #t)
           #:one-shot? #t))

(register-services (list zram))
;;; 26-openssh.scm --- OpenSSH server + host-key generation.

(define ssh-keys
  (service '(ssh-keys)
           #:documentation "Generate SSH host keys on first boot."
           #:requirement '(loopback file-systems)
           #:start (lambda _
                     (system* "/usr/bin/ssh-keygen" "-A")
                     #t)
           #:stop (const #t)
           #:one-shot? #t))

(define openssh
  (service '(openssh sshd ssh)
           #:documentation "OpenSSH server."
           #:requirement '(ssh-keys loopback file-systems)
           #:start (make-forkexec-constructor
                    '("/usr/sbin/sshd" "-D")
                    #:log-file "/var/log/sshd.log")
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list ssh-keys openssh))
;;; 27-chrony.scm --- NTP client.

;; chrony only needs /var/lib for its drift file and the network; it must NOT
;; block getty, so it lives in fully-online only.

(define chrony
  (service '(chrony ntp)
    #:documentation "chrony NTP client/server."
    #:requirement '(network-manager file-systems)
    #:start (make-forkexec-constructor
             '("/usr/sbin/chronyd" "-n" "-f" "/etc/chrony.conf")
             #:log-file "/var/log/chrony.log")
    #:stop (make-kill-destructor)
    #:respawn? #t))

(register-services (list chrony))
;;; 28-boot-stages.scm --- boot barriers and stage targets.

;; Three descriptive stage targets, plus the ~boot-ready~ barrier that gates the
;; interactive gettys. These targets perform no work themselves: their only job
;; is to express ordering through ~#:requirement~ so that the graph is obvious
;; from reading this file.
;;
;;   boot-critical : everything needed to run the store / devices.
;;   boot-ready    : the subset of the above that login actually waits on.
;;   interactive   : virtual terminals.
;;   fully-online  : networking + desktop/session daemons.
;;
;; =agetty= depends on =boot-ready= (NOT on a serial list), so unrelated
;; boot-critical services run in parallel and never block each other.

(use-modules (shepherd service))

;; boot-critical: The absolute minimum to get the kernel, storage, and
;; pseudo-filesystems ready.  guix-daemon is NOT here: on a foreign distro
;; (Void) the Guix store libraries live in /gnu/store/* and a
;; garbage-collected dependency causes exit 127.  Since Shepherd is the
;; only Guix component needed at boot, guix-daemon was moved to
;; ~fully-online~ and made non-respawning (see 16-guix-daemon.scm).
(define boot-critical
  (stage 'boot-critical "Stage 1: core system initialization."
         '(root-rw pseudo-filesystems runtime-directories
           static-device-nodes kernel-modules cgroups udev udev-settle
           file-systems tmpfs swap sysctl dmesg)))

;; boot-ready: The barrier behind which gettys wait.
;; Includes user-facing setup like console, timezone, and hwclock.
(define boot-ready
  (stage 'boot-ready "Barrier: root, devices, filesystems, hostname, logging,
and console are ready. Gettys may start once this is up."
         '(boot-critical hostname log-files syslog random-seed
           console-setup timezone hwclock)))

;; interactive: Things that require a TTY or user session.
(define interactive
  (stage 'interactive "Stage 2: local TTYs and user login."
         '(boot-ready agetty-tty1 agetty-tty2 agetty-tty3
           agetty-tty4 agetty-tty5 agetty-tty6)))

;; fully-online: Network and background daemons.
(define fully-online
  (stage 'fully-online "Stage 3: networking, D-Bus, seat/session, SSH, and friends.
These deliberately do NOT gate the gettys."
          '(interactive loopback dbus polkit elogind seatd acpid
                        network-manager zram openssh chrony guix-daemon)))

(register-services (list boot-critical boot-ready interactive fully-online))
;;; 29-power.scm --- reboot and halt targets for Shepherd.

;; Since s6-linux-init-hpr no longer works (Shepherd replaced it as PID 1),
;; we define native Shepherd targets that drive the Magic SysRq keys directly.
;;
;; IMPORTANT ordering: the old implementation ran `umount -a -r` *before*
;; triggering SysRq.  That unmounted /run, which deleted the shepherd socket
;; ("Socket deleted; stopping") and killed shepherd, and *then* fired the
;; reboot -- so if the SysRq key never took effect the box was left half-dead
;; with no supervisor.  SysRq 'b'/'o' reboot/halt the kernel immediately and
;; skip the unmount, so we trigger it FIRST (after sync+remount-ro via
;; 's'/'u') and let the kernel tear the filesystems down.

(define (sysrq . keys)
  "Write each KEY to /proc/sysrq-trigger, silently ignoring failures (e.g.
when SysRq is disabled or unavailable to this build)."
  (for-each
   (lambda (key)
     (false-if-exception
      (let ((fd (open-file "/proc/sysrq-trigger" "w")))
        (display key fd)
        (close-port fd))))
   keys))

(define reboot
  (service '(reboot)
           #:documentation "Reboot the machine via the Magic SysRq sequence."
           #:requirement '(root-rw)
           #:start (lambda _
                     (sync)
                     (sysrq "s" "u" "b")   ; sync, remount-ro, reboot
                     #t)
           #:one-shot? #t))

(define halt
  (service '(halt poweroff)
           #:documentation "Power off / halt the machine via the Magic SysRq sequence."
           #:requirement '(root-rw)
           #:start (lambda _
                     (sync)
                     (sysrq "s" "u" "o")   ; sync, remount-ro, poweroff
                     #t)
           #:one-shot? #t))

(register-services (list reboot halt))

9.3. User Shepherd entry point

;;; User Shepherd config (copy to ~/.config/shepherd/init.scm).
;;
;; Run as the regular user with:
;;   guix package -i shepherd
;;   mkdir -p ~/.config/shepherd
;;   cp -r ~/.dotfiles/shepherd/user-shepherd/services.d \
;;         ~/.config/shepherd/services.d
;;   cp ~/.dotfiles/shepherd/user-shepherd/init.scm \
;;         ~/.config/shepherd/init.scm
;;   shepherd -s /run/shepherd/$(id -u).socket \
;;       -c ~/.config/shepherd/init.scm -I
;;
;; Expects:
;;   * a system seatd socket at /run/seatd/socket (the root `seatd` service
;;     in Stage 3; this user must be in the `_seatd' group:
;;     usermod -aG _seatd $USER)
;;   * xbps packages installed (see "Required xbps packages" below)
;;   * XDG_RUNTIME_DIR=/run/user/$UID (elogind on the system side creates it
;;     via PAM when you log in on an agetty with elogind running).
;;
;; Session flow:
;;   Shepherd starts -> loads services.d/*.scm
;;     -> runtime-dirs sets up XDG_RUNTIME_DIR + DBUS_SESSION_BUS_ADDRESS
;;     -> dbus, gpg-agent, pipewire start in parallel
;;     -> sway starts
;;       -> graphical-session barrier becomes ready
;;       -> ALL GUI services start in parallel (dunst, swayidle, portals, etc.)
;;     -> sway-session barrier resolves
;;   `herd restart sway` / `herd restart pipewire` / etc. all work.

(define-module (shepherd config)
              #:declarative? #f)
(use-modules (shepherd service)
             ((ice-9 ftw) #:select (scandir)))

;; Ensure runtime environment is set *before* loading service files, so that
;; base-env() in 00-utilities.scm captures DBUS_SESSION_BUS_ADDRESS.
(let ((rt (or (getenv "XDG_RUNTIME_DIR")
              (string-append "/run/user/" (number->string (getuid))))))
  (unless (getenv "XDG_RUNTIME_DIR")
    (setenv "XDG_RUNTIME_DIR" rt))
  (unless (getenv "DBUS_SESSION_BUS_ADDRESS")
    (setenv "DBUS_SESSION_BUS_ADDRESS"
            (string-append "unix:path=" rt "/bus"))))

(define %services-directory
  (or (getenv "SHEPHERD_USER_SERVICES")
      (string-append (or (getenv "HOME") "/home/komeil")
                     "/.config/shepherd/services.d")))

;; 1. Load every *.scm (alphabetically) from the services directory.
;;    Each file registers its own services; #:requirement may reference a
;;    service defined in an earlier-loaded file (resolved lazily at start
;;    time).  00-utilities.scm sorts first so helpers are available.
(for-each
 (lambda (file)
   (load (string-append %services-directory "/" file)))
 (scandir %services-directory
          (lambda (f)
            (and (string-suffix? ".scm" f)
                 (not (string-prefix? "." f))))))

;; 2. Top-level barrier: the complete user graphical session.
;;    Its #:requirement pulls in every branch of the graph; Shepherd resolves
;;    them in parallel where the DAG allows.
(define sway-session
  (service '(sway-session desktop graphical-session-user)
           #:documentation
           "Barrier: the complete user graphical session (dbus, audio,
gpg-agent, sway, and all GUI daemons)."
           #:requirement '(dbus pipewire gpg-agent sway graphical-session)
           #:start (const #t)
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list sway-session))

;; 3. Kick off the session.
;;    (shepherd service) exports start-in-the-background, not a bare start.
;;    Resolves the graph and starts services concurrently where possible.
(start-in-the-background '(sway-session))

9.4. User Shepherd service definitions

;;; 00-utilities.scm --- shared helpers for user Shepherd services.

;; Loaded first (alphabetically); registers no services.
;; Provides binary resolution, env-var construction, and log-file helpers
;; so that individual service files read like declarative data.

(use-modules (srfi srfi-1) ; find, delete-duplicates, remove
             (shepherd service))

;; mkdir-p: create DIR and parents. Defined locally to avoid depending on
;; (ice-9 ftw) availability variations across some Guile builds.
(define* (mkdir-p dir #:optional (mode #o755))
  (let loop ((parts (string-split dir #\/))
             (path ""))
    (if (null? parts)
        #t
        (let* ((next (car parts))
               (full (if (string=? path "") next
                         (string-append path "/" next))))
          (unless (or (string=? next "") (file-exists? full))
            (catch 'system-error
              (lambda () (mkdir full mode))
              (lambda args
                ;; If the error is not EEXIST, re-throw it.
                (unless (= (system-error-errno args) EEXIST)
                  (apply throw args)))))
          (loop (cdr parts) full)))))

(define %home
  (or (getenv "HOME") "/home/komeil"))

(define %xdg-state
  (or (getenv "XDG_STATE_HOME")
      (string-append %home "/.local/state")))

(define %shepherd-dir
  (string-append %xdg-state "/shepherd"))

(define %xdg-runtime
  ;; Inherit from the login session; fall back to the standard per-UID path.
  ;; init.scm sets this via setenv before loading service files.
  (or (getenv "XDG_RUNTIME_DIR")
      (string-append "/run/user/"
                     (number->string (getuid)))))

(define %wayland-display
  ;; Sway's default socket name. If the user has a non-default display,
  ;; set WAYLAND_DISPLAY in the environment before starting the user Shepherd.
  (or (getenv "WAYLAND_DISPLAY") "wayland-0"))

(define %xdg-desktop "sway")

(define %ssh-agent-enabled
  ;; Set GPG_SSH=yes to enable SSH-agent support in gpg-agent.
  (member (getenv "GPG_SSH") '("yes" "1" "true")))

(define nilog-dir
  (begin
    (mkdir-p %shepherd-dir)
    %shepherd-dir))

(define (log-file name)
  "Return the log file path for user service NAME."
  (string-append nilog-dir "/" name ".log"))

(define* (binary name #:optional (hint ""))
  "Resolve an xbps-installed binary by trying common prefixes.
Returns the first existing path, or NAME if none found (so Shepherd
logs a clear 'No such file or directory' error rather than silently
resolving to PATH)."
  (let ((candidates
         (append
          (if (string=? hint "")
              '()
              (list (string-append hint "/" name)))
          (list (string-append "/usr/bin/" name)
                (string-append "/usr/sbin/" name)
                (string-append "/usr/libexec/" name)
                (string-append "/usr/lib/" name)
                (string-append "/bin/" name)))))
    (or (find file-exists? candidates)
        name)))

(define (base-env)
  "Minimal environment variables every user process needs: HOME, PATH,
XDG_RUNTIME_DIR, and DBUS_SESSION_BUS_ADDRESS, captured from the current
(Shepherd) process. init.scm sets DBUS_SESSION_BUS_ADDRESS via setenv
before loading services, so all children inherit the session bus address."
  (let ((path (or (getenv "PATH") "/usr/local/bin:/usr/bin:/bin"))
        (term (or (getenv "TERM") "xterm-256color"))
        (dbus (getenv "DBUS_SESSION_BUS_ADDRESS"))
        (xdg (getenv "XDG_RUNTIME_DIR")))
    (filter identity
            (list (string-append "HOME=" %home)
                  (string-append "PATH=" path)
                  (and xdg (string-append "XDG_RUNTIME_DIR=" xdg))
                  (string-append "TERM=" term)
                  (and dbus (string-append "DBUS_SESSION_BUS_ADDRESS=" dbus))))))

(define (wayland-env)
  "Environment for GUI services: base env + WAYLAND_DISPLAY + XDG_CURRENT_DESKTOP.
Every Wayland client needs these to connect to the compositor."
  (append (base-env)
          (list (string-append "WAYLAND_DISPLAY=" %wayland-display)
                (string-append "XDG_CURRENT_DESKTOP=" %xdg-desktop)
                (string-append "XDG_SESSION_TYPE=wayland"))))

(define (with-wayland-env proc)
  "Run PROC after setting WAYLAND_DISPLAY and XDG_CURRENT_DESKTOP in the
current process environment (used for one-shots that need the Wayland
display but are started via system* rather than forkexec)."
  (setenv "WAYLAND_DISPLAY" %wayland-display)
  (setenv "XDG_CURRENT_DESKTOP" %xdg-desktop)
  (setenv "XDG_SESSION_TYPE" "wayland")
  (proc))
;;; 01-runtime-dirs.scm --- user runtime directory + session env vars.

;; Sets XDG_RUNTIME_DIR (created by PAM/elogind at login, but we ensure it
;; exists as a safeguard) and DBUS_SESSION_BUS_ADDRESS so that dbus-daemon
;; and every dbus-dependent child can find the user session bus at a
;; predictable path.  This is a no-op when XDG_RUNTIME_DIR is already set
;; correctly (idempotent).
(define runtime-dirs
  (service '(runtime-dirs)
           #:documentation
           "Ensure XDG_RUNTIME_DIR exists and set session env vars on the
Shepherd process so all child services inherit them."
           #:requirement '()
           #:start
           (lambda _
             (let ((rt (or (getenv "XDG_RUNTIME_DIR")
                           (string-append "/run/user/"
                                          (number->string (getuid))))))
               (false-if-exception (mkdir-p rt))
               (setenv "XDG_RUNTIME_DIR" rt)
               (setenv "DBUS_SESSION_BUS_ADDRESS"
                       (string-append "unix:path=" rt "/bus"))
               #t))
           #:stop (const #t)
           #:one-shot? #t))

(register-services (list runtime-dirs))
;;; 03-pipewire.scm --- PipeWire audio stack.

;; Dependency chain:
;;   pipewire  ->  wireplumber  ->  pipewire-pulse
;;
;; pipewire is the core daemon; wireplumber is the session manager
;; pipewire-pulse provides PulseAudio wire-protocol compatibility
;; for legacy clients.
;;
;; The audio stack does NOT depend on graphical-session. it works headless
;; and can start before Sway.  pipewire runs in the foreground by default
;; (no --no-daemon needed — that flag doesn't exist on Void's pipewire).

(define pipewire
  (service '(pipewire)
           #:documentation "PipeWire multimedia server (core daemon)."
           #:requirement '(dbus)
           #:start
           (make-forkexec-constructor
            (list (binary "pipewire"))
            #:environment-variables (base-env)
            #:log-file (log-file "pipewire"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(define wireplumber
  (service '(wireplumber)
           #:documentation "PipeWire session manager."
           #:requirement '(pipewire)
           #:start
           (make-forkexec-constructor
            (list (binary "wireplumber"))
            #:environment-variables (base-env)
            #:log-file (log-file "wireplumber"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(define pipewire-pulse
  (service '(pipewire-pulse pulseaudio)
           #:documentation "PipeWire PulseAudio compatibility bridge."
           #:requirement '(wireplumber)
           #:start
           (make-forkexec-constructor
            (list (binary "pipewire-pulse"))
            #:environment-variables (base-env)
            #:log-file (log-file "pipewire-pulse"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list pipewire wireplumber pipewire-pulse))
;;; 04-gpg-agent.scm --- GnuPG agent (with optional SSH support).

;; GnuPG 2.2+ on Void: gpg-agent without --supervised or --daemon will
;; fail ("no gpg-agent running in this session").  Using --supervised
;; makes gpg-agent run in the foreground under Shepherd supervision.
;; SSH support is toggled via GPG_SSH at Shepherd start time (see
;; 00-utilities.scm).  When enabled, the SSH socket lands at
;; $XDG_RUNTIME_DIR/gnupg/S.gpg-agent.ssh — a path ssh-add and other
;; SSH tools find automatically via the agent's own socket discovery.

(define gpg-agent
  (service '(gpg-agent)
           #:documentation
           "GnuPG agent.  With GPG_SSH=yes in the environment, also acts
as an SSH agent (SSH_AUTH_SOCK is set automatically by the agent's socket
discovery)."
           #:requirement '(runtime-dirs)
           #:start
           (make-forkexec-constructor
            (append
             (if %ssh-agent-enabled
                 (list (binary "gpg-agent") "--supervised" "--enable-ssh-support")
                 (list (binary "gpg-agent") "--supervised"))
             '("--no-allow-external-cache"))
            #:environment-variables (base-env)
            #:log-file (log-file "gpg-agent"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list gpg-agent))
;;; 05-sway.scm --- Sway Wayland compositor (user, respawned).

;; Sway is started by the user Shepherd directly (not by an external TTY
;; login script), so `herd restart sway` works.  It requires only:
;;   * seatd socket (system service at /run/seatd/socket)
;;   * XDG_RUNTIME_DIR (from runtime-dirs, for the Wayland socket)
;;
;; Sway does NOT require dbus to run. it uses the Wayland protocol directly
;; for output/input management.  Portal integration (xdg-desktop-portal) is
;; handled separately by services that depend on BOTH graphical-session and
;; dbus.

(define sway-env
  ;; Base env plus the seatd socket address Sway needs to talk to the
  ;; system seat management daemon.
  (append (base-env)
          (list (string-append "SEATD_SOCKET=/run/seatd/socket"))))

(define sway
  (service '(sway wayland)
           #:documentation "sway Wayland compositor (user service)."
           #:requirement '(runtime-dirs dbus)
           #:start
           (make-forkexec-constructor
            (list (binary "sway"))
            #:environment-variables sway-env
            #:directory %home
            #:log-file (log-file "sway"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list sway))
;;; 06-graphical-session.scm --- barrier for the ready graphical session.

;; This is a lightweight stage (barrier) service.  It does no work itself;
;; its only purpose is its #:requirement '(sway).  All GUI-only daemons
;; depend on this barrier, so they start together once Sway has created the
;; Wayland display, but none of them block Sway startup and they all run
;; in parallel.

(define graphical-session
  (service '(graphical-session)
           #:documentation
           "Barrier: the Wayland compositor (sway) is running and the Wayland
display socket exists.  GUI-only daemons depend on this."
           #:requirement '(sway)
           #:start (const #t)
           #:stop  (const #t)
           #:one-shot? #t))

(register-services (list graphical-session))
;;; 07-portals.scm --- Wayland desktop portals.

;; Two pieces:
;; 1. portals-env (one-shot): publishes WAYLAND_DISPLAY and XDG_CURRENT_DESKTOP
;;    to the D-Bus activation environment via dbus-update-activation-environment,
;;    so dbus-activated services (the portals) inherit the Wayland session.
;;    Requires both graphical-session (sway has started) and dbus (the bus
;;    must exist to publish the env).
;; 2. The portals themselves: started explicitly for reliability rather than
;;    relying on dbus activation alone.

(define portals-env
  (service '(portals-env)
           #:documentation
           "Publish WAYLAND_DISPLAY and XDG_CURRENT_DESKTOP to the D-Bus
activation environment so dbus-activated services (portals) inherit them."
           #:requirement '(graphical-session dbus)
           #:start
           (make-system-constructor
            (string-append
             (binary "dbus-update-activation-environment")
             " WAYLAND_DISPLAY=" %wayland-display
             " XDG_CURRENT_DESKTOP=" %xdg-desktop
             " XDG_SESSION_TYPE=wayland"))
           #:stop (const #t)
           #:one-shot? #t))

(define xdg-desktop-portal
  (service '(xdg-desktop-portal)
           #:documentation
           "xdg-desktop-portal: the host D-Bus interface for desktop portal
requests (file pickers, screencasting, etc.)."
           #:requirement '(portals-env)
           #:start
           (make-forkexec-constructor
            (list (binary "xdg-desktop-portal"))
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "xdg-desktop-portal"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(define xdg-desktop-portal-wlr
  (service '(xdg-desktop-portal-wlr)
           #:documentation
           "xdg-desktop-portal-wlr: Wayland-specific portal backend
(screencasting, screenshot, idle inhibiting for sway)."
           #:requirement '(xdg-desktop-portal)
           #:start
           (make-forkexec-constructor
            (list (binary "xdg-desktop-portal-wlr"))
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "xdg-desktop-portal-wlr"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list portals-env xdg-desktop-portal xdg-desktop-portal-wlr))
;;; 08-dunst.scm --- notification daemon (Wayland).

;; dunst is a lightweight notification daemon.  It needs a Wayland connection
;; (for notification positioning) and dbus (for the org.freedesktop.Notifications
;; interface).  The dbus connection is inherited from the Shepherd env
;; (DBUS_SESSION_BUS_ADDRESS set by runtime-dirs); the Wayland display is
;; passed via #:environment-variables.

(define dunst
  (service '(dunst notifications)
           #:documentation "dunst notification daemon (Wayland)."
           #:requirement '(graphical-session dbus)
           #:start
           (make-forkexec-constructor
            (list (binary "dunst") "-mode wayland")
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "dunst"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list dunst))
;;; 09-swayidle.scm --- idle management + swaylock integration.

;; Replaces the existing `swaylock`-named service (which was actually running
;; swayidle).  swayidle monitors Sway output activity and triggers commands
;; on inactivity.  The config mirrors the Sway config's exec line:
;;   timeout 600s -> dpms off
;;   resume       -> dpms on
;;   before-sleep -> swaylock -f -c 000000
;;
;; swaylock (the locker binary) is invoked by swayidle, so it is not a
;; separate service; it is a dependency of the `swaylock` symbol being
;; available in PATH, which it is when the swaylock xbps package is installed.

(define swayidle
  (service '(swayidle)
           #:documentation
           "swayidle: idle management daemon.  Locks with swaylock on
inactivity and before sleep."
           #:requirement '(graphical-session)
           #:start
           (make-forkexec-constructor
            (list (binary "swayidle") "-w"
                  "timeout" "600" "swaymsg" "output *" "dpms" "off"
                  "resume" "swaymsg" "output *" "dpms" "on"
                  "before-sleep" "swaylock" "-f" "-c" "000000")
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "swayidle"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list swayidle))
;;; 10-sway-audio-idle-inhibit.scm --- keep screen from sleeping during audio.

;; Monitors the PulseAudio server (provided by pipewire-pulse) for active
;; playback and inhibits swayidle's idle timer while audio is playing.
;; Requires both the compositor (via graphical-session) and the audio
;; bridge (pipewire-pulse), without either, the daemon has no purpose.

(define sway-audio-idle-inhibit
  (service '(sway-audio-idle-inhibit)
           #:documentation
           "Prevent idle-sleep while audio is playing (connects to Sway IPC
and PulseAudio server)."
           #:requirement '(graphical-session pipewire-pulse)
           #:start
           (make-forkexec-constructor
            (list (binary "sway-audio-idle-inhibit"))
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "sway-audio-idle-inhibit"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list sway-audio-idle-inhibit))
;;; 11-wlsunset.scm --- day/night gamma adjustment.

;; wlsunset adjusts the screen color temperature using the
;; wlr-gamma-control-unstable-v1 protocol.  It needs a Wayland connection
;; and the Sway compositor.  Coordinates: lat 38.0792, lon 46.2887
;; (Tabriz, Iran), temperature 6000K (daytime baseline. the daemon handles
;; the day/night ramp; -t 6000 means daytime temperature).

(define wlsunset
  (service '(wlsunset)
           #:documentation "wlsunset: day/night gamma adjustment for Wayland."
           #:requirement '(graphical-session)
           #:start
           (make-forkexec-constructor
            (list (binary "wlsunset")
                  "-l" "38.0792" "-L" "46.2887" "-t" "6000")
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "wlsunset"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list wlsunset))
;;; 12-clipman.scm --- clipboard history daemon (Wayland).

;; Cliphist writer is a self-contained daemon that monitors the Wayland
;; clipboard and stores history in ~/.local/share/cliphist.db.
;; Sway config bindsym stays as a keybinding (not a daemon):
;;   bindsym Mod4+Shift+v exec cliphist list | wofi -d -i | xargs -r cliphist decode | wl-copy

(define clipman
  (service '(clipman clipboard cliphist)
           #:documentation
           "cliphist: Wayland clipboard history daemon (replaces clipman)."
           #:requirement '(graphical-session)
           #:start
           (make-forkexec-constructor
            (list (binary "cliphist") "writer")
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "clipman"))
           #:stop (make-kill-destructor)
           #:respawn? #t))

(register-services (list clipman))
;;; 13-azotebg.scm --- wallpaper setter (one-shot).

;; Azotebg is a one-shot: it queries Sway for connected outputs
;; (via swaymsg) and sets a wallpaper for each, then exits.
;; It is a #:one-shot? service that depends on
;; graphical-session (sway must be running for output queries).

(define azotebg
  (service '(azotebg wallpaper)
           #:documentation
           "Set the desktop wallpaper via azotebg (one-shot, runs after sway)."
           #:requirement '(graphical-session)
           #:start
           (make-forkexec-constructor
            (list (binary "azotebg"))
            #:environment-variables (wayland-env)
            #:directory %home
            #:log-file (log-file "azotebg"))
           #:stop (make-kill-destructor)
           #:one-shot? #t))

(register-services (list azotebg))

9.5. s6-linux-init skel files

#!/usr/bin/bash
#
# s6-linux-init stage-0 -> Shepherd handoff.
#
# This file lives in the s6-linux-init *skeleton* (skel/rc.init), not in
# `current/scripts/`.  Regenerate with:
#   s6-linux-init-maker -b /etc/s6-linux-init/skel ...
# s6-linux-init-maker copies it to current/scripts/rc.init, so edits here
# survive rebuilds.
#
# This script does THREE things and no more:
#   1. mount /gnu, /var/log, /var/tmp (the minimal set Shepherd + the store
#      need before it can run), mount a fresh tmpfs on /run, and create the
#      runtime tree (/run/shepherd, /var/log/shepherd).  /var/cache is NOT
#      mounted here (xbbs cache only, @pkg subvol); fstab handles it.
#   2. launch the Shepherd daemon (init.scm then drives the whole graph via
#      its dependency requirements).
#   3. tail Shepherd's logfile to the console with a colored spinner.
# Nothing past this point is managed by s6 -- Shepherd owns udev, dbus,
# networking, gettys, sway, etc.
#
# NOTE: s6-linux-init supervises this stage-1 script but does NOT supervise
# the Shepherd daemon launched here -- Shepherd is bg'd and orphaned.  If it
# ever exits, the socket disappears and stays gone until the next boot, so
# `herd' reports "no socket".  Keep boot steps below from deleting /run.
set -Eeuo pipefail

# /usr/local/bin holds the `herd` and `shepherd` symlinks (into /gnu/store).
# Services launched by Shepherd inherit this PATH; without it any service that
# shells out to `herd` fails at boot (e.g. the old `halt -B' boot-marker).
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin

# Optionally pull in the live Guix profile so daemons/tools upgraded later via
# `guix package` are visible to services launched further down.  Non-fatal: the
# /usr/local/bin shim above already guarantees `herd' is resolvable when
# s6-linux-init itself runs.  /run/current-guix-profile may not exist yet this
# early (activation creates it); sourcing a missing script is skipped.
if [ -r /etc/profiles/guix.sh ]; then
    # shellcheck disable=SC1091
    . /etc/profiles/guix.sh 2>/dev/null || \
        echo "rc.init: warning: /etc/profiles/guix.sh failed to source" >/dev/console 2>/dev/null || true
fi

rl="${1:-default}"
[ $# -gt 0 ] && shift || true

DISK_UUID=bded8bd1-ce84-47a6-8a90-0d0495f68111
LOGFILE=/var/log/s6-shepherd-boot.log
CONSOLE_LOGLEVEL=4
if grep -q 'loglevel=[7-9]' /proc/cmdline 2>/dev/null; then
    CONSOLE_LOGLEVEL=7
fi

# verbose + strict logger: always appends to $LOGFILE, prints color to /dev/console.
# usage: log <level> <msg>
log() {
    local level="${1:-info}" msg="$2" color=""
    case "$level" in
    critical|error) color='\033[1;31m';;
    warn)           color='\033[1;33m';;
    debug)          color='\033[0m'; [ "$CONSOLE_LOGLEVEL" -ge 7 ] || return 0;;
    info)           color='\033[1;32m'; [ "$CONSOLE_LOGLEVEL" -ge 6 ] || return 0;;
    *)              color='\033[0m';;
    esac
    printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$msg" >>"$LOGFILE" 2>/dev/null || true
    printf "${color}%s\033[0m\n" "$msg" >/dev/console 2>/dev/null || true
}

# Diagnostic: confirm rc.init is running (bypasses _log which needs /var/log)
echo "rc.init: starting (rl=$rl)" >/dev/console 2>/dev/null || true
log info "rc.init start rl=$rl (PATH=$PATH)"

# 1a. /var/log (persistent, btrfs subvol so crash logs survive reboot)
mkdir -p /var/log
mountpoint -q /var/log 2>/dev/null || \
    mount -t btrfs -o subvol=@log,noatime "/dev/disk/by-uuid/$DISK_UUID" /var/log \
    || log warn "/var/log mount failed (using existing)"

# 1b. /var/tmp
mkdir -p /var/tmp
mountpoint -q /var/tmp 2>/dev/null || \
    mount -t btrfs -o subvol=@/@tmp,noatime "/dev/disk/by-uuid/$DISK_UUID" /var/tmp \
    || log warn "/var/tmp mount failed (using existing)"

# 1c. /gnu -- the store.  No /gnu, no Shepherd; this is the hard gate.
mkdir -p /gnu
if ! mountpoint -q /gnu 2>/dev/null; then
    if ! mount -t btrfs -o subvol=@/@gnu,noatime "/dev/disk/by-uuid/$DISK_UUID" /gnu; then
        log critical "CRITICAL: /gnu mount failed"
        exec /sbin/agetty --noclear -a root tty1 linux
    fi
fi
log info "/gnu ok"

# 1d. /run -- fresh tmpfs each boot.  This is the key fix for "Socket deleted;
#     stopping": /run is recreated empty and deterministic, and we never umount
#     it from a service.  Idempotent: only mount/mkdir/chmod if not already done.
mkdir -p /run
mountpoint -q /run 2>/dev/null || \
    mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run \
    || log warn "/run tmpfs mount failed (proceeding with existing /run)"
ln -sfnT /run /var/run 2>/dev/null || true
mkdir -p /run/shepherd
chmod 700 /run/shepherd
# /var/log/shepherd holds per-service logs Shepherd's logger pulls here.
mkdir -p /var/log/shepherd

# Ensure the `socklog` group exists before Shepherd starts its services:
# 14-log-files.scm chowns the /var/log/socklog spool to root:socklog, and
# without this group every boot logs `chown: invalid group: 'root:socklog'`.
getent group socklog >/dev/null 2>&1 || groupadd -r socklog 2>/dev/null || log warn "could not create 'socklog' group"

log info "rc.init stage-0 complete, launching Shepherd"

# 1e. /sbin is not where `herd' lives; add it to the boot PATH.
SHEPHERD=/usr/local/bin/shepherd
[ -x "$SHEPHERD" ] || SHEPHERD=/gnu/current-guix-profile/bin/shepherd
if [ ! -x "$SHEPHERD" ]; then
    log critical "CRITICAL: no shepherd binary found"
    exec /sbin/agetty --noclear -a root tty1 linux
fi
log info "Using $SHEPHERD"

# 1f. Launch Shepherd once. -I = stay in foreground (we bg it); -s socket,
#     -c config, -l daemon log. Idempotent: skip if a socket is already live.
if [ ! -S /run/shepherd/socket ]; then
    "$SHEPHERD" -s /run/shepherd/socket -c /etc/shepherd/init.scm \
        -l /var/log/shepherd.log -I >/dev/null 2>&1 &
    i=0
    while [ ! -S /run/shepherd/socket ] && [ "$i" -lt 150 ]; do
        sleep 0.1
        i=$((i + 1))
    done
fi

if [ ! -S /run/shepherd/socket ]; then
    log critical "CRITICAL: shepherd socket not created"
    log critical "--- /var/log/shepherd.log ---"
    tail -n 40 /var/log/shepherd.log >>"$LOGFILE" 2>/dev/null || true
    log critical "Falling back to emergency getty"
    exec /sbin/agetty --noclear -a root tty1 linux
fi
log info "SUCCESS: shepherd up, config loaded"

# 1g. Mirror Shepherd's lifecycle log to the console with a colored spinner.
(
    set -e
    current_svc=""
    spinner_pid=""

    spin() {
        local svc="$1" i=0 chars='-\|/'
        while true; do
            printf '\r\033[0;36m→ Starting %-20s %s\033[0m' \
                "$svc" "${chars:$i:1}" >/dev/console 2>/dev/null || true
            i=$(( (i + 1) % 4 ))
            sleep 0.1
        done
    }

    tail -n 0 -F /var/log/shepherd.log 2>/dev/null |
    while IFS= read -r line; do
        case "$line" in
        *"Starting service "*)
            if [ -n "$spinner_pid" ]; then kill "$spinner_pid" 2>/dev/null || true; fi
            wait "$spinner_pid" 2>/dev/null || true
            current_svc=$(printf '%s' "$line" | sed -n 's/.*Starting service \([^.]*\).*/\1/p')
            spin "$current_svc" &
            spinner_pid=$!
            ;;
        *"has been started."*)
            if [ -n "$spinner_pid" ]; then kill "$spinner_pid" 2>/dev/null || true; fi
            wait "$spinner_pid" 2>/dev/null || true
            spinner_pid=""
            printf '\r\033[1;32m✓ Service %-20s started\033[0m\n' \
                "$(printf '%s' "$line" | sed -n 's/.*Service \([^ ]*\) has been started.*/\1/p')" >/dev/console 2>/dev/null || true
            ;;
        *"has been disabled."*|*"failed to start"*|*"Respawning too fast"*|*"is currently disabled."*)
            if [ -n "$spinner_pid" ]; then kill "$spinner_pid" 2>/dev/null || true; fi
            wait "$spinner_pid" 2>/dev/null || true
            spinner_pid=""
            printf '\r\033[1;31m✗ Service %-20s failed\033[0m\n' \
                "$(printf '%s' "$line" | sed -n 's/.*Service \([^ ]*\).*/\1/p')" >/dev/console 2>/dev/null || true
            ;;
        esac
    done
) &

log info "rc.init finished OK"
#!/bin/sh -e

### Things to do before hardware halt/reboot/poweroff.
### Ideally, it should be a single call to the service manager,
### telling it to bring all the services down.

### If your s6-linux-init-maker invocation was made with the -1
### option, messages from rc.shutdown will appear on /dev/console
### as well as be logged by the catch-all logger.
### If your s6-linux-init-maker invocation did NOT include the -1
### option, messages from rc.shutdown will only be logged by the
### catch-all logger and will NOT appear on /dev/console. In order
### to print them to /dev/console instead, you may want to
### uncomment the following line:
# exec >/dev/console 2>&1

### If your services are managed by sysv-rc:
### also remove the K11reboot link from /etc/rc6.d to prevent
### sysv-rc from rebooting prematurely - because sysvinit does
### not properly separate state changes from system init/shutdown.
# exec /etc/init.d/rc 6

### If your services are managed by OpenRC:
### also remove the "killprocs" and "mount-ro" symlinks from
### /etc/runlevels/shutdown - because OpenRC does not properly
### separate the service manager from the shutdown manager either.
# exec /sbin/openrc shutdown

### If your services are managed by s6-rc:
# exec s6-rc -v2 -bDa change
#!/bin/sh -e

### Things to do *right before* the machine gets rebooted or
### powered off, at the very end of the shutdown sequence,
### when all the filesystems are unmounted.

### This is a last resort hook; normally nothing should be
### done here (your rc.shutdown script should have taken care
### of everything) and you should leave this script empty.

### Some distributions, however, may need to perform some
### actions after unmounting the filesystems: typically if
### an additional teardown action is required on a filesystem
### after unmounting it, or if the system needs to be
### pivot_rooted before it can be shut down, etc.

### Those are all exceptional cases. If you don't know for
### certain that you need to do something here, you don't.
#!/bin/sh -e

### This script is called once at boot time by rc.init, and is
### also called by the runleveld service every time the user
### requests a machine state change via telinit.
### Ideally, it should just be a call to the service manager.

test "$#" -gt 0 || { echo 'runlevel: fatal: too few arguments' 1>&2 ; exit 100 ; }


### If your services are managed by sysv-rc:
# exec /etc/init.d/rc "$1"

### If your services are managed by OpenRC:
# exec /sbin/openrc "$1"

### If your services are managed by s6-rc:
# exec s6-rc -v2 -up change "$1"

9.6. Commands

#!/bin/sh
exec /usr/sbin/s6-linux-init -v 1 -m 0022 \
  -c /etc/s6-linux-init/current \
  -p /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin \
  -- "$@"
#!/bin/sh
exec herd start halt
#!/bin/sh
exec herd start halt
#!/bin/sh
exec herd start reboot

10. Acknowledge