lesavka/scripts/install/client.sh

514 lines
18 KiB
Bash
Raw Normal View History

2025-06-01 13:31:22 -05:00
#!/usr/bin/env bash
2025-06-27 22:51:50 -05:00
# scripts/install/client.sh - install and setup all client related apps and environments
2025-06-01 13:31:22 -05:00
set -euo pipefail
2025-06-01 14:18:42 -05:00
2025-06-01 21:59:43 -05:00
ORIG_USER=${SUDO_USER:-$(id -un)}
REF=${LESAVKA_REF:-master}
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
SCRIPT_REPO_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd)
DEFAULT_REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git
REPO_URL=${LESAVKA_REPO_URL:-}
SRC=/var/src/lesavka
INSTALL_SOURCE=${LESAVKA_INSTALL_SOURCE:-auto}
2026-04-16 13:54:25 -03:00
export TMPDIR=${TMPDIR:-/var/tmp}
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
2026-04-30 08:16:57 -03:00
CLIENT_PKI_DIR=${LESAVKA_CLIENT_PKI_DIR:-$USER_HOME/.config/lesavka/pki}
2026-04-30 11:38:16 -03:00
CLIENT_PKI_AUTO_FETCH=${LESAVKA_CLIENT_PKI_AUTO_FETCH:-1}
CLIENT_PKI_SSH_SOURCE=${LESAVKA_CLIENT_PKI_SSH_SOURCE:-theia:/etc/lesavka/lesavka-client-pki.tar.gz}
CLIENT_CAPTURE_DIR=${LESAVKA_CLIENT_CAPTURE_DIR:-$USER_HOME/Pictures/lesavka}
2025-06-01 21:58:47 -05:00
log() {
printf '==> %s\n' "$*"
}
manifest_package_version() {
local manifest=$1
[[ -f $manifest ]] || return 1
awk -F'"' '
$0 ~ /^\[package\]/ { in_package=1; next }
in_package && $0 ~ /^\[/ { exit }
in_package && $0 ~ /^[[:space:]]*version[[:space:]]*=/ { print $2; exit }
' "$manifest"
}
source_revision() {
local repo=$1
local sha=""
sha=$(run_as_user git -C "$repo" rev-parse --short HEAD 2>/dev/null || true)
if [[ -n $sha ]] && ! run_as_user git -C "$repo" diff --quiet --ignore-submodules -- 2>/dev/null; then
sha="${sha}+dirty"
fi
printf '%s\n' "$sha"
}
resolve_source_checkout() {
case "$INSTALL_SOURCE" in
auto)
if [[ -d $SCRIPT_REPO_ROOT/.git ]]; then
SRC=$SCRIPT_REPO_ROOT
log "3. Using local source checkout at $SRC"
echo " ↪ set LESAVKA_INSTALL_SOURCE=ref to install from ${REF} via Git"
return 0
fi
;;
local)
if [[ ! -d $SCRIPT_REPO_ROOT/.git ]]; then
echo "❌ LESAVKA_INSTALL_SOURCE=local requested, but $SCRIPT_REPO_ROOT is not a Git checkout." >&2
exit 1
fi
SRC=$SCRIPT_REPO_ROOT
log "3. Using local source checkout at $SRC"
return 0
;;
ref|git)
;;
*)
echo "❌ unsupported LESAVKA_INSTALL_SOURCE=$INSTALL_SOURCE (expected auto, local, or ref)" >&2
exit 1
;;
esac
log "3. Syncing source checkout for ref ${REF}"
if [[ ! -d /var/src ]]; then
sudo mkdir -p /var/src
fi
sudo chown "$ORIG_USER":"$ORIG_USER" /var/src
if [[ -d $SRC/.git ]]; then
run_as_user git -C "$SRC" fetch --all --tags --prune
else
run_as_user git clone "$REPO_URL" "$SRC"
fi
if run_as_user git -C "$SRC" rev-parse --verify --quiet "origin/$REF" >/dev/null; then
run_as_user git -C "$SRC" checkout -B "$REF" "origin/$REF"
else
run_as_user git -C "$SRC" checkout --force "$REF"
fi
}
installed_kernel_module_trees() {
local roots=(/usr/lib/modules /lib/modules)
local seen=()
local root entry
for root in "${roots[@]}"; do
[[ -d $root ]] || continue
for entry in "$root"/*; do
[[ -d $entry ]] || continue
seen+=("$(basename "$entry")")
done
done
if [[ ${#seen[@]} -eq 0 ]]; then
return 0
fi
printf '%s\n' "${seen[@]}" | awk '!seen[$0]++'
}
require_command() {
local cmd=$1
local pkg_hint=$2
if command -v "$cmd" >/dev/null 2>&1; then
return 0
fi
echo "❌ required command '$cmd' is unavailable after install (expected via $pkg_hint)" >&2
exit 1
}
2026-04-16 14:34:05 -03:00
require_linkable() {
local path=$1
local label=$2
if ldd "$path" 2>/dev/null | grep -q 'not found'; then
echo "$label is present but has unresolved shared-library dependencies:" >&2
ldd "$path" 2>/dev/null | grep 'not found' >&2 || true
exit 1
fi
}
2026-04-20 13:59:34 -03:00
require_gst_element() {
local element=$1
if gst-inspect-1.0 "$element" >/dev/null 2>&1; then
return 0
fi
echo "❌ required GStreamer element '$element' is unavailable after install." >&2
exit 1
}
gst_element_available() {
gst-inspect-1.0 "$1" >/dev/null 2>&1
}
first_available_gst_element() {
local element
for element in "$@"; do
if gst_element_available "$element"; then
printf '%s\n' "$element"
return 0
fi
done
return 1
}
report_client_media_acceleration() {
log "1e. Inspecting client media acceleration routes"
local hevc_encoder=""
local h264_decoder=""
local opus_encoder=""
local opus_decoder=""
local webrtc_dsp=""
local proprietary_bits=()
local opensource_bits=()
hevc_encoder=$(first_available_gst_element \
nvh265enc \
vah265enc \
vaapih265enc \
v4l2h265enc \
x265enc || true)
h264_decoder=$(first_available_gst_element \
nvh264dec \
nvh264sldec \
vah264dec \
vaapih264dec \
v4l2h264dec \
v4l2slh264dec \
avdec_h264 \
openh264dec || true)
opus_encoder=$(first_available_gst_element opusenc || true)
opus_decoder=$(first_available_gst_element opusdec || true)
webrtc_dsp=$(first_available_gst_element webrtcdsp || true)
for element in nvh265enc nvh264dec nvh264sldec; do
if gst_element_available "$element"; then
proprietary_bits+=("$element")
fi
done
for element in vah265enc vaapih265enc v4l2h265enc vah264dec vaapih264dec v4l2h264dec v4l2slh264dec; do
if gst_element_available "$element"; then
opensource_bits+=("$element")
fi
done
if command -v nvidia-smi >/dev/null 2>&1; then
echo " ↪ nvidia-smi is available; proprietary NVIDIA driver tooling is present"
else
echo " ↪ nvidia-smi is not available; NVIDIA proprietary tooling was not detected"
fi
if [[ ${#proprietary_bits[@]} -gt 0 ]]; then
echo " ↪ proprietary NVIDIA GStreamer route: ${proprietary_bits[*]}"
else
echo " ↪ proprietary NVIDIA GStreamer route: not exposed"
fi
if [[ ${#opensource_bits[@]} -gt 0 ]]; then
echo " ↪ open-source VAAPI/V4L2 GStreamer route: ${opensource_bits[*]}"
else
echo " ↪ open-source VAAPI/V4L2 GStreamer route: not exposed"
fi
if [[ -n $hevc_encoder ]]; then
echo " ↪ upstream HEVC encoder candidate: $hevc_encoder"
else
echo "⚠️ no HEVC encoder was detected; upstream HEVC will need NVIDIA/VAAPI/V4L2 or x265enc"
fi
if [[ -n $h264_decoder ]]; then
echo " ↪ downstream H.264 decoder candidate: $h264_decoder"
else
echo "⚠️ no H.264 decoder was detected; downstream eye preview may fall back to decodebin"
fi
if [[ -n $opus_encoder && -n $opus_decoder ]]; then
echo "✅ Opus upstream audio transport route: encoder=$opus_encoder decoder=$opus_decoder"
else
echo "⚠️ Opus upstream audio route is not fully exposed; Lesavka will fall back to PCM"
fi
if [[ -n $webrtc_dsp ]]; then
echo "✅ microphone noise suppression route: $webrtc_dsp"
else
echo " ↪ microphone noise suppression route: unavailable; raw microphone path still works"
fi
echo " ↪ override decoder route with LESAVKA_H264_DECODER=<element> or LESAVKA_H264_DECODER_PREFERENCE=software"
}
2026-04-20 13:59:34 -03:00
require_kernel_module() {
local module=$1
local why=$2
if modinfo "$module" >/dev/null 2>&1; then
return 0
fi
local running_kernel
running_kernel=$(uname -r)
mapfile -t module_trees < <(installed_kernel_module_trees)
2026-04-20 13:59:34 -03:00
echo "❌ required kernel module '$module' is unavailable for the running kernel $(uname -r)." >&2
echo " Lesavka needs it for $why." >&2
if [[ ${#module_trees[@]} -eq 0 ]]; then
echo " No kernel module trees are currently installed under /usr/lib/modules or /lib/modules." >&2
else
echo " Installed kernel module trees: ${module_trees[*]}" >&2
if [[ ! " ${module_trees[*]} " =~ [[:space:]]${running_kernel}[[:space:]] ]]; then
echo " The machine is booted into an older kernel than the modules that are currently installed." >&2
echo " Reboot into one of the installed kernels above, then rerun the installer." >&2
else
echo " The current kernel tree exists, but modinfo still cannot resolve '$module'." >&2
echo " Verify the kernel package and headers are healthy, then rerun the installer." >&2
fi
fi
2026-04-20 13:59:34 -03:00
exit 1
}
run_as_user() {
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
}
install_verified_executable() {
local src=$1
local dest=$2
local label=${3:-$dest}
local dest_dir dest_base tmp
if [[ ! -s "$src" ]]; then
echo "❌ refusing to install $label: source '$src' is missing or empty." >&2
echo " Preserving the existing installed executable at '$dest'." >&2
exit 1
fi
if [[ ! -x "$src" ]]; then
echo "❌ refusing to install $label: source '$src' is not executable." >&2
echo " Preserving the existing installed executable at '$dest'." >&2
exit 1
fi
dest_dir=$(dirname "$dest")
dest_base=$(basename "$dest")
sudo install -d -m 0755 "$dest_dir"
tmp=$(sudo mktemp "$dest_dir/.${dest_base}.install.XXXXXX")
sudo rm -f "$tmp"
sudo install -Dm755 "$src" "$tmp"
if ! sudo test -s "$tmp" || ! sudo test -x "$tmp"; then
sudo rm -f "$tmp"
echo "❌ refusing to replace $label: staged install output was not a non-empty executable." >&2
echo " Preserving the existing installed executable at '$dest'." >&2
exit 1
fi
sudo mv -f "$tmp" "$dest"
sudo chmod 0755 "$dest"
}
pacman_install() {
local log_file
log_file=$(mktemp --tmpdir="$TMPDIR" lesavka-pacman.XXXXXX.log)
if sudo pacman -Sq --needed --noconfirm "$@" 2>&1 | tee "$log_file"; then
rm -f "$log_file"
return 0
fi
if grep -Eq "breaks dependency '.*pipewire" "$log_file"; then
cat >&2 <<'MSG'
❌ Arch stopped the package transaction because PipeWire packages are at mixed exact versions.
Lesavka now installs PipeWire as one coherent set, but this host still needs a sync transaction.
Run:
sudo pacman -Syu
Then rerun the Lesavka client installer.
MSG
elif grep -Eq "failed retrieving file|failed to retrieve some files|failed to commit transaction \\(failed to retrieve some files\\)" "$log_file"; then
cat >&2 <<'MSG'
❌ Arch failed while downloading packages from the configured mirrors.
No Lesavka files were replaced. Refresh or choose healthier mirrors, then rerun the installer.
Good first retry:
sudo pacman -Syu --disable-download-timeout
If mirrors keep timing out, refresh /etc/pacman.d/mirrorlist before retrying.
MSG
fi
echo " pacman log: $log_file" >&2
exit 1
}
2026-04-30 11:38:16 -03:00
fetch_client_pki_bundle() {
[[ $CLIENT_PKI_AUTO_FETCH != 0 && $CLIENT_PKI_AUTO_FETCH != false && $CLIENT_PKI_AUTO_FETCH != no ]] || return 1
[[ $CLIENT_PKI_SSH_SOURCE == *:* ]] || return 1
local host=${CLIENT_PKI_SSH_SOURCE%%:*}
local remote_path=${CLIENT_PKI_SSH_SOURCE#*:}
local tmp_bundle
2026-04-30 12:07:31 -03:00
tmp_bundle=$(run_as_user mktemp --tmpdir="$TMPDIR" lesavka-client-pki.XXXXXX.tar.gz)
2026-04-30 11:38:16 -03:00
if run_as_user scp -q -o BatchMode=yes -o ConnectTimeout=5 \
"$host:$remote_path" "$tmp_bundle" >/dev/null 2>&1; then
printf '%s\n' "$tmp_bundle"
return 0
fi
rm -f "$tmp_bundle"
return 1
}
2026-04-30 08:16:57 -03:00
install_client_pki_bundle() {
local bundle=${LESAVKA_CLIENT_PKI_BUNDLE:-}
2026-04-30 11:38:16 -03:00
local fetched_bundle=0
2026-04-30 08:16:57 -03:00
if [[ -z $bundle ]]; then
if [[ -s "$CLIENT_PKI_DIR/ca.crt" && -s "$CLIENT_PKI_DIR/client.crt" && -s "$CLIENT_PKI_DIR/client.key" ]]; then
echo " ↪ TLS client identity already present: $CLIENT_PKI_DIR"
2026-04-30 11:38:16 -03:00
return 0
fi
if bundle=$(fetch_client_pki_bundle); then
fetched_bundle=1
echo " ↪ fetched TLS client enrollment bundle from $CLIENT_PKI_SSH_SOURCE"
2026-04-30 08:16:57 -03:00
else
2026-04-30 11:38:16 -03:00
echo "⚠️ no TLS client identity installed."
echo " Rerun with LESAVKA_CLIENT_PKI_BUNDLE=/path/to/lesavka-client-pki.tar.gz,"
echo " or make $CLIENT_PKI_SSH_SOURCE readable over SSH and rerun the installer."
echo " HTTPS/mTLS relay connections will not work until this bundle is installed."
return 0
2026-04-30 08:16:57 -03:00
fi
fi
log "5b. Installing TLS client identity"
local tmp
tmp=$(mktemp -d)
sudo tar -xzf "$bundle" -C "$tmp"
for item in ca.crt client.crt client.key; do
if [[ ! -s "$tmp/$item" ]]; then
echo "❌ TLS client bundle $bundle is missing $item" >&2
sudo rm -rf "$tmp"
exit 1
fi
done
sudo install -d -m 0700 -o "$ORIG_USER" -g "$ORIG_USER" "$CLIENT_PKI_DIR"
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/ca.crt" "$CLIENT_PKI_DIR/ca.crt"
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.crt" "$CLIENT_PKI_DIR/client.crt"
sudo install -m 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$tmp/client.key" "$CLIENT_PKI_DIR/client.key"
sudo rm -rf "$tmp"
2026-04-30 11:38:16 -03:00
if [[ $fetched_bundle == 1 ]]; then
rm -f "$bundle"
fi
2026-04-30 08:16:57 -03:00
echo " ↪ installed TLS client identity: $CLIENT_PKI_DIR"
}
2026-04-16 13:54:25 -03:00
mkdir -p "$TMPDIR"
if [[ -z $REPO_URL ]] && [[ -d $SCRIPT_REPO_ROOT/.git ]]; then
REPO_URL=$(git -C "$SCRIPT_REPO_ROOT" config --get remote.origin.url || true)
fi
REPO_URL=${REPO_URL:-$DEFAULT_REPO_URL}
log "1. Installing base packages"
pacman_install \
2026-04-16 14:34:05 -03:00
git rustup protobuf abseil-cpp gcc clang llvm-libs compiler-rt evtest base-devel libpulse \
libpipewire pipewire pipewire-audio pipewire-alsa pipewire-jack pipewire-pulse wireplumber \
alsa-utils gst-plugin-pipewire \
2025-11-30 16:16:03 -03:00
gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav \
2026-04-30 08:16:57 -03:00
wmctrl qt6-tools wl-clipboard xclip xsel desktop-file-utils openssl
2025-11-30 16:16:03 -03:00
ensure_yay() {
if command -v yay >/dev/null 2>&1; then
if run_as_user yay --version >/dev/null 2>&1; then
return
fi
fi
run_as_user env TMPDIR="$TMPDIR" bash -c 'rm -rf "$TMPDIR/yay" &&
2026-04-16 13:54:25 -03:00
cd "$TMPDIR" && git clone --depth 1 https://aur.archlinux.org/yay.git &&
cd yay && makepkg -si --noconfirm'
}
log "1b. Installing grpcurl"
if sudo pacman -Si grpcurl >/dev/null 2>&1; then
pacman_install grpcurl
else
ensure_yay
if ! run_as_user yay -S --needed --noconfirm grpcurl-bin; then
log "grpcurl AUR install failed once, rebuilding yay and retrying"
ensure_yay
run_as_user yay -S --needed --noconfirm grpcurl-bin
fi
2025-11-30 16:16:03 -03:00
fi
# 1c. input access
log "1c. Ensuring input group access for $ORIG_USER"
2025-06-29 03:46:34 -05:00
sudo usermod -aG input "$ORIG_USER"
2025-06-01 13:31:22 -05:00
log "1d. Verifying runtime tools"
require_command pactl "libpulse"
2026-04-20 13:59:34 -03:00
require_command gst-inspect-1.0 "gstreamer"
require_command arecord "alsa-utils"
require_command speaker-test "alsa-utils"
require_command wmctrl "wmctrl"
require_command qdbus6 "qt6-tools"
2026-04-16 14:34:05 -03:00
require_command protoc "protobuf"
if ! command -v wl-paste >/dev/null 2>&1 \
&& ! command -v xclip >/dev/null 2>&1 \
&& ! command -v xsel >/dev/null 2>&1; then
echo "❌ no clipboard reader found after install (expected one of wl-clipboard/xclip/xsel)" >&2
exit 1
fi
2026-04-16 14:34:05 -03:00
require_linkable "$(command -v protoc)" "protoc"
if [[ -e /usr/lib/libclang.so ]]; then
require_linkable /usr/lib/libclang.so "libclang"
fi
2026-04-20 13:59:34 -03:00
require_kernel_module snd_usb_audio "USB microphones and USB headsets"
require_gst_element pulsesrc
require_gst_element pulsesink
require_gst_element pipewiresrc
report_client_media_acceleration
2026-04-16 14:34:05 -03:00
protoc --version >/dev/null
if ! run_as_user pactl info >/dev/null 2>&1; then
echo "⚠️ pactl is installed, but no PulseAudio/PipeWire Pulse server is reachable right now."
echo " Lesavka will still install, but local speaker/mic staging may stay empty until the host audio session is up."
fi
2025-06-01 21:58:47 -05:00
# 2. Rust tool-chain for both root & user
log "2. Ensuring Rust toolchain"
2025-06-01 21:58:47 -05:00
sudo rustup default stable
run_as_user rustup default stable
2025-06-01 21:58:47 -05:00
# 3. resolve the build source. Local checkouts are preferred so development
# installs do not silently rebuild an older /var/src clone.
resolve_source_checkout
2025-06-01 14:18:42 -05:00
2025-06-01 21:58:47 -05:00
# 4. build
log "4. Building client release binary"
run_as_user env TMPDIR="$TMPDIR" bash -c "cd '$SRC/client' && cargo clean && cargo build --release"
2025-06-01 21:58:47 -05:00
# 5. install binary
log "5. Installing launchable client binaries"
install_verified_executable "$SRC/target/release/lesavka-client" /usr/local/bin/lesavka-client "lesavka-client"
sudo ln -sf /usr/local/bin/lesavka-client /usr/local/bin/lesavka
sudo install -d -m 0755 -o "$ORIG_USER" -g "$ORIG_USER" "$USER_HOME/.local/bin"
sudo ln -sf /usr/local/bin/lesavka-client "$USER_HOME/.local/bin/lesavka-client"
sudo chown -h "$ORIG_USER":"$ORIG_USER" "$USER_HOME/.local/bin/lesavka-client"
2026-04-30 08:16:57 -03:00
install_client_pki_bundle
2026-04-30 11:38:16 -03:00
sudo install -d -m 0755 -o "$ORIG_USER" -g "$ORIG_USER" "$CLIENT_CAPTURE_DIR"
echo " ↪ capture folder: $CLIENT_CAPTURE_DIR"
log "6. Registering desktop application"
sudo install -Dm644 "$SRC/client/assets/icons/hicolor/1024x1024/apps/lesavka.png" \
/usr/share/icons/hicolor/1024x1024/apps/lesavka.png
sudo install -Dm644 "$SRC/client/assets/icons/hicolor/1024x1024/apps/lesavka.png" \
/usr/share/pixmaps/lesavka.png
sudo install -Dm644 "$SRC/client/assets/linux/lesavka.desktop" \
/usr/share/applications/lesavka.desktop
if command -v update-desktop-database >/dev/null 2>&1; then
sudo update-desktop-database /usr/share/applications
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
sudo gtk-update-icon-cache -f /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
2025-06-01 13:31:22 -05:00
log "7. Removing legacy auto-start service"
sudo systemctl disable --now lesavka-client.service >/dev/null 2>&1 || true
sudo rm -f /etc/systemd/system/lesavka-client.service
2025-06-08 14:40:15 -05:00
sudo systemctl daemon-reload
echo
echo "✅ lesavka-client install complete"
INSTALLED_VERSION=$(manifest_package_version "$SRC/client/Cargo.toml" 2>/dev/null || true)
INSTALLED_SHA=$(source_revision "$SRC")
if [[ -n ${INSTALLED_VERSION:-} ]]; then
echo "➡️ Installed: lesavka-client ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
fi
echo " Binary: /usr/local/bin/lesavka-client"
echo " Launch alias: /usr/local/bin/lesavka"
echo " User PATH alias: $USER_HOME/.local/bin/lesavka-client"
echo " Desktop entry: /usr/share/applications/lesavka.desktop"
echo " Build source: $SRC/target/release/lesavka-client"
2026-04-30 08:16:57 -03:00
echo " TLS identity: $CLIENT_PKI_DIR"
2026-04-30 11:38:16 -03:00
echo " Captures: $CLIENT_CAPTURE_DIR"
echo "✅ Installed version: lesavka-client ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
echo
echo "Quick start:"
echo " KDE menu: search for Lesavka"
echo " Terminal: /usr/local/bin/lesavka"