Add Debian/Ubuntu support via a thin distro dispatch layer
Adds a parallel install path for Debian/Ubuntu hosts alongside the existing
Arch/Omarchy/Hyprland one. The Arch path is untouched at runtime; everything
new is gated on $DISTRO and (for headless) $COMPOSITOR.
Highlights:
- lib/distro.sh: detect_distro + pkg_install/pkg_remove/ca_anchor_path/
ca_update_trust dispatch helpers
- lib/packages.sh: Ubuntu sunshine install pulls LizardByte's official .deb
from GitHub releases (override via SUNSHINE_DEB_URL/SUNSHINE_DEB_VERSION);
GPU encoder packages branch per $DISTRO:$GPU_VENDOR
- bin/sunshine-stream-{do,undo,prestart}-sway.sh + files/sway-headless.*:
swaymsg-based headless capture path for hosts without Hyprland. sway runs
under a systemd-user unit that sunshine.service depends on via drop-in.
- lib/preflight.sh: clearer NVIDIA driver guidance on Ubuntu (we don't install
the driver - too many branch/kernel/Secure-Boot variants); sway-aware
headless preflight
- lib/certs.sh + lib/verify.sh + uninstall.sh: distro-aware CA trust anchor
(Arch: /etc/ca-certificates/trust-source/anchors + update-ca-trust;
Debian: /usr/local/share/ca-certificates + update-ca-certificates)
Verified on Ubuntu 24.04: ./install.sh --doctor --headless loads cleanly,
distro/GPU/compositor detection report the right values, all pre-install
failures correspond to the actual missing pieces.
This commit is contained in:
76
bin/sunshine-prestart-sway.sh
Executable file
76
bin/sunshine-prestart-sway.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sway analog of sunshine-prestart.sh. Runs as ExecStartPre for sunshine.service
|
||||
# on Ubuntu/Debian installs that use the Sway-headless path.
|
||||
#
|
||||
# Two jobs:
|
||||
# 1. Confirm sway is reachable; create HEADLESS-1 if it doesn't exist yet.
|
||||
# 2. Sync sunshine.conf's `output_name` to whatever the headless output is
|
||||
# currently named (`create_output` may auto-assign HEADLESS-2, -3, etc.
|
||||
# after a session restart).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
log() { printf '[sunshine-prestart-sway] %s\n' "$*" >&2; }
|
||||
|
||||
CONF="$HOME/.config/sunshine/sunshine.conf"
|
||||
|
||||
if [[ -z "${SWAYSOCK:-}" ]]; then
|
||||
for sock in "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/sway-ipc.*.sock; do
|
||||
[[ -S "$sock" ]] || continue
|
||||
export SWAYSOCK="$sock"
|
||||
break
|
||||
done
|
||||
fi
|
||||
if [[ -z "${SWAYSOCK:-}" ]]; then
|
||||
log "sway not running; nothing to prepare."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v swaymsg >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
log "swaymsg/jq missing; skipping prestart."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Reduce to exactly one HEADLESS-* output.
|
||||
mapfile -t headless_outputs < <(swaymsg -t get_outputs -r 2>/dev/null \
|
||||
| jq -r '.[].name | select(startswith("HEADLESS-"))' \
|
||||
| sort -V)
|
||||
existing="${headless_outputs[0]:-}"
|
||||
|
||||
if [[ -z "$existing" ]]; then
|
||||
log "No headless output present; creating one"
|
||||
swaymsg create_output HEADLESS-1 >/dev/null 2>&1 || swaymsg create_output >/dev/null 2>&1 || true
|
||||
for _ in 1 2 3 4 5; do
|
||||
existing="$(swaymsg -t get_outputs -r 2>/dev/null \
|
||||
| jq -r '.[].name | select(startswith("HEADLESS-"))' \
|
||||
| sort -V | head -1)"
|
||||
[[ -n "$existing" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
elif [[ ${#headless_outputs[@]} -gt 1 ]]; then
|
||||
# Keep the first, log the rest. Removing outputs in sway during prestart can
|
||||
# cascade workspace re-assignment, so we err on the side of leaving them.
|
||||
log "Found ${#headless_outputs[@]} headless outputs; using $existing (extras left in place)"
|
||||
fi
|
||||
|
||||
if [[ -z "$existing" ]]; then
|
||||
log "Failed to obtain a headless output; Sunshine will start without one."
|
||||
exit 0
|
||||
fi
|
||||
log "Headless output present: $existing"
|
||||
|
||||
# Sync sunshine.conf's output_name. Only touch the file if it's our managed
|
||||
# variant (has the management marker) AND the line has actually drifted.
|
||||
if [[ -f "$CONF" ]] && grep -qF '# managed-by: omarchy-moonlight' "$CONF"; then
|
||||
current="$(awk '/^output_name = / {print $3; exit}' "$CONF" 2>/dev/null || true)"
|
||||
if [[ "$current" != "$existing" ]]; then
|
||||
log "Updating sunshine.conf output_name: ${current:-(unset)} -> $existing"
|
||||
if grep -q '^output_name = ' "$CONF"; then
|
||||
sed -i "s|^output_name = .*|output_name = $existing|" "$CONF"
|
||||
else
|
||||
printf '\noutput_name = %s\n' "$existing" >> "$CONF"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
112
bin/sunshine-stream-do-sway.sh
Executable file
112
bin/sunshine-stream-do-sway.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sunshine global_prep_cmd `do` hook for the Sway-based headless capture path
|
||||
# (Debian/Ubuntu installs where Hyprland isn't available).
|
||||
#
|
||||
# On client connect:
|
||||
# - Ensures a HEADLESS-1 output exists on the running sway session
|
||||
# - Resizes it to the client's negotiated mode (WxH@FPS)
|
||||
# - Snapshots state for the undo hook
|
||||
#
|
||||
# Sunshine env vars set on connect:
|
||||
# SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, SUNSHINE_CLIENT_FPS
|
||||
#
|
||||
# This script intentionally mirrors the Hyprland do-hook's shape so debugging
|
||||
# transfers across the two paths.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}"
|
||||
HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}"
|
||||
FPS="${SUNSHINE_CLIENT_FPS:-60}"
|
||||
HEADLESS_NAME="${OMARCHY_VIRTUAL_OUTPUT:-HEADLESS-1}"
|
||||
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
HOOK_LOG="$STATE_DIR/hook.log"
|
||||
: > "$HOOK_LOG"
|
||||
log() {
|
||||
local msg
|
||||
msg="$(date +%H:%M:%S.%3N) [sunshine-do-sway] $*"
|
||||
printf '%s\n' "$msg" >&2
|
||||
printf '%s\n' "$msg" >> "$HOOK_LOG"
|
||||
}
|
||||
log "do-hook start: client=${WIDTH}x${HEIGHT}@${FPS} target=${HEADLESS_NAME}"
|
||||
|
||||
if ! command -v swaymsg >/dev/null 2>&1; then
|
||||
log "swaymsg not found; nothing to configure."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Recover SWAYSOCK if the unit env didn't propagate it. sway writes the socket
|
||||
# path into a predictable /run/user/$UID location, but the env var is the
|
||||
# clean handle.
|
||||
if [[ -z "${SWAYSOCK:-}" ]]; then
|
||||
for sock in "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/sway-ipc.*.sock; do
|
||||
[[ -S "$sock" ]] || continue
|
||||
export SWAYSOCK="$sock"
|
||||
log "Discovered SWAYSOCK=$SWAYSOCK"
|
||||
break
|
||||
done
|
||||
if [[ -z "${SWAYSOCK:-}" ]]; then
|
||||
log "sway not running (no IPC socket found); nothing to configure."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# True if an output named $HEADLESS_NAME currently exists on the session.
|
||||
_headless_present() {
|
||||
swaymsg -t get_outputs -r 2>/dev/null \
|
||||
| jq -e --arg n "$HEADLESS_NAME" '.[] | select(.name == $n)' >/dev/null
|
||||
}
|
||||
|
||||
# Create the headless output if missing. Sway's `create_output` accepts an
|
||||
# optional name; without one it auto-assigns HEADLESS-N like Hyprland does.
|
||||
if ! _headless_present; then
|
||||
log "Creating headless output $HEADLESS_NAME"
|
||||
if ! swaymsg create_output "$HEADLESS_NAME" >/dev/null 2>&1; then
|
||||
# Older sway versions ignore the name argument; fall back and rename via
|
||||
# output detection after the fact.
|
||||
swaymsg create_output >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Poll briefly for the output to appear.
|
||||
for _ in 1 2 3 4 5; do
|
||||
_headless_present && break
|
||||
sleep 0.1
|
||||
done
|
||||
if ! _headless_present; then
|
||||
# Last-ditch: take the highest-numbered HEADLESS-* that exists and treat
|
||||
# it as ours. Update HEADLESS_NAME in-memory so the resize below targets it.
|
||||
found="$(swaymsg -t get_outputs -r 2>/dev/null \
|
||||
| jq -r '[.[].name | select(startswith("HEADLESS-"))] | sort_by(.) | last // empty')"
|
||||
if [[ -n "$found" ]]; then
|
||||
HEADLESS_NAME="$found"
|
||||
log "Adopted existing headless output: $HEADLESS_NAME"
|
||||
else
|
||||
log "Failed to create a headless output; stream will rely on whatever Sunshine selects."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Snapshot state so undo can put things back. We don't move workspaces around
|
||||
# on a headless-only box (there is no other monitor), but we still record what
|
||||
# was active in case the user runs sway with a real display attached.
|
||||
swaymsg -t get_outputs -r > "$STATE_DIR/prev-outputs.json" 2>/dev/null || true
|
||||
echo "$HEADLESS_NAME" > "$STATE_DIR/headless-name"
|
||||
|
||||
# Resize the headless output. Sway accepts mode strings as "WIDTHxHEIGHT@FPSHz".
|
||||
log "Sizing $HEADLESS_NAME → ${WIDTH}x${HEIGHT}@${FPS}Hz"
|
||||
if ! swaymsg output "$HEADLESS_NAME" mode "${WIDTH}x${HEIGHT}@${FPS}Hz" >/dev/null 2>&1; then
|
||||
log "Mode set with refresh rate failed; retrying without refresh"
|
||||
swaymsg output "$HEADLESS_NAME" mode "${WIDTH}x${HEIGHT}" >/dev/null 2>&1 || \
|
||||
log "Mode set failed; sway will keep the previous mode."
|
||||
fi
|
||||
|
||||
# Focus the headless output so window placement lands there.
|
||||
swaymsg focus output "$HEADLESS_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
post="$(swaymsg -t get_outputs -r 2>/dev/null \
|
||||
| jq -r '.[] | "\(.name) \(.current_mode.width)x\(.current_mode.height)@\(.current_mode.refresh) focused=\(.focused)"' \
|
||||
| tr '\n' ';' || true)"
|
||||
log "post-state outputs: $post"
|
||||
log "Stream ready: ${WIDTH}x${HEIGHT}@${FPS} on $HEADLESS_NAME"
|
||||
50
bin/sunshine-stream-undo-sway.sh
Executable file
50
bin/sunshine-stream-undo-sway.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sunshine global_prep_cmd `undo` hook for the Sway-based headless path.
|
||||
# Cheaper than the Hyprland undo: there's no workspace shuffling to reverse on
|
||||
# a true headless box. We just keep the headless output alive for the next
|
||||
# connect (creating one is ~free, removing it forces sway to renegotiate
|
||||
# focused output every cycle).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
HEADLESS_NAME="${OMARCHY_VIRTUAL_OUTPUT:-HEADLESS-1}"
|
||||
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
|
||||
HOOK_LOG="$STATE_DIR/hook.log"
|
||||
log() {
|
||||
local msg
|
||||
msg="$(date +%H:%M:%S.%3N) [sunshine-undo-sway] $*"
|
||||
printf '%s\n' "$msg" >&2
|
||||
printf '%s\n' "$msg" >> "$HOOK_LOG" 2>/dev/null || true
|
||||
}
|
||||
log "undo-hook start"
|
||||
|
||||
if ! command -v swaymsg >/dev/null 2>&1; then
|
||||
log "swaymsg not found; nothing to undo."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "${SWAYSOCK:-}" ]]; then
|
||||
for sock in "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/sway-ipc.*.sock; do
|
||||
[[ -S "$sock" ]] || continue
|
||||
export SWAYSOCK="$sock"
|
||||
break
|
||||
done
|
||||
if [[ -z "${SWAYSOCK:-}" ]]; then
|
||||
log "sway not running; nothing to undo."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remember the headless name across the connect cycle if we adopted a
|
||||
# different output name in the do-hook.
|
||||
if [[ -f "$STATE_DIR/headless-name" ]]; then
|
||||
HEADLESS_NAME="$(cat "$STATE_DIR/headless-name" 2>/dev/null || echo "$HEADLESS_NAME")"
|
||||
fi
|
||||
|
||||
# On a server with no real outputs, removing HEADLESS-1 leaves sway with zero
|
||||
# outputs and any future create_output starts numbering at -2, -3, etc.
|
||||
# Cheaper to keep it alive.
|
||||
log "Keeping $HEADLESS_NAME alive for the next stream"
|
||||
|
||||
rm -f "$STATE_DIR/prev-outputs.json" "$STATE_DIR/headless-name"
|
||||
log "Stream teardown complete"
|
||||
Reference in New Issue
Block a user