SPS · log · files · refs · tags · releases · people · languages · GitHub · commit
1794243
Add stock units for new daemons and a rolling recipe upgrade tool.
Parents
Stat
.gitignore | 2 + Makefile | 1 + PACKAGE-MAINTAINER.md | 76 ++ README.md | 4 + lib/setup/services | 27 + lib/setup/sv/dinit/avahi-daemon | 5 + lib/setup/sv/dinit/conntrackd | 4 + lib/setup/sv/dinit/distccd | 4 + lib/setup/sv/dinit/dnsmasq | 4 + lib/setup/sv/dinit/dropbear | 4 + lib/setup/sv/dinit/fail2ban | 4 + lib/setup/sv/dinit/haproxy | 4 + lib/setup/sv/dinit/libvirtd | 5 + lib/setup/sv/dinit/lighttpd | 4 + lib/setup/sv/dinit/lxc-net | 4 + lib/setup/sv/dinit/mariadbd | 4 + lib/setup/sv/dinit/memcached | 4 + lib/setup/sv/dinit/nfs-server | 4 + lib/setup/sv/dinit/openvpn | 4 + lib/setup/sv/dinit/postgresql | 4 + lib/setup/sv/dinit/tinyproxy | 4 + lib/setup/sv/dinit/tor | 4 + lib/setup/sv/dinit/unbound | 4 + lib/setup/sv/dinit/usbmuxd | 4 + lib/setup/sv/dinit/valkey | 4 + lib/setup/sv/openrc/avahi-daemon | 11 + lib/setup/sv/openrc/conntrackd | 11 + lib/setup/sv/openrc/distccd | 11 + lib/setup/sv/openrc/dnsmasq | 11 + lib/setup/sv/openrc/dropbear | 18 + lib/setup/sv/openrc/fail2ban | 11 + lib/setup/sv/openrc/haproxy | 11 + lib/setup/sv/openrc/libvirtd | 11 + lib/setup/sv/openrc/lighttpd | 11 + lib/setup/sv/openrc/lxc-net | 10 + lib/setup/sv/openrc/mariadbd | 16 + lib/setup/sv/openrc/memcached | 11 + lib/setup/sv/openrc/nfs-server | 17 + lib/setup/sv/openrc/openvpn | 11 + lib/setup/sv/openrc/postgresql | 16 + lib/setup/sv/openrc/tinyproxy | 11 + lib/setup/sv/openrc/tor | 11 + lib/setup/sv/openrc/unbound | 11 + lib/setup/sv/openrc/usbmuxd | 11 + lib/setup/sv/openrc/valkey | 11 + lib/setup/sv/runit/avahi-daemon/run | 3 + lib/setup/sv/runit/conntrackd/run | 3 + lib/setup/sv/runit/distccd/run | 3 + lib/setup/sv/runit/dnsmasq/run | 3 + lib/setup/sv/runit/dropbear/run | 7 + lib/setup/sv/runit/fail2ban/run | 3 + lib/setup/sv/runit/haproxy/run | 3 + lib/setup/sv/runit/libvirtd/run | 3 + lib/setup/sv/runit/lighttpd/run | 3 + lib/setup/sv/runit/lxc-net/run | 3 + lib/setup/sv/runit/mariadbd/run | 4 + lib/setup/sv/runit/memcached/run | 3 + lib/setup/sv/runit/nfs-server/run | 6 + lib/setup/sv/runit/openvpn/run | 3 + lib/setup/sv/runit/postgresql/run | 4 + lib/setup/sv/runit/tinyproxy/run | 3 + lib/setup/sv/runit/tor/run | 3 + lib/setup/sv/runit/unbound/run | 3 + lib/setup/sv/runit/usbmuxd/run | 3 + lib/setup/sv/runit/valkey/run | 3 + lib/setup/sv/s6/avahi-daemon/run | 3 + lib/setup/sv/s6/conntrackd/run | 3 + lib/setup/sv/s6/distccd/run | 3 + lib/setup/sv/s6/dnsmasq/run | 3 + lib/setup/sv/s6/dropbear/run | 7 + lib/setup/sv/s6/fail2ban/run | 3 + lib/setup/sv/s6/haproxy/run | 3 + lib/setup/sv/s6/libvirtd/run | 3 + lib/setup/sv/s6/lighttpd/run | 3 + lib/setup/sv/s6/lxc-net/run | 3 + lib/setup/sv/s6/mariadbd/run | 4 + lib/setup/sv/s6/memcached/run | 3 + lib/setup/sv/s6/nfs-server/run | 6 + lib/setup/sv/s6/openvpn/run | 3 + lib/setup/sv/s6/postgresql/run | 4 + lib/setup/sv/s6/tinyproxy/run | 3 + lib/setup/sv/s6/tor/run | 3 + lib/setup/sv/s6/unbound/run | 3 + lib/setup/sv/s6/usbmuxd/run | 3 + lib/setup/sv/s6/valkey/run | 3 + lib/setup/sv/shepherd/avahi-daemon.scm | 9 + lib/setup/sv/shepherd/conntrackd.scm | 9 + lib/setup/sv/shepherd/distccd.scm | 9 + lib/setup/sv/shepherd/dnsmasq.scm | 9 + lib/setup/sv/shepherd/dropbear.scm | 9 + lib/setup/sv/shepherd/fail2ban.scm | 9 + lib/setup/sv/shepherd/haproxy.scm | 9 + lib/setup/sv/shepherd/libvirtd.scm | 9 + lib/setup/sv/shepherd/lighttpd.scm | 9 + lib/setup/sv/shepherd/lxc-net.scm | 9 + lib/setup/sv/shepherd/mariadbd.scm | 9 + lib/setup/sv/shepherd/memcached.scm | 9 + lib/setup/sv/shepherd/nfs-server.scm | 9 + lib/setup/sv/shepherd/openvpn.scm | 9 + lib/setup/sv/shepherd/postgresql.scm | 9 + lib/setup/sv/shepherd/tinyproxy.scm | 9 + lib/setup/sv/shepherd/tor.scm | 9 + lib/setup/sv/shepherd/unbound.scm | 9 + lib/setup/sv/shepherd/usbmuxd.scm | 9 + lib/setup/sv/shepherd/valkey.scm | 9 + lib/setup/sv/systemd/avahi-daemon.service | 10 + lib/setup/sv/systemd/conntrackd.service | 10 + lib/setup/sv/systemd/distccd.service | 10 + lib/setup/sv/systemd/dnsmasq.service | 10 + lib/setup/sv/systemd/dropbear.service | 10 + lib/setup/sv/systemd/fail2ban.service | 10 + lib/setup/sv/systemd/haproxy.service | 10 + lib/setup/sv/systemd/libvirtd.service | 10 + lib/setup/sv/systemd/lighttpd.service | 10 + lib/setup/sv/systemd/lxc-net.service | 11 + lib/setup/sv/systemd/mariadb.service | 11 + lib/setup/sv/systemd/memcached.service | 10 + lib/setup/sv/systemd/nfs-server.service | 10 + lib/setup/sv/systemd/openvpn.service | 10 + lib/setup/sv/systemd/postgresql.service | 11 + lib/setup/sv/systemd/tinyproxy.service | 10 + lib/setup/sv/systemd/tor.service | 10 + lib/setup/sv/systemd/unbound.service | 10 + lib/setup/sv/systemd/usbmuxd.service | 10 + lib/setup/sv/systemd/valkey.service | 10 + tests/test_services.sh | 12 + tests/test_stock_services.sh | 30 + tests/test_upgrade_recipes.sh | 37 + tools/upgrade-recipes | 1154 +++++++++++++++++++++++++++++ tools/write-stock-services.py | 343 +++++++++ 130 files changed, 2531 insertions(+)
Diff
diff --git a/.gitignore b/.gitignore
index 3f084a0..45e4ae9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@
*.tmp
.sps-*
/dist/
+__pycache__/
+*.pyc
diff --git a/Makefile b/Makefile
index 3380561..a52bb83 100644
--- a/Makefile
+++ b/Makefile
@@ -34,6 +34,7 @@ lint:
[ -f $$script ] || continue; \
$(POSIX_SHELL) -n $$script; \
done
+ @python3 -m py_compile tools/upgrade-recipes tools/write-stock-services.py
@set -e; for script in lib/mkiso/initrd-init lib/mkiso/live-rc \
lib/mkiso/live-plasma lib/mkiso/live-plasma-session \
lib/mkiso/live-init lib/mkiso/udhcpc-script; do \
diff --git a/PACKAGE-MAINTAINER.md b/PACKAGE-MAINTAINER.md
index b2907c1..e451def 100644
--- a/PACKAGE-MAINTAINER.md
+++ b/PACKAGE-MAINTAINER.md
@@ -311,3 +311,79 @@ dependencies and runtime linkage.
relationships. Also inspect modes, ownership, symlink targets, installed paths,
and the resulting archive manually. After removal, account for intentionally
retained modified protected files and any state created by lifecycle hooks.
+
+## Init scripts for daemons
+
+Mapped daemons live in `lib/setup/services`. After `pkin`, SPS enables the
+service for the init named in `/etc/sps/init`. Supported inits are `systemd`,
+`openrc`, `s6`, `runit`, `dinit`, and `shepherd`.
+
+A package should ship a systemd unit under `$PKG/usr/lib/systemd/system` and
+an OpenRC script under `$PKG/etc/init.d` (see `seatd`, `openssh`, and the
+database/network daemons in extra). When the chosen init has no file in the
+package, `pkin` copies a stock script from `lib/setup/sv/<init>/` and then
+enables it. Add a stock file for every supported init whenever you introduce a
+new daemon:
+
+```text
+lib/setup/sv/systemd/NAME.service
+lib/setup/sv/openrc/NAME
+lib/setup/sv/runit/NAME/run
+lib/setup/sv/s6/NAME/run
+lib/setup/sv/dinit/NAME
+lib/setup/sv/shepherd/NAME.scm
+```
+
+`tools/write-stock-services.py` regenerates those stock files from its service
+table and installs the systemd plus OpenRC copies next to extra recipes.
+
+## Rolling upgrades (`tools/upgrade-recipes`)
+
+Splux is a rolling distro. Maintainers bump every recipe with a real upstream
+tarball by running one command against the core and extra trees:
+
+```sh
+python3 tools/upgrade-recipes \
+ --trees /usr/src/sps/core:/usr/src/sps/extra
+```
+
+That prints a plan. It does not write files. To apply it:
+
+```sh
+python3 tools/upgrade-recipes \
+ --trees /usr/src/sps/core:/usr/src/sps/extra \
+ --apply
+```
+
+`--apply` downloads each newer tarball, writes a real SHA-256, sets `release`
+to 1, and rewrites `source` URLs. It then unpacks the new source and scans
+Meson, CMake, Autoconf, and pkg-config files for libraries the recipe does not
+declare. Names that already exist in the trees are appended as `depend`. Names
+that do not exist are listed as `missing`. Pass `--scaffold-missing` to draft a
+conservative extra recipe when a source URL can be resolved.
+
+Other useful switches:
+
+```text
+--only gtk4,qemu,llvm limit the run
+--jobs 12 parallel upstream probes
+--scan-deps download and scan on a dry-run
+--no-scan-deps skip tarball work (version plan only)
+--include-kernel allow linux and firmware recipes
+--json machine-readable plan
+--selftest offline checks
+```
+
+Qt, KDE Frameworks, Plasma, and GStreamer modules are pinned together so one
+sibling cannot race ahead of the rest. GNOME even-minor libraries (gtk, glib,
+libadwaita, ...) skip odd development series. Tags containing alpha, beta, rc,
+or git are ignored. Kernel and firmware recipes stay pinned unless you pass
+`--include-kernel`.
+
+Set `SPS_RECIPE_TREES` instead of `--trees` if you prefer. Set `GITHUB_TOKEN`
+if GitHub rate-limits the probe. Downloaded tarballs are cached under
+`~/.cache/sps-upgrade` (override with `SPS_UPGRADE_CACHE`).
+
+Run `src update` in the recipe trees after a successful apply, then build the
+packages that moved.
+
diff --git a/README.md b/README.md
index f66b1e6..bdb2751 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,10 @@ SPS is a set of tools rather than one large front end:
style). Optional; other projects using SPS do not need it.
- `mkiso` builds an SPS Linux live ISO (systemd by default; busybox init if
systemd is not on the build host) you can flash and boot into `setup`.
+- `tools/upgrade-recipes` is a maintainer command, not a runtime package tool.
+ It walks core and extra recipes, finds the newest stable upstream, rewrites
+ version/source/hash, and records newly required libraries. See
+ [PACKAGE-MAINTAINER.md](PACKAGE-MAINTAINER.md).
The split matters. `pkin` does not search repositories. `pkdel` does not solve
dependencies. `src` does not install anything. `sget` ties the lower-level tools
diff --git a/lib/setup/services b/lib/setup/services
index 333d263..019e8c1 100644
--- a/lib/setup/services
+++ b/lib/setup/services
@@ -17,3 +17,30 @@ cupsd cups.service multi-user.target cupsd default
NetworkManager NetworkManager.service multi-user.target NetworkManager default
pipewire pipewire.service default.target pipewire default
wireplumber wireplumber.service default.target wireplumber default
+fail2ban fail2ban.service multi-user.target fail2ban default
+unbound unbound.service multi-user.target unbound default
+dnsmasq dnsmasq.service multi-user.target dnsmasq default
+openvpn openvpn.service multi-user.target openvpn default
+tor tor.service multi-user.target tor default
+dropbear dropbear.service multi-user.target dropbear default
+tinyproxy tinyproxy.service multi-user.target tinyproxy default
+haproxy haproxy.service multi-user.target haproxy default
+lighttpd lighttpd.service multi-user.target lighttpd default
+avahi avahi-daemon.service multi-user.target avahi-daemon default
+avahi-daemon avahi-daemon.service multi-user.target avahi-daemon default
+nfs-utils nfs-server.service multi-user.target nfs-server default
+nfs-server nfs-server.service multi-user.target nfs-server default
+libvirt libvirtd.service multi-user.target libvirtd default
+libvirtd libvirtd.service multi-user.target libvirtd default
+postgresql postgresql.service multi-user.target postgresql default
+mariadb mariadb.service multi-user.target mariadbd default
+mariadbd mariadb.service multi-user.target mariadbd default
+memcached memcached.service multi-user.target memcached default
+valkey valkey.service multi-user.target valkey default
+conntrack-tools conntrackd.service multi-user.target conntrackd default
+conntrackd conntrackd.service multi-user.target conntrackd default
+usbmuxd usbmuxd.service multi-user.target usbmuxd default
+distcc distccd.service multi-user.target distccd default
+distccd distccd.service multi-user.target distccd default
+lxc lxc-net.service multi-user.target lxc-net default
+lxc-net lxc-net.service multi-user.target lxc-net default
diff --git a/lib/setup/sv/dinit/avahi-daemon b/lib/setup/sv/dinit/avahi-daemon
new file mode 100644
index 0000000..9df7772
--- /dev/null
+++ b/lib/setup/sv/dinit/avahi-daemon
@@ -0,0 +1,5 @@
+type = process
+command = /usr/sbin/avahi-daemon --no-chroot
+restart = true
+smooth-recovery = true
+depends-on = dbus
diff --git a/lib/setup/sv/dinit/conntrackd b/lib/setup/sv/dinit/conntrackd
new file mode 100644
index 0000000..02d9a0f
--- /dev/null
+++ b/lib/setup/sv/dinit/conntrackd
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/conntrackd -C /etc/conntrackd/conntrackd.conf
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/distccd b/lib/setup/sv/dinit/distccd
new file mode 100644
index 0000000..4ca8f86
--- /dev/null
+++ b/lib/setup/sv/dinit/distccd
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/distccd --no-detach --allow 127.0.0.1 --log-stderr
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/dnsmasq b/lib/setup/sv/dinit/dnsmasq
new file mode 100644
index 0000000..aaadb9b
--- /dev/null
+++ b/lib/setup/sv/dinit/dnsmasq
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/dnsmasq -k
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/dropbear b/lib/setup/sv/dinit/dropbear
new file mode 100644
index 0000000..54291af
--- /dev/null
+++ b/lib/setup/sv/dinit/dropbear
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/dropbear -F -E
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/fail2ban b/lib/setup/sv/dinit/fail2ban
new file mode 100644
index 0000000..8757a25
--- /dev/null
+++ b/lib/setup/sv/dinit/fail2ban
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/fail2ban-server -xf --logtarget=stdout
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/haproxy b/lib/setup/sv/dinit/haproxy
new file mode 100644
index 0000000..76ba874
--- /dev/null
+++ b/lib/setup/sv/dinit/haproxy
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -db
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/libvirtd b/lib/setup/sv/dinit/libvirtd
new file mode 100644
index 0000000..36a96af
--- /dev/null
+++ b/lib/setup/sv/dinit/libvirtd
@@ -0,0 +1,5 @@
+type = process
+command = /usr/sbin/libvirtd
+restart = true
+smooth-recovery = true
+depends-on = dbus
diff --git a/lib/setup/sv/dinit/lighttpd b/lib/setup/sv/dinit/lighttpd
new file mode 100644
index 0000000..8908b81
--- /dev/null
+++ b/lib/setup/sv/dinit/lighttpd
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/lxc-net b/lib/setup/sv/dinit/lxc-net
new file mode 100644
index 0000000..828b4bb
--- /dev/null
+++ b/lib/setup/sv/dinit/lxc-net
@@ -0,0 +1,4 @@
+type = process
+command = /usr/libexec/lxc/lxc-net start
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/mariadbd b/lib/setup/sv/dinit/mariadbd
new file mode 100644
index 0000000..baeb6f3
--- /dev/null
+++ b/lib/setup/sv/dinit/mariadbd
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/mariadbd --datadir=/var/lib/mysql
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/memcached b/lib/setup/sv/dinit/memcached
new file mode 100644
index 0000000..5980ecb
--- /dev/null
+++ b/lib/setup/sv/dinit/memcached
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/memcached -u nobody
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/nfs-server b/lib/setup/sv/dinit/nfs-server
new file mode 100644
index 0000000..302f7e0
--- /dev/null
+++ b/lib/setup/sv/dinit/nfs-server
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/rpc.mountd -F
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/openvpn b/lib/setup/sv/dinit/openvpn
new file mode 100644
index 0000000..7f78bbb
--- /dev/null
+++ b/lib/setup/sv/dinit/openvpn
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/openvpn --config /etc/openvpn/server.conf
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/postgresql b/lib/setup/sv/dinit/postgresql
new file mode 100644
index 0000000..21dce21
--- /dev/null
+++ b/lib/setup/sv/dinit/postgresql
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/postgres -D /var/lib/postgres/data
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/tinyproxy b/lib/setup/sv/dinit/tinyproxy
new file mode 100644
index 0000000..b3ab674
--- /dev/null
+++ b/lib/setup/sv/dinit/tinyproxy
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/tinyproxy -d
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/tor b/lib/setup/sv/dinit/tor
new file mode 100644
index 0000000..47f7a85
--- /dev/null
+++ b/lib/setup/sv/dinit/tor
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/tor --RunAsDaemon 0
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/unbound b/lib/setup/sv/dinit/unbound
new file mode 100644
index 0000000..6168a99
--- /dev/null
+++ b/lib/setup/sv/dinit/unbound
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/unbound -d
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/usbmuxd b/lib/setup/sv/dinit/usbmuxd
new file mode 100644
index 0000000..1d0fdd8
--- /dev/null
+++ b/lib/setup/sv/dinit/usbmuxd
@@ -0,0 +1,4 @@
+type = process
+command = /usr/sbin/usbmuxd -f
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/dinit/valkey b/lib/setup/sv/dinit/valkey
new file mode 100644
index 0000000..968cf02
--- /dev/null
+++ b/lib/setup/sv/dinit/valkey
@@ -0,0 +1,4 @@
+type = process
+command = /usr/bin/valkey-server --daemonize no --bind 127.0.0.1
+restart = true
+smooth-recovery = true
diff --git a/lib/setup/sv/openrc/avahi-daemon b/lib/setup/sv/openrc/avahi-daemon
new file mode 100755
index 0000000..5148c09
--- /dev/null
+++ b/lib/setup/sv/openrc/avahi-daemon
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="Avahi mDNS/DNS-SD daemon"
+command="/usr/sbin/avahi-daemon"
+command_args="--no-chroot"
+command_background="yes"
+pidfile="/run/avahi-daemon.pid"
+
+depend() {
+ need dbus
+ after dbus
+}
diff --git a/lib/setup/sv/openrc/conntrackd b/lib/setup/sv/openrc/conntrackd
new file mode 100755
index 0000000..e7f0a93
--- /dev/null
+++ b/lib/setup/sv/openrc/conntrackd
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="connection tracking daemon"
+command="/usr/sbin/conntrackd"
+command_args="-C /etc/conntrackd/conntrackd.conf"
+command_background="yes"
+pidfile="/run/conntrackd.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/distccd b/lib/setup/sv/openrc/distccd
new file mode 100755
index 0000000..b33f847
--- /dev/null
+++ b/lib/setup/sv/openrc/distccd
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="distributed C/C++ compilation daemon"
+command="/usr/bin/distccd"
+command_args="--no-detach --allow 127.0.0.1 --log-stderr"
+command_background="yes"
+pidfile="/run/distccd.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/dnsmasq b/lib/setup/sv/openrc/dnsmasq
new file mode 100755
index 0000000..9ae93ec
--- /dev/null
+++ b/lib/setup/sv/openrc/dnsmasq
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="DNS and DHCP server"
+command="/usr/sbin/dnsmasq"
+command_args="-k"
+command_background="yes"
+pidfile="/run/dnsmasq.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/dropbear b/lib/setup/sv/openrc/dropbear
new file mode 100755
index 0000000..9844850
--- /dev/null
+++ b/lib/setup/sv/openrc/dropbear
@@ -0,0 +1,18 @@
+#!/sbin/openrc-run
+description="Dropbear SSH server"
+command="/usr/sbin/dropbear"
+command_args="-F -E"
+command_background="yes"
+pidfile="/run/dropbear.pid"
+
+depend() {
+ use net
+ after net
+}
+
+start_pre() {
+ if command -v dropbearkey >/dev/null 2>&1; then
+ mkdir -p /etc/dropbear
+ [ -f /etc/dropbear/dropbear_rsa_host_key ] || dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key >/dev/null
+ fi
+}
diff --git a/lib/setup/sv/openrc/fail2ban b/lib/setup/sv/openrc/fail2ban
new file mode 100755
index 0000000..96ef6b5
--- /dev/null
+++ b/lib/setup/sv/openrc/fail2ban
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="fail2ban authentication ban daemon"
+command="/usr/bin/fail2ban-server"
+command_args="-xf --logtarget=stdout"
+command_background="yes"
+pidfile="/run/fail2ban.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/haproxy b/lib/setup/sv/openrc/haproxy
new file mode 100755
index 0000000..bb47359
--- /dev/null
+++ b/lib/setup/sv/openrc/haproxy
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="HAProxy load balancer"
+command="/usr/sbin/haproxy"
+command_args="-f /etc/haproxy/haproxy.cfg -db"
+command_background="yes"
+pidfile="/run/haproxy.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/libvirtd b/lib/setup/sv/openrc/libvirtd
new file mode 100755
index 0000000..a362121
--- /dev/null
+++ b/lib/setup/sv/openrc/libvirtd
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="libvirt virtualization daemon"
+command="/usr/sbin/libvirtd"
+command_args=""
+command_background="yes"
+pidfile="/run/libvirtd.pid"
+
+depend() {
+ need dbus
+ after dbus
+}
diff --git a/lib/setup/sv/openrc/lighttpd b/lib/setup/sv/openrc/lighttpd
new file mode 100755
index 0000000..3eeefde
--- /dev/null
+++ b/lib/setup/sv/openrc/lighttpd
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="lighttpd web server"
+command="/usr/sbin/lighttpd"
+command_args="-D -f /etc/lighttpd/lighttpd.conf"
+command_background="yes"
+pidfile="/run/lighttpd.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/lxc-net b/lib/setup/sv/openrc/lxc-net
new file mode 100755
index 0000000..f3688fe
--- /dev/null
+++ b/lib/setup/sv/openrc/lxc-net
@@ -0,0 +1,10 @@
+#!/sbin/openrc-run
+description="LXC bridge networking"
+command="/usr/libexec/lxc/lxc-net"
+command_args="start"
+command_background="no"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/mariadbd b/lib/setup/sv/openrc/mariadbd
new file mode 100755
index 0000000..9947ea1
--- /dev/null
+++ b/lib/setup/sv/openrc/mariadbd
@@ -0,0 +1,16 @@
+#!/sbin/openrc-run
+description="MariaDB database server"
+command="/usr/sbin/mariadbd"
+command_args="--datadir=/var/lib/mysql"
+command_background="yes"
+pidfile="/run/mariadbd.pid"
+command_user="mysql"
+
+depend() {
+ use net
+ after net
+}
+
+start_pre() {
+ install -d -m 0750 /run/mysqld /var/lib/mysql || :
+}
diff --git a/lib/setup/sv/openrc/memcached b/lib/setup/sv/openrc/memcached
new file mode 100755
index 0000000..0854cf7
--- /dev/null
+++ b/lib/setup/sv/openrc/memcached
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="memcached object cache"
+command="/usr/bin/memcached"
+command_args="-u nobody"
+command_background="yes"
+pidfile="/run/memcached.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/nfs-server b/lib/setup/sv/openrc/nfs-server
new file mode 100755
index 0000000..4427f36
--- /dev/null
+++ b/lib/setup/sv/openrc/nfs-server
@@ -0,0 +1,17 @@
+#!/sbin/openrc-run
+description="NFS server"
+command="/usr/sbin/rpc.mountd"
+command_args="-F"
+command_background="yes"
+pidfile="/run/nfs-server.pid"
+
+depend() {
+ use net
+ after net
+}
+
+start_pre() {
+ if command -v exportfs >/dev/null 2>&1; then exportfs -ra || :; fi
+ if command -v rpc.statd >/dev/null 2>&1; then rpc.statd --no-notify || :; fi
+ if command -v rpc.nfsd >/dev/null 2>&1; then rpc.nfsd 8 || :; fi
+}
diff --git a/lib/setup/sv/openrc/openvpn b/lib/setup/sv/openrc/openvpn
new file mode 100755
index 0000000..f9f0267
--- /dev/null
+++ b/lib/setup/sv/openrc/openvpn
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="OpenVPN server"
+command="/usr/sbin/openvpn"
+command_args="--config /etc/openvpn/server.conf"
+command_background="yes"
+pidfile="/run/openvpn.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/postgresql b/lib/setup/sv/openrc/postgresql
new file mode 100755
index 0000000..cfa8a33
--- /dev/null
+++ b/lib/setup/sv/openrc/postgresql
@@ -0,0 +1,16 @@
+#!/sbin/openrc-run
+description="PostgreSQL database server"
+command="/usr/bin/postgres"
+command_args="-D /var/lib/postgres/data"
+command_background="yes"
+pidfile="/run/postgresql.pid"
+command_user="postgres"
+
+depend() {
+ use net
+ after net
+}
+
+start_pre() {
+ install -d -m 0755 /run/postgresql /var/lib/postgres/data || :
+}
diff --git a/lib/setup/sv/openrc/tinyproxy b/lib/setup/sv/openrc/tinyproxy
new file mode 100755
index 0000000..ac4a18f
--- /dev/null
+++ b/lib/setup/sv/openrc/tinyproxy
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="Tinyproxy HTTP proxy"
+command="/usr/bin/tinyproxy"
+command_args="-d"
+command_background="yes"
+pidfile="/run/tinyproxy.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/tor b/lib/setup/sv/openrc/tor
new file mode 100755
index 0000000..7e4f3af
--- /dev/null
+++ b/lib/setup/sv/openrc/tor
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="Tor anonymity daemon"
+command="/usr/bin/tor"
+command_args="--RunAsDaemon 0"
+command_background="yes"
+pidfile="/run/tor.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/unbound b/lib/setup/sv/openrc/unbound
new file mode 100755
index 0000000..da3cd2d
--- /dev/null
+++ b/lib/setup/sv/openrc/unbound
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="validating recursive DNS resolver"
+command="/usr/sbin/unbound"
+command_args="-d"
+command_background="yes"
+pidfile="/run/unbound.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/usbmuxd b/lib/setup/sv/openrc/usbmuxd
new file mode 100755
index 0000000..3837993
--- /dev/null
+++ b/lib/setup/sv/openrc/usbmuxd
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="iOS USB multiplexing daemon"
+command="/usr/sbin/usbmuxd"
+command_args="-f"
+command_background="yes"
+pidfile="/run/usbmuxd.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/openrc/valkey b/lib/setup/sv/openrc/valkey
new file mode 100755
index 0000000..2b11464
--- /dev/null
+++ b/lib/setup/sv/openrc/valkey
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+description="Valkey data store"
+command="/usr/bin/valkey-server"
+command_args="--daemonize no --bind 127.0.0.1"
+command_background="yes"
+pidfile="/run/valkey.pid"
+
+depend() {
+ use net
+ after net
+}
diff --git a/lib/setup/sv/runit/avahi-daemon/run b/lib/setup/sv/runit/avahi-daemon/run
new file mode 100755
index 0000000..2d0d88e
--- /dev/null
+++ b/lib/setup/sv/runit/avahi-daemon/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/avahi-daemon --no-chroot
diff --git a/lib/setup/sv/runit/conntrackd/run b/lib/setup/sv/runit/conntrackd/run
new file mode 100755
index 0000000..9e1a1fb
--- /dev/null
+++ b/lib/setup/sv/runit/conntrackd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/conntrackd -C /etc/conntrackd/conntrackd.conf
diff --git a/lib/setup/sv/runit/distccd/run b/lib/setup/sv/runit/distccd/run
new file mode 100755
index 0000000..a40a418
--- /dev/null
+++ b/lib/setup/sv/runit/distccd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/distccd --no-detach --allow 127.0.0.1 --log-stderr
diff --git a/lib/setup/sv/runit/dnsmasq/run b/lib/setup/sv/runit/dnsmasq/run
new file mode 100755
index 0000000..c605f8b
--- /dev/null
+++ b/lib/setup/sv/runit/dnsmasq/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/dnsmasq -k
diff --git a/lib/setup/sv/runit/dropbear/run b/lib/setup/sv/runit/dropbear/run
new file mode 100755
index 0000000..f2b653f
--- /dev/null
+++ b/lib/setup/sv/runit/dropbear/run
@@ -0,0 +1,7 @@
+#!/bin/sh
+exec 2>&1
+if command -v dropbearkey >/dev/null 2>&1; then
+ mkdir -p /etc/dropbear
+ [ -f /etc/dropbear/dropbear_rsa_host_key ] || dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key >/dev/null
+fi
+exec /usr/sbin/dropbear -F -E
diff --git a/lib/setup/sv/runit/fail2ban/run b/lib/setup/sv/runit/fail2ban/run
new file mode 100755
index 0000000..7bc8461
--- /dev/null
+++ b/lib/setup/sv/runit/fail2ban/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/fail2ban-server -xf --logtarget=stdout
diff --git a/lib/setup/sv/runit/haproxy/run b/lib/setup/sv/runit/haproxy/run
new file mode 100755
index 0000000..4c215a4
--- /dev/null
+++ b/lib/setup/sv/runit/haproxy/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -db
diff --git a/lib/setup/sv/runit/libvirtd/run b/lib/setup/sv/runit/libvirtd/run
new file mode 100755
index 0000000..47c5776
--- /dev/null
+++ b/lib/setup/sv/runit/libvirtd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/libvirtd
diff --git a/lib/setup/sv/runit/lighttpd/run b/lib/setup/sv/runit/lighttpd/run
new file mode 100755
index 0000000..bf94dcc
--- /dev/null
+++ b/lib/setup/sv/runit/lighttpd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf
diff --git a/lib/setup/sv/runit/lxc-net/run b/lib/setup/sv/runit/lxc-net/run
new file mode 100755
index 0000000..74db834
--- /dev/null
+++ b/lib/setup/sv/runit/lxc-net/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/libexec/lxc/lxc-net start
diff --git a/lib/setup/sv/runit/mariadbd/run b/lib/setup/sv/runit/mariadbd/run
new file mode 100755
index 0000000..93d30aa
--- /dev/null
+++ b/lib/setup/sv/runit/mariadbd/run
@@ -0,0 +1,4 @@
+#!/bin/sh
+exec 2>&1
+install -d -m 0750 /run/mysqld /var/lib/mysql || :
+exec /usr/sbin/mariadbd --datadir=/var/lib/mysql
diff --git a/lib/setup/sv/runit/memcached/run b/lib/setup/sv/runit/memcached/run
new file mode 100755
index 0000000..9191b7b
--- /dev/null
+++ b/lib/setup/sv/runit/memcached/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/memcached -u nobody
diff --git a/lib/setup/sv/runit/nfs-server/run b/lib/setup/sv/runit/nfs-server/run
new file mode 100755
index 0000000..f1f191d
--- /dev/null
+++ b/lib/setup/sv/runit/nfs-server/run
@@ -0,0 +1,6 @@
+#!/bin/sh
+exec 2>&1
+if command -v exportfs >/dev/null 2>&1; then exportfs -ra || :; fi
+if command -v rpc.statd >/dev/null 2>&1; then rpc.statd --no-notify || :; fi
+if command -v rpc.nfsd >/dev/null 2>&1; then rpc.nfsd 8 || :; fi
+exec /usr/sbin/rpc.mountd -F
diff --git a/lib/setup/sv/runit/openvpn/run b/lib/setup/sv/runit/openvpn/run
new file mode 100755
index 0000000..bd917d7
--- /dev/null
+++ b/lib/setup/sv/runit/openvpn/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/openvpn --config /etc/openvpn/server.conf
diff --git a/lib/setup/sv/runit/postgresql/run b/lib/setup/sv/runit/postgresql/run
new file mode 100755
index 0000000..4ce630d
--- /dev/null
+++ b/lib/setup/sv/runit/postgresql/run
@@ -0,0 +1,4 @@
+#!/bin/sh
+exec 2>&1
+install -d -m 0755 /run/postgresql /var/lib/postgres/data || :
+exec /usr/bin/postgres -D /var/lib/postgres/data
diff --git a/lib/setup/sv/runit/tinyproxy/run b/lib/setup/sv/runit/tinyproxy/run
new file mode 100755
index 0000000..0a286fa
--- /dev/null
+++ b/lib/setup/sv/runit/tinyproxy/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/tinyproxy -d
diff --git a/lib/setup/sv/runit/tor/run b/lib/setup/sv/runit/tor/run
new file mode 100755
index 0000000..e83de8c
--- /dev/null
+++ b/lib/setup/sv/runit/tor/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/tor --RunAsDaemon 0
diff --git a/lib/setup/sv/runit/unbound/run b/lib/setup/sv/runit/unbound/run
new file mode 100755
index 0000000..9ec1a54
--- /dev/null
+++ b/lib/setup/sv/runit/unbound/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/unbound -d
diff --git a/lib/setup/sv/runit/usbmuxd/run b/lib/setup/sv/runit/usbmuxd/run
new file mode 100755
index 0000000..568bc71
--- /dev/null
+++ b/lib/setup/sv/runit/usbmuxd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/usbmuxd -f
diff --git a/lib/setup/sv/runit/valkey/run b/lib/setup/sv/runit/valkey/run
new file mode 100755
index 0000000..2aec40b
--- /dev/null
+++ b/lib/setup/sv/runit/valkey/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/valkey-server --daemonize no --bind 127.0.0.1
diff --git a/lib/setup/sv/s6/avahi-daemon/run b/lib/setup/sv/s6/avahi-daemon/run
new file mode 100755
index 0000000..2d0d88e
--- /dev/null
+++ b/lib/setup/sv/s6/avahi-daemon/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/avahi-daemon --no-chroot
diff --git a/lib/setup/sv/s6/conntrackd/run b/lib/setup/sv/s6/conntrackd/run
new file mode 100755
index 0000000..9e1a1fb
--- /dev/null
+++ b/lib/setup/sv/s6/conntrackd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/conntrackd -C /etc/conntrackd/conntrackd.conf
diff --git a/lib/setup/sv/s6/distccd/run b/lib/setup/sv/s6/distccd/run
new file mode 100755
index 0000000..a40a418
--- /dev/null
+++ b/lib/setup/sv/s6/distccd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/distccd --no-detach --allow 127.0.0.1 --log-stderr
diff --git a/lib/setup/sv/s6/dnsmasq/run b/lib/setup/sv/s6/dnsmasq/run
new file mode 100755
index 0000000..c605f8b
--- /dev/null
+++ b/lib/setup/sv/s6/dnsmasq/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/dnsmasq -k
diff --git a/lib/setup/sv/s6/dropbear/run b/lib/setup/sv/s6/dropbear/run
new file mode 100755
index 0000000..f2b653f
--- /dev/null
+++ b/lib/setup/sv/s6/dropbear/run
@@ -0,0 +1,7 @@
+#!/bin/sh
+exec 2>&1
+if command -v dropbearkey >/dev/null 2>&1; then
+ mkdir -p /etc/dropbear
+ [ -f /etc/dropbear/dropbear_rsa_host_key ] || dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key >/dev/null
+fi
+exec /usr/sbin/dropbear -F -E
diff --git a/lib/setup/sv/s6/fail2ban/run b/lib/setup/sv/s6/fail2ban/run
new file mode 100755
index 0000000..7bc8461
--- /dev/null
+++ b/lib/setup/sv/s6/fail2ban/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/fail2ban-server -xf --logtarget=stdout
diff --git a/lib/setup/sv/s6/haproxy/run b/lib/setup/sv/s6/haproxy/run
new file mode 100755
index 0000000..4c215a4
--- /dev/null
+++ b/lib/setup/sv/s6/haproxy/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -db
diff --git a/lib/setup/sv/s6/libvirtd/run b/lib/setup/sv/s6/libvirtd/run
new file mode 100755
index 0000000..47c5776
--- /dev/null
+++ b/lib/setup/sv/s6/libvirtd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/libvirtd
diff --git a/lib/setup/sv/s6/lighttpd/run b/lib/setup/sv/s6/lighttpd/run
new file mode 100755
index 0000000..bf94dcc
--- /dev/null
+++ b/lib/setup/sv/s6/lighttpd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf
diff --git a/lib/setup/sv/s6/lxc-net/run b/lib/setup/sv/s6/lxc-net/run
new file mode 100755
index 0000000..74db834
--- /dev/null
+++ b/lib/setup/sv/s6/lxc-net/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/libexec/lxc/lxc-net start
diff --git a/lib/setup/sv/s6/mariadbd/run b/lib/setup/sv/s6/mariadbd/run
new file mode 100755
index 0000000..93d30aa
--- /dev/null
+++ b/lib/setup/sv/s6/mariadbd/run
@@ -0,0 +1,4 @@
+#!/bin/sh
+exec 2>&1
+install -d -m 0750 /run/mysqld /var/lib/mysql || :
+exec /usr/sbin/mariadbd --datadir=/var/lib/mysql
diff --git a/lib/setup/sv/s6/memcached/run b/lib/setup/sv/s6/memcached/run
new file mode 100755
index 0000000..9191b7b
--- /dev/null
+++ b/lib/setup/sv/s6/memcached/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/memcached -u nobody
diff --git a/lib/setup/sv/s6/nfs-server/run b/lib/setup/sv/s6/nfs-server/run
new file mode 100755
index 0000000..f1f191d
--- /dev/null
+++ b/lib/setup/sv/s6/nfs-server/run
@@ -0,0 +1,6 @@
+#!/bin/sh
+exec 2>&1
+if command -v exportfs >/dev/null 2>&1; then exportfs -ra || :; fi
+if command -v rpc.statd >/dev/null 2>&1; then rpc.statd --no-notify || :; fi
+if command -v rpc.nfsd >/dev/null 2>&1; then rpc.nfsd 8 || :; fi
+exec /usr/sbin/rpc.mountd -F
diff --git a/lib/setup/sv/s6/openvpn/run b/lib/setup/sv/s6/openvpn/run
new file mode 100755
index 0000000..bd917d7
--- /dev/null
+++ b/lib/setup/sv/s6/openvpn/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/openvpn --config /etc/openvpn/server.conf
diff --git a/lib/setup/sv/s6/postgresql/run b/lib/setup/sv/s6/postgresql/run
new file mode 100755
index 0000000..4ce630d
--- /dev/null
+++ b/lib/setup/sv/s6/postgresql/run
@@ -0,0 +1,4 @@
+#!/bin/sh
+exec 2>&1
+install -d -m 0755 /run/postgresql /var/lib/postgres/data || :
+exec /usr/bin/postgres -D /var/lib/postgres/data
diff --git a/lib/setup/sv/s6/tinyproxy/run b/lib/setup/sv/s6/tinyproxy/run
new file mode 100755
index 0000000..0a286fa
--- /dev/null
+++ b/lib/setup/sv/s6/tinyproxy/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/tinyproxy -d
diff --git a/lib/setup/sv/s6/tor/run b/lib/setup/sv/s6/tor/run
new file mode 100755
index 0000000..e83de8c
--- /dev/null
+++ b/lib/setup/sv/s6/tor/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/tor --RunAsDaemon 0
diff --git a/lib/setup/sv/s6/unbound/run b/lib/setup/sv/s6/unbound/run
new file mode 100755
index 0000000..9ec1a54
--- /dev/null
+++ b/lib/setup/sv/s6/unbound/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/unbound -d
diff --git a/lib/setup/sv/s6/usbmuxd/run b/lib/setup/sv/s6/usbmuxd/run
new file mode 100755
index 0000000..568bc71
--- /dev/null
+++ b/lib/setup/sv/s6/usbmuxd/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/sbin/usbmuxd -f
diff --git a/lib/setup/sv/s6/valkey/run b/lib/setup/sv/s6/valkey/run
new file mode 100755
index 0000000..2aec40b
--- /dev/null
+++ b/lib/setup/sv/s6/valkey/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+exec 2>&1
+exec /usr/bin/valkey-server --daemonize no --bind 127.0.0.1
diff --git a/lib/setup/sv/shepherd/avahi-daemon.scm b/lib/setup/sv/shepherd/avahi-daemon.scm
new file mode 100644
index 0000000..bb0dd8c
--- /dev/null
+++ b/lib/setup/sv/shepherd/avahi-daemon.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(avahi-daemon)
+ #:requirement '(dbus)
+ #:documentation "Avahi mDNS/DNS-SD daemon"
+ #:start (make-forkexec-constructor '("/usr/sbin/avahi-daemon" "--no-chroot"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/conntrackd.scm b/lib/setup/sv/shepherd/conntrackd.scm
new file mode 100644
index 0000000..5ed14bb
--- /dev/null
+++ b/lib/setup/sv/shepherd/conntrackd.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(conntrackd)
+ #:requirement '()
+ #:documentation "connection tracking daemon"
+ #:start (make-forkexec-constructor '("/usr/sbin/conntrackd" "-C" "/etc/conntrackd/conntrackd.conf"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/distccd.scm b/lib/setup/sv/shepherd/distccd.scm
new file mode 100644
index 0000000..5eaffc2
--- /dev/null
+++ b/lib/setup/sv/shepherd/distccd.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(distccd)
+ #:requirement '()
+ #:documentation "distributed C/C++ compilation daemon"
+ #:start (make-forkexec-constructor '("/usr/bin/distccd" "--no-detach" "--allow" "127.0.0.1" "--log-stderr"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/dnsmasq.scm b/lib/setup/sv/shepherd/dnsmasq.scm
new file mode 100644
index 0000000..9a59f2b
--- /dev/null
+++ b/lib/setup/sv/shepherd/dnsmasq.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(dnsmasq)
+ #:requirement '()
+ #:documentation "DNS and DHCP server"
+ #:start (make-forkexec-constructor '("/usr/sbin/dnsmasq" "-k"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/dropbear.scm b/lib/setup/sv/shepherd/dropbear.scm
new file mode 100644
index 0000000..cb18306
--- /dev/null
+++ b/lib/setup/sv/shepherd/dropbear.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(dropbear)
+ #:requirement '()
+ #:documentation "Dropbear SSH server"
+ #:start (make-forkexec-constructor '("/usr/sbin/dropbear" "-F" "-E"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/fail2ban.scm b/lib/setup/sv/shepherd/fail2ban.scm
new file mode 100644
index 0000000..b1d61e9
--- /dev/null
+++ b/lib/setup/sv/shepherd/fail2ban.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(fail2ban)
+ #:requirement '()
+ #:documentation "fail2ban authentication ban daemon"
+ #:start (make-forkexec-constructor '("/usr/bin/fail2ban-server" "-xf" "--logtarget=stdout"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/haproxy.scm b/lib/setup/sv/shepherd/haproxy.scm
new file mode 100644
index 0000000..5a07060
--- /dev/null
+++ b/lib/setup/sv/shepherd/haproxy.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(haproxy)
+ #:requirement '()
+ #:documentation "HAProxy load balancer"
+ #:start (make-forkexec-constructor '("/usr/sbin/haproxy" "-f" "/etc/haproxy/haproxy.cfg" "-db"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/libvirtd.scm b/lib/setup/sv/shepherd/libvirtd.scm
new file mode 100644
index 0000000..0bd32ae
--- /dev/null
+++ b/lib/setup/sv/shepherd/libvirtd.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(libvirtd)
+ #:requirement '(dbus)
+ #:documentation "libvirt virtualization daemon"
+ #:start (make-forkexec-constructor '("/usr/sbin/libvirtd"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/lighttpd.scm b/lib/setup/sv/shepherd/lighttpd.scm
new file mode 100644
index 0000000..434cd7d
--- /dev/null
+++ b/lib/setup/sv/shepherd/lighttpd.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(lighttpd)
+ #:requirement '()
+ #:documentation "lighttpd web server"
+ #:start (make-forkexec-constructor '("/usr/sbin/lighttpd" "-D" "-f" "/etc/lighttpd/lighttpd.conf"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/lxc-net.scm b/lib/setup/sv/shepherd/lxc-net.scm
new file mode 100644
index 0000000..184f0ea
--- /dev/null
+++ b/lib/setup/sv/shepherd/lxc-net.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(lxc-net)
+ #:requirement '()
+ #:documentation "LXC bridge networking"
+ #:start (make-forkexec-constructor '("/usr/libexec/lxc/lxc-net" "start"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/mariadbd.scm b/lib/setup/sv/shepherd/mariadbd.scm
new file mode 100644
index 0000000..acdf28f
--- /dev/null
+++ b/lib/setup/sv/shepherd/mariadbd.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(mariadbd)
+ #:requirement '()
+ #:documentation "MariaDB database server"
+ #:start (make-forkexec-constructor '("/usr/sbin/mariadbd" "--datadir=/var/lib/mysql"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/memcached.scm b/lib/setup/sv/shepherd/memcached.scm
new file mode 100644
index 0000000..0bacd54
--- /dev/null
+++ b/lib/setup/sv/shepherd/memcached.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(memcached)
+ #:requirement '()
+ #:documentation "memcached object cache"
+ #:start (make-forkexec-constructor '("/usr/bin/memcached" "-u" "nobody"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/nfs-server.scm b/lib/setup/sv/shepherd/nfs-server.scm
new file mode 100644
index 0000000..057ecdd
--- /dev/null
+++ b/lib/setup/sv/shepherd/nfs-server.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(nfs-server)
+ #:requirement '()
+ #:documentation "NFS server"
+ #:start (make-forkexec-constructor '("/usr/sbin/rpc.mountd" "-F"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/openvpn.scm b/lib/setup/sv/shepherd/openvpn.scm
new file mode 100644
index 0000000..605b9e8
--- /dev/null
+++ b/lib/setup/sv/shepherd/openvpn.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(openvpn)
+ #:requirement '()
+ #:documentation "OpenVPN server"
+ #:start (make-forkexec-constructor '("/usr/sbin/openvpn" "--config" "/etc/openvpn/server.conf"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/postgresql.scm b/lib/setup/sv/shepherd/postgresql.scm
new file mode 100644
index 0000000..c89446f
--- /dev/null
+++ b/lib/setup/sv/shepherd/postgresql.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(postgresql)
+ #:requirement '()
+ #:documentation "PostgreSQL database server"
+ #:start (make-forkexec-constructor '("/usr/bin/postgres" "-D" "/var/lib/postgres/data"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/tinyproxy.scm b/lib/setup/sv/shepherd/tinyproxy.scm
new file mode 100644
index 0000000..8b9c994
--- /dev/null
+++ b/lib/setup/sv/shepherd/tinyproxy.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(tinyproxy)
+ #:requirement '()
+ #:documentation "Tinyproxy HTTP proxy"
+ #:start (make-forkexec-constructor '("/usr/bin/tinyproxy" "-d"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/tor.scm b/lib/setup/sv/shepherd/tor.scm
new file mode 100644
index 0000000..c6693b0
--- /dev/null
+++ b/lib/setup/sv/shepherd/tor.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(tor)
+ #:requirement '()
+ #:documentation "Tor anonymity daemon"
+ #:start (make-forkexec-constructor '("/usr/bin/tor" "--RunAsDaemon" "0"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/unbound.scm b/lib/setup/sv/shepherd/unbound.scm
new file mode 100644
index 0000000..860a11a
--- /dev/null
+++ b/lib/setup/sv/shepherd/unbound.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(unbound)
+ #:requirement '()
+ #:documentation "validating recursive DNS resolver"
+ #:start (make-forkexec-constructor '("/usr/sbin/unbound" "-d"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/usbmuxd.scm b/lib/setup/sv/shepherd/usbmuxd.scm
new file mode 100644
index 0000000..1138cbb
--- /dev/null
+++ b/lib/setup/sv/shepherd/usbmuxd.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(usbmuxd)
+ #:requirement '()
+ #:documentation "iOS USB multiplexing daemon"
+ #:start (make-forkexec-constructor '("/usr/sbin/usbmuxd" "-f"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/shepherd/valkey.scm b/lib/setup/sv/shepherd/valkey.scm
new file mode 100644
index 0000000..2762921
--- /dev/null
+++ b/lib/setup/sv/shepherd/valkey.scm
@@ -0,0 +1,9 @@
+(use-modules (shepherd service))
+(register-services
+ (list (service
+ '(valkey)
+ #:requirement '()
+ #:documentation "Valkey data store"
+ #:start (make-forkexec-constructor '("/usr/bin/valkey-server" "--daemonize" "no" "--bind" "127.0.0.1"))
+ #:stop (make-kill-destructor)
+ #:respawn? #t)))
diff --git a/lib/setup/sv/systemd/avahi-daemon.service b/lib/setup/sv/systemd/avahi-daemon.service
new file mode 100644
index 0000000..e661fb4
--- /dev/null
+++ b/lib/setup/sv/systemd/avahi-daemon.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Avahi mDNS/DNS-SD daemon
+After=network.target dbus.service
+
+[Service]
+ExecStart=/usr/sbin/avahi-daemon --no-chroot
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/conntrackd.service b/lib/setup/sv/systemd/conntrackd.service
new file mode 100644
index 0000000..a2c78b8
--- /dev/null
+++ b/lib/setup/sv/systemd/conntrackd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=connection tracking daemon
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/conntrackd -C /etc/conntrackd/conntrackd.conf
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/distccd.service b/lib/setup/sv/systemd/distccd.service
new file mode 100644
index 0000000..c916d75
--- /dev/null
+++ b/lib/setup/sv/systemd/distccd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=distributed C/C++ compilation daemon
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/distccd --no-detach --allow 127.0.0.1 --log-stderr
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/dnsmasq.service b/lib/setup/sv/systemd/dnsmasq.service
new file mode 100644
index 0000000..ad65d05
--- /dev/null
+++ b/lib/setup/sv/systemd/dnsmasq.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=DNS and DHCP server
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/dnsmasq -k
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/dropbear.service b/lib/setup/sv/systemd/dropbear.service
new file mode 100644
index 0000000..8b581e9
--- /dev/null
+++ b/lib/setup/sv/systemd/dropbear.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Dropbear SSH server
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/dropbear -F -E
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/fail2ban.service b/lib/setup/sv/systemd/fail2ban.service
new file mode 100644
index 0000000..0fb885a
--- /dev/null
+++ b/lib/setup/sv/systemd/fail2ban.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=fail2ban authentication ban daemon
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/fail2ban-server -xf --logtarget=stdout
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/haproxy.service b/lib/setup/sv/systemd/haproxy.service
new file mode 100644
index 0000000..7d23486
--- /dev/null
+++ b/lib/setup/sv/systemd/haproxy.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=HAProxy load balancer
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -db
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/libvirtd.service b/lib/setup/sv/systemd/libvirtd.service
new file mode 100644
index 0000000..a930a04
--- /dev/null
+++ b/lib/setup/sv/systemd/libvirtd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=libvirt virtualization daemon
+After=network.target dbus.service
+
+[Service]
+ExecStart=/usr/sbin/libvirtd
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/lighttpd.service b/lib/setup/sv/systemd/lighttpd.service
new file mode 100644
index 0000000..51a2a0b
--- /dev/null
+++ b/lib/setup/sv/systemd/lighttpd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=lighttpd web server
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/lxc-net.service b/lib/setup/sv/systemd/lxc-net.service
new file mode 100644
index 0000000..83d2f4b
--- /dev/null
+++ b/lib/setup/sv/systemd/lxc-net.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=LXC bridge networking
+After=network.target
+
+[Service]
+ExecStart=/usr/libexec/lxc/lxc-net start
+Type=oneshot
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/mariadb.service b/lib/setup/sv/systemd/mariadb.service
new file mode 100644
index 0000000..ae43341
--- /dev/null
+++ b/lib/setup/sv/systemd/mariadb.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=MariaDB database server
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/mariadbd --datadir=/var/lib/mysql
+Restart=on-failure
+User=mysql
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/memcached.service b/lib/setup/sv/systemd/memcached.service
new file mode 100644
index 0000000..59a5c27
--- /dev/null
+++ b/lib/setup/sv/systemd/memcached.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=memcached object cache
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/memcached -u nobody
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/nfs-server.service b/lib/setup/sv/systemd/nfs-server.service
new file mode 100644
index 0000000..8010151
--- /dev/null
+++ b/lib/setup/sv/systemd/nfs-server.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=NFS server
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/rpc.mountd -F
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/openvpn.service b/lib/setup/sv/systemd/openvpn.service
new file mode 100644
index 0000000..a79608a
--- /dev/null
+++ b/lib/setup/sv/systemd/openvpn.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=OpenVPN server
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/openvpn --config /etc/openvpn/server.conf
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/postgresql.service b/lib/setup/sv/systemd/postgresql.service
new file mode 100644
index 0000000..c357732
--- /dev/null
+++ b/lib/setup/sv/systemd/postgresql.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=PostgreSQL database server
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/postgres -D /var/lib/postgres/data
+Restart=on-failure
+User=postgres
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/tinyproxy.service b/lib/setup/sv/systemd/tinyproxy.service
new file mode 100644
index 0000000..c84cb3d
--- /dev/null
+++ b/lib/setup/sv/systemd/tinyproxy.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Tinyproxy HTTP proxy
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/tinyproxy -d
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/tor.service b/lib/setup/sv/systemd/tor.service
new file mode 100644
index 0000000..a84fc3b
--- /dev/null
+++ b/lib/setup/sv/systemd/tor.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Tor anonymity daemon
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/tor --RunAsDaemon 0
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/unbound.service b/lib/setup/sv/systemd/unbound.service
new file mode 100644
index 0000000..8eba19f
--- /dev/null
+++ b/lib/setup/sv/systemd/unbound.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=validating recursive DNS resolver
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/unbound -d
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/usbmuxd.service b/lib/setup/sv/systemd/usbmuxd.service
new file mode 100644
index 0000000..0e54cdd
--- /dev/null
+++ b/lib/setup/sv/systemd/usbmuxd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=iOS USB multiplexing daemon
+After=network.target
+
+[Service]
+ExecStart=/usr/sbin/usbmuxd -f
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/setup/sv/systemd/valkey.service b/lib/setup/sv/systemd/valkey.service
new file mode 100644
index 0000000..f82eefd
--- /dev/null
+++ b/lib/setup/sv/systemd/valkey.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Valkey data store
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/valkey-server --daemonize no --bind 127.0.0.1
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/tests/test_services.sh b/tests/test_services.sh
index b49bf53..d47d882 100644
--- a/tests/test_services.sh
+++ b/tests/test_services.sh
@@ -97,6 +97,18 @@ enabled=$(setup_service_enable_one "$root2" shepherd sshd) ||
[ -f "$root2/etc/shepherd.d/sshd.scm" ] ||
fail 'stock shepherd sshd.scm was not copied'
+enabled=$(setup_service_enable_one "$root2" runit postgresql) ||
+ fail 'runit enable of postgresql from stock scripts failed'
+[ -f "$root2/etc/sv/postgresql/run" ] ||
+ fail 'stock runit postgresql/run was not copied'
+[ -L "$root2/etc/runit/runsvdir/default/postgresql" ] ||
+ fail 'runit postgresql was not linked into runsvdir/default'
+
+enabled=$(setup_service_enable_one "$root2" dinit unbound) ||
+ fail 'dinit enable of unbound from stock scripts failed'
+[ -f "$root2/etc/dinit.d/unbound" ] ||
+ fail 'stock dinit unbound was not copied'
+
# Do not overwrite a package-shipped run script.
mkdir -p "$root2/etc/sv/sshd"
printf '%s\n' '#!/bin/sh' 'exec /opt/custom/sshd' >"$root2/etc/sv/sshd/run"
diff --git a/tests/test_stock_services.sh b/tests/test_stock_services.sh
new file mode 100755
index 0000000..a999eae
--- /dev/null
+++ b/tests/test_stock_services.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Every mapped daemon must have stock scripts for all supported inits.
+
+set -eu
+
+case $0 in */*) test_dir=${0%/*} ;; *) test_dir=. ;; esac
+project_dir=$(CDPATH= cd "$test_dir/.." 2>/dev/null && pwd) || exit 1
+fail() { printf 'test_stock_services: %s\n' "$*" >&2; exit 1; }
+
+map=$project_dir/lib/setup/services
+sv=$project_dir/lib/setup/sv
+[ -f "$map" ] || fail "missing $map"
+
+count=0
+while read -r pkg unit _wanted rc _level; do
+ case $pkg in
+ ''|\#*) continue ;;
+ esac
+ [ -n "$unit" ] && [ -n "$rc" ] || fail "bad map row for $pkg"
+ [ -f "$sv/systemd/$unit" ] || fail "missing systemd stock $unit ($pkg)"
+ [ -f "$sv/openrc/$rc" ] || fail "missing openrc stock $rc ($pkg)"
+ [ -f "$sv/runit/$rc/run" ] || fail "missing runit stock $rc/run ($pkg)"
+ [ -f "$sv/s6/$rc/run" ] || fail "missing s6 stock $rc/run ($pkg)"
+ [ -f "$sv/dinit/$rc" ] || fail "missing dinit stock $rc ($pkg)"
+ [ -f "$sv/shepherd/$rc.scm" ] || fail "missing shepherd stock $rc.scm ($pkg)"
+ count=$((count + 1))
+done <"$map"
+
+[ "$count" -ge 20 ] || fail "expected at least 20 mapped services, got $count"
+printf '%s\n' "stock service files present for $count map rows"
diff --git a/tests/test_upgrade_recipes.sh b/tests/test_upgrade_recipes.sh
new file mode 100755
index 0000000..5ad6d28
--- /dev/null
+++ b/tests/test_upgrade_recipes.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+set -eu
+
+case $0 in */*) test_dir=${0%/*} ;; *) test_dir=. ;; esac
+project_dir=$(CDPATH= cd "$test_dir/.." 2>/dev/null && pwd) || exit 1
+fail() { printf 'test_upgrade_recipes: %s\n' "$*" >&2; exit 1; }
+
+tool=$project_dir/tools/upgrade-recipes
+[ -f "$tool" ] || fail "missing $tool"
+command -v python3 >/dev/null 2>&1 || fail 'python3 is required for upgrade-recipes'
+
+python3 "$tool" --selftest >/dev/null || fail 'upgrade-recipes --selftest failed'
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/sps-upgrade-test.XXXXXX") || fail mktemp
+trap 'rm -rf "$tmp"' 0 HUP INT TERM
+
+mkdir -p "$tmp/tree/demo"
+cat >"$tmp/tree/demo/recipe" <<'EOF'
+name demo
+version 1.0
+release 3
+arch any
+description demo: offline fixture for upgrade-recipes
+
+source https://example.org/demo-1.0.tar.gz
+hash sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+
+depend filesystem
+install mkdir -p "$PKG/usr/share/demo"
+install printf '%s\n' "${version}" >"$PKG/usr/share/demo/version"
+EOF
+
+# A dry-run against an unreachable host must still parse the tree.
+out=$(python3 "$tool" --trees "$tmp/tree" --only demo --jobs 1 2>/dev/null || true)
+printf '%s\n' "$out" | grep -q 'summary' || fail "plan output missing summary: $out"
+
+printf '%s\n' 'upgrade-recipes tests passed'
diff --git a/tools/upgrade-recipes b/tools/upgrade-recipes
new file mode 100755
index 0000000..a2796b1
--- /dev/null
+++ b/tools/upgrade-recipes
@@ -0,0 +1,1154 @@
+#!/usr/bin/env python3
+"""Upgrade SPS recipe trees to the newest upstream stable release.
+
+Maintainers run one command against core and extra. The tool detects the
+current source, finds the newest non-prerelease upstream, rewrites
+version/source/hash, resets release to 1, and scans the new tarball for
+libraries the recipe does not yet declare. Names that already exist in the
+trees are added as depend or builddep. Names that do not exist are reported,
+and with --scaffold-missing a conservative extra recipe is drafted when a
+source URL can be resolved.
+
+Default is a dry-run plan. Pass --apply to write recipes.
+"""
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import re
+import ssl
+import sys
+import tarfile
+import tempfile
+import zipfile
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from pathlib import Path
+from urllib.error import HTTPError, URLError
+from urllib.parse import urlparse
+from urllib.request import Request, urlopen
+
+UA = "sps-upgrade-recipes/1.0 (+https://github.com/RobertFlexx/SPS)"
+CACHE = Path(os.environ.get("SPS_UPGRADE_CACHE", str(Path.home() / ".cache" / "sps-upgrade")))
+GITHUB_API = "https://api.github.com"
+REPOLOGY = "https://repology.org/api/v1/project/{}"
+
+PRE_RE = re.compile(
+ r"(?:^|[.\-_])(?:alpha|beta|rc|pre|preview|dev|git|snapshot|nightly|test)(?:\d|$)|"
+ r"(?:alpha|beta|rc)\d",
+ re.I,
+)
+GNOME_EVEN = {
+ "gtk", "gtk4", "glib", "pango", "gdk-pixbuf", "atk", "at-spi2-core",
+ "libadwaita", "libsoup", "gtksourceview", "vte", "gexiv2",
+}
+
+# pkg-config / cmake / meson names -> SPS package names
+PC_MAP = {
+ "libssl": "openssl", "libcrypto": "openssl", "openssl": "openssl",
+ "zlib": "zlib", "libz": "zlib", "liblzma": "xz", "libzstd": "zstd",
+ "libbz2": "bzip2", "bzip2": "bzip2", "libpcre2-8": "pcre2", "libpcre": "pcre",
+ "libpng": "libpng", "libpng16": "libpng", "libjpeg": "libjpeg-turbo",
+ "libturbojpeg": "libjpeg-turbo", "libtiff-4": "tiff", "libwebp": "libwebp",
+ "freetype2": "freetype", "harfbuzz": "harfbuzz", "graphite2": "graphite2",
+ "fontconfig": "fontconfig", "cairo": "cairo", "pango": "pango",
+ "pangocairo": "pango", "gdk-pixbuf-2.0": "gdk-pixbuf",
+ "gtk+-3.0": "gtk3", "gtk4": "gtk4", "libadwaita-1": "libadwaita",
+ "glib-2.0": "glib", "gio-2.0": "glib", "gobject-2.0": "glib",
+ "gmodule-2.0": "glib", "gthread-2.0": "glib",
+ "libxml-2.0": "libxml2", "libxslt": "libxslt", "icu-uc": "icu",
+ "icu-i18n": "icu", "sqlite3": "sqlite", "libcurl": "curl",
+ "libarchive": "libarchive", "expat": "expat", "libevent": "libevent",
+ "ncurses": "ncurses", "ncursesw": "ncurses", "readline": "readline",
+ "uuid": "util-linux", "mount": "util-linux", "blkid": "util-linux",
+ "libudev": "systemd", "libelf": "elfutils", "libdw": "elfutils",
+ "libcap": "libcap", "libcap-ng": "libcap-ng", "libseccomp": "libseccomp",
+ "dbus-1": "dbus", "libsystemd": "systemd", "libpulse": "pulseaudio",
+ "alsa": "alsa-lib", "alsa-lib": "alsa-lib",
+ "x11": "libx11", "xext": "libxext", "xrandr": "libxrandr",
+ "xinerama": "libxinerama", "xfixes": "libxfixes", "xi": "libxi",
+ "xkbcommon": "libxkbcommon", "wayland-client": "wayland",
+ "wayland-server": "wayland", "wayland-cursor": "wayland",
+ "wayland-egl": "wayland", "egl": "mesa", "glesv2": "mesa",
+ "gbm": "mesa", "dri": "mesa", "libdrm": "libdrm",
+ "vulkan": "vulkan-loader", "epoxy": "libepoxy",
+ "pixman-1": "pixman", "lcms2": "lcms2", "libass": "libass",
+ "libavcodec": "ffmpeg", "libavformat": "ffmpeg", "libavutil": "ffmpeg",
+ "libswscale": "ffmpeg", "libswresample": "ffmpeg",
+ "opus": "opus", "vorbis": "libvorbis", "ogg": "libogg",
+ "flac": "flac", "sndfile": "libsndfile", "samplerate": "libsamplerate",
+ "fftw3": "fftw", "fftw3f": "fftw", "libopenjp2": "openjpeg",
+ "Qt6Core": "qt6-qtbase", "Qt6Gui": "qt6-qtbase", "Qt6Widgets": "qt6-qtbase",
+ "Qt6Network": "qt6-qtbase", "Qt6Qml": "qt6-qtdeclarative",
+ "Qt6Quick": "qt6-qtdeclarative", "Qt6Svg": "qt6-qtsvg",
+ "Qt6WebEngineCore": "qt6-qtwebengine", "libplacebo": "libplacebo",
+ "libva": "libva", "vpx": "libvpx", "dav1d": "dav1d",
+ "x264": "x264", "x265": "x265", "aom": "aom", "SvtAv1Enc": "svt-av1",
+ "libbrotlidec": "brotli", "libbrotlienc": "brotli",
+ "libnghttp2": "nghttp2", "libidn2": "libidn2", "libpsl": "libpsl",
+ "gnutls": "gnutls", "nss": "nss", "nspr": "nspr",
+ "libgit2": "libgit2", "yaml-0.1": "libyaml", "libyaml": "libyaml",
+ "json-c": "json-c", "fmt": "fmt", "spdlog": "spdlog",
+ "protobuf": "protobuf", "re2": "re2", "snappy": "snappy",
+ "libssh": "libssh", "libssh2": "libssh2",
+ "fuse3": "fuse3", "fuse": "fuse", "libacl": "acl", "libattr": "attr",
+ "python-3.13": "python", "python3": "python", "lua": "lua",
+ "luajit": "luajit", "ruby": "ruby", "libpopt": "popt",
+ "libnl-3.0": "libnl", "libnl-genl-3.0": "libnl",
+ "libtirpc": "libtirpc", "libyajl": "yajl", "yajl": "yajl",
+ "libvirt": "libvirt", "libslirp": "libslirp",
+ "spice-server": "spice", "spice-protocol": "spice-protocol",
+ "gl": "mesa", "glew": "glew", "sdl2": "sdl2", "SDL2": "sdl2",
+ "sdl3": "sdl3", "gmp": "gmp", "mpfr": "mpfr", "mpc": "mpc",
+ "isl": "isl", "libffi": "libffi", "libtasn1": "libtasn1",
+ "nettle": "nettle", "hogweed": "nettle", "libidn": "libidn",
+ "jansson": "jansson", "libmicrohttpd": "libmicrohttpd",
+ "lmdb": "lmdb", "gdbm": "gdbm", "libaio": "libaio",
+ "liburing": "liburing", "jemalloc": "jemalloc",
+ "boost": "boost", "icu-io": "icu",
+}
+
+SKIP_DEPS = {
+ "threads", "thread", "dl", "dlib", "m", "rt", "pthread", "c", "stdc++",
+ "gcc", "gomp", "atomic", "iconv", "intl", "ws2_32", "ole32", "shell32",
+ "user32", "advapi32", "kernel32", "ntdll", "android", "log",
+ "CoreFoundation", "Foundation", "cocoa", "objc", "resolv",
+ "execinfo", "util", "kvm", "procstat", "inotify",
+}
+
+HOST_TOOLS = {
+ "cmake", "ninja", "meson", "pkgconf", "pkg-config", "python", "python3",
+ "perl", "bison", "flex", "gperf", "gawk", "m4", "autoconf", "automake",
+ "libtool", "gettext", "intltool", "patch", "make", "gcc", "g++", "cc",
+ "c++", "rustc", "cargo", "go", "nasm", "yasm",
+}
+
+# Packages upgraded together. First existing member supplies the version.
+PIN_GROUPS = [
+ re.compile(r"^qt6-qt"),
+ re.compile(r"^kf6-"),
+ re.compile(r"^plasma-"),
+ re.compile(r"^gst-"),
+]
+
+NO_AUTO = {
+ "linux", "linux-desktop", "linux-firmware", "splux-base", "filesystem",
+ "daily-drive", "qol-cli", "workstation-extra", "qt6", "kf6", "clang",
+}
+
+
+class HttpError(RuntimeError):
+ pass
+
+
+def log(msg: str) -> None:
+ print(msg, file=sys.stderr, flush=True)
+
+
+def http_get(url: str, timeout: int = 30, binary: bool = False, accept: str = ""):
+ headers = {"User-Agent": UA}
+ if accept:
+ headers["Accept"] = accept
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
+ if token and "api.github.com" in url:
+ headers["Authorization"] = f"Bearer {token}"
+ req = Request(url, headers=headers)
+ ctx = ssl.create_default_context()
+ try:
+ with urlopen(req, timeout=timeout, context=ctx) as resp:
+ data = resp.read()
+ if binary:
+ return data
+ return data.decode("utf-8", errors="replace")
+ except HTTPError as exc:
+ raise HttpError(f"{url}: HTTP {exc.code}") from exc
+ except URLError as exc:
+ raise HttpError(f"{url}: {exc.reason}") from exc
+
+
+def version_tokens(version: str) -> list[tuple[str, str]]:
+ tokens = []
+ i = 0
+ v = version or ""
+ while i < len(v):
+ c = v[i]
+ if c == "~":
+ tokens.append(("tilde", c))
+ i += 1
+ elif c.isdigit():
+ j = i
+ while j < len(v) and v[j].isdigit():
+ j += 1
+ tokens.append(("number", v[i:j]))
+ i = j
+ elif c.isalpha():
+ j = i
+ while j < len(v) and v[j].isalpha():
+ j += 1
+ tokens.append(("text", v[i:j].lower()))
+ i = j
+ else:
+ i += 1
+ return tokens
+
+
+def version_compare(left: str, right: str) -> int:
+ lt, rt = version_tokens(left), version_tokens(right)
+ n = max(len(lt), len(rt))
+ for i in range(n):
+ if i >= len(lt):
+ rest = rt[i:]
+ if rest and rest[0][0] == "tilde":
+ return 1
+ return -1 if rest else 0
+ if i >= len(rt):
+ rest = lt[i:]
+ if rest and rest[0][0] == "tilde":
+ return -1
+ return 1 if rest else 0
+ (lt_t, lt_v), (rt_t, rt_v) = lt[i], rt[i]
+ if lt_t == "tilde" or rt_t == "tilde":
+ if lt_t == rt_t:
+ continue
+ return -1 if lt_t == "tilde" else 1
+ if lt_t == "number" and rt_t == "number":
+ a, b = lt_v.lstrip("0") or "0", rt_v.lstrip("0") or "0"
+ if len(a) != len(b):
+ return -1 if len(a) < len(b) else 1
+ if a != b:
+ return -1 if a < b else 1
+ continue
+ if lt_t != rt_t:
+ return -1 if lt_t == "number" else 1
+ if lt_v != rt_v:
+ return -1 if lt_v < rt_v else 1
+ return 0
+
+
+def version_newer(candidate: str, current: str) -> bool:
+ return version_compare(candidate, current) > 0
+
+
+def is_prerelease(version: str) -> bool:
+ if not version:
+ return True
+ if PRE_RE.search(version):
+ return True
+ return False
+
+
+def gnome_stable(name: str, version: str) -> bool:
+ key = name.lower().replace("gtk4", "gtk")
+ if key not in GNOME_EVEN and name.lower() not in GNOME_EVEN:
+ return True
+ parts = re.findall(r"\d+", version)
+ if len(parts) < 2:
+ return True
+ return int(parts[1]) % 2 == 0
+
+
+def parse_recipe(path: Path) -> dict:
+ rec = {
+ "path": path,
+ "name": "",
+ "version": "",
+ "release": "1",
+ "arch": "",
+ "description": "",
+ "source": [],
+ "hash": [],
+ "depend": [],
+ "builddep": [],
+ "optional": [],
+ "conflict": [],
+ "prepare": [],
+ "configure": [],
+ "build": [],
+ "install": [],
+ "raw": path.read_text(encoding="utf-8"),
+ }
+ key = None
+ buf = []
+ for line in rec["raw"].splitlines():
+ if not line.strip() or line.lstrip().startswith("#"):
+ continue
+ if line.endswith("\\"):
+ piece = line[:-1]
+ if key is None:
+ parts = piece.split(None, 1)
+ if len(parts) != 2:
+ continue
+ key, val = parts[0], parts[1]
+ buf = [val]
+ else:
+ buf.append(piece.strip())
+ continue
+ if key is not None:
+ buf.append(line.strip())
+ rec.setdefault(key, [])
+ if key in rec and isinstance(rec[key], list):
+ rec[key].append(" ".join(buf).strip())
+ key = None
+ buf = []
+ continue
+ parts = line.split(None, 1)
+ if len(parts) != 2:
+ continue
+ k, v = parts[0], parts[1]
+ if k in ("name", "version", "release", "arch", "description"):
+ rec[k] = v
+ elif k in rec and isinstance(rec[k], list):
+ rec[k].append(v)
+ rec["names"] = rec["name"]
+ return rec
+
+
+def recipe_has_source(rec: dict) -> bool:
+ return bool(rec.get("source"))
+
+
+def sha256_bytes(data: bytes) -> str:
+ return hashlib.sha256(data).hexdigest()
+
+
+def sha256_file(path: Path) -> str:
+ h = hashlib.sha256()
+ with path.open("rb") as fh:
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
+ h.update(chunk)
+ return h.hexdigest()
+
+
+def cache_path(url: str) -> Path:
+ CACHE.mkdir(parents=True, exist_ok=True)
+ name = hashlib.sha256(url.encode()).hexdigest()[:20] + "-" + url.rstrip("/").split("/")[-1][:80]
+ return CACHE / name
+
+
+def download(url: str) -> Path:
+ dest = cache_path(url)
+ if dest.exists() and dest.stat().st_size > 0:
+ return dest
+ tmp = dest.with_suffix(dest.suffix + ".part")
+ data = http_get(url, timeout=120, binary=True)
+ tmp.write_bytes(data)
+ tmp.replace(dest)
+ return dest
+
+
+def strip_tag(tag: str) -> str:
+ tag = tag.strip()
+ tag = re.sub(r"^(refs/tags/)", "", tag)
+ tag = re.sub(r"^(v|V|release-|Release-|version-|VERSION-)", "", tag)
+ tag = re.sub(r"^(llvmorg-)", "", tag)
+ return tag.lstrip("v")
+
+
+def github_repo(url: str) -> tuple[str, str] | None:
+ m = re.search(r"github\.com/([^/]+)/([^/]+)", url)
+ if not m:
+ return None
+ owner, repo = m.group(1), m.group(2)
+ repo = re.sub(r"\.git$", "", repo)
+ return owner, repo
+
+
+def latest_github(owner: str, repo: str) -> str | None:
+ try:
+ body = http_get(
+ f"{GITHUB_API}/repos/{owner}/{repo}/releases/latest",
+ accept="application/vnd.github+json",
+ )
+ data = json.loads(body)
+ tag = data.get("tag_name") or ""
+ if data.get("prerelease") or data.get("draft"):
+ tag = ""
+ ver = strip_tag(tag)
+ if ver and not is_prerelease(ver):
+ return ver
+ except (HttpError, json.JSONDecodeError, TypeError):
+ pass
+ try:
+ body = http_get(
+ f"{GITHUB_API}/repos/{owner}/{repo}/tags?per_page=30",
+ accept="application/vnd.github+json",
+ )
+ tags = json.loads(body)
+ best = None
+ for item in tags:
+ ver = strip_tag(item.get("name") or "")
+ if not ver or is_prerelease(ver):
+ continue
+ if best is None or version_newer(ver, best):
+ best = ver
+ return best
+ except (HttpError, json.JSONDecodeError, TypeError):
+ return None
+
+
+def latest_repology(name: str) -> str | None:
+ try:
+ body = http_get(REPOLOGY.format(name), timeout=40)
+ rows = json.loads(body)
+ except (HttpError, json.JSONDecodeError):
+ return None
+ best = None
+ for row in rows:
+ if row.get("status") not in {"newest", "unique"}:
+ continue
+ ver = str(row.get("version") or "")
+ if not ver or is_prerelease(ver):
+ continue
+ if not gnome_stable(name, ver):
+ continue
+ if best is None or version_newer(ver, best):
+ best = ver
+ return best
+
+
+def latest_gnome(url: str, name: str) -> str | None:
+ m = re.search(r"download\.gnome\.org/sources/([^/]+)/", url)
+ module = m.group(1) if m else name
+ try:
+ body = http_get(f"https://download.gnome.org/sources/{module}/cache.json")
+ data = json.loads(body)
+ except (HttpError, json.JSONDecodeError):
+ return None
+ versions = []
+ if isinstance(data, list) and len(data) >= 2 and isinstance(data[1], dict):
+ pkgmap = data[1].get(module) or data[1].get(name) or {}
+ for series, items in pkgmap.items():
+ if not gnome_stable(module, series + ".0"):
+ continue
+ if isinstance(items, list):
+ versions.extend(str(x) for x in items)
+ best = None
+ for ver in versions:
+ if is_prerelease(ver):
+ continue
+ if not gnome_stable(module, ver):
+ continue
+ if best is None or version_newer(ver, best):
+ best = ver
+ return best
+
+
+def latest_gnu(url: str, name: str) -> str | None:
+ parsed = urlparse(url)
+ directory = url.rsplit("/", 1)[0] + "/"
+ try:
+ body = http_get(directory)
+ except HttpError:
+ return None
+ versions = re.findall(
+ rf"href=\"{re.escape(name)}-([0-9][0-9A-Za-z.+~_-]*)\.(?:tar\.(?:gz|xz|bz2|lz)|tgz)\"",
+ body,
+ )
+ best = None
+ for ver in versions:
+ if is_prerelease(ver):
+ continue
+ if best is None or version_newer(ver, best):
+ best = ver
+ return best
+
+
+def latest_qemu() -> str | None:
+ try:
+ body = http_get("https://download.qemu.org/")
+ except HttpError:
+ return None
+ versions = re.findall(r"href=\"qemu-([0-9]+\.[0-9]+\.[0-9]+)\.tar\.xz\"", body)
+ best = None
+ for ver in versions:
+ if best is None or version_newer(ver, best):
+ best = ver
+ return best
+
+
+def latest_pypi(name: str) -> str | None:
+ try:
+ body = http_get(f"https://pypi.org/pypi/{name}/json")
+ data = json.loads(body)
+ ver = data.get("info", {}).get("version")
+ if ver and not is_prerelease(ver):
+ return ver
+ except (HttpError, json.JSONDecodeError):
+ return None
+ return None
+
+
+def detect_latest(rec: dict) -> str | None:
+ name = rec["name"]
+ url = rec["source"][0] if rec["source"] else ""
+ found: list[str] = []
+
+ if "download.qemu.org" in url or name == "qemu":
+ v = latest_qemu()
+ if v:
+ found.append(v)
+ if "download.gnome.org" in url:
+ v = latest_gnome(url, name)
+ if v:
+ found.append(v)
+ if "ftp.gnu.org" in url or "ftpmirror.gnu.org" in url:
+ v = latest_gnu(url, name)
+ if v:
+ found.append(v)
+ gh = github_repo(url)
+ if gh:
+ v = latest_github(*gh)
+ if v:
+ found.append(v)
+ if "pypi.org" in url or "files.pythonhosted.org" in url:
+ v = latest_pypi(name)
+ if v:
+ found.append(v)
+
+ v = latest_repology(name)
+ if v:
+ found.append(v)
+
+ best = None
+ for ver in found:
+ if is_prerelease(ver):
+ continue
+ if not gnome_stable(name, ver):
+ continue
+ if best is None or version_newer(ver, best):
+ best = ver
+ return best
+
+
+def _series(version: str) -> str:
+ parts = version.split(".")
+ if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit():
+ return f"{parts[0]}.{parts[1]}"
+ return version
+
+
+def rewrite_url(url: str, old: str, new: str) -> str:
+ if not old or old == new:
+ return url
+ updated = url
+ variants = [
+ (old, new),
+ (old.replace(".", "_"), new.replace(".", "_")),
+ (old.replace(".", "-"), new.replace(".", "-")),
+ (old.replace(".", ""), new.replace(".", "")),
+ ]
+ for a, b in variants:
+ if a and a in updated:
+ updated = updated.replace(a, b)
+ break
+ old_s, new_s = _series(old), _series(new)
+ if old_s != new_s and f"/{old_s}/" in updated:
+ updated = updated.replace(f"/{old_s}/", f"/{new_s}/")
+ return updated
+
+
+def extract_archive(archive: Path, dest: Path) -> Path:
+ dest.mkdir(parents=True, exist_ok=True)
+ name = archive.name.lower()
+ if name.endswith(".zip"):
+ with zipfile.ZipFile(archive) as zf:
+ zf.extractall(dest)
+ else:
+ with tarfile.open(archive, "r:*") as tf:
+ try:
+ tf.extractall(dest, filter="data")
+ except TypeError:
+ tf.extractall(dest)
+ children = [p for p in dest.iterdir() if p.name not in {".", ".."}]
+ if len(children) == 1 and children[0].is_dir():
+ return children[0]
+ return dest
+
+
+MESON_DEP = re.compile(
+ r"""dependency\(\s*['\"]([^'\"]+)['\"]([^)]*)\)""",
+ re.M,
+)
+CMAKE_PKG = re.compile(
+ r"""find_package\(\s*([A-Za-z0-9_+.-]+)[^)]*REQUIRED""",
+ re.M,
+)
+CMAKE_PC = re.compile(
+ r"""pkg_check_modules\(\s*[A-Za-z0-9_]+\s+(?:REQUIRED\s+)?([A-Za-z0-9_+\-.]+)""",
+ re.I,
+)
+AUTOCONF_PC = re.compile(
+ r"""PKG_CHECK_MODULES\(\s*\[[^\]]+\]\s*,\s*\[([^\]>=]+)""",
+)
+PC_REQUIRES = re.compile(
+ r"^(?:Requires(?:\.private)?):\s*(.+)$",
+ re.M | re.I,
+)
+
+
+def scan_build_files(root: Path) -> tuple[set[str], set[str]]:
+ """Return (runtime-ish names, build-ish names) as pkg-config/cmake ids."""
+ runtime: set[str] = set()
+ build: set[str] = set()
+ files: list[Path] = []
+ for pat in ("meson.build", "CMakeLists.txt", "configure.ac", "configure.in"):
+ files.extend(root.rglob(pat))
+ files.extend(root.rglob("*.pc.in"))
+ files.extend(root.rglob("*.pc"))
+ seen = 0
+ for path in files:
+ if seen > 80:
+ break
+ try:
+ if path.stat().st_size > 2_000_000:
+ continue
+ text = path.read_text(encoding="utf-8", errors="replace")
+ except OSError:
+ continue
+ seen += 1
+ name = path.name
+ if name in {"meson.build", "meson_options.txt"}:
+ for match in MESON_DEP.finditer(text):
+ pc, rest = match.group(1), match.group(2)
+ if re.search(r"required\s*:\s*false", rest, re.I):
+ continue
+ runtime.add(pc)
+ elif name == "CMakeLists.txt":
+ for match in CMAKE_PKG.finditer(text):
+ runtime.add(match.group(1))
+ for match in CMAKE_PC.finditer(text):
+ runtime.add(match.group(1).split()[0])
+ elif name in {"configure.ac", "configure.in"}:
+ for match in AUTOCONF_PC.finditer(text):
+ runtime.add(match.group(1).strip().split()[0])
+ elif name.endswith(".pc") or name.endswith(".pc.in"):
+ for match in PC_REQUIRES.finditer(text):
+ for tok in re.split(r"[ ,]+", match.group(1)):
+ tok = re.sub(r"[<>=].*$", "", tok).strip()
+ if tok:
+ runtime.add(tok)
+ return runtime, build
+
+
+def map_dep(raw: str, known: dict[str, Path]) -> str | None:
+ raw = raw.strip()
+ if not raw or raw in SKIP_DEPS:
+ return None
+ if raw in PC_MAP:
+ mapped = PC_MAP[raw]
+ return mapped if mapped in known else mapped
+ lower = raw.lower()
+ if lower in known:
+ return lower
+ if raw in known:
+ return raw
+ slug = lower.replace("_", "-")
+ if slug in known:
+ return slug
+ if slug.startswith("lib") and slug[3:] in known:
+ return slug[3:]
+ if not slug.startswith("lib") and f"lib{slug}" in known:
+ return f"lib{slug}"
+ # gtk+-3.0 -> gtk3 already in map; qt6 bits
+ m = re.match(r"qt6(.+)", slug)
+ if m:
+ cand = "qt6-qt" + m.group(1).lower()
+ if cand in known:
+ return cand
+ return None
+
+
+def load_tree_names(trees: list[Path]) -> dict[str, Path]:
+ names: dict[str, Path] = {}
+ for tree in trees:
+ for recipe in tree.rglob("recipe"):
+ rec = parse_recipe(recipe)
+ if rec["name"]:
+ names[rec["name"]] = recipe
+ return names
+
+
+def rewrite_recipe(rec: dict, new_ver: str, new_urls: list[str], new_hashes: list[str],
+ extra_depend: list[str], extra_builddep: list[str]) -> str:
+ old_ver = rec["version"]
+ lines = rec["raw"].splitlines()
+ out: list[str] = []
+ hash_i = 0
+ source_i = 0
+ seen_depend = False
+ inserted_deps = False
+ have_depend = set(rec["depend"])
+ have_build = set(rec["builddep"])
+
+ def flush_new_deps(dest: list[str]) -> None:
+ nonlocal inserted_deps
+ if inserted_deps:
+ return
+ for name in extra_depend:
+ if name not in have_depend:
+ dest.append(f"depend {name}")
+ have_depend.add(name)
+ for name in extra_builddep:
+ if name not in have_build:
+ dest.append(f"builddep {name}")
+ have_build.add(name)
+ inserted_deps = True
+
+ for line in lines:
+ if line.startswith("version"):
+ out.append(f"version {new_ver}")
+ continue
+ if line.startswith("release"):
+ out.append("release 1")
+ continue
+ if line.startswith("source"):
+ if source_i < len(new_urls):
+ out.append(f"source {new_urls[source_i]}")
+ source_i += 1
+ else:
+ out.append(rewrite_url(line, old_ver, new_ver) if old_ver else line)
+ continue
+ if line.startswith("hash"):
+ if hash_i < len(new_hashes):
+ out.append(f"hash sha256:{new_hashes[hash_i]}")
+ hash_i += 1
+ else:
+ out.append(line)
+ continue
+ if line.startswith("depend"):
+ seen_depend = True
+ out.append(line)
+ continue
+ if seen_depend and not line.startswith(("depend", "builddep", "optional", "conflict")):
+ flush_new_deps(out)
+ seen_depend = False
+ if old_ver and old_ver in line and line.startswith(("prepare", "configure", "build", "install")):
+ out.append(line.replace(old_ver, new_ver))
+ continue
+ out.append(line)
+ flush_new_deps(out)
+ text = "\n".join(out)
+ if not text.endswith("\n"):
+ text += "\n"
+ return text
+
+
+def scaffold_recipe(tree: Path, name: str, version: str, url: str, digest: str,
+ depend: list[str]) -> Path:
+ category = "libs"
+ dest = tree / category / name
+ dest.mkdir(parents=True, exist_ok=True)
+ deps = "\n".join(f"depend {d}" for d in (["filesystem"] + depend))
+ body = (
+ f"name {name}\n"
+ f"version {version}\n"
+ f"release 1\n"
+ f"description {name}: auto-scaffolded runtime dependency\n"
+ f"\n"
+ f"source {url}\n"
+ f"hash sha256:{digest}\n"
+ f"\n"
+ f"{deps}\n"
+ f"\n"
+ f"configure ./configure --prefix=/usr --libdir=/usr/lib64 --disable-static\n"
+ f"build make -j\"$MAKEJOBS\"\n"
+ f"install make install DESTDIR=\"$PKG\"\n"
+ )
+ recipe = dest / "recipe"
+ recipe.write_text(body, encoding="utf-8")
+ return recipe
+
+
+def guess_scaffold_url(name: str, version: str) -> str | None:
+ # Common GitHub-style; last resort only.
+ try:
+ body = http_get(
+ f"{GITHUB_API}/search/repositories?q={name}+in:name&per_page=3",
+ accept="application/vnd.github+json",
+ )
+ data = json.loads(body)
+ items = data.get("items") or []
+ for item in items:
+ if item.get("name", "").lower() != name.lower():
+ continue
+ full = item.get("full_name")
+ if not full:
+ continue
+ return f"https://github.com/{full}/archive/refs/tags/v{version}.tar.gz"
+ except (HttpError, json.JSONDecodeError):
+ return None
+ return None
+
+
+def pin_group_key(name: str) -> int | None:
+ for i, rx in enumerate(PIN_GROUPS):
+ if rx.search(name):
+ return i
+ return None
+
+
+class PlanItem:
+ def __init__(self, rec: dict):
+ self.rec = rec
+ self.name = rec["name"]
+ self.old = rec["version"]
+ self.new = rec["version"]
+ self.urls: list[str] = list(rec["source"])
+ self.hashes: list[str] = []
+ self.add_depend: list[str] = []
+ self.add_builddep: list[str] = []
+ self.missing: list[str] = []
+ self.status = "current"
+ self.detail = ""
+ self.group: int | None = pin_group_key(self.name)
+
+
+def collect_recipes(trees: list[Path], only: set[str] | None) -> list[dict]:
+ recs = []
+ for tree in trees:
+ for recipe in sorted(tree.rglob("recipe")):
+ rec = parse_recipe(recipe)
+ if not rec["name"]:
+ continue
+ if only and rec["name"] not in only:
+ continue
+ recs.append(rec)
+ return recs
+
+
+def probe_one(rec: dict, include_kernel: bool) -> PlanItem:
+ item = PlanItem(rec)
+ name = rec["name"]
+ if name in NO_AUTO and name not in {"qt6"}:
+ item.status = "skip"
+ item.detail = "metapackage or distro glue"
+ return item
+ if not include_kernel and name in {"linux", "linux-desktop", "linux-firmware"}:
+ item.status = "skip"
+ item.detail = "kernel/firmware (pass --include-kernel)"
+ return item
+ if not recipe_has_source(rec):
+ item.status = "skip"
+ item.detail = "no upstream source"
+ return item
+ try:
+ latest = detect_latest(rec)
+ except HttpError as exc:
+ item.status = "error"
+ item.detail = str(exc)
+ return item
+ if not latest:
+ item.status = "unknown"
+ item.detail = "could not detect a newer stable"
+ return item
+ if not version_newer(latest, rec["version"]):
+ item.status = "current"
+ item.new = rec["version"]
+ return item
+ item.new = latest
+ item.status = "update"
+ item.urls = [rewrite_url(u, rec["version"], latest) for u in rec["source"]]
+ return item
+
+
+def apply_pin_groups(items: list[PlanItem]) -> None:
+ by_group: dict[int, list[PlanItem]] = {}
+ for item in items:
+ if item.group is None:
+ continue
+ by_group.setdefault(item.group, []).append(item)
+ for members in by_group.values():
+ best = None
+ for item in members:
+ if item.status in {"update", "current"}:
+ if best is None or version_newer(item.new, best):
+ best = item.new
+ if not best:
+ continue
+ for item in members:
+ if item.status in {"skip", "error"}:
+ continue
+ if item.old != best:
+ item.new = best
+ item.status = "update"
+ item.urls = [rewrite_url(u, item.old, best) for u in item.rec["source"]]
+ item.detail = "pinned with sibling modules"
+
+
+def inspect_new_deps(item: PlanItem, known: dict[str, Path]) -> None:
+ if item.status != "update" or not item.urls:
+ return
+ url = item.urls[0]
+ try:
+ archive = download(url)
+ item.hashes = [sha256_file(archive)]
+ for extra in item.urls[1:]:
+ item.hashes.append(sha256_file(download(extra)))
+ except (HttpError, OSError) as exc:
+ item.status = "error"
+ item.detail = f"download failed: {exc}"
+ return
+ runtime, _build = set(), set()
+ try:
+ with tempfile.TemporaryDirectory(prefix="sps-upg-") as tmp:
+ root = extract_archive(archive, Path(tmp) / "src")
+ runtime, _build = scan_build_files(root)
+ except (tarfile.TarError, zipfile.BadZipFile, OSError) as exc:
+ item.detail = (item.detail + f" (scan skipped: {exc})").strip()
+ return
+ have = set()
+ for spec in item.rec["depend"] + item.rec["builddep"]:
+ have.add(re.split(r"[<>=]", spec, 1)[0])
+ for raw in sorted(runtime):
+ mapped = map_dep(raw, known)
+ if mapped is None:
+ if raw.lower() in SKIP_DEPS or raw in HOST_TOOLS:
+ continue
+ item.missing.append(raw)
+ continue
+ if mapped in have or mapped == item.name or mapped in HOST_TOOLS:
+ continue
+ if mapped not in known:
+ item.missing.append(mapped)
+ continue
+ item.add_depend.append(mapped)
+ have.add(mapped)
+
+
+def write_item(item: PlanItem) -> None:
+ text = rewrite_recipe(
+ item.rec, item.new, item.urls, item.hashes,
+ item.add_depend, item.add_builddep,
+ )
+ item.rec["path"].write_text(text, encoding="utf-8")
+
+
+def format_plan(items: list[PlanItem]) -> str:
+ lines = []
+ counts = {}
+ for item in items:
+ counts[item.status] = counts.get(item.status, 0) + 1
+ if item.status == "current":
+ continue
+ extra = ""
+ if item.add_depend:
+ extra += f" +depend {', '.join(item.add_depend)}"
+ if item.missing:
+ extra += f" missing {', '.join(item.missing)}"
+ if item.detail:
+ extra += f" ({item.detail})"
+ if item.status == "update":
+ lines.append(f"UPDATE {item.name:28} {item.old} -> {item.new}{extra}")
+ elif item.status == "skip":
+ lines.append(f"SKIP {item.name:28} {item.detail}")
+ elif item.status == "unknown":
+ lines.append(f"UNKNOWN {item.name:28} {item.old} {item.detail}")
+ else:
+ lines.append(f"ERROR {item.name:28} {item.detail}")
+ summary = " ".join(f"{k}={v}" for k, v in sorted(counts.items()))
+ lines.append(f"summary {summary}")
+ return "\n".join(lines) + "\n"
+
+
+def selftest() -> int:
+ assert version_compare("1.2.3", "1.2.3") == 0
+ assert version_compare("1.2.10", "1.2.9") > 0
+ assert version_compare("2.0", "1.9.9") > 0
+ assert version_compare("1.0~rc1", "1.0") < 0
+ assert is_prerelease("1.2.3-rc1")
+ assert not is_prerelease("1.2.3")
+ assert gnome_stable("gtk", "4.22.4")
+ assert not gnome_stable("gtk", "4.23.1")
+ url = rewrite_url(
+ "https://download.gnome.org/sources/gtk/4.18/gtk-4.18.6.tar.xz",
+ "4.18.6", "4.22.4",
+ )
+ assert "4.22.4" in url and "4.18.6" not in url
+ # series directory also rewritten
+ assert "/4.22/" in url or "4.22.4" in url
+ raw = map_dep("libssl", {"openssl": Path(".")})
+ assert raw == "openssl"
+ meson = "dependency('libpng', required: true)\ndependency('threads')\n"
+ with tempfile.TemporaryDirectory() as tmp:
+ p = Path(tmp)
+ (p / "meson.build").write_text(meson, encoding="utf-8")
+ runtime, _ = scan_build_files(p)
+ assert "libpng" in runtime
+ assert "threads" in runtime
+ rec_text = (
+ "name demo\n"
+ "version 1.0\n"
+ "release 3\n"
+ "description demo: test\n"
+ "\n"
+ "source https://example.org/demo-1.0.tar.gz\n"
+ "hash sha256:" + ("a" * 64) + "\n"
+ "\n"
+ "depend filesystem\n"
+ "install make install DESTDIR=\"$PKG\"\n"
+ )
+ with tempfile.TemporaryDirectory() as tmp:
+ recipe = Path(tmp) / "recipe"
+ recipe.write_text(rec_text, encoding="utf-8")
+ rec = parse_recipe(recipe)
+ assert rec["name"] == "demo"
+ new = rewrite_recipe(rec, "1.1", ["https://example.org/demo-1.1.tar.gz"],
+ ["b" * 64], ["zlib"], [])
+ assert "version 1.1" in new
+ assert "release 1" in new
+ assert "depend zlib" in new
+ assert "demo-1.1.tar.gz" in new
+ print("selftest ok")
+ return 0
+
+
+def main(argv: list[str] | None = None) -> int:
+ ap = argparse.ArgumentParser(
+ description="Upgrade SPS recipes to the newest upstream stable release.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=(
+ "Examples:\n"
+ " tools/upgrade-recipes --trees /usr/src/sps/core:/usr/src/sps/extra\n"
+ " tools/upgrade-recipes --trees $PWD/../sps-core:$PWD/../sps-extra --apply\n"
+ " tools/upgrade-recipes --trees extra --only gtk4,qemu,llvm --apply\n"
+ ),
+ )
+ ap.add_argument("--trees", default="",
+ help="colon-separated recipe trees (core:extra)")
+ ap.add_argument("--apply", action="store_true",
+ help="write recipe changes (default is a plan only)")
+ ap.add_argument("--scan-deps", action="store_true",
+ help="download new tarballs and scan for undeclared dependencies "
+ "(implied by --apply)")
+ ap.add_argument("--no-scan-deps", action="store_true",
+ help="do not download tarballs or scan dependencies")
+ ap.add_argument("--only", default="",
+ help="comma-separated package names")
+ ap.add_argument("--jobs", type=int, default=8,
+ help="parallel upstream probes (default 8)")
+ ap.add_argument("--include-kernel", action="store_true",
+ help="allow linux and firmware recipes")
+ ap.add_argument("--scaffold-missing", action="store_true",
+ help="draft extra recipes for newly required unpackaged deps")
+ ap.add_argument("--json", action="store_true",
+ help="print machine-readable plan")
+ ap.add_argument("--selftest", action="store_true",
+ help="run offline unit checks and exit")
+ args = ap.parse_args(argv)
+
+ if args.selftest:
+ return selftest()
+
+ trees = [Path(p) for p in args.trees.split(":") if p]
+ if not trees:
+ env = os.environ.get("SPS_RECIPE_TREES", "")
+ trees = [Path(p) for p in env.split(":") if p]
+ if not trees:
+ ap.error("pass --trees CORE:EXTRA (or set SPS_RECIPE_TREES)")
+
+ for tree in trees:
+ if not tree.is_dir():
+ ap.error(f"recipe tree is not a directory: {tree}")
+
+ only = {x.strip() for x in args.only.split(",") if x.strip()} or None
+ recs = collect_recipes(trees, only)
+ if not recs:
+ log("no recipes found")
+ return 1
+
+ log(f"scanning {len(recs)} recipes in {len(trees)} tree(s)")
+ items: list[PlanItem] = []
+ with ThreadPoolExecutor(max_workers=max(1, args.jobs)) as pool:
+ futs = {pool.submit(probe_one, rec, args.include_kernel): rec for rec in recs}
+ for fut in as_completed(futs):
+ item = fut.result()
+ items.append(item)
+ if item.status == "update":
+ log(f"plan {item.name} {item.old} -> {item.new}")
+ items.sort(key=lambda i: i.name)
+ apply_pin_groups(items)
+
+ known = load_tree_names(trees)
+ extra_tree = trees[-1]
+ updates = [i for i in items if i.status == "update"]
+ do_scan = (args.apply or args.scan_deps) and not args.no_scan_deps
+ if do_scan:
+ for item in updates:
+ log(f"fetch {item.name} {item.new}")
+ inspect_new_deps(item, known)
+ for miss in list(item.missing):
+ mapped = map_dep(miss, known) or miss
+ if mapped in known:
+ if mapped not in item.add_depend:
+ item.add_depend.append(mapped)
+ item.missing.remove(miss)
+ continue
+ if not args.scaffold_missing:
+ continue
+ latest = latest_repology(mapped) or item.new
+ url = guess_scaffold_url(mapped, latest)
+ if not url:
+ continue
+ try:
+ archive = download(url)
+ digest = sha256_file(archive)
+ except (HttpError, OSError):
+ continue
+ path = scaffold_recipe(extra_tree, mapped, latest, url, digest, [])
+ known[mapped] = path
+ item.add_depend.append(mapped)
+ item.missing.remove(miss)
+ log(f"scaffold {mapped} -> {path}")
+ elif args.apply:
+ for item in updates:
+ log(f"fetch {item.name} {item.new}")
+ if not item.urls:
+ item.status = "error"
+ item.detail = "no source URL"
+ continue
+ try:
+ item.hashes = [sha256_file(download(u)) for u in item.urls]
+ except (HttpError, OSError) as exc:
+ item.status = "error"
+ item.detail = f"download failed: {exc}"
+
+ if args.json:
+ payload = []
+ for item in items:
+ payload.append({
+ "name": item.name,
+ "status": item.status,
+ "old": item.old,
+ "new": item.new,
+ "urls": item.urls,
+ "add_depend": item.add_depend,
+ "missing": item.missing,
+ "detail": item.detail,
+ })
+ print(json.dumps(payload, indent=2))
+ else:
+ sys.stdout.write(format_plan(items))
+
+ if not args.apply:
+ return 0 if not any(i.status == "error" for i in items) else 2
+
+ written = 0
+ for item in items:
+ if item.status != "update":
+ continue
+ if not item.hashes:
+ log(f"skip write {item.name}: no hash")
+ continue
+ write_item(item)
+ written += 1
+ log(f"wrote {item.rec['path']}")
+ log(f"applied {written} recipe update(s)")
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(main())
+ except KeyboardInterrupt:
+ raise SystemExit(130)
diff --git a/tools/write-stock-services.py b/tools/write-stock-services.py
new file mode 100755
index 0000000..6535c4b
--- /dev/null
+++ b/tools/write-stock-services.py
@@ -0,0 +1,343 @@
+#!/usr/bin/env python3
+"""Write stock init scripts for every supported init, and ship systemd plus
+OpenRC copies next to extra recipes that provide the daemon.
+
+Supported inits: systemd, openrc, s6, runit, dinit, shepherd.
+"""
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+
+SPS = Path(__file__).resolve().parents[1]
+SV = SPS / "lib" / "setup" / "sv"
+MAP = SPS / "lib" / "setup" / "services"
+
+# package, rc, unit, wanted, runlevel, description, argv, flags
+# flags: dbus, user, pre (list of shell lines for supervised run scripts)
+SERVICES = [
+ ("fail2ban", "fail2ban", "fail2ban.service", "multi-user.target", "default",
+ "fail2ban authentication ban daemon",
+ ["/usr/bin/fail2ban-server", "-xf", "--logtarget=stdout"], {}),
+ ("unbound", "unbound", "unbound.service", "multi-user.target", "default",
+ "validating recursive DNS resolver",
+ ["/usr/sbin/unbound", "-d"], {}),
+ ("dnsmasq", "dnsmasq", "dnsmasq.service", "multi-user.target", "default",
+ "DNS and DHCP server",
+ ["/usr/sbin/dnsmasq", "-k"], {}),
+ ("openvpn", "openvpn", "openvpn.service", "multi-user.target", "default",
+ "OpenVPN server",
+ ["/usr/sbin/openvpn", "--config", "/etc/openvpn/server.conf"], {}),
+ ("tor", "tor", "tor.service", "multi-user.target", "default",
+ "Tor anonymity daemon",
+ ["/usr/bin/tor", "--RunAsDaemon", "0"], {}),
+ ("dropbear", "dropbear", "dropbear.service", "multi-user.target", "default",
+ "Dropbear SSH server",
+ ["/usr/sbin/dropbear", "-F", "-E"],
+ {"pre": ["if command -v dropbearkey >/dev/null 2>&1; then",
+ " mkdir -p /etc/dropbear",
+ " [ -f /etc/dropbear/dropbear_rsa_host_key ] || dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key >/dev/null",
+ "fi"]}),
+ ("tinyproxy", "tinyproxy", "tinyproxy.service", "multi-user.target", "default",
+ "Tinyproxy HTTP proxy",
+ ["/usr/bin/tinyproxy", "-d"], {}),
+ ("haproxy", "haproxy", "haproxy.service", "multi-user.target", "default",
+ "HAProxy load balancer",
+ ["/usr/sbin/haproxy", "-f", "/etc/haproxy/haproxy.cfg", "-db"], {}),
+ ("lighttpd", "lighttpd", "lighttpd.service", "multi-user.target", "default",
+ "lighttpd web server",
+ ["/usr/sbin/lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"], {}),
+ ("avahi", "avahi-daemon", "avahi-daemon.service", "multi-user.target", "default",
+ "Avahi mDNS/DNS-SD daemon",
+ ["/usr/sbin/avahi-daemon", "--no-chroot"],
+ {"dbus": True}),
+ ("nfs-utils", "nfs-server", "nfs-server.service", "multi-user.target", "default",
+ "NFS server",
+ ["/usr/sbin/rpc.mountd", "-F"],
+ {"pre": ["if command -v exportfs >/dev/null 2>&1; then exportfs -ra || :; fi",
+ "if command -v rpc.statd >/dev/null 2>&1; then rpc.statd --no-notify || :; fi",
+ "if command -v rpc.nfsd >/dev/null 2>&1; then rpc.nfsd 8 || :; fi"]}),
+ ("libvirt", "libvirtd", "libvirtd.service", "multi-user.target", "default",
+ "libvirt virtualization daemon",
+ ["/usr/sbin/libvirtd"],
+ {"dbus": True}),
+ ("postgresql", "postgresql", "postgresql.service", "multi-user.target", "default",
+ "PostgreSQL database server",
+ ["/usr/bin/postgres", "-D", "/var/lib/postgres/data"],
+ {"user": "postgres",
+ "pre": ["install -d -m 0755 /run/postgresql /var/lib/postgres/data || :"]}),
+ ("mariadb", "mariadbd", "mariadb.service", "multi-user.target", "default",
+ "MariaDB database server",
+ ["/usr/sbin/mariadbd", "--datadir=/var/lib/mysql"],
+ {"user": "mysql",
+ "pre": ["install -d -m 0750 /run/mysqld /var/lib/mysql || :"]}),
+ ("memcached", "memcached", "memcached.service", "multi-user.target", "default",
+ "memcached object cache",
+ ["/usr/bin/memcached", "-u", "nobody"], {}),
+ ("valkey", "valkey", "valkey.service", "multi-user.target", "default",
+ "Valkey data store",
+ ["/usr/bin/valkey-server", "--daemonize", "no", "--bind", "127.0.0.1"], {}),
+ ("conntrack-tools", "conntrackd", "conntrackd.service", "multi-user.target", "default",
+ "connection tracking daemon",
+ ["/usr/sbin/conntrackd", "-C", "/etc/conntrackd/conntrackd.conf"], {}),
+ ("usbmuxd", "usbmuxd", "usbmuxd.service", "multi-user.target", "default",
+ "iOS USB multiplexing daemon",
+ ["/usr/sbin/usbmuxd", "-f"], {}),
+ ("distcc", "distccd", "distccd.service", "multi-user.target", "default",
+ "distributed C/C++ compilation daemon",
+ ["/usr/bin/distccd", "--no-detach", "--allow", "127.0.0.1", "--log-stderr"], {}),
+ ("lxc", "lxc-net", "lxc-net.service", "multi-user.target", "default",
+ "LXC bridge networking",
+ ["/usr/libexec/lxc/lxc-net", "start"],
+ {"oneshot": True}),
+]
+
+ALIASES = {
+ "nfs-server": "nfs-utils",
+ "libvirtd": "libvirt",
+ "conntrackd": "conntrack-tools",
+ "avahi-daemon": "avahi",
+ "mariadbd": "mariadb",
+ "distccd": "distcc",
+ "lxc-net": "lxc",
+}
+
+
+def sh_join(argv: list[str]) -> str:
+ return " ".join(argv)
+
+
+def scm_list(argv: list[str]) -> str:
+ return " ".join(f'"{a}"' for a in argv)
+
+
+def systemd_unit(svc: dict) -> str:
+ after = ["network.target"]
+ if svc["flags"].get("dbus"):
+ after.append("dbus.service")
+ lines = [
+ "[Unit]",
+ f"Description={svc['desc']}",
+ f"After={' '.join(after)}",
+ "",
+ "[Service]",
+ f"ExecStart={sh_join(svc['argv'])}",
+ ]
+ if svc["flags"].get("oneshot"):
+ lines.append("Type=oneshot")
+ lines.append("RemainAfterExit=yes")
+ else:
+ lines.append("Restart=on-failure")
+ user = svc["flags"].get("user")
+ if user:
+ lines.append(f"User={user}")
+ lines += ["", "[Install]", f"WantedBy={svc['wanted']}", ""]
+ return "\n".join(lines)
+
+
+def openrc_script(svc: dict) -> str:
+ depend = [" use net", " after net"]
+ if svc["flags"].get("dbus"):
+ depend = [" need dbus", " after dbus"]
+ pre = svc["flags"].get("pre") or []
+ user = svc["flags"].get("user")
+ body = [
+ "#!/sbin/openrc-run",
+ f'description="{svc["desc"]}"',
+ f'command="{svc["argv"][0]}"',
+ f'command_args="{" ".join(svc["argv"][1:])}"',
+ ]
+ if svc["flags"].get("oneshot"):
+ body.append('command_background="no"')
+ else:
+ body.extend([
+ 'command_background="yes"',
+ f'pidfile="/run/{svc["rc"]}.pid"',
+ ])
+ if user:
+ body.append(f'command_user="{user}"')
+ body.append("")
+ body.append("depend() {")
+ body.extend(depend)
+ body.append("}")
+ if pre:
+ body.append("")
+ body.append("start_pre() {")
+ body.extend(f"\t{line}" for line in pre)
+ body.append("}")
+ body.append("")
+ return "\n".join(body)
+
+
+def supervised_run(svc: dict) -> str:
+ lines = ["#!/bin/sh", "exec 2>&1"]
+ for line in svc["flags"].get("pre") or []:
+ lines.append(line)
+ lines.append(f"exec {sh_join(svc['argv'])}")
+ lines.append("")
+ return "\n".join(lines)
+
+
+def dinit_file(svc: dict) -> str:
+ lines = [
+ "type = process",
+ f"command = {sh_join(svc['argv'])}",
+ "restart = true",
+ "smooth-recovery = true",
+ ]
+ if svc["flags"].get("dbus"):
+ lines.append("depends-on = dbus")
+ lines.append("")
+ return "\n".join(lines)
+
+
+def shepherd_file(svc: dict) -> str:
+ req = "'(dbus)" if svc["flags"].get("dbus") else "'()"
+ return (
+ "(use-modules (shepherd service))\n"
+ "(register-services\n"
+ " (list (service\n"
+ f" '({svc['rc']})\n"
+ f" #:requirement {req}\n"
+ f" #:documentation \"{svc['desc']}\"\n"
+ f" #:start (make-forkexec-constructor '({scm_list(svc['argv'])}))\n"
+ " #:stop (make-kill-destructor)\n"
+ " #:respawn? #t)))\n"
+ )
+
+
+def write_file(path: Path, text: str, mode: int = 0o644) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text, encoding="utf-8")
+ path.chmod(mode)
+
+
+def parse_services() -> list[dict]:
+ out = []
+ for pkg, rc, unit, wanted, level, desc, argv, flags in SERVICES:
+ out.append({
+ "pkg": pkg, "rc": rc, "unit": unit, "wanted": wanted,
+ "level": level, "desc": desc, "argv": argv, "flags": flags,
+ })
+ return out
+
+
+def write_stock(services: list[dict]) -> None:
+ for svc in services:
+ write_file(SV / "systemd" / svc["unit"], systemd_unit(svc), 0o644)
+ write_file(SV / "openrc" / svc["rc"], openrc_script(svc), 0o755)
+ write_file(SV / "runit" / svc["rc"] / "run", supervised_run(svc), 0o755)
+ write_file(SV / "s6" / svc["rc"] / "run", supervised_run(svc), 0o755)
+ write_file(SV / "dinit" / svc["rc"], dinit_file(svc), 0o644)
+ write_file(SV / "shepherd" / f"{svc['rc']}.scm", shepherd_file(svc), 0o644)
+
+
+def update_map(services: list[dict]) -> None:
+ existing = MAP.read_text(encoding="utf-8")
+ lines = existing.rstrip() + "\n"
+ have = set()
+ for raw in existing.splitlines():
+ if not raw.strip() or raw.lstrip().startswith("#"):
+ continue
+ have.add(raw.split()[0])
+ added = []
+ for svc in services:
+ row = f"{svc['pkg']}\t{svc['unit']}\t{svc['wanted']}\t{svc['rc']}\t{svc['level']}\n"
+ if svc["pkg"] not in have:
+ lines += row
+ have.add(svc["pkg"])
+ added.append(svc["pkg"])
+ for alias, pkg in ALIASES.items():
+ if pkg == svc["pkg"] and alias not in have:
+ lines += (
+ f"{alias}\t{svc['unit']}\t{svc['wanted']}\t"
+ f"{svc['rc']}\t{svc['level']}\n"
+ )
+ have.add(alias)
+ added.append(alias)
+ MAP.write_text(lines, encoding="utf-8")
+ return added
+
+
+def find_recipe(tree: Path, name: str) -> Path | None:
+ for recipe in tree.rglob("recipe"):
+ try:
+ for line in recipe.read_text(encoding="utf-8").splitlines():
+ if line.startswith("name"):
+ rec_name = line.split(None, 1)[1].strip()
+ if rec_name == name:
+ return recipe
+ break
+ except OSError:
+ continue
+ return None
+
+
+def bump_release(text: str) -> str:
+ out = []
+ done = False
+ for line in text.splitlines(keepends=True):
+ if not done and line.startswith("release"):
+ parts = line.split(None, 1)
+ try:
+ rel = int(parts[1].strip())
+ except (IndexError, ValueError):
+ rel = 1
+ line = f"release {rel + 1}\n"
+ done = True
+ out.append(line)
+ return "".join(out)
+
+
+def install_recipe_files(tree: Path, services: list[dict]) -> list[str]:
+ touched = []
+ for svc in services:
+ recipe = find_recipe(tree, svc["pkg"])
+ if recipe is None:
+ continue
+ files = recipe.parent / "files"
+ write_file(files / svc["unit"], systemd_unit(svc), 0o644)
+ write_file(files / f"openrc-{svc['rc']}", openrc_script(svc), 0o755)
+ text = recipe.read_text(encoding="utf-8")
+ marker = f"$SRC/files/{svc['unit']}"
+ if marker not in text:
+ text = bump_release(text)
+ if not text.endswith("\n"):
+ text += "\n"
+ text += (
+ 'install mkdir -p "$PKG/usr/lib/systemd/system" "$PKG/etc/init.d"\n'
+ f'install install -m 0644 "$SRC/files/{svc["unit"]}" '
+ f'"$PKG/usr/lib/systemd/system/{svc["unit"]}"\n'
+ f'install install -m 0755 "$SRC/files/openrc-{svc["rc"]}" '
+ f'"$PKG/etc/init.d/{svc["rc"]}"\n'
+ )
+ recipe.write_text(text, encoding="utf-8")
+ touched.append(str(recipe))
+ return touched
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__)
+ ap.add_argument("--extra", default="/tmp/sps-extra",
+ help="extra recipe tree (default: /tmp/sps-extra)")
+ ap.add_argument("--core", default="", help="optional core recipe tree")
+ args = ap.parse_args()
+ services = parse_services()
+ write_stock(services)
+ added = update_map(services)
+ extra = Path(args.extra)
+ touched = []
+ if extra.is_dir():
+ touched.extend(install_recipe_files(extra, services))
+ if args.core:
+ core = Path(args.core)
+ if core.is_dir():
+ touched.extend(install_recipe_files(core, services))
+ print(f"stock services: {len(services)}")
+ print(f"map added: {', '.join(added) if added else '(none)'}")
+ print(f"recipes updated: {len(touched)}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())