diff --git a/bin/sunshine-prestart-sway.sh b/bin/sunshine-prestart-sway.sh new file mode 100755 index 0000000..b691981 --- /dev/null +++ b/bin/sunshine-prestart-sway.sh @@ -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 diff --git a/bin/sunshine-stream-do-sway.sh b/bin/sunshine-stream-do-sway.sh new file mode 100755 index 0000000..f8ba559 --- /dev/null +++ b/bin/sunshine-stream-do-sway.sh @@ -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" diff --git a/bin/sunshine-stream-undo-sway.sh b/bin/sunshine-stream-undo-sway.sh new file mode 100755 index 0000000..f504de1 --- /dev/null +++ b/bin/sunshine-stream-undo-sway.sh @@ -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" diff --git a/files/sway-headless.config b/files/sway-headless.config new file mode 100644 index 0000000..f3bde01 --- /dev/null +++ b/files/sway-headless.config @@ -0,0 +1,30 @@ +# Minimal sway config for a headless Sunshine host. Installed by +# omarchy-moonlight as ~/.config/sway/config-headless. Loaded by the +# sway-headless.service systemd-user unit on Debian/Ubuntu installs. +# +# Goals: +# - Boot sway with a single headless output named HEADLESS-1 so Sunshine's +# wlr capture has a stable target. +# - No keybindings, no bars, no animations. There's no human at the console. +# - The sunshine-stream-do-sway.sh hook adjusts HEADLESS-1's mode per client +# connect; this config is just the boot-time baseline. + +# Create the headless output at startup. Sway accepts `output HEADLESS-1 +# enable` only after the output exists, so we issue create_output here. +exec swaymsg create_output HEADLESS-1 + +# Default mode — overridden per-connect by the do-hook. +output HEADLESS-1 mode 1920x1080@60Hz +output HEADLESS-1 background #1a1a1a solid_color + +# No idle locking on a headless box; no XWayland (would just waste DRM resources). +xwayland disable + +# Don't enable animations / focus-follow / etc — there's no user input here. +focus_follows_mouse no +default_border none +default_floating_border none + +# A minimal placeholder so sway has something to display. The actual stream +# content lives in whatever app the user launches via sunshine commands. +exec --no-startup-id true diff --git a/files/sway-headless.service b/files/sway-headless.service new file mode 100644 index 0000000..0dbfb8b --- /dev/null +++ b/files/sway-headless.service @@ -0,0 +1,27 @@ +[Unit] +Description=Headless Sway compositor for Sunshine wlr capture +# Don't auto-restart on `systemctl --user stop` — but bring sway back if it +# actually crashes mid-stream. +After=graphical-session-pre.target +PartOf=graphical-session.target + +[Service] +Type=simple + +# Tell wlroots to use the headless backend (no DRM master needed) and skip +# libinput device probing — there are no input devices on a real headless box. +# The Vulkan renderer is preferred on NVIDIA + handles NVENC capture cleanly; +# wlroots falls back to GLES2 automatically if Vulkan isn't usable. +Environment=WLR_BACKENDS=headless +Environment=WLR_LIBINPUT_NO_DEVICES=1 +Environment=WLR_RENDERER=vulkan +Environment=XDG_SESSION_TYPE=wayland + +ExecStart=/usr/bin/sway --config %h/.config/sway/config-headless --unsupported-gpu + +Restart=on-failure +RestartSec=2s +TimeoutStopSec=5s + +[Install] +WantedBy=default.target diff --git a/install.sh b/install.sh index b89193f..edac113 100755 --- a/install.sh +++ b/install.sh @@ -8,6 +8,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" +# shellcheck source=lib/distro.sh +source "$SCRIPT_DIR/lib/distro.sh" # shellcheck source=lib/detect.sh source "$SCRIPT_DIR/lib/detect.sh" # shellcheck source=lib/preflight.sh @@ -142,14 +144,14 @@ setup_install_log() { main() { setup_install_log require_not_root - require_arch - require_yay + require_supported_distro step "Detecting system" detect_all compute_stream_mode export STREAM_MODE - info "Host: $HOSTNAME_SHORT GPU: $GPU_VENDOR Session: $SESSION_TYPE" + info "Distro: $DISTRO ($DISTRO_ID ${DISTRO_VERSION:-}) Host: $HOSTNAME_SHORT" + info "GPU: $GPU_VENDOR Session: $SESSION_TYPE Compositor: $COMPOSITOR" info "Mode: $STREAM_MODE" if [[ $DOCTOR_ONLY -eq 1 ]]; then @@ -160,6 +162,17 @@ main() { step "Preflight checks" preflight_all + # On headless mode, make sure we have a compositor we can drive. On Arch + # this is typically Hyprland (already on Omarchy); on Ubuntu we install Sway + # and switch COMPOSITOR before installing hooks. + if [[ "$STREAM_MODE" == "headless" && "$COMPOSITOR" == "none" ]]; then + step "Installing headless compositor" + install_headless_compositor + COMPOSITOR="$(detect_compositor)" + export COMPOSITOR + info "Compositor (after install): $COMPOSITOR" + fi + if [[ $INSTALL_SUNSHINE -eq 1 ]]; then step "Installing Sunshine and GPU encoder support" install_sunshine diff --git a/lib/certs.sh b/lib/certs.sh index b60a033..162ccbb 100644 --- a/lib/certs.sh +++ b/lib/certs.sh @@ -21,14 +21,21 @@ SUNSHINE_CRED_DIR="$HOME/.config/sunshine/credentials" SUNSHINE_CERT="$SUNSHINE_CRED_DIR/cacert.pem" SUNSHINE_KEY="$SUNSHINE_CRED_DIR/cakey.pem" -SYSTEM_TRUST_ANCHOR="/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem" +# Resolved per-distro via lib/distro.sh: +# Arch: /etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem +# Debian: /usr/local/share/ca-certificates/omarchy-stream-ca.crt +# Read it via ca_anchor_path; do not hard-code here. # --- 1Password helpers ---------------------------------------------------- op_require_signin() { if ! command -v op >/dev/null 2>&1; then err "1Password CLI ('op') not found on PATH." - err "Install it: yay -S 1password-cli" + case "$DISTRO" in + arch) err "Install it: yay -S 1password-cli" ;; + debian) err "Install it: https://developer.1password.com/docs/cli/get-started/ (apt repo or .deb)" ;; + *) err "Install the 1Password CLI from https://developer.1password.com/docs/cli/get-started/" ;; + esac return 1 fi if ! op whoami >/dev/null 2>&1; then @@ -124,16 +131,25 @@ EOF install_ca_to_system_trust() { local ca_pem="$1" - # Idempotent: compare sha256 first to avoid pointless update-ca-trust runs. - if [[ -f "$SYSTEM_TRUST_ANCHOR" ]] \ - && cmp -s "$ca_pem" "$SYSTEM_TRUST_ANCHOR"; then + local anchor + anchor="$(ca_anchor_path)" + if [[ -z "$anchor" ]]; then + warn "Don't know how to install CA on distro '$DISTRO' — skipping system trust step." + return 0 + fi + # Idempotent: compare sha256 first to avoid pointless update-ca-* runs. + if [[ -f "$anchor" ]] && cmp -s "$ca_pem" "$anchor"; then ok "CA already in system trust store" return 0 fi - info "Installing CA into $SYSTEM_TRUST_ANCHOR" - as_root install -m 0644 "$ca_pem" "$SYSTEM_TRUST_ANCHOR" - as_root update-ca-trust extract >/dev/null - ok "System trust store refreshed (update-ca-trust)" + # Debian's update-ca-certificates only picks up files under + # /usr/local/share/ca-certificates/ that end in .crt. The path returned by + # ca_anchor_path already accounts for that. + info "Installing CA into $anchor" + as_root mkdir -p "$(dirname "$anchor")" + as_root install -m 0644 "$ca_pem" "$anchor" + ca_update_trust + ok "System trust store refreshed" } # --- Top-level orchestration --------------------------------------------- diff --git a/lib/common.sh b/lib/common.sh index 80c1fd1..df3f01c 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -29,34 +29,26 @@ require_not_root() { fi } -require_arch() { - if [[ ! -f /etc/arch-release ]] && ! grep -q '^ID=arch' /etc/os-release 2>/dev/null; then - err "This script targets Arch Linux (Omarchy). /etc/arch-release not found." - exit 1 - fi -} - -require_yay() { - if ! command -v yay >/dev/null 2>&1; then - err "yay is required to install AUR packages. Install yay first." - exit 1 - fi -} - -# True if package is installed (pacman -Qi). -pkg_installed() { pacman -Qi "$1" >/dev/null 2>&1; } - -# Install one or more packages via yay if any are missing. +# Package management (pkg_installed, pkg_install, etc.) and distro detection +# live in lib/distro.sh. Source it after this file. +# +# yay_install is kept as an alias for code paths that explicitly want AUR +# packages even on a distro-agnostic call site (rare). On non-Arch distros it +# falls back to pkg_install. yay_install() { - local missing=() - local p - for p in "$@"; do - pkg_installed "$p" || missing+=("$p") - done - if [[ ${#missing[@]} -eq 0 ]]; then - ok "Already installed: $*" - return 0 + if [[ "${DISTRO:-}" == "arch" ]] && command -v yay >/dev/null 2>&1; then + local missing=() + local p + for p in "$@"; do + pkg_installed "$p" || missing+=("$p") + done + if [[ ${#missing[@]} -eq 0 ]]; then + ok "Already installed: $*" + return 0 + fi + info "Installing (AUR): ${missing[*]}" + yay -S --needed --noconfirm "${missing[@]}" + else + pkg_install "$@" fi - info "Installing: ${missing[*]}" - yay -S --needed --noconfirm "${missing[@]}" } diff --git a/lib/detect.sh b/lib/detect.sh index 2c6c0b8..5bb5af1 100644 --- a/lib/detect.sh +++ b/lib/detect.sh @@ -1,25 +1,47 @@ #!/usr/bin/env bash -# Detect GPU vendor, session type, hostname. +# Detect GPU vendor, session type, hostname, compositor. detect_gpu_vendor() { local vga vga="$(lspci -nn 2>/dev/null | grep -iE 'vga|3d|display' || true)" - if grep -qi 'nvidia' <<<"$vga"; then + # Prefer the first discrete/dedicated entry — VM hosts often expose a + # placeholder BOCHS/QEMU VGA device before the real GPU. + local nvidia_line amd_line intel_line + nvidia_line="$(grep -i 'nvidia' <<<"$vga" | head -n1)" + amd_line="$(grep -iE 'amd|advanced micro devices|ati' <<<"$vga" | head -n1)" + intel_line="$(grep -i 'intel' <<<"$vga" | head -n1)" + if [[ -n "$nvidia_line" ]]; then echo "nvidia" - elif grep -qiE 'amd|advanced micro devices|ati' <<<"$vga"; then + elif [[ -n "$amd_line" ]]; then echo "amd" - elif grep -qi 'intel' <<<"$vga"; then + elif [[ -n "$intel_line" ]]; then echo "intel" else echo "unknown" fi } +# Decide which wlroots-based compositor to drive for headless capture. +# Returns one of: +# hyprland - hyprctl available (preferred when present, matches existing hooks) +# sway - sway/swaymsg available +# none - neither installed yet (install_headless_compositor will fix this) +detect_compositor() { + if command -v hyprctl >/dev/null 2>&1; then + echo "hyprland" + elif command -v swaymsg >/dev/null 2>&1 || command -v sway >/dev/null 2>&1; then + echo "sway" + else + echo "none" + fi +} + detect_all() { HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)" GPU_VENDOR="$(detect_gpu_vendor)" SESSION_TYPE="${XDG_SESSION_TYPE:-unknown}" - export HOSTNAME_SHORT GPU_VENDOR SESSION_TYPE + COMPOSITOR="$(detect_compositor)" + export HOSTNAME_SHORT GPU_VENDOR SESSION_TYPE COMPOSITOR if [[ "$SESSION_TYPE" != "wayland" ]]; then warn "Session type is '$SESSION_TYPE' (not wayland). KMS capture still works at the TTY/DRM level, but Hyprland-specific paths assume Wayland." diff --git a/lib/distro.sh b/lib/distro.sh new file mode 100644 index 0000000..b510fb8 --- /dev/null +++ b/lib/distro.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Distro detection + small dispatch layer so the rest of the installer can +# stay distro-agnostic. Two backends supported today: Arch (Omarchy) and +# Debian/Ubuntu. +# +# Sourced once at the top of install.sh / uninstall.sh. detect_distro must +# run before any of the dispatch helpers; require_supported_distro calls it +# for you. + +# Populated by detect_distro: +# DISTRO - "arch" | "debian" (ubuntu folds into debian) +# DISTRO_ID - raw ID from /etc/os-release (e.g. "ubuntu", "arch") +# DISTRO_VERSION - VERSION_ID from /etc/os-release (e.g. "24.04"), empty on Arch +detect_distro() { + if [[ -n "${DISTRO:-}" ]]; then + return 0 + fi + + local id="" id_like="" version_id="" + if [[ -r /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + id="${ID:-}" + id_like="${ID_LIKE:-}" + version_id="${VERSION_ID:-}" + fi + + DISTRO_ID="$id" + DISTRO_VERSION="$version_id" + + case "$id" in + arch|manjaro|endeavouros|omarchy) + DISTRO="arch" + ;; + ubuntu|debian|pop|linuxmint) + DISTRO="debian" + ;; + *) + # Fall back to ID_LIKE. + if [[ " $id_like " == *" arch "* ]]; then + DISTRO="arch" + elif [[ " $id_like " == *" debian "* || " $id_like " == *" ubuntu "* ]]; then + DISTRO="debian" + elif [[ -f /etc/arch-release ]]; then + DISTRO="arch" + elif command -v apt-get >/dev/null 2>&1; then + DISTRO="debian" + elif command -v pacman >/dev/null 2>&1; then + DISTRO="arch" + else + DISTRO="unknown" + fi + ;; + esac + export DISTRO DISTRO_ID DISTRO_VERSION +} + +require_supported_distro() { + detect_distro + case "$DISTRO" in + arch) + if ! command -v yay >/dev/null 2>&1; then + err "yay is required to install AUR packages on Arch. Install yay first." + exit 1 + fi + ;; + debian) + if ! command -v apt-get >/dev/null 2>&1; then + err "apt-get not found — Debian/Ubuntu install path requires it." + exit 1 + fi + ;; + *) + err "Unsupported distro (ID='${DISTRO_ID:-unknown}'). Supported: Arch family, Debian/Ubuntu family." + exit 1 + ;; + esac +} + +# --------------------------------------------------------------------------- +# Package query / install dispatch. + +# True if package is installed. +pkg_installed() { + case "$DISTRO" in + arch) pacman -Qi "$1" >/dev/null 2>&1 ;; + debian) dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q '^install ok installed$' ;; + *) return 1 ;; + esac +} + +# Install one or more packages. Idempotent: only installs missing ones. +# On Arch, uses yay; on Debian/Ubuntu, uses apt-get. The Arch-only yay_install +# function below is kept as an alias for code that explicitly wants AUR. +pkg_install() { + local missing=() + local p + for p in "$@"; do + pkg_installed "$p" || missing+=("$p") + done + if [[ ${#missing[@]} -eq 0 ]]; then + ok "Already installed: $*" + return 0 + fi + info "Installing: ${missing[*]}" + case "$DISTRO" in + arch) + yay -S --needed --noconfirm "${missing[@]}" + ;; + debian) + _apt_ensure_updated + as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${missing[@]}" + ;; + *) + err "Don't know how to install packages on distro '$DISTRO'" + return 1 + ;; + esac +} + +# Cache `apt-get update` for this script run; running it on every pkg_install +# call would be slow and noisy. +_APT_UPDATED=0 +_apt_ensure_updated() { + [[ "$_APT_UPDATED" -eq 1 ]] && return 0 + info "Refreshing apt package lists" + as_root env DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null + _APT_UPDATED=1 +} + +# Install a local .deb file. Resolves its dependencies via apt. +deb_install_local() { + local deb_path="$1" + [[ -f "$deb_path" ]] || { err "deb not found: $deb_path"; return 1; } + _apt_ensure_updated + info "Installing $(basename "$deb_path") via apt-get" + as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$deb_path" +} + +# Remove a package if installed. Quiet no-op if absent. +pkg_remove() { + local p + for p in "$@"; do + pkg_installed "$p" || continue + case "$DISTRO" in + arch) as_root pacman -Rns --noconfirm "$p" ;; + debian) as_root env DEBIAN_FRONTEND=noninteractive apt-get purge -y "$p" ;; + esac + done +} + +# --------------------------------------------------------------------------- +# CA trust-store dispatch. +# +# Arch: /etc/ca-certificates/trust-source/anchors/.pem + update-ca-trust +# Debian: /usr/local/share/ca-certificates/.crt + update-ca-certificates + +# Path where we'll drop our CA anchor for this distro. Stable across runs. +ca_anchor_path() { + case "$DISTRO" in + arch) echo "/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem" ;; + debian) echo "/usr/local/share/ca-certificates/omarchy-stream-ca.crt" ;; + *) echo "" ;; + esac +} + +# Refresh the system trust store after writing a new anchor. +ca_update_trust() { + case "$DISTRO" in + arch) as_root update-ca-trust extract >/dev/null ;; + debian) as_root update-ca-certificates >/dev/null ;; + *) warn "Unknown distro — CA trust update skipped"; return 1 ;; + esac +} diff --git a/lib/headless.sh b/lib/headless.sh index 2b8a1e4..5f18f8e 100644 --- a/lib/headless.sh +++ b/lib/headless.sh @@ -3,27 +3,52 @@ # location and ensure they're executable. The actual sunshine.conf entries # (capture=wlr, output_name=HEADLESS-1, global_prep_cmd=[...]) are written # by lib/config.sh. +# +# Two compositor backends supported: +# - hyprland (default on Omarchy/Arch): bin/sunshine-stream-{do,undo}.sh +# - sway (default on Debian/Ubuntu): bin/sunshine-stream-{do,undo}-sway.sh +# detect_compositor (lib/detect.sh) decides which to install. The script names +# at the install target are *always* sunshine-stream-do.sh / -undo.sh, so the +# rest of the installer (config.sh, verify.sh) doesn't have to branch. HEADLESS_BIN_DIR="$HOME/.local/share/omarchy-moonlight/bin" DO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-do.sh" UNDO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-undo.sh" -export DO_SCRIPT UNDO_SCRIPT +PRESTART_SCRIPT="$HEADLESS_BIN_DIR/sunshine-prestart.sh" +export DO_SCRIPT UNDO_SCRIPT PRESTART_SCRIPT + +# Resolve which hook source files to install based on the detected compositor. +_headless_hook_sources() { + case "${COMPOSITOR:-hyprland}" in + sway) + echo "$SCRIPT_DIR/bin/sunshine-stream-do-sway.sh" + echo "$SCRIPT_DIR/bin/sunshine-stream-undo-sway.sh" + echo "$SCRIPT_DIR/bin/sunshine-prestart-sway.sh" + ;; + hyprland|*) + echo "$SCRIPT_DIR/bin/sunshine-stream-do.sh" + echo "$SCRIPT_DIR/bin/sunshine-stream-undo.sh" + echo "$SCRIPT_DIR/bin/sunshine-prestart.sh" + ;; + esac +} install_headless_hooks() { - # Install hook scripts to ~/.local/share so they don't disappear if the - # repo gets moved or deleted. Sunshine's config will reference these stable paths. mkdir -p "$HEADLESS_BIN_DIR" - install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-do.sh" "$DO_SCRIPT" - install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-undo.sh" "$UNDO_SCRIPT" - install -m 0755 "$SCRIPT_DIR/bin/sunshine-prestart.sh" "$HEADLESS_BIN_DIR/sunshine-prestart.sh" - ok "Installed prep-cmd + prestart hooks to $HEADLESS_BIN_DIR" + mapfile -t srcs < <(_headless_hook_sources) + local do_src="${srcs[0]}" undo_src="${srcs[1]}" pre_src="${srcs[2]}" + + install -m 0755 "$do_src" "$DO_SCRIPT" + install -m 0755 "$undo_src" "$UNDO_SCRIPT" + install -m 0755 "$pre_src" "$PRESTART_SCRIPT" + ok "Installed prep-cmd + prestart hooks ($COMPOSITOR) to $HEADLESS_BIN_DIR" } # Install a systemd-user drop-in that pre-creates HEADLESS-1 before Sunshine -# starts, so the encoder probe at startup sees a valid Wayland output. Without -# this, Sunshine reports a fatal "Unable to find display or encoder during -# startup" on every restart, even though streaming works once a client connects. +# starts. Without this, Sunshine reports a fatal "Unable to find display or +# encoder during startup" on every restart, even though streaming works once +# a client connects. install_headless_prestart_dropin() { local dropin_src="$SCRIPT_DIR/files/headless-prestart.conf" if [[ ! -f "$dropin_src" ]]; then @@ -31,8 +56,9 @@ install_headless_prestart_dropin() { return 1 fi - # Resolve the actual unit name. Prefer sunshine.service when present (alias - # or sunshine-bin); fall back to the AUR source pkg's reverse-DNS name. + # Resolve the actual unit name. Prefer sunshine.service when present (alias, + # sunshine-bin, or the .deb on Ubuntu); fall back to the AUR source pkg's + # reverse-DNS name. local unit="" for u in sunshine.service app-dev.lizardbyte.app.Sunshine.service; do if systemctl --user list-unit-files "$u" >/dev/null 2>&1 \ diff --git a/lib/packages.sh b/lib/packages.sh index 5e2bcb6..59ccbf4 100644 --- a/lib/packages.sh +++ b/lib/packages.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash # Install Sunshine, Moonlight, and GPU-specific hardware-encode dependencies. +# Branches by $DISTRO (set by lib/distro.sh). + +# --- Arch defaults -------------------------------------------------------- # Default to the precompiled AUR build for a fast install (~seconds instead of # the ~10 minute source compile). Override with SUNSHINE_PKG=sunshine to build @@ -7,7 +10,24 @@ : "${SUNSHINE_PKG:=sunshine-bin}" : "${MOONLIGHT_PKG:=moonlight-qt}" +# --- Debian/Ubuntu defaults ---------------------------------------------- + +# LizardByte ships official .deb builds per Ubuntu release on GitHub. +# Resolved at runtime by _ubuntu_sunshine_deb_url to match this host. +: "${SUNSHINE_DEB_URL:=}" +: "${SUNSHINE_DEB_VERSION:=latest}" + install_sunshine() { + case "$DISTRO" in + arch) _install_sunshine_arch ;; + debian) _install_sunshine_debian ;; + *) err "install_sunshine: unsupported distro '$DISTRO'"; return 1 ;; + esac +} + +# --- Arch implementation ------------------------------------------------- + +_install_sunshine_arch() { # Ensure runtime deps useful for capture/diagnostics across vendors. yay_install pipewire-pulse vulkan-tools libva-utils jq @@ -59,6 +79,71 @@ install_sunshine() { fi } +# --- Debian/Ubuntu implementation ---------------------------------------- + +# LizardByte's sunshine .deb assets are named per Ubuntu codename / version, +# e.g. sunshine-ubuntu-24.04-amd64.deb. Resolve the right one for this host. +_ubuntu_sunshine_deb_filename() { + local arch + arch="$(dpkg --print-architecture 2>/dev/null || echo amd64)" + local v="${DISTRO_VERSION:-24.04}" + echo "sunshine-ubuntu-${v}-${arch}.deb" +} + +# Resolve the download URL. If SUNSHINE_DEB_URL is set, honor it (escape hatch +# for offline mirrors / version pinning). Otherwise build a GitHub Releases +# URL — 'latest' uses the redirecting /latest/download/ alias. +_ubuntu_sunshine_deb_url() { + if [[ -n "$SUNSHINE_DEB_URL" ]]; then + echo "$SUNSHINE_DEB_URL" + return 0 + fi + local file + file="$(_ubuntu_sunshine_deb_filename)" + if [[ "$SUNSHINE_DEB_VERSION" == "latest" ]]; then + echo "https://github.com/LizardByte/Sunshine/releases/latest/download/${file}" + else + echo "https://github.com/LizardByte/Sunshine/releases/download/${SUNSHINE_DEB_VERSION}/${file}" + fi +} + +_install_sunshine_debian() { + # Universal runtime deps. libva-utils gives `vainfo`; jq is used by hooks. + # pipewire-pulse is the Ubuntu 24.04+ default audio path; on older releases + # `pulseaudio-utils` works too — we don't force the codename split since + # sunshine just needs *a* PulseAudio API endpoint. + pkg_install jq vulkan-tools libva-utils curl ca-certificates + + if pkg_installed sunshine; then + ok "sunshine already installed (dpkg)" + return 0 + fi + + local url + url="$(_ubuntu_sunshine_deb_url)" + local tmpdir deb_path + tmpdir="$(mktemp -d /tmp/omarchy-sunshine.XXXXXX)" + # shellcheck disable=SC2064 + trap "rm -rf '$tmpdir'" RETURN + deb_path="$tmpdir/$(_ubuntu_sunshine_deb_filename)" + + info "Downloading Sunshine .deb: $url" + if ! curl -fL --retry 3 -o "$deb_path" "$url"; then + err "Failed to download $url" + err "If your Ubuntu version doesn't have a prebuilt .deb, set SUNSHINE_DEB_URL" + err "or SUNSHINE_DEB_VERSION (e.g. SUNSHINE_DEB_VERSION=v2025.118.84544 ./install.sh)." + return 1 + fi + + deb_install_local "$deb_path" + + if ! command -v sunshine >/dev/null 2>&1; then + err "sunshine command not on PATH after install — package layout unexpected." + return 1 + fi + ok "Installed sunshine from $(basename "$deb_path")" +} + # True if every shared library sunshine links against resolves on this system. sunshine_runtime_deps_ok() { local bin @@ -68,17 +153,40 @@ sunshine_runtime_deps_ok() { } install_moonlight() { - yay_install "$MOONLIGHT_PKG" + case "$DISTRO" in + arch) + yay_install "$MOONLIGHT_PKG" + ;; + debian) + # moonlight-qt is published as a PPA + flatpak. On a typical Ubuntu host + # the flatpak is the lowest-friction install path; falling back to apt + # requires adding the cloudsmith PPA. For a headless server (the primary + # Ubuntu target here) the client side is almost never wanted — so this + # is best-effort. + if pkg_installed moonlight-qt; then + ok "moonlight-qt already installed" + return 0 + fi + if command -v flatpak >/dev/null 2>&1; then + info "Installing moonlight-qt via flatpak" + as_root flatpak install -y flathub com.moonlight_stream.Moonlight || { + warn "flatpak install of Moonlight failed — install it manually if needed." + } + else + warn "moonlight-qt: no apt package in Ubuntu's default repos and no flatpak available." + warn " Install flatpak first, or grab the .deb from https://github.com/moonlight-stream/moonlight-qt/releases" + warn " Skipping — headless hosts rarely need the client anyway." + fi + ;; + esac } install_gpu_encoder_packages() { - case "$GPU_VENDOR" in - nvidia) - # NVENC works through the proprietary driver. libva-nvidia-driver lets some - # apps use VAAPI on NVIDIA; not strictly required for Sunshine NVENC but useful. + case "$DISTRO:$GPU_VENDOR" in + arch:nvidia) yay_install nvidia-utils libva-nvidia-driver ;; - amd) + arch:amd) # VAAPI (mesa) + Vulkan for AMD hardware encode paths. # libva-mesa-driver is now provided by mesa (merged upstream); mesa-vdpau # was removed from official repos. Naming them here makes yay fall back to @@ -86,11 +194,47 @@ install_gpu_encoder_packages() { # `provides=(libva-mesa-driver mesa-vdpau)`. yay_install mesa vulkan-radeon ;; - intel) + arch:intel) yay_install intel-media-driver vulkan-intel ;; + debian:nvidia) + # On Ubuntu, the proprietary driver is usually already installed via + # `ubuntu-drivers autoinstall` or the Server install path. Don't force a + # specific nvidia-* version — they vary by release / driver branch. + # Pull only the userspace VAAPI bridge if available; harmless if missing. + pkg_install libnvidia-encode-no-dkms 2>/dev/null \ + || pkg_install libnvidia-encode-575 2>/dev/null \ + || pkg_install libnvidia-encode-565 2>/dev/null \ + || pkg_install libnvidia-encode-560 2>/dev/null \ + || info "NVENC userspace library not found via a known package name — relying on the existing driver install." + ;; + debian:amd) + pkg_install mesa-va-drivers mesa-vulkan-drivers vainfo + ;; + debian:intel) + pkg_install intel-media-va-driver-non-free mesa-vulkan-drivers + ;; *) - info "Unknown GPU vendor; skipping vendor-specific encoder packages." + info "Unknown distro/GPU combination ($DISTRO:$GPU_VENDOR); skipping vendor-specific encoder packages." + ;; + esac +} + +# Install a wlroots-based compositor for headless capture on systems without +# Hyprland. Currently means: Sway on Debian/Ubuntu. On Arch the existing +# Hyprland flow is the canonical path; we only fall back to Sway if Hyprland +# isn't installed (rare on Omarchy). +install_headless_compositor() { + case "$DISTRO" in + debian) + pkg_install sway wlr-randr + ;; + arch) + # Hyprland is presumed installed on Omarchy. Only act if it's missing. + if ! command -v hyprctl >/dev/null 2>&1; then + warn "hyprctl not found on Arch — falling back to Sway for headless capture." + yay_install sway wlr-randr + fi ;; esac } diff --git a/lib/preflight.sh b/lib/preflight.sh index 4b1784d..b9f37f9 100644 --- a/lib/preflight.sh +++ b/lib/preflight.sh @@ -35,7 +35,22 @@ preflight_gpu() { case "$GPU_VENDOR" in nvidia) if ! command -v nvidia-smi >/dev/null 2>&1; then - warn "nvidia-smi not found yet — nvidia-utils will be installed shortly." + case "$DISTRO" in + arch) + warn "nvidia-smi not found yet — nvidia-utils will be installed shortly." + ;; + debian) + # On Ubuntu the NVIDIA driver install isn't our job; we don't pull + # in nvidia-driver-* because the right version depends on the + # kernel / Secure Boot / cloud-vendor combo. Tell the user. + err "nvidia-smi not found and no NVIDIA kernel module loaded." + err "Install the driver before re-running this installer. Common paths on Ubuntu:" + err " sudo ubuntu-drivers install # picks the recommended branch" + err " sudo apt install nvidia-driver-550-server # explicit pin" + err "Then reboot (or modprobe nvidia) so 'nvidia-smi -L' returns the GPU." + exit 1 + ;; + esac return 0 fi if ! nvidia-smi -L >/dev/null 2>&1; then @@ -53,7 +68,7 @@ preflight_gpu() { fi ;; intel) - ok "Intel GPU — will install intel-media-driver" + ok "Intel GPU — encoder packages will be installed in the packages step." ;; esac } @@ -100,30 +115,41 @@ preflight_audio() { preflight_headless() { # Only relevant in headless mode. Checks are non-fatal: install can proceed - # even if Hyprland isn't reachable right now (hooks just won't function until - # the user logs into Hyprland on the host). + # even if the compositor isn't reachable right now (hooks just won't + # function until it is). [[ "${STREAM_MODE:-}" == "headless" ]] || return 0 - if command -v hyprctl >/dev/null 2>&1; then - ok "hyprctl on PATH" - else - warn "hyprctl not found. Headless prep-cmd hooks will fail until Hyprland is installed and reachable." - fi + case "${COMPOSITOR:-none}" in + hyprland) + ok "hyprctl on PATH" + if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + ok "Hyprland instance signature present in environment" + else + local rt="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + if compgen -G "$rt/hypr/*/" >/dev/null 2>&1; then + ok "Hyprland runtime directory found under $rt/hypr/" + else + warn "Hyprland not currently running. Install will proceed; hooks engage on next Hyprland login." + fi + fi + ;; + sway) + ok "swaymsg/sway on PATH" + local rt="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + if compgen -G "$rt/sway-ipc.*.sock" >/dev/null 2>&1; then + ok "Sway IPC socket present under $rt" + else + info "Sway not running yet — install will start sway-headless.service before sunshine." + fi + ;; + none|*) + warn "No wlroots compositor detected. Install will attempt to install one (Sway on Debian/Ubuntu)." + ;; + esac if pkg_installed jq; then ok "jq installed (prep-cmd hooks have their parser)" else info "jq not installed yet — will be installed in the packages step." fi - - if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then - ok "Hyprland instance signature present in environment" - else - local rt="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" - if compgen -G "$rt/hypr/*/" >/dev/null 2>&1; then - ok "Hyprland runtime directory found under $rt/hypr/" - else - warn "Hyprland does not appear to be running. Install will proceed; hooks will only work once you log into Hyprland on the host." - fi - fi } diff --git a/lib/service.sh b/lib/service.sh index 5adafe5..bd14c9f 100644 --- a/lib/service.sh +++ b/lib/service.sh @@ -1,11 +1,15 @@ #!/usr/bin/env bash # Enable Sunshine as a systemd --user service and turn on lingering so it -# runs at boot without a graphical login. +# runs at boot without a graphical login. On Ubuntu installs that use the +# Sway-headless capture path, also installs + enables sway-headless.service +# and wires sunshine.service to depend on it. ensure_sunshine_unit_present() { # Case 1: a sunshine.service unit already exists in any path systemd-user - # scans. sunshine-bin ships /usr/lib/systemd/user/sunshine.service directly. + # scans. The LizardByte .deb on Ubuntu drops it at /lib/systemd/user/. + # sunshine-bin on Arch drops it at /usr/lib/systemd/user/. for p in \ + /lib/systemd/user/sunshine.service \ /usr/lib/systemd/user/sunshine.service \ /etc/systemd/user/sunshine.service \ "$HOME/.config/systemd/user/sunshine.service" \ @@ -20,6 +24,7 @@ ensure_sunshine_unit_present() { local fqdn_unit="" for p in \ /usr/lib/systemd/user/app-dev.lizardbyte.app.Sunshine.service \ + /lib/systemd/user/app-dev.lizardbyte.app.Sunshine.service \ /etc/systemd/user/app-dev.lizardbyte.app.Sunshine.service do [[ -f "$p" ]] && { fqdn_unit="$p"; break; } @@ -44,13 +49,64 @@ ensure_sunshine_unit_present() { ok "Installed $HOME/.config/systemd/user/sunshine.service" } +# Install and enable the headless sway compositor unit + config (Debian/Ubuntu +# headless path only). sunshine.service gets a drop-in making it depend on +# sway-headless.service so the wlr capture has something to talk to. +ensure_sway_headless_unit() { + [[ "$DISTRO" == "debian" ]] || return 0 + [[ "${STREAM_MODE:-}" == "headless" ]] || return 0 + [[ "${COMPOSITOR:-}" == "sway" ]] || return 0 + + local cfg_src="$SCRIPT_DIR/files/sway-headless.config" + local svc_src="$SCRIPT_DIR/files/sway-headless.service" + if [[ ! -f "$cfg_src" || ! -f "$svc_src" ]]; then + err "Missing sway-headless source files in $SCRIPT_DIR/files/" + return 1 + fi + + mkdir -p "$HOME/.config/sway" "$HOME/.config/systemd/user" + install -m 0644 "$cfg_src" "$HOME/.config/sway/config-headless" + install -m 0644 "$svc_src" "$HOME/.config/systemd/user/sway-headless.service" + + # Wire sunshine.service to wait for sway-headless.service. Done via a + # drop-in so we don't overwrite the upstream unit shipped by the .deb. + local sun_dropin_dir="$HOME/.config/systemd/user/sunshine.service.d" + mkdir -p "$sun_dropin_dir" + cat >"$sun_dropin_dir/sway-headless.conf" <<'EOF' +# Installed by omarchy-moonlight. Sunshine's wlr capture needs a running +# wlroots compositor; sway-headless provides one on headless servers. +[Unit] +After=sway-headless.service +Requires=sway-headless.service + +[Service] +# Inherit the sway IPC socket location so hooks can talk to swaymsg. +Environment=XDG_SESSION_TYPE=wayland +Environment=WAYLAND_DISPLAY=wayland-1 +EOF + + systemctl --user daemon-reload + systemctl --user enable sway-headless.service >/dev/null + if ! systemctl --user is-active --quiet sway-headless.service; then + info "Starting sway-headless.service" + systemctl --user restart sway-headless.service || { + err "sway-headless.service failed to start. Inspect: journalctl --user -u sway-headless" + return 1 + } + # Give sway a beat to create its IPC socket. + sleep 1 + fi + ok "sway-headless.service active" +} + enable_sunshine_service() { - # The AUR 'sunshine' (source) package doesn't always ship a systemd user unit - # at the standard /usr/lib/systemd/user/sunshine.service path. If systemd - # can't find one, drop our own copy into ~/.config/systemd/user/. ensure_sunshine_unit_present systemctl --user daemon-reload + # If we're on the Debian+Sway headless path, install the sway-headless unit + # before sunshine so the dependency chain is satisfied when we start it. + ensure_sway_headless_unit + # In headless mode, install a drop-in that pre-creates HEADLESS-1 before # Sunshine starts. Done here because the drop-in target name depends on # which unit ensure_sunshine_unit_present resolved. @@ -59,7 +115,7 @@ enable_sunshine_service() { fi if ! systemctl --user list-unit-files sunshine.service >/dev/null 2>&1; then - err "sunshine.service still not found after fallback. Inspect: find /usr ~/.config -name sunshine.service" + err "sunshine.service still not found after fallback. Inspect: find /usr /lib ~/.config -name sunshine.service" return 1 fi @@ -78,7 +134,6 @@ enable_sunshine_service() { systemctl --user reset-failed sunshine.service 2>/dev/null || true info "Starting sunshine.service (user)" - # Restart so a re-run picks up new config / new caps. Tolerate first-launch races. systemctl --user restart sunshine.service || systemctl --user start sunshine.service || { err "Failed to start sunshine.service. Check: journalctl --user -u sunshine" return 1 diff --git a/lib/verify.sh b/lib/verify.sh index 32c4269..330d285 100644 --- a/lib/verify.sh +++ b/lib/verify.sh @@ -119,8 +119,10 @@ verify_install() { info "Sunshine cert not present yet (will be generated on first start)" fi - if [[ -f /etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem ]]; then - ok "omarchy-stream CA installed in system trust store" + local _anchor + _anchor="$(ca_anchor_path 2>/dev/null || true)" + if [[ -n "$_anchor" && -f "$_anchor" ]]; then + ok "omarchy-stream CA installed in system trust store ($_anchor)" else info "omarchy-stream CA not in system trust store (only matters if --no-certs was used)" fi diff --git a/uninstall.sh b/uninstall.sh index 18f67ca..2254703 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -6,6 +6,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" +# shellcheck source=lib/distro.sh +source "$SCRIPT_DIR/lib/distro.sh" + +detect_distro PURGE=0 KEEP_MOONLIGHT=0 @@ -21,7 +25,7 @@ Usage: $(basename "$0") [--purge] [--keep-moonlight] [--remove-ca-trust] --purge Also delete ~/.config/sunshine and ~/.local/share/sunshine --keep-moonlight Do not uninstall moonlight-qt - --remove-ca-trust Remove the omarchy-stream CA from /etc/ca-certificates + --remove-ca-trust Remove the omarchy-stream CA from the system trust store (default: leave it — other hosts/services may rely on it) EOF exit 0 ;; @@ -32,8 +36,9 @@ done require_not_root -step "Stopping Sunshine service" +step "Stopping services" systemctl --user disable --now sunshine.service 2>/dev/null || true +systemctl --user disable --now sway-headless.service 2>/dev/null || true step "Removing user lingering (if enabled by us)" if loginctl show-user "$USER" -p Linger --value 2>/dev/null | grep -qx yes; then @@ -42,19 +47,37 @@ if loginctl show-user "$USER" -p Linger --value 2>/dev/null | grep -qx yes; then fi step "Removing packages" -# Remove -debug siblings first so they don't collide with re-installation later. -for pkg in sunshine-debug sunshine-bin-debug sunshine sunshine-bin; do - if pacman -Qi "$pkg" >/dev/null 2>&1; then - as_root pacman -Rns --noconfirm "$pkg" - fi -done -if [[ $KEEP_MOONLIGHT -eq 0 ]]; then - for pkg in moonlight-qt moonlight-qt-bin; do - if pacman -Qi "$pkg" >/dev/null 2>&1; then - as_root pacman -Rns --noconfirm "$pkg" +case "$DISTRO" in + arch) + # Remove -debug siblings first so they don't collide with re-installation later. + pkg_remove sunshine-debug sunshine-bin-debug sunshine sunshine-bin + if [[ $KEEP_MOONLIGHT -eq 0 ]]; then + pkg_remove moonlight-qt moonlight-qt-bin fi - done -fi + ;; + debian) + pkg_remove sunshine + if [[ $KEEP_MOONLIGHT -eq 0 ]]; then + pkg_remove moonlight-qt + fi + ;; + *) + warn "Unknown distro; skipping package removal." + ;; +esac + +step "Removing user-installed systemd units + drop-ins" +rm -f \ + "$HOME/.config/systemd/user/sway-headless.service" \ + "$HOME/.config/systemd/user/sunshine.service.d/sway-headless.conf" \ + "$HOME/.config/systemd/user/sunshine.service.d/headless-prestart.conf" \ + "$HOME/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d/headless-prestart.conf" +# Clean empty .d directories +rmdir --ignore-fail-on-non-empty \ + "$HOME/.config/systemd/user/sunshine.service.d" \ + "$HOME/.config/systemd/user/app-dev.lizardbyte.app.Sunshine.service.d" \ + 2>/dev/null || true +systemctl --user daemon-reload 2>/dev/null || true step "Removing udev rule (if we wrote one)" if [[ -f /etc/udev/rules.d/60-uinput.rules ]]; then @@ -64,10 +87,10 @@ fi if [[ $REMOVE_CA_TRUST -eq 1 ]]; then step "Removing omarchy-stream CA from system trust store" - anchor="/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem" - if [[ -f "$anchor" ]]; then + anchor="$(ca_anchor_path)" + if [[ -n "$anchor" && -f "$anchor" ]]; then as_root rm -f "$anchor" - as_root update-ca-trust extract >/dev/null + ca_update_trust ok "Removed $anchor and refreshed trust store" else info "CA anchor not present; nothing to remove" @@ -75,11 +98,12 @@ if [[ $REMOVE_CA_TRUST -eq 1 ]]; then fi if [[ $PURGE -eq 1 ]]; then - step "Purging Sunshine user data" - rm -rf "$HOME/.config/sunshine" "$HOME/.local/share/sunshine" + step "Purging Sunshine + sway-headless user data" + rm -rf "$HOME/.config/sunshine" "$HOME/.local/share/sunshine" "$HOME/.local/share/omarchy-moonlight" + rm -f "$HOME/.config/sway/config-headless" fi ok "Uninstall complete. Firewall rules and 'input' group membership were left in place." if [[ $REMOVE_CA_TRUST -eq 0 ]]; then - info "The omarchy-stream CA was left in /etc/ca-certificates (--remove-ca-trust to drop it)." + info "The omarchy-stream CA was left in the system trust store (--remove-ca-trust to drop it)." fi