#!/usr/bin/env bash # # Fluxer Debian/Ubuntu installer — apt repo setup + instance URL + GPU-aware flags. # # Usage: # INSTANCE=https://fluxer.example.com ./install-fluxer-debian.sh # # Env: # EDITION fluxer (only stable is published for deb; default: fluxer) # INSTANCE your instance base URL (prompted if unset) # DESKTOP_ONLY=1 skip repo/install, only rewrite the .desktop entry # ASSUME_YES=1 never prompt (also passes -y to apt-get) # GPU force detection: nvidia | intel | amd | none # SESSION force session type: wayland | x11 # HW_ENCODE=1 enable hardware video *encode* (Intel/AMD only; default 0: # upstream Chromium on Linux rarely delivers it, decode is # what matters for watching streams) # DISABLE_GPU_SANDBOX=1 pass --disable-gpu-sandbox (NVIDIA only; weakens the # Chromium sandbox, so it is OFF by default — but note the # sandbox can block /dev/nvidia* and force software GL) # ALLOW_HTTP=1 allow a plain http:// instance URL (loopback is always allowed) # WM_CLASS override StartupWMClass in the .desktop entry # VERIFY=1 run the launch-based flag check even if the static # probe of the app bundle can't confirm the flag exists # EXTRA_FLAGS extra flags, space-separated, restricted charset. # --enable-features= / --disable-features= are MERGED into # ours (Chromium keeps only the last occurrence of each). # # NOTE: --fluxer-app-url is not documented publicly (checked Sep 2026). The # script probes the installed app bundle for it and warns if it isn't found. # Only point the desktop app at an instance you control. # set -euo pipefail EDITION="${EDITION:-fluxer}" INSTANCE="${INSTANCE:-https://fluxer.systux.xyz}" KEY_URL='https://pkgs.fluxer.com/keys/fluxer-archive-keyring.gpg' SOURCES_URL='https://pkgs.fluxer.com/deb/fluxer.sources' SOURCES_FILE='/etc/apt/sources.list.d/fluxer.sources' KEYRING='/etc/apt/keyrings/fluxer-archive-keyring.gpg' KEY_ID='09D01339EE128925F75E675C855C5BDE34D205D2' HAVE_APT=0 command -v apt-get >/dev/null && command -v dpkg >/dev/null && HAVE_APT=1 TMPFILES=() trap 'rm -f "${TMPFILES[@]}"' EXIT info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; } die() { printf '\033[1;31m ✗\033[0m %s\n' "$*" >&2; exit 1; } confirm() { # confirm "question" -> default yes; non-interactive => yes only with ASSUME_YES [[ "${ASSUME_YES:-0}" == 1 ]] && return 0 [[ -t 0 ]] || return 1 local ans; read -rp "$1 [Y/n] " ans [[ ! "$ans" =~ ^[Nn] ]] } join_by() { local IFS="$1"; shift; printf '%s' "$*"; } APT_FLAGS=() if [[ "${ASSUME_YES:-0}" == 1 ]]; then APT_FLAGS+=(-y); fi # ------------------------------------------------------------ preflight (( EUID != 0 )) || die "run as your normal user, not root (sudo is used where needed)" [[ "$EDITION" == fluxer ]] \ || die "only stable 'fluxer' is published for Debian/Ubuntu (got '$EDITION')" if [[ -z "$INSTANCE" ]]; then [[ -t 0 ]] || die "INSTANCE not set and no terminal to prompt on" read -rp "Instance URL (e.g. https://fluxer.example.com): " INSTANCE fi INSTANCE="${INSTANCE%/}" # Restricted charset: keeps the .desktop Exec= line free of characters that # would need escaping/quoting under the Desktop Entry spec. No userinfo (@), # no query/fragment, no IPv6 literals. URL_RE='^(https?)://([A-Za-z0-9._-]+)(:[0-9]+)?(/[A-Za-z0-9._/-]*)?$' [[ "$INSTANCE" =~ $URL_RE ]] || die "invalid instance URL: '$INSTANCE'" URL_SCHEME="${BASH_REMATCH[1]}" URL_HOST="${BASH_REMATCH[2]}" if [[ "$URL_SCHEME" == http && "${ALLOW_HTTP:-0}" != 1 ]]; then [[ "$URL_HOST" =~ ^(localhost|127\.[0-9]+\.[0-9]+\.[0-9]+)$ ]] \ || die "plain http:// is only accepted for loopback; use https:// or set ALLOW_HTTP=1" fi info "Checking $INSTANCE …" if curl -fsS -o /dev/null --max-time 10 "$INSTANCE/"; then ok "$INSTANCE reachable" else warn "$INSTANCE did not answer — continuing anyway (server may be down)" fi # ---------------------------------------------------------------- repo setup if [[ "${DESKTOP_ONLY:-0}" != 1 ]]; then (( HAVE_APT )) || die "apt-get/dpkg not found — this script is for Debian/Ubuntu-based systems" sudo -v info "Setting up the Fluxer apt repository…" if grep -qs 'pkgs\.fluxer\.com/deb' "$SOURCES_FILE"; then ok "repository already present" else sudo install -d -m 0755 /etc/apt/keyrings keyfile="$(mktemp)" TMPFILES+=("$keyfile") curl -fsSL -o "$keyfile" "$KEY_URL" # Refuse to install anything that doesn't contain the expected fingerprint. key_info="$(gpg --show-keys --with-colons "$keyfile" 2>/dev/null || true)" grep -qE "^fpr:.*:${KEY_ID}:" <<<"$key_info" \ || die "downloaded keyring does not contain expected fingerprint $KEY_ID" sudo install -m 0644 "$keyfile" "$KEYRING" sudo curl -fsSL -o "$SOURCES_FILE" "$SOURCES_URL" grep -qs 'pkgs\.fluxer\.com/deb' "$SOURCES_FILE" \ || die "downloaded sources file doesn't reference the Fluxer repo" ok "repository added to $SOURCES_FILE" fi info "Installing $EDITION…" sudo apt-get update sudo apt-get install "${APT_FLAGS[@]}" "$EDITION" ok "$EDITION installed" fi # ---------------------------------------------------------- resolve binary BIN="$(command -v "$EDITION" || true)" if [[ -z "$BIN" ]]; then for cand in "/opt/$EDITION/$EDITION" "/opt/fluxer/fluxer" "$HOME/opt/fluxer/fluxer"; do if [[ -x "$cand" ]]; then BIN="$cand"; break; fi done fi [[ -n "$BIN" ]] || die "cannot find the $EDITION binary" [[ ! "$BIN" =~ [[:space:]] ]] || die "binary path contains whitespace, unsupported: '$BIN'" info "Binary: $BIN" # ------------------------------------------------- detect session + GPU if [[ -n "${SESSION:-}" ]]; then [[ "$SESSION" =~ ^(wayland|x11)$ ]] || die "SESSION must be wayland | x11 (got '$SESSION')" else SESSION="${XDG_SESSION_TYPE:-unknown}" if [[ -n "${WAYLAND_DISPLAY:-}" ]]; then SESSION=wayland; fi if [[ "$SESSION" != wayland && "$SESSION" != x11 ]]; then warn "session type is '$SESSION' (TTY/SSH?) — assuming x11-style minimal flags; set SESSION=wayland|x11 to override" SESSION=x11 fi fi info "Session type: $SESSION" # Read vendor IDs straight from sysfs: no lspci dependency, and (unlike # `lspci | grep intel`) it only looks at actual GPUs, not every Intel chipset. IS_NVIDIA=0 IS_INTEL=0 IS_AMD=0 for f in /sys/class/drm/card*/device/vendor; do [[ -r "$f" ]] || continue case "$(<"$f")" in 0x10de) IS_NVIDIA=1 ;; 0x8086) IS_INTEL=1 ;; 0x1002) IS_AMD=1 ;; esac done if [[ -n "${GPU:-}" ]]; then IS_NVIDIA=0 IS_INTEL=0 IS_AMD=0 case "$GPU" in nvidia) IS_NVIDIA=1 ;; intel) IS_INTEL=1 ;; amd) IS_AMD=1 ;; none) ;; *) die "GPU must be nvidia | intel | amd | none (got '$GPU')" ;; esac fi info "GPU: nvidia=$IS_NVIDIA intel=$IS_INTEL amd=$IS_AMD" if (( IS_NVIDIA && IS_INTEL )); then warn "hybrid Intel+NVIDIA detected — using the NVIDIA path; override with GPU=intel if the iGPU renders" fi # ------------------------------------------------- driver prerequisites if (( IS_NVIDIA )) && (( HAVE_APT )); then if dpkg -s libva-nvidia-driver &>/dev/null; then ok "libva-nvidia-driver present" ver="$(dpkg-query -W -f='${Version}' libva-nvidia-driver 2>/dev/null | cut -d- -f1)" if dpkg --compare-versions "${ver:-0}" lt 0.0.18; then warn "libva-nvidia-driver $ver is older than 0.0.18 — Chromium VA-API on NVIDIA reportedly needs 0.0.18+" fi else warn "libva-nvidia-driver missing — VA-API on NVIDIA needs it (not in official Debian/Ubuntu archives; build it from upstream or use your GPU vendor's packages)" fi # nvidia-vaapi-driver requires DRM modesetting. modeset_file=/sys/module/nvidia_drm/parameters/modeset if [[ -r "$modeset_file" ]]; then [[ "$(<"$modeset_file")" == Y ]] \ || warn "nvidia_drm modeset is off — nvidia-vaapi-driver requires nvidia-drm.modeset=1" else warn "nvidia_drm module not loaded — can't check nvidia-drm.modeset=1" fi if command -v vainfo >/dev/null; then vainfo_out="$(LIBVA_DRIVER_NAME=nvidia vainfo 2>&1 || true)" if grep -q 'VAEntrypointVLD' <<<"$vainfo_out"; then ok "vainfo reports decode profiles" else warn "vainfo shows no decode entrypoints — check nvidia-utils + reboot if you just installed drivers" fi else warn "vainfo not installed (Debian/Ubuntu package 'vainfo') — skipping VA-API check" fi fi # ------------------------------------------------- build flag set # Collect features once and emit a single --enable-features / --disable-features: # Chromium doesn't merge repeated flags, the last one wins. # # Feature names follow Chromium >= 131 (Vaapi* decode features were renamed # Accelerated*). Hardware decode is on by default on Wayland from Chromium # 143, so AcceleratedVideoDecodeLinuxGL is belt-and-braces there. # nvidia-vaapi-driver is decode-only, so no encoder feature on NVIDIA. FLAGS=("--fluxer-app-url=${INSTANCE}" --ozone-platform-hint=auto) ENABLE=() DISABLE=() PREFIX="" if (( IS_NVIDIA )); then # VaapiOnNvidiaGPUs: Chromium skips nvidia-drm for VA-API unless it is set # (crbug 1492880 — verified in vaapi_wrapper.cc, the "Should skip nVidia # device" warning disappears with it). ENABLE+=(AcceleratedVideoDecodeLinuxGL VaapiOnNvidiaGPUs) FLAGS+=(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist --disable-gpu-driver-bug-workarounds) # libva must be told which driver to load at *launch*, not just for vainfo. PREFIX="env LIBVA_DRIVER_NAME=nvidia " if [[ "${DISABLE_GPU_SANDBOX:-0}" == 1 ]]; then warn "DISABLE_GPU_SANDBOX=1 — the Chromium GPU sandbox will be disabled" FLAGS+=(--disable-gpu-sandbox) else warn "GPU sandbox left enabled — if video stays software-decoded, retry with DISABLE_GPU_SANDBOX=1 (it can block /dev/nvidia*)" fi elif (( IS_INTEL || IS_AMD )); then ENABLE+=(AcceleratedVideoDecodeLinuxGL) if [[ "${HW_ENCODE:-0}" == 1 ]]; then ENABLE+=(AcceleratedVideoEncoder); fi FLAGS+=(--ignore-gpu-blocklist) fi if [[ "$SESSION" == wayland ]]; then ENABLE+=(WebRTCPipeWireCapturer) else info "X11 session — no PipeWire forcing (native screen capture)" fi # EXTRA_FLAGS: merge feature lists instead of appending a second switch that # would silently override ours. if [[ -n "${EXTRA_FLAGS:-}" ]]; then read -ra extra <<<"$EXTRA_FLAGS" for tok in "${extra[@]}"; do [[ "$tok" =~ ^[A-Za-z0-9._:/=,+-]+$ ]] \ || die "EXTRA_FLAGS token can't be used in a .desktop Exec line: '$tok'" case "$tok" in --enable-features=*) IFS=, read -ra t <<<"${tok#*=}"; ENABLE+=("${t[@]}") ;; --disable-features=*) IFS=, read -ra t <<<"${tok#*=}"; DISABLE+=("${t[@]}") ;; *) FLAGS+=("$tok") ;; esac done fi if (( ${#ENABLE[@]} )); then FLAGS+=("--enable-features=$(join_by , "${ENABLE[@]}")"); fi if (( ${#DISABLE[@]} )); then FLAGS+=("--disable-features=$(join_by , "${DISABLE[@]}")"); fi LAUNCH="${PREFIX}${BIN} ${FLAGS[*]}" info "Launch command: $LAUNCH" # ------------------------------------------------- write .desktop override APP_NAME="Fluxer"; APP_COMMENT="Fluxer desktop" DESKTOP_DIR="$HOME/.local/share/applications" DESKTOP_FILE="" mkdir -p "$DESKTOP_DIR" # Derive the entry from the package's own .desktop file (same filename, so it # overrides rather than duplicates; MimeType/Actions/StartupWMClass stay as the # package ships them). Only the Exec= command word, the main Name= and # DBusActivatable are touched. FIRST_NAME_DONE=0 write_from_template() { # write_from_template TEMPLATE OUTPUT local tmpl="$1" out="$2" line rest first args FIRST_NAME_DONE=0 : >"$out" while IFS= read -r line || [[ -n "$line" ]]; do case "$line" in Exec=*) rest="${line#Exec=}" first="${rest%% *}" case "$first" in \"*|env|*=*) return 1 ;; # quoted path / env wrapper / VAR=val: don't guess esac args="" if [[ "$rest" == *" "* ]]; then args=" ${rest#* }"; fi printf 'Exec=%s%s\n' "$LAUNCH" "$args" >>"$out" ;; Name=*) if (( FIRST_NAME_DONE == 0 )); then printf '%s (%s)\n' "$line" "$INSTANCE" >>"$out" FIRST_NAME_DONE=1 else printf '%s\n' "$line" >>"$out" fi ;; DBusActivatable=*) : ;; # D-Bus activation would bypass our Exec= line StartupWMClass=*) if [[ -n "${WM_CLASS:-}" ]]; then printf 'StartupWMClass=%s\n' "$WM_CLASS" >>"$out" else printf '%s\n' "$line" >>"$out"; fi ;; *) printf '%s\n' "$line" >>"$out" ;; esac done <"$tmpl" } tmpl="$(dpkg -L "$EDITION" 2>/dev/null \ | grep -E '/applications/[^/]+\.desktop$' | head -n 1 || true)" if [[ -n "$tmpl" && -r "$tmpl" ]]; then out="$(mktemp)"; TMPFILES+=("$out") if write_from_template "$tmpl" "$out"; then DESKTOP_FILE="$DESKTOP_DIR/$(basename "$tmpl")" install -m 0644 "$out" "$DESKTOP_FILE" ok "derived $DESKTOP_FILE from $tmpl" else warn "packaged .desktop has an Exec= form I won't rewrite blindly — generating a fresh entry" fi else warn "no packaged .desktop found — generating a fresh entry" fi if [[ -z "$DESKTOP_FILE" ]]; then DESKTOP_FILE="$DESKTOP_DIR/$EDITION.desktop" # Fresh entries keep it minimal: no Actions (their --fluxer-task flags are # unverified), MimeType kept so fluxer:// links keep working. # StartupWMClass is a guess — check with xprop / your compositor and set # WM_CLASS if the launcher can't match the window. { cat <"$DESKTOP_FILE" ok "wrote $DESKTOP_FILE (overrides /usr/share/applications)" fi update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true if command -v desktop-file-validate >/dev/null; then if desktop-file-validate "$DESKTOP_FILE"; then ok "desktop entry validates" else warn "desktop-file-validate reported issues (see above)"; fi fi # ------------------------------------------------- verify flags exist # Static probe first (no side effects): do the flag names appear in the app bundle? real_bin="$(readlink -f "$BIN")" ASAR="" for cand in "$(dirname "$real_bin")/resources/app.asar" "$(dirname "$real_bin")/resources/app"; do if [[ -e "$cand" ]]; then ASAR="$cand"; break; fi done bundle_has() { # 0 = found, 1 = not found, 2 = can't tell [[ -n "$ASAR" ]] || return 2 local rc=0 grep -raqF -- "$1" "$ASAR" 2>/dev/null || rc=$? case "$rc" in 0) return 0 ;; 1) return 1 ;; *) return 2 ;; esac } info "Checking that the app knows the flags we pass…" rc=0; bundle_has 'fluxer-app-url' || rc=$? case "$rc" in 0) ok "--fluxer-app-url found in the app bundle" ;; 1) warn "--fluxer-app-url NOT found in the app bundle — the instance override will probably be ignored" ;; *) warn "couldn't locate the app bundle to check --fluxer-app-url (looked next to $real_bin)" ;; esac debug_rc=0; bundle_has 'fluxer-debug-info' || debug_rc=$? if (( debug_rc == 0 )) || [[ "${VERIFY:-0}" == 1 ]]; then if pgrep -f -- "$real_bin" >/dev/null 2>&1; then warn "$EDITION is already running (single-instance) — skipping the launch-based check" else # Only reached if the flag exists (or VERIFY=1); otherwise this would just # open the app window for 15 seconds. dbg="$(timeout 15 "$BIN" "--fluxer-app-url=${INSTANCE}" --fluxer-debug-info 2>&1 || true)" if grep -qF -- "$INSTANCE" <<<"$dbg"; then ok "app reports the instance override: $INSTANCE" else warn "could not confirm the override from --fluxer-debug-info output" fi fi else info "skipping launch-based check (--fluxer-debug-info not found in the bundle; VERIFY=1 to force)" fi echo ok "Done. Quit any running $EDITION first (tray → Quit, single-instance!), then launch from the app menu."