#!/usr/bin/env bash # Fresh Ubuntu 26.04 LTS -> my desktop, in one pass. Run as your user; it sudos as needed. # Safe to re-run: each step checks before it changes anything. Reboot when it finishes, # then run `sudo flatpak update` once so the Flatpak apps get the NVIDIA GL runtime. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Second NVMe (970 EVO Plus, label "Data"): SteamLibrary + emulator files. Never formatted. DATA_UUID=29db4bd3-b743-417d-aa39-4da4acc9df3f DATA_MNT=/mnt/data # server-marvin SMB share, mounted if you give it credentials. \040 is fstab's escaped space. MARVIN_SHARE='//192.168.1.193/Personal\040Files\040-\040ckoch' MARVIN_MNT=/mnt/server-marvin-personal MARVIN_CREDS=/etc/samba/credentials-marvin step() { printf '\n\033[1;34m== %s ==\033[0m\n' "$*"; } warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } apt_install() { sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"; } [[ $EUID -ne 0 ]] || die "run as your user, not with sudo" . /etc/os-release [[ $ID == ubuntu ]] || die "expected Ubuntu, found $ID" [[ $VERSION_ID == 26.04 ]] || warn "written for 26.04; this is $VERSION_ID — carrying on" CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME} # Ask for the password once and keep sudo alive for the whole run (Flatpaks take a while). sudo -v while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null & keepalive=$! trap 'kill "$keepalive" 2>/dev/null' EXIT # --------------------------------------------------------------------------------------- step "Apt repositories" sudo apt-get update apt_install curl gpg ca-certificates software-properties-common for c in universe multiverse restricted; do sudo add-apt-repository -y -n "$c"; done sudo dpkg --add-architecture i386 # Steam + 32-bit game libraries sudo install -d -m 0755 /etc/apt/keyrings # Docker CE — Docker's own repo, not the snap or Ubuntu's docker.io if [[ ! -f /etc/apt/sources.list.d/docker.sources ]]; then sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null </dev/null fi # VS Code — Microsoft's repo (was rpm-ostree-layered on Bazzite) if [[ ! -f /etc/apt/sources.list.d/vscode.sources ]]; then curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \ | sudo gpg --dearmor --yes -o /usr/share/keyrings/microsoft.gpg sudo tee /etc/apt/sources.list.d/vscode.sources >/dev/null </dev/null fi # Claude desktop — Anthropic's repo (ran inside an Ubuntu distrobox on Bazzite) if [[ ! -f /etc/apt/sources.list.d/claude-desktop.list ]]; then key=/usr/share/keyrings/claude-desktop-archive-keyring.asc sudo curl -fsSLo "$key" https://downloads.claude.ai/claude-desktop/key.asc if ! gpg --show-keys --with-colons "$key" 2>/dev/null \ | grep -q '^fpr:::::::::31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE:'; then sudo rm -f "$key"; die "Claude desktop signing key has the wrong fingerprint — not adding the repo" fi echo "deb [arch=amd64,arm64 signed-by=$key] https://downloads.claude.ai/claude-desktop/apt/stable stable main" \ | sudo tee /etc/apt/sources.list.d/claude-desktop.list >/dev/null fi sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y # --------------------------------------------------------------------------------------- step "Packages" # steam-installer refuses to install non-interactively until its license is accepted. echo 'steam-installer steam/question select I AGREE' | sudo debconf-set-selections echo 'steam-installer steam/license note ' | sudo debconf-set-selections apt_install \ build-essential cmake pkg-config git git-lfs wget jq zstd unzip p7zip-full rsync htop \ python3-venv python3-pip pipx ffmpeg cifs-utils flatpak \ steam-installer steam-devices gamemode mangohud lutris vulkan-tools \ docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \ nvidia-container-toolkit \ code claude-desktop # --------------------------------------------------------------------------------------- step "NVIDIA driver (open kernel modules — the only kind the RTX 5090 supports)" apt_install ubuntu-drivers-common installed_driver() { { dpkg-query -W -f='${Status} ${Package}\n' 'nvidia-driver-*' 2>/dev/null || true; } \ | awk '/^install ok installed / {print $4}' | sort -V | tail -1 } if [[ $(installed_driver) != *-open ]]; then ubuntu-drivers devices 2>/dev/null | grep -E 'model|driver' || true # Picks Ubuntu's recommended branch plus its prebuilt, signed kernel modules (no DKMS). sudo ubuntu-drivers install fi driver=$(installed_driver) [[ $driver == nvidia-driver-*-open ]] \ || die "got '${driver:-no driver}', but the 5090 needs an -open driver — check 'ubuntu-drivers devices'" branch=${driver#nvidia-driver-}; branch=${branch%-open} apt_install "libnvidia-gl-$branch:i386" # 32-bit GL/Vulkan for Steam and Proton echo "using $driver" # --------------------------------------------------------------------------------------- step "Docker, GPU containers, Claude's Cowork VM" sudo usermod -aG docker "$USER" # docker without sudo (after reboot) getent group kvm >/dev/null && sudo usermod -aG kvm "$USER" # Cowork needs /dev/kvm + vhost-vsock echo vhost_vsock | sudo tee /etc/modules-load.d/vhost_vsock.conf >/dev/null if ! grep -q nvidia /etc/docker/daemon.json 2>/dev/null; then sudo nvidia-ctk runtime configure --runtime=docker # enables `docker run --gpus all` sudo systemctl restart docker fi # --------------------------------------------------------------------------------------- step "Flatpak apps" sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo mapfile -t apps < <(sed -e 's/#.*//' -e 's/[[:space:]]//g' -e '/^$/d' "$REPO/flatpaks.txt") sudo flatpak install -y --noninteractive --system flathub "${apps[@]}" # Firefox's read-only peek at landingPage, as set by hand on Bazzite flatpak override --user org.mozilla.firefox --filesystem="$HOME/Documents/git/landingPage:ro" # Firefox/Thunderbird come from Flathub instead, so restoring ~/.var/app brings the old # profiles back untouched. Drop Ubuntu's snaps and the debs that pull them back in. for s in firefox thunderbird; do if snap list "$s" >/dev/null 2>&1; then sudo snap remove --purge "$s"; fi if dpkg -s "$s" >/dev/null 2>&1; then sudo apt-get remove -y "$s"; fi done xdg-settings set default-web-browser org.mozilla.firefox.desktop 2>/dev/null || true # Keep Flatpaks updated daily, like Bazzite did. Starts next boot. sudo tee /etc/systemd/system/flatpak-update.service >/dev/null <<'EOF' [Unit] Description=Update system Flatpaks Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/usr/bin/flatpak update --system --noninteractive --assumeyes EOF sudo tee /etc/systemd/system/flatpak-update.timer >/dev/null <<'EOF' [Unit] Description=Update system Flatpaks daily [Timer] OnBootSec=10min OnUnitActiveSec=1d Persistent=true [Install] WantedBy=timers.target EOF sudo systemctl daemon-reload sudo systemctl enable flatpak-update.timer # --------------------------------------------------------------------------------------- step "Git credentials in the keyring" # On Bazzite, KDE Wallet fed git your Gitea password through ksshaskpass. GNOME's keyring does # the same job via git's libsecret helper, which Ubuntu ships as source only — so build it. if ! git config --global --get credential.helper >/dev/null 2>&1; then helper="" src=/usr/share/doc/git/contrib/credential/libsecret if [[ -e $src/git-credential-libsecret.c || -e $src/git-credential-libsecret.c.gz ]]; then apt_install libsecret-1-dev libglib2.0-dev tmp=$(mktemp -d) if [[ -e $src/git-credential-libsecret.c.gz ]]; then zcat "$src/git-credential-libsecret.c.gz" > "$tmp/h.c" else cp "$src/git-credential-libsecret.c" "$tmp/h.c" fi read -ra cf < <(pkg-config --cflags libsecret-1 glib-2.0) read -ra lf < <(pkg-config --libs libsecret-1 glib-2.0) if cc -O2 "${cf[@]}" "$tmp/h.c" -o "$tmp/git-credential-libsecret" "${lf[@]}" 2>/dev/null; then sudo install -m 755 "$tmp/git-credential-libsecret" /usr/local/bin/ helper=/usr/local/bin/git-credential-libsecret fi rm -rf "$tmp" fi if [[ -n $helper ]]; then git config --global credential.helper "$helper" else git config --global credential.helper 'cache --timeout=604800' warn "no keyring helper — git will hold your Gitea password for a week at a time instead" fi fi step "Claude Code CLI" # No Node or .NET on the host by choice — LudosData and landingPage build in Docker. # If you want them back: sudo apt install mise dotnet-sdk-10.0 (mise needs ppa:jdxcode/mise). command -v claude >/dev/null || [[ -x ~/.local/bin/claude ]] || curl -fsSL https://claude.ai/install.sh | bash # --------------------------------------------------------------------------------------- step "Data drive + server-marvin share" sudo mkdir -p "$DATA_MNT" "$MARVIN_MNT" if ! grep -q "$DATA_UUID" /etc/fstab; then echo "UUID=$DATA_UUID $DATA_MNT ext4 defaults,nofail,x-gvfs-show,x-gvfs-name=Data 0 2" \ | sudo tee -a /etc/fstab >/dev/null fi sudo blkid -U "$DATA_UUID" >/dev/null || warn "Data drive ($DATA_UUID) not found — fstab entry is nofail, so boot is fine" if [[ ! -f $MARVIN_CREDS ]]; then echo "server-marvin SMB share — press Enter to skip mounting it." read -rp " username: " smb_user if [[ -n $smb_user ]]; then read -rsp " password: " smb_pass; echo sudo install -d -m 0755 /etc/samba printf 'username=%s\npassword=%s\n' "$smb_user" "$smb_pass" \ | sudo install -m 600 /dev/stdin "$MARVIN_CREDS" unset smb_pass fi fi if [[ -f $MARVIN_CREDS ]] && ! grep -q "$MARVIN_MNT" /etc/fstab; then echo "$MARVIN_SHARE $MARVIN_MNT cifs credentials=$MARVIN_CREDS,uid=$(id -u),gid=$(id -g),iocharset=utf8,vers=3.0,x-systemd.automount,_netdev,nofail 0 0" \ | sudo tee -a /etc/fstab >/dev/null fi sudo systemctl daemon-reload sudo mount -a || warn "a mount failed — check 'sudo mount -a' (is server-marvin up? right password?)" # --------------------------------------------------------------------------------------- step "Small carry-overs from Bazzite" # Xbox controllers over Bluetooth (Bazzite set this as a kernel argument) echo 'options bluetooth disable_ertm=1' | sudo tee /etc/modprobe.d/bluetooth-xbox.conf >/dev/null step "Done" cat <<'EOF' Reboot now — the NVIDIA driver and your docker/kvm groups only take effect after that. Then, once: sudo flatpak update # pulls the NVIDIA GL runtime the Flatpak apps need. # Skip it and apps render in software until the daily # timer catches up ~10 minutes after boot. Then sign in to things, and add /mnt/data/SteamLibrary in Steam > Settings > Storage. EOF