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"
|
||||||
30
files/sway-headless.config
Normal file
30
files/sway-headless.config
Normal file
@@ -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
|
||||||
27
files/sway-headless.service
Normal file
27
files/sway-headless.service
Normal file
@@ -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
|
||||||
19
install.sh
19
install.sh
@@ -8,6 +8,8 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
# shellcheck source=lib/common.sh
|
# shellcheck source=lib/common.sh
|
||||||
source "$SCRIPT_DIR/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
|
# shellcheck source=lib/detect.sh
|
||||||
source "$SCRIPT_DIR/lib/detect.sh"
|
source "$SCRIPT_DIR/lib/detect.sh"
|
||||||
# shellcheck source=lib/preflight.sh
|
# shellcheck source=lib/preflight.sh
|
||||||
@@ -142,14 +144,14 @@ setup_install_log() {
|
|||||||
main() {
|
main() {
|
||||||
setup_install_log
|
setup_install_log
|
||||||
require_not_root
|
require_not_root
|
||||||
require_arch
|
require_supported_distro
|
||||||
require_yay
|
|
||||||
|
|
||||||
step "Detecting system"
|
step "Detecting system"
|
||||||
detect_all
|
detect_all
|
||||||
compute_stream_mode
|
compute_stream_mode
|
||||||
export 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"
|
info "Mode: $STREAM_MODE"
|
||||||
|
|
||||||
if [[ $DOCTOR_ONLY -eq 1 ]]; then
|
if [[ $DOCTOR_ONLY -eq 1 ]]; then
|
||||||
@@ -160,6 +162,17 @@ main() {
|
|||||||
step "Preflight checks"
|
step "Preflight checks"
|
||||||
preflight_all
|
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
|
if [[ $INSTALL_SUNSHINE -eq 1 ]]; then
|
||||||
step "Installing Sunshine and GPU encoder support"
|
step "Installing Sunshine and GPU encoder support"
|
||||||
install_sunshine
|
install_sunshine
|
||||||
|
|||||||
34
lib/certs.sh
34
lib/certs.sh
@@ -21,14 +21,21 @@
|
|||||||
SUNSHINE_CRED_DIR="$HOME/.config/sunshine/credentials"
|
SUNSHINE_CRED_DIR="$HOME/.config/sunshine/credentials"
|
||||||
SUNSHINE_CERT="$SUNSHINE_CRED_DIR/cacert.pem"
|
SUNSHINE_CERT="$SUNSHINE_CRED_DIR/cacert.pem"
|
||||||
SUNSHINE_KEY="$SUNSHINE_CRED_DIR/cakey.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 ----------------------------------------------------
|
# --- 1Password helpers ----------------------------------------------------
|
||||||
|
|
||||||
op_require_signin() {
|
op_require_signin() {
|
||||||
if ! command -v op >/dev/null 2>&1; then
|
if ! command -v op >/dev/null 2>&1; then
|
||||||
err "1Password CLI ('op') not found on PATH."
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! op whoami >/dev/null 2>&1; then
|
if ! op whoami >/dev/null 2>&1; then
|
||||||
@@ -124,16 +131,25 @@ EOF
|
|||||||
|
|
||||||
install_ca_to_system_trust() {
|
install_ca_to_system_trust() {
|
||||||
local ca_pem="$1"
|
local ca_pem="$1"
|
||||||
# Idempotent: compare sha256 first to avoid pointless update-ca-trust runs.
|
local anchor
|
||||||
if [[ -f "$SYSTEM_TRUST_ANCHOR" ]] \
|
anchor="$(ca_anchor_path)"
|
||||||
&& cmp -s "$ca_pem" "$SYSTEM_TRUST_ANCHOR"; then
|
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"
|
ok "CA already in system trust store"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
info "Installing CA into $SYSTEM_TRUST_ANCHOR"
|
# Debian's update-ca-certificates only picks up files under
|
||||||
as_root install -m 0644 "$ca_pem" "$SYSTEM_TRUST_ANCHOR"
|
# /usr/local/share/ca-certificates/ that end in .crt. The path returned by
|
||||||
as_root update-ca-trust extract >/dev/null
|
# ca_anchor_path already accounts for that.
|
||||||
ok "System trust store refreshed (update-ca-trust)"
|
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 ---------------------------------------------
|
# --- Top-level orchestration ---------------------------------------------
|
||||||
|
|||||||
@@ -29,34 +29,26 @@ require_not_root() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
require_arch() {
|
# Package management (pkg_installed, pkg_install, etc.) and distro detection
|
||||||
if [[ ! -f /etc/arch-release ]] && ! grep -q '^ID=arch' /etc/os-release 2>/dev/null; then
|
# live in lib/distro.sh. Source it after this file.
|
||||||
err "This script targets Arch Linux (Omarchy). /etc/arch-release not found."
|
#
|
||||||
exit 1
|
# yay_install is kept as an alias for code paths that explicitly want AUR
|
||||||
fi
|
# packages even on a distro-agnostic call site (rare). On non-Arch distros it
|
||||||
}
|
# falls back to pkg_install.
|
||||||
|
|
||||||
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.
|
|
||||||
yay_install() {
|
yay_install() {
|
||||||
local missing=()
|
if [[ "${DISTRO:-}" == "arch" ]] && command -v yay >/dev/null 2>&1; then
|
||||||
local p
|
local missing=()
|
||||||
for p in "$@"; do
|
local p
|
||||||
pkg_installed "$p" || missing+=("$p")
|
for p in "$@"; do
|
||||||
done
|
pkg_installed "$p" || missing+=("$p")
|
||||||
if [[ ${#missing[@]} -eq 0 ]]; then
|
done
|
||||||
ok "Already installed: $*"
|
if [[ ${#missing[@]} -eq 0 ]]; then
|
||||||
return 0
|
ok "Already installed: $*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "Installing (AUR): ${missing[*]}"
|
||||||
|
yay -S --needed --noconfirm "${missing[@]}"
|
||||||
|
else
|
||||||
|
pkg_install "$@"
|
||||||
fi
|
fi
|
||||||
info "Installing: ${missing[*]}"
|
|
||||||
yay -S --needed --noconfirm "${missing[@]}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,47 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Detect GPU vendor, session type, hostname.
|
# Detect GPU vendor, session type, hostname, compositor.
|
||||||
|
|
||||||
detect_gpu_vendor() {
|
detect_gpu_vendor() {
|
||||||
local vga
|
local vga
|
||||||
vga="$(lspci -nn 2>/dev/null | grep -iE 'vga|3d|display' || true)"
|
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"
|
echo "nvidia"
|
||||||
elif grep -qiE 'amd|advanced micro devices|ati' <<<"$vga"; then
|
elif [[ -n "$amd_line" ]]; then
|
||||||
echo "amd"
|
echo "amd"
|
||||||
elif grep -qi 'intel' <<<"$vga"; then
|
elif [[ -n "$intel_line" ]]; then
|
||||||
echo "intel"
|
echo "intel"
|
||||||
else
|
else
|
||||||
echo "unknown"
|
echo "unknown"
|
||||||
fi
|
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() {
|
detect_all() {
|
||||||
HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)"
|
HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)"
|
||||||
GPU_VENDOR="$(detect_gpu_vendor)"
|
GPU_VENDOR="$(detect_gpu_vendor)"
|
||||||
SESSION_TYPE="${XDG_SESSION_TYPE:-unknown}"
|
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
|
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."
|
warn "Session type is '$SESSION_TYPE' (not wayland). KMS capture still works at the TTY/DRM level, but Hyprland-specific paths assume Wayland."
|
||||||
|
|||||||
174
lib/distro.sh
Normal file
174
lib/distro.sh
Normal file
@@ -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/<name>.pem + update-ca-trust
|
||||||
|
# Debian: /usr/local/share/ca-certificates/<name>.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
|
||||||
|
}
|
||||||
@@ -3,27 +3,52 @@
|
|||||||
# location and ensure they're executable. The actual sunshine.conf entries
|
# location and ensure they're executable. The actual sunshine.conf entries
|
||||||
# (capture=wlr, output_name=HEADLESS-1, global_prep_cmd=[...]) are written
|
# (capture=wlr, output_name=HEADLESS-1, global_prep_cmd=[...]) are written
|
||||||
# by lib/config.sh.
|
# 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"
|
HEADLESS_BIN_DIR="$HOME/.local/share/omarchy-moonlight/bin"
|
||||||
|
|
||||||
DO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-do.sh"
|
DO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-do.sh"
|
||||||
UNDO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-undo.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_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"
|
mkdir -p "$HEADLESS_BIN_DIR"
|
||||||
install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-do.sh" "$DO_SCRIPT"
|
mapfile -t srcs < <(_headless_hook_sources)
|
||||||
install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-undo.sh" "$UNDO_SCRIPT"
|
local do_src="${srcs[0]}" undo_src="${srcs[1]}" pre_src="${srcs[2]}"
|
||||||
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"
|
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
|
# 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
|
# starts. Without this, Sunshine reports a fatal "Unable to find display or
|
||||||
# this, Sunshine reports a fatal "Unable to find display or encoder during
|
# encoder during startup" on every restart, even though streaming works once
|
||||||
# startup" on every restart, even though streaming works once a client connects.
|
# a client connects.
|
||||||
install_headless_prestart_dropin() {
|
install_headless_prestart_dropin() {
|
||||||
local dropin_src="$SCRIPT_DIR/files/headless-prestart.conf"
|
local dropin_src="$SCRIPT_DIR/files/headless-prestart.conf"
|
||||||
if [[ ! -f "$dropin_src" ]]; then
|
if [[ ! -f "$dropin_src" ]]; then
|
||||||
@@ -31,8 +56,9 @@ install_headless_prestart_dropin() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve the actual unit name. Prefer sunshine.service when present (alias
|
# 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.
|
# sunshine-bin, or the .deb on Ubuntu); fall back to the AUR source pkg's
|
||||||
|
# reverse-DNS name.
|
||||||
local unit=""
|
local unit=""
|
||||||
for u in sunshine.service app-dev.lizardbyte.app.Sunshine.service; do
|
for u in sunshine.service app-dev.lizardbyte.app.Sunshine.service; do
|
||||||
if systemctl --user list-unit-files "$u" >/dev/null 2>&1 \
|
if systemctl --user list-unit-files "$u" >/dev/null 2>&1 \
|
||||||
|
|||||||
160
lib/packages.sh
160
lib/packages.sh
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Install Sunshine, Moonlight, and GPU-specific hardware-encode dependencies.
|
# 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
|
# 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
|
# the ~10 minute source compile). Override with SUNSHINE_PKG=sunshine to build
|
||||||
@@ -7,7 +10,24 @@
|
|||||||
: "${SUNSHINE_PKG:=sunshine-bin}"
|
: "${SUNSHINE_PKG:=sunshine-bin}"
|
||||||
: "${MOONLIGHT_PKG:=moonlight-qt}"
|
: "${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() {
|
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.
|
# Ensure runtime deps useful for capture/diagnostics across vendors.
|
||||||
yay_install pipewire-pulse vulkan-tools libva-utils jq
|
yay_install pipewire-pulse vulkan-tools libva-utils jq
|
||||||
|
|
||||||
@@ -59,6 +79,71 @@ install_sunshine() {
|
|||||||
fi
|
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.
|
# True if every shared library sunshine links against resolves on this system.
|
||||||
sunshine_runtime_deps_ok() {
|
sunshine_runtime_deps_ok() {
|
||||||
local bin
|
local bin
|
||||||
@@ -68,17 +153,40 @@ sunshine_runtime_deps_ok() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_moonlight() {
|
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() {
|
install_gpu_encoder_packages() {
|
||||||
case "$GPU_VENDOR" in
|
case "$DISTRO:$GPU_VENDOR" in
|
||||||
nvidia)
|
arch: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.
|
|
||||||
yay_install nvidia-utils libva-nvidia-driver
|
yay_install nvidia-utils libva-nvidia-driver
|
||||||
;;
|
;;
|
||||||
amd)
|
arch:amd)
|
||||||
# VAAPI (mesa) + Vulkan for AMD hardware encode paths.
|
# VAAPI (mesa) + Vulkan for AMD hardware encode paths.
|
||||||
# libva-mesa-driver is now provided by mesa (merged upstream); mesa-vdpau
|
# 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
|
# 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)`.
|
# `provides=(libva-mesa-driver mesa-vdpau)`.
|
||||||
yay_install mesa vulkan-radeon
|
yay_install mesa vulkan-radeon
|
||||||
;;
|
;;
|
||||||
intel)
|
arch:intel)
|
||||||
yay_install intel-media-driver vulkan-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
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,22 @@ preflight_gpu() {
|
|||||||
case "$GPU_VENDOR" in
|
case "$GPU_VENDOR" in
|
||||||
nvidia)
|
nvidia)
|
||||||
if ! command -v nvidia-smi >/dev/null 2>&1; then
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! nvidia-smi -L >/dev/null 2>&1; then
|
if ! nvidia-smi -L >/dev/null 2>&1; then
|
||||||
@@ -53,7 +68,7 @@ preflight_gpu() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
intel)
|
intel)
|
||||||
ok "Intel GPU — will install intel-media-driver"
|
ok "Intel GPU — encoder packages will be installed in the packages step."
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -100,30 +115,41 @@ preflight_audio() {
|
|||||||
|
|
||||||
preflight_headless() {
|
preflight_headless() {
|
||||||
# Only relevant in headless mode. Checks are non-fatal: install can proceed
|
# 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
|
# even if the compositor isn't reachable right now (hooks just won't
|
||||||
# the user logs into Hyprland on the host).
|
# function until it is).
|
||||||
[[ "${STREAM_MODE:-}" == "headless" ]] || return 0
|
[[ "${STREAM_MODE:-}" == "headless" ]] || return 0
|
||||||
|
|
||||||
if command -v hyprctl >/dev/null 2>&1; then
|
case "${COMPOSITOR:-none}" in
|
||||||
ok "hyprctl on PATH"
|
hyprland)
|
||||||
else
|
ok "hyprctl on PATH"
|
||||||
warn "hyprctl not found. Headless prep-cmd hooks will fail until Hyprland is installed and reachable."
|
if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
fi
|
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
|
if pkg_installed jq; then
|
||||||
ok "jq installed (prep-cmd hooks have their parser)"
|
ok "jq installed (prep-cmd hooks have their parser)"
|
||||||
else
|
else
|
||||||
info "jq not installed yet — will be installed in the packages step."
|
info "jq not installed yet — will be installed in the packages step."
|
||||||
fi
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Enable Sunshine as a systemd --user service and turn on lingering so it
|
# 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() {
|
ensure_sunshine_unit_present() {
|
||||||
# Case 1: a sunshine.service unit already exists in any path systemd-user
|
# 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 \
|
for p in \
|
||||||
|
/lib/systemd/user/sunshine.service \
|
||||||
/usr/lib/systemd/user/sunshine.service \
|
/usr/lib/systemd/user/sunshine.service \
|
||||||
/etc/systemd/user/sunshine.service \
|
/etc/systemd/user/sunshine.service \
|
||||||
"$HOME/.config/systemd/user/sunshine.service" \
|
"$HOME/.config/systemd/user/sunshine.service" \
|
||||||
@@ -20,6 +24,7 @@ ensure_sunshine_unit_present() {
|
|||||||
local fqdn_unit=""
|
local fqdn_unit=""
|
||||||
for p in \
|
for p in \
|
||||||
/usr/lib/systemd/user/app-dev.lizardbyte.app.Sunshine.service \
|
/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
|
/etc/systemd/user/app-dev.lizardbyte.app.Sunshine.service
|
||||||
do
|
do
|
||||||
[[ -f "$p" ]] && { fqdn_unit="$p"; break; }
|
[[ -f "$p" ]] && { fqdn_unit="$p"; break; }
|
||||||
@@ -44,13 +49,64 @@ ensure_sunshine_unit_present() {
|
|||||||
ok "Installed $HOME/.config/systemd/user/sunshine.service"
|
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() {
|
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
|
ensure_sunshine_unit_present
|
||||||
systemctl --user daemon-reload
|
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
|
# 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
|
# Sunshine starts. Done here because the drop-in target name depends on
|
||||||
# which unit ensure_sunshine_unit_present resolved.
|
# which unit ensure_sunshine_unit_present resolved.
|
||||||
@@ -59,7 +115,7 @@ enable_sunshine_service() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! systemctl --user list-unit-files sunshine.service >/dev/null 2>&1; then
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -78,7 +134,6 @@ enable_sunshine_service() {
|
|||||||
systemctl --user reset-failed sunshine.service 2>/dev/null || true
|
systemctl --user reset-failed sunshine.service 2>/dev/null || true
|
||||||
|
|
||||||
info "Starting sunshine.service (user)"
|
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 || {
|
systemctl --user restart sunshine.service || systemctl --user start sunshine.service || {
|
||||||
err "Failed to start sunshine.service. Check: journalctl --user -u sunshine"
|
err "Failed to start sunshine.service. Check: journalctl --user -u sunshine"
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -119,8 +119,10 @@ verify_install() {
|
|||||||
info "Sunshine cert not present yet (will be generated on first start)"
|
info "Sunshine cert not present yet (will be generated on first start)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f /etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem ]]; then
|
local _anchor
|
||||||
ok "omarchy-stream CA installed in system trust store"
|
_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
|
else
|
||||||
info "omarchy-stream CA not in system trust store (only matters if --no-certs was used)"
|
info "omarchy-stream CA not in system trust store (only matters if --no-certs was used)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
64
uninstall.sh
64
uninstall.sh
@@ -6,6 +6,10 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
# shellcheck source=lib/common.sh
|
# shellcheck source=lib/common.sh
|
||||||
source "$SCRIPT_DIR/lib/common.sh"
|
source "$SCRIPT_DIR/lib/common.sh"
|
||||||
|
# shellcheck source=lib/distro.sh
|
||||||
|
source "$SCRIPT_DIR/lib/distro.sh"
|
||||||
|
|
||||||
|
detect_distro
|
||||||
|
|
||||||
PURGE=0
|
PURGE=0
|
||||||
KEEP_MOONLIGHT=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
|
--purge Also delete ~/.config/sunshine and ~/.local/share/sunshine
|
||||||
--keep-moonlight Do not uninstall moonlight-qt
|
--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)
|
(default: leave it — other hosts/services may rely on it)
|
||||||
EOF
|
EOF
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -32,8 +36,9 @@ done
|
|||||||
|
|
||||||
require_not_root
|
require_not_root
|
||||||
|
|
||||||
step "Stopping Sunshine service"
|
step "Stopping services"
|
||||||
systemctl --user disable --now sunshine.service 2>/dev/null || true
|
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)"
|
step "Removing user lingering (if enabled by us)"
|
||||||
if loginctl show-user "$USER" -p Linger --value 2>/dev/null | grep -qx yes; then
|
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
|
fi
|
||||||
|
|
||||||
step "Removing packages"
|
step "Removing packages"
|
||||||
# Remove -debug siblings first so they don't collide with re-installation later.
|
case "$DISTRO" in
|
||||||
for pkg in sunshine-debug sunshine-bin-debug sunshine sunshine-bin; do
|
arch)
|
||||||
if pacman -Qi "$pkg" >/dev/null 2>&1; then
|
# Remove -debug siblings first so they don't collide with re-installation later.
|
||||||
as_root pacman -Rns --noconfirm "$pkg"
|
pkg_remove sunshine-debug sunshine-bin-debug sunshine sunshine-bin
|
||||||
fi
|
if [[ $KEEP_MOONLIGHT -eq 0 ]]; then
|
||||||
done
|
pkg_remove moonlight-qt moonlight-qt-bin
|
||||||
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"
|
|
||||||
fi
|
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)"
|
step "Removing udev rule (if we wrote one)"
|
||||||
if [[ -f /etc/udev/rules.d/60-uinput.rules ]]; then
|
if [[ -f /etc/udev/rules.d/60-uinput.rules ]]; then
|
||||||
@@ -64,10 +87,10 @@ fi
|
|||||||
|
|
||||||
if [[ $REMOVE_CA_TRUST -eq 1 ]]; then
|
if [[ $REMOVE_CA_TRUST -eq 1 ]]; then
|
||||||
step "Removing omarchy-stream CA from system trust store"
|
step "Removing omarchy-stream CA from system trust store"
|
||||||
anchor="/etc/ca-certificates/trust-source/anchors/omarchy-stream-ca.pem"
|
anchor="$(ca_anchor_path)"
|
||||||
if [[ -f "$anchor" ]]; then
|
if [[ -n "$anchor" && -f "$anchor" ]]; then
|
||||||
as_root rm -f "$anchor"
|
as_root rm -f "$anchor"
|
||||||
as_root update-ca-trust extract >/dev/null
|
ca_update_trust
|
||||||
ok "Removed $anchor and refreshed trust store"
|
ok "Removed $anchor and refreshed trust store"
|
||||||
else
|
else
|
||||||
info "CA anchor not present; nothing to remove"
|
info "CA anchor not present; nothing to remove"
|
||||||
@@ -75,11 +98,12 @@ if [[ $REMOVE_CA_TRUST -eq 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $PURGE -eq 1 ]]; then
|
if [[ $PURGE -eq 1 ]]; then
|
||||||
step "Purging Sunshine user data"
|
step "Purging Sunshine + sway-headless user data"
|
||||||
rm -rf "$HOME/.config/sunshine" "$HOME/.local/share/sunshine"
|
rm -rf "$HOME/.config/sunshine" "$HOME/.local/share/sunshine" "$HOME/.local/share/omarchy-moonlight"
|
||||||
|
rm -f "$HOME/.config/sway/config-headless"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ok "Uninstall complete. Firewall rules and 'input' group membership were left in place."
|
ok "Uninstall complete. Firewall rules and 'input' group membership were left in place."
|
||||||
if [[ $REMOVE_CA_TRUST -eq 0 ]]; then
|
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
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user