summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-16 15:22:00 +0300
committerPaul Buetow <paul@buetow.org>2026-05-16 15:22:00 +0300
commit91d5fec541ecc9147d89a2c25f3ba76ce1895bb7 (patch)
tree848c677cb4b3748dc2d104f30f4d77ab068fc894
parent98217b5ab29265d2662bebf0a1d946eaead80dbd (diff)
frontends + packages: add dserver/dtail support for FreeBSD and Rocky
Adds FreeBSD .tpl variants of the existing dserver templates and a matching pkg-dtail-freebsd.sh packaging script, plus a pkg-dtail-rpm.sh script and packages/files/dtail-rocky/ (systemd units, key-cache script, dtail.json) for the Rocky Linux dtail build.
-rw-r--r--frontends/etc/dserver/dtail-freebsd.json.tpl127
-rw-r--r--frontends/etc/rc.d/dserver-freebsd.tpl30
-rw-r--r--frontends/scripts/dserver-update-key-cache-freebsd.sh.tpl33
-rwxr-xr-xpackages/files/dtail-rocky/dserver-update-key-cache.sh54
-rw-r--r--packages/files/dtail-rocky/dserver-update-keycache.service6
-rw-r--r--packages/files/dtail-rocky/dserver-update-keycache.timer11
-rw-r--r--packages/files/dtail-rocky/dserver.service22
-rw-r--r--packages/files/dtail-rocky/dtail.json131
-rw-r--r--packages/scripts/pkg-dtail-freebsd.sh79
-rwxr-xr-xpackages/scripts/pkg-dtail-rpm.sh140
10 files changed, 633 insertions, 0 deletions
diff --git a/frontends/etc/dserver/dtail-freebsd.json.tpl b/frontends/etc/dserver/dtail-freebsd.json.tpl
new file mode 100644
index 0000000..cd99560
--- /dev/null
+++ b/frontends/etc/dserver/dtail-freebsd.json.tpl
@@ -0,0 +1,127 @@
+{
+ "Client": {
+ "TermColorsEnable": true,
+ "TermColors": {
+ "Remote": {
+ "DelimiterAttr": "Dim",
+ "DelimiterBg": "Blue",
+ "DelimiterFg": "Cyan",
+ "RemoteAttr": "Dim",
+ "RemoteBg": "Blue",
+ "RemoteFg": "White",
+ "CountAttr": "Dim",
+ "CountBg": "Blue",
+ "CountFg": "White",
+ "HostnameAttr": "Bold",
+ "HostnameBg": "Blue",
+ "HostnameFg": "White",
+ "IDAttr": "Dim",
+ "IDBg": "Blue",
+ "IDFg": "White",
+ "StatsOkAttr": "None",
+ "StatsOkBg": "Green",
+ "StatsOkFg": "Black",
+ "StatsWarnAttr": "None",
+ "StatsWarnBg": "Red",
+ "StatsWarnFg": "White",
+ "TextAttr": "None",
+ "TextBg": "Black",
+ "TextFg": "White"
+ },
+ "Client": {
+ "DelimiterAttr": "Dim",
+ "DelimiterBg": "Yellow",
+ "DelimiterFg": "Black",
+ "ClientAttr": "Dim",
+ "ClientBg": "Yellow",
+ "ClientFg": "Black",
+ "HostnameAttr": "Dim",
+ "HostnameBg": "Yellow",
+ "HostnameFg": "Black",
+ "TextAttr": "None",
+ "TextBg": "Black",
+ "TextFg": "White"
+ },
+ "Server": {
+ "DelimiterAttr": "AttrDim",
+ "DelimiterBg": "BgCyan",
+ "DelimiterFg": "FgBlack",
+ "ServerAttr": "AttrDim",
+ "ServerBg": "BgCyan",
+ "ServerFg": "FgBlack",
+ "HostnameAttr": "AttrBold",
+ "HostnameBg": "BgCyan",
+ "HostnameFg": "FgBlack",
+ "TextAttr": "AttrNone",
+ "TextBg": "BgBlack",
+ "TextFg": "FgWhite"
+ },
+ "Common": {
+ "SeverityErrorAttr": "AttrBold",
+ "SeverityErrorBg": "BgRed",
+ "SeverityErrorFg": "FgWhite",
+ "SeverityFatalAttr": "AttrBold",
+ "SeverityFatalBg": "BgMagenta",
+ "SeverityFatalFg": "FgWhite",
+ "SeverityWarnAttr": "AttrBold",
+ "SeverityWarnBg": "BgBlack",
+ "SeverityWarnFg": "FgWhite"
+ },
+ "MaprTable": {
+ "DataAttr": "AttrNone",
+ "DataBg": "BgBlue",
+ "DataFg": "FgWhite",
+ "DelimiterAttr": "AttrDim",
+ "DelimiterBg": "BgBlue",
+ "DelimiterFg": "FgWhite",
+ "HeaderAttr": "AttrBold",
+ "HeaderBg": "BgBlue",
+ "HeaderFg": "FgWhite",
+ "HeaderDelimiterAttr": "AttrDim",
+ "HeaderDelimiterBg": "BgBlue",
+ "HeaderDelimiterFg": "FgWhite",
+ "HeaderSortKeyAttr": "AttrUnderline",
+ "HeaderGroupKeyAttr": "AttrReverse",
+ "RawQueryAttr": "AttrDim",
+ "RawQueryBg": "BgBlack",
+ "RawQueryFg": "FgCyan"
+ }
+ }
+ },
+ "Server": {
+ "SSHBindAddress": "0.0.0.0",
+ "HostKeyFile": "/var/run/dserver/cache/ssh_host_key",
+ "HostKeyBits": 2048,
+ "MapreduceLogFormat": "default",
+ "MaxConcurrentCats": 2,
+ "MaxConcurrentTails": 50,
+ "MaxConnections": 50,
+ "MaxLineLength": 1048576,
+ "Permissions": {
+ "Default": [
+ "readfiles:^/.*$"
+ ],
+ "Users": {
+ "paul": [
+ "readfiles:^/.*$"
+ ],
+ "pbuetow": [
+ "readfiles:^/.*$"
+ ],
+ "jamesblake": [
+ "readfiles:^/tmp/foo.log$",
+ "readfiles:^/.*$",
+ "readfiles:!^/tmp/bar.log$"
+ ]
+ }
+ }
+ },
+ "Common": {
+ "LogDir": "/var/log/dserver",
+ "Logger": "Fout",
+ "LogRotation": "Daily",
+ "CacheDir": "/var/run/dserver/cache",
+ "SSHPort": 2222,
+ "LogLevel": "Info"
+ }
+}
diff --git a/frontends/etc/rc.d/dserver-freebsd.tpl b/frontends/etc/rc.d/dserver-freebsd.tpl
new file mode 100644
index 0000000..6110c0c
--- /dev/null
+++ b/frontends/etc/rc.d/dserver-freebsd.tpl
@@ -0,0 +1,30 @@
+#!/bin/sh
+#
+# PROVIDE: dserver
+# REQUIRE: LOGIN
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="dserver"
+rcvar="dserver_enable"
+desc="DTail distributed log server"
+
+# Use daemon(8) explicitly: rc.subr's dserver_user wraps with su -m but does
+# not fork, so the service command blocks. daemon(8) -u runs as the target
+# user and properly detaches from the terminal.
+procname="/usr/local/bin/dserver"
+command="/usr/sbin/daemon"
+command_args="-u dserver -o /var/log/dserver/dserver.log -- /usr/local/bin/dserver -cfg /usr/local/etc/dserver/dtail.json"
+
+start_precmd="dserver_precmd"
+
+dserver_precmd()
+{
+ install -d -o dserver -m 0755 /var/log/dserver
+ install -d -o dserver -m 0755 /var/run/dserver
+ install -d -o dserver -m 0755 /var/run/dserver/cache
+}
+
+load_rc_config $name
+run_rc_command "$1"
diff --git a/frontends/scripts/dserver-update-key-cache-freebsd.sh.tpl b/frontends/scripts/dserver-update-key-cache-freebsd.sh.tpl
new file mode 100644
index 0000000..22173d7
--- /dev/null
+++ b/frontends/scripts/dserver-update-key-cache-freebsd.sh.tpl
@@ -0,0 +1,33 @@
+#!/bin/sh
+# Refresh the dserver SSH key cache from user authorized_keys files.
+# Called by /usr/local/etc/periodic/daily/200.dserver-update-key-cache.
+
+CACHEDIR=/var/run/dserver/cache
+DSERVER_USER=dserver
+DSERVER_GROUP=dserver
+
+echo 'Updating SSH key cache'
+
+ls /home/ | while read remoteuser; do
+ keysfile="/home/$remoteuser/.ssh/authorized_keys"
+
+ if [ -f "$keysfile" ]; then
+ cachefile="$CACHEDIR/$remoteuser.authorized_keys"
+ echo "Caching $keysfile -> $cachefile"
+
+ cp "$keysfile" "$cachefile"
+ chown "$DSERVER_USER:$DSERVER_GROUP" "$cachefile"
+ chmod 600 "$cachefile"
+ fi
+done
+
+# Remove stale cache entries for users whose authorized_keys no longer exist
+find "$CACHEDIR" -name '*.authorized_keys' -type f | while read cachefile; do
+ remoteuser=$(basename "$cachefile" .authorized_keys)
+ if [ ! -f "/home/$remoteuser/.ssh/authorized_keys" ]; then
+ echo "Deleting obsolete cache file $cachefile"
+ rm "$cachefile"
+ fi
+done
+
+echo 'All set...'
diff --git a/packages/files/dtail-rocky/dserver-update-key-cache.sh b/packages/files/dtail-rocky/dserver-update-key-cache.sh
new file mode 100755
index 0000000..831f5be
--- /dev/null
+++ b/packages/files/dtail-rocky/dserver-update-key-cache.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+declare -r CACHEDIR=/var/run/dserver/cache
+declare -r DSERVER_USER=dserver
+declare -r DSERVER_GROUP=dserver
+
+cache_keys() {
+ local remoteuser=$1
+ local home_dir=$2
+ local keysfile=$home_dir/.ssh/authorized_keys
+ local cachefile=$CACHEDIR/$remoteuser.authorized_keys
+
+ if [[ -f "$keysfile" ]]; then
+ echo "Caching $keysfile -> $cachefile"
+ cp "$keysfile" "$cachefile"
+ chown "$DSERVER_USER:$DSERVER_GROUP" "$cachefile"
+ chmod 600 "$cachefile"
+ fi
+}
+
+expected_key_path() {
+ local remoteuser=$1
+
+ if [[ "$remoteuser" == "root" ]]; then
+ printf '%s\n' /root/.ssh/authorized_keys
+ return
+ fi
+
+ printf '/home/%s/.ssh/authorized_keys\n' "$remoteuser"
+}
+
+echo "Updating SSH key cache"
+
+mkdir -p "$CACHEDIR"
+
+cache_keys root /root
+
+while IFS= read -r remoteuser; do
+ cache_keys "$remoteuser" "/home/$remoteuser"
+done < <(find /home -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
+
+find "$CACHEDIR" -name '*.authorized_keys' -type f | while read -r cachefile; do
+ remoteuser=$(basename "$cachefile" | cut -d. -f1)
+ keysfile=$(expected_key_path "$remoteuser")
+
+ if [[ ! -f "$keysfile" ]]; then
+ echo "Deleting obsolete cache file $cachefile"
+ rm -f "$cachefile"
+ fi
+done
+
+echo "All set..."
diff --git a/packages/files/dtail-rocky/dserver-update-keycache.service b/packages/files/dtail-rocky/dserver-update-keycache.service
new file mode 100644
index 0000000..dddab12
--- /dev/null
+++ b/packages/files/dtail-rocky/dserver-update-keycache.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Refresh DServer SSH key cache
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/dserver-update-key-cache.sh
diff --git a/packages/files/dtail-rocky/dserver-update-keycache.timer b/packages/files/dtail-rocky/dserver-update-keycache.timer
new file mode 100644
index 0000000..339011d
--- /dev/null
+++ b/packages/files/dtail-rocky/dserver-update-keycache.timer
@@ -0,0 +1,11 @@
+[Unit]
+Description=Refresh DServer SSH key cache every 30 minutes
+
+[Timer]
+OnBootSec=2m
+OnCalendar=*:0/30
+Persistent=true
+Unit=dserver-update-keycache.service
+
+[Install]
+WantedBy=timers.target
diff --git a/packages/files/dtail-rocky/dserver.service b/packages/files/dtail-rocky/dserver.service
new file mode 100644
index 0000000..f43a5ce
--- /dev/null
+++ b/packages/files/dtail-rocky/dserver.service
@@ -0,0 +1,22 @@
+[Unit]
+Description=DTail server
+After=network.target
+
+[Service]
+Slice=dserver.slice
+User=dserver
+Group=dserver
+ExecStart=/usr/local/bin/dserver -cfg /etc/dserver/dtail.json
+WorkingDirectory=/var/run/dserver
+RuntimeDirectory=dserver
+RuntimeDirectoryMode=0755
+ExecStartPre=/usr/bin/mkdir -p /var/run/dserver/cache /var/run/dserver/log
+NoNewPrivileges=true
+PrivateDevices=true
+PrivateTmp=true
+CPUAccounting=true
+MemoryAccounting=true
+BlockIOAccounting=true
+
+[Install]
+WantedBy=multi-user.target
diff --git a/packages/files/dtail-rocky/dtail.json b/packages/files/dtail-rocky/dtail.json
new file mode 100644
index 0000000..eaa0a39
--- /dev/null
+++ b/packages/files/dtail-rocky/dtail.json
@@ -0,0 +1,131 @@
+{
+ "Client": {
+ "TermColorsEnable": true,
+ "TermColors": {
+ "Remote": {
+ "DelimiterAttr": "Dim",
+ "DelimiterBg": "Blue",
+ "DelimiterFg": "Cyan",
+ "RemoteAttr": "Dim",
+ "RemoteBg": "Blue",
+ "RemoteFg": "White",
+ "CountAttr": "Dim",
+ "CountBg": "Blue",
+ "CountFg": "White",
+ "HostnameAttr": "Bold",
+ "HostnameBg": "Blue",
+ "HostnameFg": "White",
+ "IDAttr": "Dim",
+ "IDBg": "Blue",
+ "IDFg": "White",
+ "StatsOkAttr": "None",
+ "StatsOkBg": "Green",
+ "StatsOkFg": "Black",
+ "StatsWarnAttr": "None",
+ "StatsWarnBg": "Red",
+ "StatsWarnFg": "White",
+ "TextAttr": "None",
+ "TextBg": "Black",
+ "TextFg": "White"
+ },
+ "Client": {
+ "DelimiterAttr": "Dim",
+ "DelimiterBg": "Yellow",
+ "DelimiterFg": "Black",
+ "ClientAttr": "Dim",
+ "ClientBg": "Yellow",
+ "ClientFg": "Black",
+ "HostnameAttr": "Dim",
+ "HostnameBg": "Yellow",
+ "HostnameFg": "Black",
+ "TextAttr": "None",
+ "TextBg": "Black",
+ "TextFg": "White"
+ },
+ "Server": {
+ "DelimiterAttr": "AttrDim",
+ "DelimiterBg": "BgCyan",
+ "DelimiterFg": "FgBlack",
+ "ServerAttr": "AttrDim",
+ "ServerBg": "BgCyan",
+ "ServerFg": "FgBlack",
+ "HostnameAttr": "AttrBold",
+ "HostnameBg": "BgCyan",
+ "HostnameFg": "FgBlack",
+ "TextAttr": "AttrNone",
+ "TextBg": "BgBlack",
+ "TextFg": "FgWhite"
+ },
+ "Common": {
+ "SeverityErrorAttr": "AttrBold",
+ "SeverityErrorBg": "BgRed",
+ "SeverityErrorFg": "FgWhite",
+ "SeverityFatalAttr": "AttrBold",
+ "SeverityFatalBg": "BgMagenta",
+ "SeverityFatalFg": "FgWhite",
+ "SeverityWarnAttr": "AttrBold",
+ "SeverityWarnBg": "BgBlack",
+ "SeverityWarnFg": "FgWhite"
+ },
+ "MaprTable": {
+ "DataAttr": "AttrNone",
+ "DataBg": "BgBlue",
+ "DataFg": "FgWhite",
+ "DelimiterAttr": "AttrDim",
+ "DelimiterBg": "BgBlue",
+ "DelimiterFg": "FgWhite",
+ "HeaderAttr": "AttrBold",
+ "HeaderBg": "BgBlue",
+ "HeaderFg": "FgWhite",
+ "HeaderDelimiterAttr": "AttrDim",
+ "HeaderDelimiterBg": "BgBlue",
+ "HeaderDelimiterFg": "FgWhite",
+ "HeaderSortKeyAttr": "AttrUnderline",
+ "HeaderGroupKeyAttr": "AttrReverse",
+ "RawQueryAttr": "AttrDim",
+ "RawQueryBg": "BgBlack",
+ "RawQueryFg": "FgCyan"
+ }
+ }
+ },
+ "Server": {
+ "SSHBindAddress": "0.0.0.0",
+ "HostKeyFile": "cache/ssh_host_key",
+ "HostKeyBits": 2048,
+ "MapreduceLogFormat": "default",
+ "MaxConcurrentCats": 2,
+ "MaxConcurrentTails": 50,
+ "MaxConnections": 50,
+ "MaxLineLength": 1048576,
+ "TurboBoostDisable": false,
+ "Permissions": {
+ "Default": [
+ "readfiles:^/.*$"
+ ],
+ "Users": {
+ "paul": [
+ "readfiles:^/.*$"
+ ],
+ "pbuetow": [
+ "readfiles:^/.*$"
+ ],
+ "jamesblake": [
+ "readfiles:^/tmp/foo.log$",
+ "readfiles:^/.*$",
+ "readfiles:!^/tmp/bar.log$"
+ ],
+ "root": [
+ "readfiles:^/.*$"
+ ]
+ }
+ }
+ },
+ "Common": {
+ "LogDir": "log",
+ "Logger": "Fout",
+ "LogRotation": "Daily",
+ "CacheDir": "cache",
+ "SSHPort": 2222,
+ "LogLevel": "Info"
+ }
+}
diff --git a/packages/scripts/pkg-dtail-freebsd.sh b/packages/scripts/pkg-dtail-freebsd.sh
new file mode 100644
index 0000000..1d42aaf
--- /dev/null
+++ b/packages/scripts/pkg-dtail-freebsd.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+# Build a FreeBSD dtail package from pre-compiled binaries and upload to the repo PV.
+# Run on f0 via SSH. Called by the Makefile.
+#
+# Arguments:
+# $1 — version (e.g. 4.3.2-ng)
+# $2 — PV destination (e.g. /data/nfs/k3svolumes/pkgrepo/freebsd/FreeBSD:15:amd64/latest)
+
+set -e
+
+VERSION="$1"
+PV_DEST="$2"
+NAME="dtail"
+COMMENT="Distributed log tail and grep tool"
+DESC="DTail is a distributed DevOps tool for tailing, grepping, catting, and mapping across many remote machines at once via SSH."
+MAINTAINER="paul@buetow.org"
+WWW="https://codeberg.org/snonux/dtail"
+
+WORKDIR="/tmp/${NAME}-freebsd-pkg"
+rm -rf "$WORKDIR"
+mkdir -p \
+ "$WORKDIR/stage/usr/local/bin" \
+ "$WORKDIR/stage/usr/local/etc/rc.d" \
+ "$WORKDIR/stage/usr/local/etc/dserver" \
+ "$WORKDIR/out/All"
+
+# Binaries (cross-compiled linux→freebsd with nozstd; .zst logs not supported)
+for bin in dserver dcat dgrep dmap dtail dtailhealth; do
+ cp "/tmp/dtail-freebsd-binaries/${bin}" "$WORKDIR/stage/usr/local/bin/${bin}"
+ chmod 755 "$WORKDIR/stage/usr/local/bin/${bin}"
+done
+
+# Key cache helper (sh-compatible; walks /home/ on FreeBSD)
+cp "/tmp/dtail-freebsd-binaries/dserver-update-key-cache.sh" \
+ "$WORKDIR/stage/usr/local/bin/dserver-update-key-cache.sh"
+chmod 555 "$WORKDIR/stage/usr/local/bin/dserver-update-key-cache.sh"
+
+# rc.d script (FreeBSD rc.subr style; config at /usr/local/etc/dserver/dtail.json)
+cp "/tmp/dtail-freebsd-binaries/dserver.rc" \
+ "$WORKDIR/stage/usr/local/etc/rc.d/dserver"
+chmod 555 "$WORKDIR/stage/usr/local/etc/rc.d/dserver"
+
+# Config
+cp "/tmp/dtail-freebsd-binaries/dtail.json" \
+ "$WORKDIR/stage/usr/local/etc/dserver/dtail.json"
+chmod 644 "$WORKDIR/stage/usr/local/etc/dserver/dtail.json"
+
+# Packing list — paths relative to /usr/local prefix
+cat > "$WORKDIR/plist" <<'PLIST'
+bin/dserver
+bin/dcat
+bin/dgrep
+bin/dmap
+bin/dtail
+bin/dtailhealth
+bin/dserver-update-key-cache.sh
+etc/rc.d/dserver
+etc/dserver/dtail.json
+PLIST
+
+# Package manifest (UCL)
+cat > "$WORKDIR/+MANIFEST" <<MANIFEST
+name: "${NAME}"
+version: "${VERSION}"
+origin: "local/${NAME}"
+comment: "${COMMENT}"
+desc: "${DESC}"
+maintainer: "${MAINTAINER}"
+www: "${WWW}"
+prefix: /usr/local
+MANIFEST
+
+# Build package, regenerate repo metadata, copy to PV
+doas pkg create -M "$WORKDIR/+MANIFEST" -p "$WORKDIR/plist" -r "$WORKDIR/stage" -o "$WORKDIR/out/All"
+doas pkg repo "$WORKDIR/out"
+doas cp -Rf "$WORKDIR/out/"* "$PV_DEST/"
+
+rm -rf "$WORKDIR" /tmp/dtail-freebsd-binaries /tmp/pkg-dtail-freebsd.sh
+echo "FreeBSD package ${NAME}-${VERSION} uploaded to repo"
diff --git a/packages/scripts/pkg-dtail-rpm.sh b/packages/scripts/pkg-dtail-rpm.sh
new file mode 100755
index 0000000..b654933
--- /dev/null
+++ b/packages/scripts/pkg-dtail-rpm.sh
@@ -0,0 +1,140 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if [[ $# -ne 5 ]]; then
+ echo "usage: $0 <x86_64|aarch64> <version> <dtail-src> <asset-dir> <out-dir>" >&2
+ exit 1
+fi
+
+rpm_arch=$1
+raw_version=$2
+dtail_src=$3
+asset_dir=$4
+out_dir=$5
+
+case "$rpm_arch" in
+ x86_64)
+ goarch=amd64
+ ;;
+ aarch64)
+ goarch=arm64
+ ;;
+ *)
+ echo "unsupported rpm arch: $rpm_arch" >&2
+ exit 1
+ ;;
+esac
+
+rpm_version=${raw_version%%-*}
+rpm_release=1
+if [[ "$raw_version" == *-* ]]; then
+ suffix=${raw_version#"$rpm_version"-}
+ suffix=${suffix//[^A-Za-z0-9.+~]/.}
+ rpm_release="1.${suffix}"
+fi
+
+workdir=$(mktemp -d)
+trap 'rm -rf "$workdir"' EXIT
+
+pkgroot="$workdir/root"
+topdir="$workdir/rpmbuild"
+mkdir -p \
+ "$pkgroot/usr/local/bin" \
+ "$pkgroot/etc/dserver" \
+ "$pkgroot/usr/lib/systemd/system" \
+ "$pkgroot/usr/share/licenses/dtail" \
+ "$topdir/BUILD" \
+ "$topdir/BUILDROOT" \
+ "$topdir/RPMS" \
+ "$topdir/SOURCES" \
+ "$topdir/SPECS" \
+ "$topdir/SRPMS"
+
+if [[ -n "${DTAIL_PREBUILT_ROOT:-}" ]]; then
+ cp -a "$DTAIL_PREBUILT_ROOT/." "$pkgroot/"
+else
+ for bin in dserver dcat dgrep dmap dtail dtailhealth; do
+ (
+ cd "$dtail_src"
+ CGO_ENABLED=0 GOOS=linux GOARCH="$goarch" go build -tags nozstd -o "$pkgroot/usr/local/bin/$bin" "./cmd/$bin/main.go"
+ )
+ done
+
+ install -m 0644 "$asset_dir/dtail.json" "$pkgroot/etc/dserver/dtail.json"
+ install -m 0755 "$asset_dir/dserver-update-key-cache.sh" "$pkgroot/usr/local/bin/dserver-update-key-cache.sh"
+ install -m 0644 "$asset_dir/dserver.service" "$pkgroot/usr/lib/systemd/system/dserver.service"
+ install -m 0644 "$asset_dir/dserver-update-keycache.service" "$pkgroot/usr/lib/systemd/system/dserver-update-keycache.service"
+ install -m 0644 "$asset_dir/dserver-update-keycache.timer" "$pkgroot/usr/lib/systemd/system/dserver-update-keycache.timer"
+ install -m 0644 "$dtail_src/LICENSE" "$pkgroot/usr/share/licenses/dtail/LICENSE"
+fi
+
+cp -a "$pkgroot" "$topdir/SOURCES/root"
+
+cat >"$topdir/SPECS/dtail.spec" <<EOF
+Name: dtail
+Version: $rpm_version
+Release: $rpm_release
+Summary: Distributed log tail and grep tool
+License: Apache-2.0
+URL: https://codeberg.org/snonux/dtail
+BuildArch: $rpm_arch
+Requires(pre): shadow-utils
+Requires(post): systemd
+Requires(preun): systemd
+Requires(postun): systemd
+
+%description
+DTail is a distributed DevOps tool for tailing, grepping, catting, and
+mapping across many remote machines at once via SSH.
+
+%prep
+
+%build
+
+%install
+mkdir -p %{buildroot}
+cp -a %{_sourcedir}/root/. %{buildroot}/
+
+%pre
+getent group dserver >/dev/null || groupadd -r dserver
+getent passwd dserver >/dev/null || useradd -r -g dserver -d /var/lib/dserver -s /sbin/nologin -c "DTail server" dserver
+exit 0
+
+%post
+systemctl daemon-reload >/dev/null 2>&1 || true
+
+%preun
+if [ \$1 -eq 0 ]; then
+ systemctl --no-reload disable --now dserver-update-keycache.timer >/dev/null 2>&1 || true
+fi
+
+%postun
+systemctl daemon-reload >/dev/null 2>&1 || true
+
+%files
+%license /usr/share/licenses/dtail/LICENSE
+%dir /etc/dserver
+%config(noreplace) /etc/dserver/dtail.json
+/usr/local/bin/dserver
+/usr/local/bin/dcat
+/usr/local/bin/dgrep
+/usr/local/bin/dmap
+/usr/local/bin/dtail
+/usr/local/bin/dtailhealth
+/usr/local/bin/dserver-update-key-cache.sh
+/usr/lib/systemd/system/dserver.service
+/usr/lib/systemd/system/dserver-update-keycache.service
+/usr/lib/systemd/system/dserver-update-keycache.timer
+
+%changelog
+* Sat Apr 11 2026 Paul Buetow <paul@buetow.org> - $rpm_version-$rpm_release
+- Package DTail for Rocky Linux 9
+EOF
+
+mkdir -p "$out_dir"
+rpmbuild --quiet \
+ --define "_topdir $topdir" \
+ --target "$rpm_arch" \
+ -bb "$topdir/SPECS/dtail.spec"
+cp "$topdir/RPMS/$rpm_arch"/*.rpm "$out_dir/"