Ubuntu / Debian Setup Manager
A collection of hardened install scripts for a fresh Ubuntu / Debian desktop, driven by an interactive menu.sh. Each script auto-detects the distro, uses signed-by APT keyrings where applicable (no apt-key), and is idempotent (safe to re-run).
Quick start — run the menu
One-liner — fetches menu.sh and runs it. You'll see a numeric picker and can select one option, several, or all:
bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)"
How to use the picker:
- Enter one or more numbers separated by spaces, e.g.
1 6 11 0runs every option in order-1exits
The menu caches sudo credentials up-front so multi-task runs don't keep re-prompting, and falls back to per-user execution for the three scripts that must not run as root (JetBrains Toolbox, IBus Pinyin, Nerd Fonts).
Available scripts
| # | Script | Runs as | What it does |
|---|---|---|---|
| 1 | install-chrome.sh |
sudo | Installs Google Chrome Stable from Google's official APT repo using a signed-by keyring. |
| 2 | install-firefox.sh |
sudo | Removes the Firefox Snap and installs Firefox from Mozilla's official APT repo, with APT pinning so it stays on the Mozilla build. Verifies the Mozilla signing-key fingerprint. |
| 3 | install-thunderbird.sh |
sudo | Ubuntu: removes the Snap, adds the Mozilla Team PPA, and pins it. Debian: installs from the standard repos. |
| 4 | install-1password.sh |
sudo | Configures the 1Password APT repo with debsig signature policy. Arch-aware (amd64 / arm64). |
| 5 | install-espanso.sh |
sudo | Installs Espanso. Auto-detects Wayland vs X11 from $XDG_SESSION_TYPE and registers the systemd-user service as the invoking desktop user (not root). |
| 6 | install-libreoffice.sh |
sudo | Detects the latest stable release on documentfoundation.org, downloads the matching .deb tarball, verifies the published MD5, and installs. Purges the distro's libreoffice* first to avoid library conflicts (opt-out with --keep-distro-libreoffice). |
| 7 | install-obsidian.sh |
sudo | Installs the latest official Obsidian amd64 .deb from obsidianmd/obsidian-releases. SHA-256 verified against the GitHub release asset digest when available. Honors $GITHUB_TOKEN to avoid API rate limits. |
| 8 | install-drawio.sh |
sudo | Installs the latest official draw.io Desktop .deb from jgraph/drawio-desktop. SHA-256 verified against the GitHub release asset digest when available. Honors $GITHUB_TOKEN to avoid API rate limits. |
| 9 | install-vscode.sh |
sudo | Microsoft's official code APT repo, signed-by keyring. --insiders flag installs code-insiders instead. |
| 10 | install-jetbrains-toolbox.sh |
user | Per-user install into ~/.local/share/JetBrains/Toolbox. SHA-256 verified against JetBrains' release feed. x86_64 + aarch64. Drops a .desktop launcher. |
| 11 | install-bruno.sh |
sudo | Bruno API client from the official APT repo. Keyserver fetch is wrapped in a 5-attempt retry/backoff because keyserver.ubuntu.com is occasionally flaky. |
| 12 | install-ipatool.sh |
sudo | Installs the latest release of majd/ipatool from GitHub. SHA-256 verified against the release checksums.txt. Honors $GITHUB_TOKEN to avoid API rate limits. |
| 13 | install-qbittorrent.sh |
sudo | Installs the latest official qBittorrent x86_64 AppImage from qbittorrent/qBittorrent. SHA-256 verified against the GitHub release asset digest when available. Installs launcher, icons, and torrent/magnet handlers. |
| 14 | install-network_drive.sh |
sudo | Discovers SMB shares on a Synology NAS and adds them to /etc/fstab under /mnt/Synology with x-systemd.automount. fstab block is managed via begin/end markers so re-runs replace rather than duplicate. Credentials file is 0600. Best-effort GNOME Dock pin. |
| 15 | install-ibus-pinyin.sh |
user | Installs ibus-libpinyin and Simplified Chinese language packs, restarts the IBus daemon, and idempotently adds ('ibus', 'libpinyin') to GNOME's input sources via gsettings. |
| 16 | install-font.sh |
user | Installs the latest Ubuntu Sans Nerd Font and JetBrains Mono Nerd Font to ~/.local/share/fonts, then refreshes the font cache. No sudo needed. |
| 17 | timedatectl-fix.sh |
sudo | Sets the hardware clock to UTC to avoid time drift when dual-booting Linux and Windows. |
| 18 | install-localsend.sh |
sudo | Installs the latest official LocalSend .deb from localsend/localsend. SHA-256 verified against the GitHub release asset digest when available. |
| 19 | install-telegram.sh |
sudo | Installs the latest official Telegram Desktop Linux build from Telegram's latest download endpoint. Adds launcher and tg: URL handler. |
| 20 | install-discord.sh |
sudo | Installs the latest official Discord Linux .deb from Discord's latest download endpoint. |
"Runs as user" entries must be invoked as your normal desktop user, not via sudo. The other entries elevate via sudo internally and the menu primes sudo -v up-front, so you'll only be prompted once.
Running a single script directly
If you'd rather skip the menu, each script can be run on its own. Use the right invocation pattern for that script's privilege mode:
# sudo scripts (1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 17, 18, 19, 20) — pipe through sudo bash
curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-firefox.sh | sudo bash
# user scripts (10, 15, 16) — DO NOT use sudo; they install per-user
bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-font.sh)"
Most installers accept --help and --dry-run (the latter prints what would happen without executing). For example:
curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-libreoffice.sh \
| bash -s -- --help
What "hardened" means here
The hardened installers generally share this robustness baseline:
set -euo pipefail+ IFS hygiene +ERRtrap reporting the failing line number/etc/os-releasedistro auto-detection (no hardcoded codenames)dpkg --print-architecture/uname -mfor architecture (amd64 / arm64 / armhf / x86_64 / aarch64 as applicable to each upstream)- Idempotent — re-running a script does not duplicate APT sources, fstab entries, gsettings entries, or
.desktopfiles signed-bykeyrings in/etc/apt/keyrings(no deprecatedapt-key add)- Checksum verification where the upstream publishes one (JetBrains SHA-256, LibreOffice MD5, Obsidian SHA-256, ipatool SHA-256, draw.io SHA-256, qBittorrent SHA-256, LocalSend SHA-256)
- Per-user installers refuse to run as root; system installers refuse to run as non-root
Supported distros
- Ubuntu (any modern release; some scripts target Ubuntu 25.10 specifically but work elsewhere)
- Debian (most scripts;
install-thunderbird.shtakes a different code path since Debian has no PPAs)
Other distros are rejected up-front rather than failing later in unpredictable ways.
| 1 | #!/usr/bin/env bash |
| 2 | # install-1password.sh — Install 1Password desktop from the official 1Password APT repo. |
| 3 | # Hardened: arch-aware, signed-by keyring, debsig verification dir, idempotent, --dry-run. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | IFS=$'\n\t' |
| 7 | |
| 8 | readonly SCRIPT_NAME="${0##*/}" |
| 9 | DRY_RUN=0 |
| 10 | |
| 11 | usage() { |
| 12 | cat <<EOF |
| 13 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 14 | |
| 15 | Configures the official 1Password APT repository (with debsig policy and |
| 16 | keyring) and installs the 1Password desktop application. Idempotent: safe to |
| 17 | re-run. |
| 18 | |
| 19 | Options: |
| 20 | --dry-run Print actions without executing. |
| 21 | --help, -h Show this help. |
| 22 | EOF |
| 23 | } |
| 24 | |
| 25 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 26 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 27 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 28 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 29 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 30 | |
| 31 | while (( $# )); do |
| 32 | case "$1" in |
| 33 | --dry-run) DRY_RUN=1 ;; |
| 34 | -h|--help) usage; exit 0 ;; |
| 35 | *) die "Unknown argument: $1 (try --help)" ;; |
| 36 | esac |
| 37 | shift |
| 38 | done |
| 39 | |
| 40 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 41 | |
| 42 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 43 | # shellcheck disable=SC1091 |
| 44 | . /etc/os-release |
| 45 | case "${ID:-}:${ID_LIKE:-}" in |
| 46 | *ubuntu*|*debian*) : ;; |
| 47 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 48 | esac |
| 49 | |
| 50 | # 1Password publishes builds for amd64 and arm64. |
| 51 | ARCH="$(dpkg --print-architecture)" |
| 52 | case "$ARCH" in |
| 53 | amd64|arm64) : ;; |
| 54 | *) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;; |
| 55 | esac |
| 56 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 57 | |
| 58 | export DEBIAN_FRONTEND=noninteractive |
| 59 | |
| 60 | KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg |
| 61 | SOURCES=/etc/apt/sources.list.d/1password.list |
| 62 | DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22 |
| 63 | DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22 |
| 64 | |
| 65 | log "Installing prerequisites..." |
| 66 | run "apt-get update -qq" |
| 67 | run "apt-get install -y curl gpg ca-certificates" |
| 68 | |
| 69 | log "Configuring 1Password APT repository..." |
| 70 | run "install -d -m 0755 /etc/apt/keyrings" |
| 71 | if [[ ! -s "$KEYRING" ]]; then |
| 72 | run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'" |
| 73 | run "chmod 0644 '$KEYRING'" |
| 74 | fi |
| 75 | |
| 76 | DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main" |
| 77 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 78 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 79 | fi |
| 80 | |
| 81 | log "Installing debsig policy (verifies package signatures on install)..." |
| 82 | run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'" |
| 83 | if [[ ! -s "${DEBSIG_POLICY_DIR}/1password.pol" ]]; then |
| 84 | run "curl -fsSL https://downloads.1password.com/linux/debian/debsig/1password.pol -o '${DEBSIG_POLICY_DIR}/1password.pol'" |
| 85 | fi |
| 86 | if [[ ! -s "${DEBSIG_KEYRING_DIR}/debsig.gpg" ]]; then |
| 87 | run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '${DEBSIG_KEYRING_DIR}/debsig.gpg'" |
| 88 | fi |
| 89 | |
| 90 | log "Installing 1Password..." |
| 91 | run "apt-get update -qq" |
| 92 | run "apt-get install -y 1password" |
| 93 | |
| 94 | if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then |
| 95 | log "1Password binary available at: $(command -v 1password)" |
| 96 | fi |
| 97 | log "Done." |
| 98 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-bruno.sh — Install Bruno API client from the official APT repository. |
| 3 | # Hardened: arch-aware, signed-by keyring with retries (keyserver flake), idempotent, |
| 4 | # clean desktop entry, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | |
| 12 | usage() { |
| 13 | cat <<EOF |
| 14 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 15 | |
| 16 | Configures the official Bruno APT repository (signed-by keyring fetched from |
| 17 | keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to |
| 18 | re-run. |
| 19 | |
| 20 | Options: |
| 21 | --dry-run Print actions without executing. |
| 22 | --help, -h Show this help. |
| 23 | EOF |
| 24 | } |
| 25 | |
| 26 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 27 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 28 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 29 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 30 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 31 | |
| 32 | while (( $# )); do |
| 33 | case "$1" in |
| 34 | --dry-run) DRY_RUN=1 ;; |
| 35 | -h|--help) usage; exit 0 ;; |
| 36 | *) die "Unknown argument: $1 (try --help)" ;; |
| 37 | esac |
| 38 | shift |
| 39 | done |
| 40 | |
| 41 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 42 | |
| 43 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 44 | # shellcheck disable=SC1091 |
| 45 | . /etc/os-release |
| 46 | case "${ID:-}:${ID_LIKE:-}" in |
| 47 | *ubuntu*|*debian*) : ;; |
| 48 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 49 | esac |
| 50 | |
| 51 | ARCH="$(dpkg --print-architecture)" |
| 52 | [[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)." |
| 53 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 54 | |
| 55 | export DEBIAN_FRONTEND=noninteractive |
| 56 | |
| 57 | KEYRING=/etc/apt/keyrings/bruno.gpg |
| 58 | SOURCES=/etc/apt/sources.list.d/bruno.list |
| 59 | KEY_ID="9FA6017ECABE0266" |
| 60 | DESKTOP=/usr/share/applications/bruno.desktop |
| 61 | |
| 62 | log "Installing prerequisites..." |
| 63 | run "apt-get update -qq" |
| 64 | run "apt-get install -y curl gpg ca-certificates" |
| 65 | |
| 66 | log "Configuring Bruno APT repository..." |
| 67 | run "install -d -m 0755 /etc/apt/keyrings" |
| 68 | |
| 69 | if [[ ! -s "$KEYRING" ]]; then |
| 70 | # keyserver.ubuntu.com is occasionally flaky — retry a few times. |
| 71 | ok=0 |
| 72 | for attempt in 1 2 3 4 5; do |
| 73 | if (( DRY_RUN )); then |
| 74 | printf ' DRY-RUN: fetch key 0x%s (attempt %d)\n' "$KEY_ID" "$attempt" |
| 75 | ok=1 |
| 76 | break |
| 77 | fi |
| 78 | if curl -fsSL --max-time 30 "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${KEY_ID}" \ |
| 79 | | gpg --dearmor -o "$KEYRING" 2>/dev/null && [[ -s "$KEYRING" ]]; then |
| 80 | ok=1 |
| 81 | break |
| 82 | fi |
| 83 | warn "Key fetch failed (attempt $attempt/5); retrying in $((attempt*2))s..." |
| 84 | sleep "$((attempt*2))" |
| 85 | done |
| 86 | (( ok )) || die "Could not retrieve Bruno signing key after 5 attempts." |
| 87 | run "chmod 0644 '$KEYRING'" |
| 88 | fi |
| 89 | |
| 90 | DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable" |
| 91 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 92 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 93 | fi |
| 94 | |
| 95 | log "Installing Bruno..." |
| 96 | run "apt-get update -qq" |
| 97 | run "apt-get install -y bruno" |
| 98 | |
| 99 | log "Writing desktop entry..." |
| 100 | if (( DRY_RUN )); then |
| 101 | printf ' DRY-RUN: write %s\n' "$DESKTOP" |
| 102 | else |
| 103 | cat >"$DESKTOP" <<'EOF' |
| 104 | [Desktop Entry] |
| 105 | Name=Bruno |
| 106 | Comment=Open-source API Client |
| 107 | Exec=bruno %U |
| 108 | Terminal=false |
| 109 | Type=Application |
| 110 | Icon=bruno |
| 111 | Categories=Development;Utility; |
| 112 | StartupNotify=true |
| 113 | EOF |
| 114 | chmod 0644 "$DESKTOP" |
| 115 | fi |
| 116 | |
| 117 | log "Done. Bruno installed." |
| 118 |
| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # install-chrome.sh — Native Google Chrome Installer for Ubuntu/Debian |
| 4 | # Downloads and installs the latest official stable release without Snap. |
| 5 | # |
| 6 | # Usage: |
| 7 | # sudo ./install-chrome.sh |
| 8 | |
| 9 | set -euo pipefail |
| 10 | IFS=$'\n\t' |
| 11 | |
| 12 | # Ensure the script is run as root |
| 13 | if (( EUID != 0 )); then |
| 14 | echo "[ERROR] This script must be run as root (via sudo)." >&2 |
| 15 | exit 1 |
| 16 | fi |
| 17 | |
| 18 | echo "[1/4] Installing necessary prerequisites..." |
| 19 | apt-get update -y |
| 20 | apt-get install -y curl gnupg2 ca-certificates |
| 21 | |
| 22 | echo "[2/4] Setting up Google's official repository keys..." |
| 23 | # Create directory for Third-Party APT keys if it doesn't exist |
| 24 | install -d -m 0755 /etc/apt/keyrings |
| 25 | |
| 26 | # Fetch the official signing key and save it securely |
| 27 | curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | \ |
| 28 | gpg --dearmor --yes -o /etc/apt/keyrings/google-chrome.gpg |
| 29 | |
| 30 | # Configure the APT repository to strictly use that signing key |
| 31 | echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \ |
| 32 | > /etc/apt/sources.list.d/google-chrome.list |
| 33 | |
| 34 | echo "[3/4] Fetching package lists and installing Google Chrome..." |
| 35 | apt-get update -y |
| 36 | apt-get install -y google-chrome-stable |
| 37 | |
| 38 | echo "[4/4] Verifying installation..." |
| 39 | if command -v google-chrome &>/dev/null; then |
| 40 | echo "------------------------------------------------------" |
| 41 | echo "SUCCESS: Google Chrome has been installed successfully!" |
| 42 | echo "Version: $(google-chrome --version)" |
| 43 | echo "------------------------------------------------------" |
| 44 | else |
| 45 | echo "[ERROR] Google Chrome installation could not be verified." >&2 |
| 46 | exit 1 |
| 47 | fi |
| 1 | #!/usr/bin/env bash |
| 2 | # install-discord.sh — Install Discord from the latest official Linux .deb. |
| 3 | # Hardened: official latest endpoint, idempotent, --dry-run. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | IFS=$'\n\t' |
| 7 | |
| 8 | readonly SCRIPT_NAME="${0##*/}" |
| 9 | DRY_RUN=0 |
| 10 | DOWNLOAD_URL="https://discord.com/api/download?platform=linux&format=deb" |
| 11 | |
| 12 | usage() { |
| 13 | cat <<EOF |
| 14 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 15 | |
| 16 | Downloads Discord from Discord's official latest Linux .deb endpoint and |
| 17 | installs it with apt-get. |
| 18 | |
| 19 | Options: |
| 20 | --dry-run Print actions without executing. |
| 21 | --help, -h Show this help. |
| 22 | EOF |
| 23 | } |
| 24 | |
| 25 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 26 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 27 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 28 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 29 | |
| 30 | while (( $# )); do |
| 31 | case "$1" in |
| 32 | --dry-run) DRY_RUN=1 ;; |
| 33 | -h|--help) usage; exit 0 ;; |
| 34 | *) die "Unknown argument: $1 (try --help)" ;; |
| 35 | esac |
| 36 | shift |
| 37 | done |
| 38 | |
| 39 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 40 | |
| 41 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 42 | # shellcheck disable=SC1091 |
| 43 | . /etc/os-release |
| 44 | case "${ID:-}:${ID_LIKE:-}" in |
| 45 | *ubuntu*|*debian*) : ;; |
| 46 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 47 | esac |
| 48 | |
| 49 | ARCH="$(dpkg --print-architecture)" |
| 50 | [[ "$ARCH" == "amd64" ]] || die "Discord's official Linux .deb targets amd64 only (detected: $ARCH)." |
| 51 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 52 | |
| 53 | export DEBIAN_FRONTEND=noninteractive |
| 54 | log "Installing prerequisites..." |
| 55 | run "apt-get update -qq" |
| 56 | run "apt-get install -y curl ca-certificates" |
| 57 | |
| 58 | STAGE="$(mktemp -d -t discord.XXXXXX)" |
| 59 | trap 'rm -rf "$STAGE"' EXIT |
| 60 | DEB="$STAGE/discord.deb" |
| 61 | |
| 62 | log "Downloading latest Discord .deb..." |
| 63 | run "curl -fL -o '$DEB' '$DOWNLOAD_URL'" |
| 64 | |
| 65 | if (( DRY_RUN )); then |
| 66 | printf ' DRY-RUN: apt-get install -y %s\n' "$DEB" |
| 67 | log "Done." |
| 68 | exit 0 |
| 69 | fi |
| 70 | |
| 71 | log "Installing Discord..." |
| 72 | run "apt-get install -y '$DEB'" |
| 73 | |
| 74 | if command -v discord >/dev/null 2>&1; then |
| 75 | log "Installed: $(discord --version 2>/dev/null || dpkg-query -W -f='${Version}' discord 2>/dev/null || echo Discord)" |
| 76 | else |
| 77 | log "Installed: $(dpkg-query -W -f='${Version}' discord 2>/dev/null || echo Discord)" |
| 78 | fi |
| 79 | log "Done." |
| 80 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-drawio.sh — Install draw.io Desktop from the latest official GitHub release. |
| 3 | # Hardened: arch-aware, GitHub API token support, SHA-256 verification from the |
| 4 | # release asset digest, idempotent, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | REPO="jgraph/drawio-desktop" |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 16 | |
| 17 | Resolves the latest release of jgraph/drawio-desktop, downloads the official |
| 18 | Linux .deb package for your architecture, verifies its SHA-256 against the |
| 19 | GitHub release asset digest when available, and installs it with apt-get. |
| 20 | |
| 21 | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the |
| 22 | GitHub API request (avoids rate limits). |
| 23 | |
| 24 | Options: |
| 25 | --dry-run Print actions without executing. |
| 26 | --help, -h Show this help. |
| 27 | EOF |
| 28 | } |
| 29 | |
| 30 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 31 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 32 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 33 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 34 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 35 | |
| 36 | while (( $# )); do |
| 37 | case "$1" in |
| 38 | --dry-run) DRY_RUN=1 ;; |
| 39 | -h|--help) usage; exit 0 ;; |
| 40 | *) die "Unknown argument: $1 (try --help)" ;; |
| 41 | esac |
| 42 | shift |
| 43 | done |
| 44 | |
| 45 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 46 | |
| 47 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 48 | # shellcheck disable=SC1091 |
| 49 | . /etc/os-release |
| 50 | case "${ID:-}:${ID_LIKE:-}" in |
| 51 | *ubuntu*|*debian*) : ;; |
| 52 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 53 | esac |
| 54 | |
| 55 | ARCH="$(dpkg --print-architecture)" |
| 56 | case "$ARCH" in |
| 57 | amd64|arm64) : ;; |
| 58 | *) die "draw.io Desktop publishes Linux .deb assets for amd64 and arm64 only (detected: $ARCH)." ;; |
| 59 | esac |
| 60 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 61 | |
| 62 | export DEBIAN_FRONTEND=noninteractive |
| 63 | log "Installing prerequisites..." |
| 64 | run "apt-get update -qq" |
| 65 | run "apt-get install -y curl jq ca-certificates" |
| 66 | |
| 67 | GH_HDRS=(-H "Accept: application/vnd.github+json") |
| 68 | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") |
| 69 | |
| 70 | log "Querying GitHub API for latest release of $REPO..." |
| 71 | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" |
| 72 | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" |
| 73 | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." |
| 74 | log "Latest release: $TAG" |
| 75 | |
| 76 | ASSET_REGEX="^drawio-${ARCH}-[^/]+\\.deb$" |
| 77 | DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 78 | ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .name' <<<"$RELEASE_JSON" | head -n1)" |
| 79 | EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)" |
| 80 | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No $ARCH .deb asset found in latest draw.io Desktop release." |
| 81 | |
| 82 | STAGE="$(mktemp -d -t drawio.XXXXXX)" |
| 83 | trap 'rm -rf "$STAGE"' EXIT |
| 84 | DEB="$STAGE/$ASSET_NAME" |
| 85 | |
| 86 | log "Downloading $ASSET_NAME..." |
| 87 | run "curl -fsSL -o '$DEB' '$DOWNLOAD_URL'" |
| 88 | |
| 89 | if (( DRY_RUN )); then |
| 90 | [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \ |
| 91 | && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \ |
| 92 | || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n' |
| 93 | printf ' DRY-RUN: apt-get install -y %s\n' "$DEB" |
| 94 | log "Done." |
| 95 | exit 0 |
| 96 | fi |
| 97 | |
| 98 | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then |
| 99 | log "Verifying SHA-256..." |
| 100 | ACTUAL_SHA="$(sha256sum "$DEB" | awk '{print $1}')" |
| 101 | [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA" |
| 102 | log "SHA-256 ok." |
| 103 | else |
| 104 | warn "No SHA-256 asset digest in GitHub release metadata; skipping verification." |
| 105 | fi |
| 106 | |
| 107 | log "Installing draw.io Desktop..." |
| 108 | run "apt-get install -y '$DEB'" |
| 109 | |
| 110 | if (( ! DRY_RUN )) && command -v drawio >/dev/null 2>&1; then |
| 111 | log "Installed: $(drawio --version 2>/dev/null || echo "$TAG")" |
| 112 | fi |
| 113 | log "Done." |
| 114 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-espanso.sh — Install Espanso (text expander) with Wayland or X11 build. |
| 3 | # Auto-detects session type; falls back to X11 if Wayland is not active. |
| 4 | # Hardened: handles apt temp file permissions, package collisions, and ghost processes. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | FORCE_VARIANT="" # "wayland" | "x11" |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help] |
| 16 | |
| 17 | Installs Espanso from the latest GitHub release. Auto-detects whether to use |
| 18 | the Wayland or X11 build based on \$XDG_SESSION_TYPE of the invoking user |
| 19 | (SUDO_USER). The systemd-user service is registered for that user, not root. |
| 20 | |
| 21 | Options: |
| 22 | --wayland Force the Wayland build. |
| 23 | --x11 Force the X11 build. |
| 24 | --dry-run Print actions without executing. |
| 25 | --help, -h Show this help. |
| 26 | EOF |
| 27 | } |
| 28 | |
| 29 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 30 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 31 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 32 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 33 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 34 | |
| 35 | while (( $# )); do |
| 36 | case "$1" in |
| 37 | --dry-run) DRY_RUN=1 ;; |
| 38 | --wayland) FORCE_VARIANT=wayland ;; |
| 39 | --x11) FORCE_VARIANT=x11 ;; |
| 40 | -h|--help) usage; exit 0 ;; |
| 41 | *) die "Unknown argument: $1 (try --help)" ;; |
| 42 | esac |
| 43 | shift |
| 44 | done |
| 45 | |
| 46 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 47 | |
| 48 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 49 | # shellcheck disable=SC1091 |
| 50 | . /etc/os-release |
| 51 | case "${ID:-}:${ID_LIKE:-}" in |
| 52 | *ubuntu*|*debian*) : ;; |
| 53 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 54 | esac |
| 55 | |
| 56 | ARCH="$(dpkg --print-architecture)" |
| 57 | [[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)." |
| 58 | |
| 59 | ACTUAL_USER="${SUDO_USER:-${USER:-}}" |
| 60 | [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user; cannot register the service as root." |
| 61 | USER_ID="$(id -u "$ACTUAL_USER")" |
| 62 | |
| 63 | # Detect session type from the invoking user's environment, fallback to env, fallback to wayland |
| 64 | if [[ -n "$FORCE_VARIANT" ]]; then |
| 65 | VARIANT="$FORCE_VARIANT" |
| 66 | else |
| 67 | SESSION_TYPE="$(sudo -u "$ACTUAL_USER" -i printenv XDG_SESSION_TYPE 2>/dev/null || true)" |
| 68 | [[ -z "$SESSION_TYPE" ]] && SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}" |
| 69 | case "$SESSION_TYPE" in |
| 70 | wayland) VARIANT=wayland ;; |
| 71 | x11|tty) VARIANT=x11 ;; |
| 72 | *) warn "Unknown XDG_SESSION_TYPE='$SESSION_TYPE'; defaulting to wayland."; VARIANT=wayland ;; |
| 73 | esac |
| 74 | fi |
| 75 | log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT" |
| 76 | |
| 77 | export DEBIAN_FRONTEND=noninteractive |
| 78 | TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)" |
| 79 | # Ensure the _apt user can read the temporary file to prevent permission denied errors |
| 80 | chmod 644 "$TEMP_DEB" |
| 81 | trap 'rm -f "$TEMP_DEB"' EXIT |
| 82 | |
| 83 | DEB_NAME="espanso-debian-${VARIANT}-amd64.deb" |
| 84 | URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}" |
| 85 | |
| 86 | log "Installing prerequisites..." |
| 87 | run "apt-get update -qq" |
| 88 | run "apt-get install -y wget libcap2-bin psmisc" |
| 89 | |
| 90 | log "Downloading ${DEB_NAME}..." |
| 91 | run "wget -qO '$TEMP_DEB' '$URL'" |
| 92 | |
| 93 | log "Removing conflicting Espanso packages (if any)..." |
| 94 | # Suppress output and errors if packages don't exist |
| 95 | run "apt-get remove -y espanso espanso-wayland >/dev/null 2>&1 || true" |
| 96 | |
| 97 | log "Installing package..." |
| 98 | run "apt-get install -y '$TEMP_DEB'" |
| 99 | |
| 100 | ESPANSO_BIN="$(command -v espanso || true)" |
| 101 | [[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install." |
| 102 | |
| 103 | if [[ "$VARIANT" == "wayland" ]]; then |
| 104 | log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..." |
| 105 | run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'" |
| 106 | fi |
| 107 | |
| 108 | log "Clearing ghost processes to prevent start timeouts..." |
| 109 | run "sudo -u '$ACTUAL_USER' killall espanso 2>/dev/null || true" |
| 110 | |
| 111 | log "Registering & starting espanso service for $ACTUAL_USER..." |
| 112 | # Register may fail if already registered — treat that as success. |
| 113 | if (( DRY_RUN )); then |
| 114 | printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso service register || true\n' "$ACTUAL_USER" "$USER_ID" |
| 115 | printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso start || true\n' "$ACTUAL_USER" "$USER_ID" |
| 116 | else |
| 117 | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso service register || true |
| 118 | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso restart || \ |
| 119 | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso start || true |
| 120 | fi |
| 121 | |
| 122 | log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER." |
| 123 | if [[ "$VARIANT" == "wayland" ]]; then |
| 124 | log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml" |
| 125 | fi |
| 1 | #!/usr/bin/env bash |
| 2 | # install-firefox.sh — Install Firefox from Mozilla's official APT repository. |
| 3 | # Removes the Snap transition package and pins Mozilla as the source of truth. |
| 4 | # Hardened: distro-detect, idempotent, signed-by keyring, ERR trap, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | ASSUME_YES=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help] |
| 16 | |
| 17 | Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the |
| 18 | official Mozilla APT repository (signed-by keyring + APT pin), then installs |
| 19 | Firefox so it auto-updates from Mozilla. |
| 20 | |
| 21 | Options: |
| 22 | --dry-run Print the actions without executing them. |
| 23 | --yes, -y Pass -y to apt for non-interactive install. |
| 24 | --help, -h Show this help. |
| 25 | EOF |
| 26 | } |
| 27 | |
| 28 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 29 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 30 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 31 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 32 | |
| 33 | on_err() { local rc=$? line=$1; printf '\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n' "${SCRIPT_NAME%.sh}" "$line" "$rc" >&2; } |
| 34 | trap 'on_err $LINENO' ERR |
| 35 | |
| 36 | while (( $# )); do |
| 37 | case "$1" in |
| 38 | --dry-run) DRY_RUN=1 ;; |
| 39 | -y|--yes) ASSUME_YES=1 ;; |
| 40 | -h|--help) usage; exit 0 ;; |
| 41 | *) die "Unknown argument: $1 (try --help)" ;; |
| 42 | esac |
| 43 | shift |
| 44 | done |
| 45 | |
| 46 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 47 | |
| 48 | [[ -r /etc/os-release ]] || die "/etc/os-release not found; cannot detect distro." |
| 49 | # shellcheck disable=SC1091 |
| 50 | . /etc/os-release |
| 51 | case "${ID:-}:${ID_LIKE:-}" in |
| 52 | *ubuntu*|*debian*) : ;; |
| 53 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;; |
| 54 | esac |
| 55 | log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})" |
| 56 | |
| 57 | APT_YES=() |
| 58 | (( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use |
| 59 | export DEBIAN_FRONTEND=noninteractive |
| 60 | |
| 61 | KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc |
| 62 | SOURCES=/etc/apt/sources.list.d/mozilla.list |
| 63 | PIN=/etc/apt/preferences.d/mozilla |
| 64 | |
| 65 | log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..." |
| 66 | if command -v snap >/dev/null 2>&1; then |
| 67 | run "snap disable firefox >/dev/null 2>&1 || true" |
| 68 | run "snap remove --purge firefox >/dev/null 2>&1 || true" |
| 69 | fi |
| 70 | run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true" |
| 71 | run "rm -f /usr/bin/firefox" |
| 72 | |
| 73 | log "Installing prerequisites (wget, gpg, ca-certificates)..." |
| 74 | run "apt-get update -qq" |
| 75 | run "apt-get install ${APT_YES[*]} wget gpg ca-certificates" |
| 76 | |
| 77 | log "Configuring Mozilla APT repository..." |
| 78 | run "install -d -m 0755 /etc/apt/keyrings" |
| 79 | if [[ ! -s "$KEYRING" ]]; then |
| 80 | run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg" |
| 81 | run "chmod 0644 '$KEYRING'" |
| 82 | else |
| 83 | log "Keyring already present at $KEYRING (skipping download)." |
| 84 | fi |
| 85 | |
| 86 | # Verify key fingerprint matches Mozilla's published fingerprint |
| 87 | EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3" |
| 88 | ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')" |
| 89 | if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then |
| 90 | warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually." |
| 91 | else |
| 92 | log "Mozilla key fingerprint verified." |
| 93 | fi |
| 94 | |
| 95 | DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main' |
| 96 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 97 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 98 | fi |
| 99 | |
| 100 | log "Pinning Mozilla repo to priority 1000..." |
| 101 | if [[ ! -f "$PIN" ]] || ! grep -q 'origin packages.mozilla.org' "$PIN"; then |
| 102 | if (( DRY_RUN )); then |
| 103 | printf ' DRY-RUN: write %s\n' "$PIN" |
| 104 | else |
| 105 | cat >"$PIN" <<'EOF' |
| 106 | Package: * |
| 107 | Pin: origin packages.mozilla.org |
| 108 | Pin-Priority: 1000 |
| 109 | EOF |
| 110 | fi |
| 111 | fi |
| 112 | |
| 113 | log "Installing Firefox from Mozilla repo..." |
| 114 | run "apt-get update -qq" |
| 115 | run "apt-get install ${APT_YES[*]} firefox" |
| 116 | |
| 117 | if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then |
| 118 | log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')" |
| 119 | fi |
| 120 | |
| 121 | log "Done. Firefox installed from Mozilla APT and will auto-update." |
| 122 |
| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | set -euo pipefail |
| 4 | IFS=$'\n\t' |
| 5 | |
| 6 | readonly SCRIPT_NAME="${0##*/}" |
| 7 | readonly FONT_BASE_DIR="$HOME/.local/share/fonts" |
| 8 | readonly TMP_DIR="$(mktemp -d)" |
| 9 | |
| 10 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 11 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 12 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 13 | |
| 14 | cleanup() { |
| 15 | rm -rf "$TMP_DIR" |
| 16 | } |
| 17 | trap cleanup EXIT |
| 18 | |
| 19 | if (( EUID == 0 )); then |
| 20 | die "Do not run as root. Run as your normal desktop user so fonts install under $HOME." |
| 21 | fi |
| 22 | |
| 23 | for tool in curl unzip fc-cache find; do |
| 24 | command -v "$tool" >/dev/null 2>&1 || die "Missing required tool: $tool" |
| 25 | done |
| 26 | |
| 27 | latest_nerd_font_url() { |
| 28 | local asset_name="$1" |
| 29 | local url |
| 30 | |
| 31 | url="$( |
| 32 | curl -fsSL https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \ |
| 33 | | grep "browser_download_url.*${asset_name}" \ |
| 34 | | cut -d '"' -f 4 \ |
| 35 | | head -n 1 \ |
| 36 | || true |
| 37 | )" |
| 38 | |
| 39 | [[ -n "$url" ]] || die "Could not find download URL for $asset_name. Check connectivity or GitHub API limits." |
| 40 | printf '%s' "$url" |
| 41 | } |
| 42 | |
| 43 | install_nerd_font() { |
| 44 | local display_name="$1" |
| 45 | local asset_name="$2" |
| 46 | local font_dir="$3" |
| 47 | local download_url="$4" |
| 48 | local zip_path="$TMP_DIR/${asset_name}.zip" |
| 49 | local extract_dir="$TMP_DIR/${asset_name}" |
| 50 | local installed_count |
| 51 | |
| 52 | if [[ -z "$download_url" ]]; then |
| 53 | log "Finding latest $display_name release..." |
| 54 | download_url="$(latest_nerd_font_url "${asset_name}.zip")" |
| 55 | fi |
| 56 | |
| 57 | log "Downloading $display_name..." |
| 58 | curl -fL --retry 3 --retry-delay 2 -o "$zip_path" "$download_url" |
| 59 | |
| 60 | log "Extracting $display_name..." |
| 61 | mkdir -p "$extract_dir" "$font_dir" |
| 62 | unzip -q -o "$zip_path" -d "$extract_dir" |
| 63 | |
| 64 | installed_count="$( |
| 65 | find "$extract_dir" -type f \( -name '*.ttf' -o -name '*.otf' \) \ |
| 66 | -exec cp {} "$font_dir/" \; \ |
| 67 | -printf '.' \ |
| 68 | | wc -c |
| 69 | )" |
| 70 | |
| 71 | [[ "$installed_count" -gt 0 ]] || die "No font files found in ${asset_name}.zip" |
| 72 | find "$font_dir" -type f \( -name '*.ttf' -o -name '*.otf' \) -exec chmod 0644 {} + |
| 73 | log "Installed $installed_count font files to $font_dir" |
| 74 | } |
| 75 | |
| 76 | mkdir -p "$FONT_BASE_DIR" |
| 77 | |
| 78 | install_nerd_font \ |
| 79 | "Ubuntu Sans Nerd Font" \ |
| 80 | "UbuntuSans" \ |
| 81 | "$FONT_BASE_DIR/UbuntuSans" \ |
| 82 | "" |
| 83 | |
| 84 | install_nerd_font \ |
| 85 | "JetBrains Mono Nerd Font" \ |
| 86 | "JetBrainsMono" \ |
| 87 | "$FONT_BASE_DIR/JetBrainsMonoNerdFont" \ |
| 88 | "" |
| 89 | |
| 90 | log "Updating font cache..." |
| 91 | fc-cache -f "$FONT_BASE_DIR" |
| 92 | |
| 93 | if command -v fc-match >/dev/null 2>&1; then |
| 94 | log "Resolved terminal font: $(fc-match 'JetBrainsMono Nerd Font Mono')" |
| 95 | fi |
| 96 | |
| 97 | log "Done. Configure your terminal to use JetBrainsMono Nerd Font Mono for Starship Powerline symbols." |
| 98 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-ibus-rime.sh — Install IBus + Rime and add it |
| 3 | # to GNOME's Input Sources. Tuned for Ubuntu 26.04 (GNOME 50 / Wayland) but |
| 4 | # works on any modern Ubuntu/Debian GNOME desktop. |
| 5 | # |
| 6 | # Hardened: distro-detect, runs as the desktop user (not root) for gsettings, |
| 7 | # uses sudo only for apt steps, idempotent re-runs, ERR trap, --dry-run. |
| 8 | |
| 9 | set -euo pipefail |
| 10 | IFS=$'\n\t' |
| 11 | |
| 12 | readonly SCRIPT_NAME="${0##*/}" |
| 13 | DRY_RUN=0 |
| 14 | SKIP_GSETTINGS=0 |
| 15 | |
| 16 | usage() { |
| 17 | cat <<EOF |
| 18 | Usage: $SCRIPT_NAME [--dry-run] [--skip-gsettings] [--help] |
| 19 | |
| 20 | Purges old ibus-libpinyin, installs ibus-rime (plus Chinese language packs), |
| 21 | configures Rime to default to Simplified Pinyin, and registers 'Rime' |
| 22 | as a GNOME input source. Idempotent. |
| 23 | |
| 24 | Do NOT run with sudo: gsettings is per-user and must run as the desktop user. |
| 25 | The script invokes sudo internally only for apt-get steps. |
| 26 | |
| 27 | Options: |
| 28 | --skip-gsettings Install packages but don't touch GNOME input sources |
| 29 | (useful on non-GNOME desktops; configure manually afterwards). |
| 30 | --dry-run Print actions without executing. |
| 31 | --help, -h Show this help. |
| 32 | |
| 33 | Switch input methods at runtime with: Super + Space |
| 34 | EOF |
| 35 | } |
| 36 | |
| 37 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 38 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 39 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 40 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 41 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 42 | |
| 43 | while (( $# )); do |
| 44 | case "$1" in |
| 45 | --dry-run) DRY_RUN=1 ;; |
| 46 | --skip-gsettings) SKIP_GSETTINGS=1 ;; |
| 47 | -h|--help) usage; exit 0 ;; |
| 48 | *) die "Unknown argument: $1 (try --help)" ;; |
| 49 | esac |
| 50 | shift |
| 51 | done |
| 52 | |
| 53 | # Resolve the desktop user. If invoked via sudo, gsettings must target SUDO_USER. |
| 54 | if (( EUID == 0 )); then |
| 55 | DESKTOP_USER="${SUDO_USER:-}" |
| 56 | [[ -n "$DESKTOP_USER" && "$DESKTOP_USER" != "root" ]] \ |
| 57 | || die "Run as a normal user (the script will call sudo itself for apt). gsettings can't run as root." |
| 58 | else |
| 59 | DESKTOP_USER="$USER" |
| 60 | fi |
| 61 | |
| 62 | # Look up the user's UID and Home Directory for configs and DBus. |
| 63 | USER_ENTRY="$(getent passwd "$DESKTOP_USER")" || die "User '$DESKTOP_USER' not found in passwd." |
| 64 | USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")" |
| 65 | USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")" |
| 66 | |
| 67 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 68 | # shellcheck disable=SC1091 |
| 69 | . /etc/os-release |
| 70 | case "${ID:-}:${ID_LIKE:-}" in |
| 71 | *ubuntu*|*debian*) : ;; |
| 72 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;; |
| 73 | esac |
| 74 | log "Detected: ${PRETTY_NAME:-unknown}, desktop user: $DESKTOP_USER" |
| 75 | |
| 76 | # Helper: invoke a command as $DESKTOP_USER with a working DBus address. |
| 77 | run_as_user() { |
| 78 | local cmd=("$@") |
| 79 | if (( DRY_RUN )); then |
| 80 | printf ' DRY-RUN (as %s): %s\n' "$DESKTOP_USER" "${cmd[*]}" |
| 81 | return 0 |
| 82 | fi |
| 83 | if (( EUID == 0 )); then |
| 84 | sudo -u "$DESKTOP_USER" \ |
| 85 | DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ |
| 86 | XDG_RUNTIME_DIR="/run/user/$USER_ID" \ |
| 87 | "${cmd[@]}" |
| 88 | else |
| 89 | "${cmd[@]}" |
| 90 | fi |
| 91 | } |
| 92 | |
| 93 | # Helper: invoke a command with sudo when the caller is non-root. |
| 94 | sudo_run() { |
| 95 | if (( DRY_RUN )); then |
| 96 | printf ' DRY-RUN (sudo): %s\n' "$*" |
| 97 | return 0 |
| 98 | fi |
| 99 | if (( EUID == 0 )); then "$@"; else sudo "$@"; fi |
| 100 | } |
| 101 | |
| 102 | export DEBIAN_FRONTEND=noninteractive |
| 103 | |
| 104 | log "Removing old ibus-libpinyin if present..." |
| 105 | sudo_run apt-get remove --purge -y ibus-libpinyin || true |
| 106 | |
| 107 | log "Installing IBus Rime + Chinese language packs..." |
| 108 | sudo_run apt-get update -qq |
| 109 | sudo_run apt-get install -y \ |
| 110 | ibus \ |
| 111 | ibus-rime \ |
| 112 | language-pack-zh-hans \ |
| 113 | language-pack-gnome-zh-hans |
| 114 | |
| 115 | log "Configuring Rime to permanently default to Simplified Pinyin..." |
| 116 | RIME_DIR="$USER_HOME/.config/ibus/rime" |
| 117 | if (( DRY_RUN )); then |
| 118 | printf ' DRY-RUN (as %s): Create %s/default.custom.yaml\n' "$DESKTOP_USER" "$RIME_DIR" |
| 119 | else |
| 120 | if (( EUID == 0 )); then |
| 121 | sudo -u "$DESKTOP_USER" mkdir -p "$RIME_DIR" |
| 122 | sudo -u "$DESKTOP_USER" tee "$RIME_DIR/default.custom.yaml" > /dev/null <<'EOF' |
| 123 | patch: |
| 124 | schema_list: |
| 125 | - schema: luna_pinyin_simp |
| 126 | EOF |
| 127 | else |
| 128 | mkdir -p "$RIME_DIR" |
| 129 | tee "$RIME_DIR/default.custom.yaml" > /dev/null <<'EOF' |
| 130 | patch: |
| 131 | schema_list: |
| 132 | - schema: luna_pinyin_simp |
| 133 | EOF |
| 134 | fi |
| 135 | fi |
| 136 | |
| 137 | # Make sure ibus-daemon picks up the new engines and config for the user. |
| 138 | if command -v ibus >/dev/null 2>&1; then |
| 139 | log "Restarting ibus-daemon for $DESKTOP_USER..." |
| 140 | # `ibus exit` will fail if no daemon is running — treat as non-fatal. |
| 141 | run_as_user ibus exit >/dev/null 2>&1 || true |
| 142 | # Start fresh in the background; -drx replaces a running daemon. |
| 143 | if (( ! DRY_RUN )); then |
| 144 | sudo -u "$DESKTOP_USER" \ |
| 145 | DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ |
| 146 | XDG_RUNTIME_DIR="/run/user/$USER_ID" \ |
| 147 | sh -c 'nohup ibus-daemon -drx >/dev/null 2>&1 &' || \ |
| 148 | warn "ibus-daemon restart returned non-zero (non-fatal)." |
| 149 | sleep 1 |
| 150 | fi |
| 151 | fi |
| 152 | |
| 153 | if (( SKIP_GSETTINGS )); then |
| 154 | log "Skipping GNOME input-sources update (--skip-gsettings)." |
| 155 | log "Done. Add 'Chinese (Rime)' manually under Settings → Keyboard → Input Sources." |
| 156 | exit 0 |
| 157 | fi |
| 158 | |
| 159 | # Only manage gsettings if GNOME schemas are present. |
| 160 | if ! run_as_user gsettings list-schemas 2>/dev/null | grep -q '^org.gnome.desktop.input-sources$'; then |
| 161 | warn "GNOME schema org.gnome.desktop.input-sources not found; skipping gsettings update." |
| 162 | log "Done. Add 'Chinese (Rime)' manually in your DE's input settings." |
| 163 | exit 0 |
| 164 | fi |
| 165 | |
| 166 | log "Adding 'Rime' to GNOME Input Sources (idempotent)..." |
| 167 | |
| 168 | CURRENT_SOURCES="$(run_as_user gsettings get org.gnome.desktop.input-sources sources 2>/dev/null || echo '[]')" |
| 169 | # Strip the optional "@as " type annotation gvariant sometimes prepends. |
| 170 | CLEAN_SOURCES="${CURRENT_SOURCES#@as }" |
| 171 | |
| 172 | if [[ "$CLEAN_SOURCES" == *"'ibus', 'rime'"* ]]; then |
| 173 | log "Rime already present in input sources — nothing to do." |
| 174 | else |
| 175 | if [[ "$CLEAN_SOURCES" == *"'ibus', 'libpinyin'"* ]]; then |
| 176 | # Dynamically replace libpinyin with rime |
| 177 | NEW_SOURCES="${CLEAN_SOURCES//\'libpinyin\'/\'rime\'}" |
| 178 | elif [[ -z "$CLEAN_SOURCES" || "$CLEAN_SOURCES" == "[]" || "$CLEAN_SOURCES" == "@as []" ]]; then |
| 179 | NEW_SOURCES="[('xkb', 'us'), ('ibus', 'rime')]" |
| 180 | else |
| 181 | # Insert the rime tuple before the closing bracket of the existing list. |
| 182 | NEW_SOURCES="${CLEAN_SOURCES%]*}, ('ibus', 'rime')]" |
| 183 | fi |
| 184 | run_as_user gsettings set org.gnome.desktop.input-sources sources "$NEW_SOURCES" |
| 185 | log "Input sources updated to include: ('ibus', 'rime')" |
| 186 | fi |
| 187 | |
| 188 | log "---" |
| 189 | log "Done! Rime is installed and configured for Simplified Pinyin." |
| 190 | log "Switch input methods with: Super + Space" |
| 191 | log "If 'Chinese (Rime)' doesn't appear in the top bar right away, log out and back in." |
| 1 | #!/usr/bin/env bash |
| 2 | # install-ipatool.sh — Install ipatool from the latest GitHub release. |
| 3 | # Hardened: arch detection (amd64/arm64), GitHub API token support, SHA-256 verification |
| 4 | # from the release checksum file, atomic install to /usr/local/bin, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | REPO="majd/ipatool" |
| 12 | INSTALL_PATH="/usr/local/bin/ipatool" |
| 13 | |
| 14 | usage() { |
| 15 | cat <<EOF |
| 16 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 17 | |
| 18 | Resolves the latest release of majd/ipatool, downloads the tarball for your |
| 19 | architecture, verifies its SHA-256 against the published checksums.txt, and |
| 20 | installs the binary to /usr/local/bin/ipatool atomically. |
| 21 | |
| 22 | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the |
| 23 | GitHub API request (avoids rate limits). |
| 24 | |
| 25 | Options: |
| 26 | --dry-run Print actions without executing. |
| 27 | --help, -h Show this help. |
| 28 | EOF |
| 29 | } |
| 30 | |
| 31 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 32 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 33 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 34 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 35 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 36 | |
| 37 | while (( $# )); do |
| 38 | case "$1" in |
| 39 | --dry-run) DRY_RUN=1 ;; |
| 40 | -h|--help) usage; exit 0 ;; |
| 41 | *) die "Unknown argument: $1 (try --help)" ;; |
| 42 | esac |
| 43 | shift |
| 44 | done |
| 45 | |
| 46 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 47 | |
| 48 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 49 | # shellcheck disable=SC1091 |
| 50 | . /etc/os-release |
| 51 | log "Detected: ${PRETTY_NAME:-unknown}" |
| 52 | |
| 53 | ARCH_RAW="$(uname -m)" |
| 54 | case "$ARCH_RAW" in |
| 55 | x86_64) GO_ARCH=amd64 ;; |
| 56 | aarch64) GO_ARCH=arm64 ;; |
| 57 | armv7l|armv6l) GO_ARCH=arm ;; |
| 58 | *) die "Unsupported architecture: $ARCH_RAW" ;; |
| 59 | esac |
| 60 | ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz" |
| 61 | |
| 62 | export DEBIAN_FRONTEND=noninteractive |
| 63 | log "Installing prerequisites..." |
| 64 | run "apt-get update -qq" |
| 65 | run "apt-get install -y curl jq tar ca-certificates libsecret-1-0" |
| 66 | |
| 67 | GH_HDRS=(-H "Accept: application/vnd.github+json") |
| 68 | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") |
| 69 | |
| 70 | log "Querying GitHub API for latest release of $REPO..." |
| 71 | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" |
| 72 | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" |
| 73 | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." |
| 74 | log "Latest release: $TAG" |
| 75 | |
| 76 | DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 77 | CHECKSUM_URL="$(jq -r '.assets[] | select(.name | test("checksums?\\.txt$")) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 78 | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No release asset matching '*${ASSET_SUFFIX}'." |
| 79 | |
| 80 | STAGE="$(mktemp -d -t ipatool.XXXXXX)" |
| 81 | trap 'rm -rf "$STAGE"' EXIT |
| 82 | TARBALL="$STAGE/ipatool.tar.gz" |
| 83 | |
| 84 | log "Downloading $(basename "$DOWNLOAD_URL")..." |
| 85 | run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'" |
| 86 | |
| 87 | if [[ -n "$CHECKSUM_URL" ]]; then |
| 88 | log "Verifying SHA-256..." |
| 89 | EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk -v f="$(basename "$DOWNLOAD_URL")" '$2 ~ f || $2 == "*"f {print $1; exit}')" |
| 90 | ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')" |
| 91 | if [[ -n "$EXPECTED" && "$EXPECTED" != "$ACTUAL" ]]; then |
| 92 | die "SHA-256 mismatch: expected=$EXPECTED actual=$ACTUAL" |
| 93 | fi |
| 94 | [[ -n "$EXPECTED" ]] && log "SHA-256 ok." || warn "Asset not listed in checksums.txt; skipping." |
| 95 | else |
| 96 | warn "No checksums.txt in release; skipping SHA-256 verification." |
| 97 | fi |
| 98 | |
| 99 | log "Extracting..." |
| 100 | run "tar -xzf '$TARBALL' -C '$STAGE'" |
| 101 | BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)" |
| 102 | if [[ -z "$BINARY_PATH" ]]; then |
| 103 | # Some releases ship the binary without +x; relax the find |
| 104 | BINARY_PATH="$(find "$STAGE" -type f -name ipatool -not -name '*.tar.gz' | head -n1 || true)" |
| 105 | fi |
| 106 | [[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive." |
| 107 | |
| 108 | log "Installing to $INSTALL_PATH..." |
| 109 | run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'" |
| 110 | |
| 111 | if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then |
| 112 | log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)" |
| 113 | fi |
| 114 | log "Done." |
| 115 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-jetbrains-toolbox.sh — Install JetBrains Toolbox into the invoking user's home. |
| 3 | # Hardened: dependency check w/ auto-install, SHA-256 verification against JetBrains' |
| 4 | # release metadata, idempotent (replaces atomically), .desktop entry, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | ASSUME_YES=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help] |
| 16 | |
| 17 | Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and |
| 18 | creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is |
| 19 | verified against the SHA-256 published in JetBrains' release feed. |
| 20 | |
| 21 | Do NOT run with sudo: Toolbox is a per-user install. If a missing system |
| 22 | package (curl/jq/tar/libfuse2) needs installing, the script will call sudo |
| 23 | just for that step. |
| 24 | |
| 25 | Options: |
| 26 | --yes, -y Auto-confirm prompts for installing missing system packages. |
| 27 | --dry-run Print actions without executing. |
| 28 | --help, -h Show this help. |
| 29 | EOF |
| 30 | } |
| 31 | |
| 32 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 33 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 34 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 35 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 36 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 37 | |
| 38 | while (( $# )); do |
| 39 | case "$1" in |
| 40 | --dry-run) DRY_RUN=1 ;; |
| 41 | -y|--yes) ASSUME_YES=1 ;; |
| 42 | -h|--help) usage; exit 0 ;; |
| 43 | *) die "Unknown argument: $1 (try --help)" ;; |
| 44 | esac |
| 45 | shift |
| 46 | done |
| 47 | |
| 48 | (( EUID != 0 )) || die "Do NOT run as root. Toolbox is a per-user install." |
| 49 | |
| 50 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 51 | # shellcheck disable=SC1091 |
| 52 | . /etc/os-release |
| 53 | case "${ID:-}:${ID_LIKE:-}" in |
| 54 | *ubuntu*|*debian*) PKG_MGR=apt ;; |
| 55 | *fedora*|*rhel*) PKG_MGR=dnf ;; |
| 56 | *arch*) PKG_MGR=pacman ;; |
| 57 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 58 | esac |
| 59 | log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)" |
| 60 | |
| 61 | confirm() { |
| 62 | (( ASSUME_YES )) && return 0 |
| 63 | read -rp "$1 [y/N] " ans |
| 64 | [[ "$ans" =~ ^[Yy] ]] |
| 65 | } |
| 66 | |
| 67 | ensure_pkg() { |
| 68 | local pkg="$1" probe="$2" |
| 69 | if eval "$probe" >/dev/null 2>&1; then return 0; fi |
| 70 | if ! confirm "Package '$pkg' is missing. Install via sudo $PKG_MGR?"; then |
| 71 | die "'$pkg' is required." |
| 72 | fi |
| 73 | case "$PKG_MGR" in |
| 74 | apt) run "sudo apt-get update -qq && sudo apt-get install -y '$pkg'" ;; |
| 75 | dnf) run "sudo dnf install -y '$pkg'" ;; |
| 76 | pacman) run "sudo pacman -Sy --noconfirm '$pkg'" ;; |
| 77 | esac |
| 78 | } |
| 79 | |
| 80 | # Map deps: command-or-package-probe |
| 81 | ensure_pkg curl "command -v curl" |
| 82 | ensure_pkg jq "command -v jq" |
| 83 | ensure_pkg tar "command -v tar" |
| 84 | case "$PKG_MGR" in |
| 85 | apt) ensure_pkg libfuse2 "dpkg -s libfuse2" ;; |
| 86 | dnf) ensure_pkg fuse-libs "rpm -q fuse-libs" ;; |
| 87 | pacman) ensure_pkg fuse2 "pacman -Q fuse2" ;; |
| 88 | esac |
| 89 | |
| 90 | ARCH_RAW="$(uname -m)" |
| 91 | case "$ARCH_RAW" in |
| 92 | x86_64) TOOLBOX_ARCH_KEY=linux ;; |
| 93 | aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;; |
| 94 | *) die "Unsupported architecture: $ARCH_RAW" ;; |
| 95 | esac |
| 96 | |
| 97 | log "Querying JetBrains release feed..." |
| 98 | RELEASE_JSON="$(curl -fsSL 'https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release' \ |
| 99 | -H 'Origin: https://www.jetbrains.com' \ |
| 100 | -H 'Referer: https://www.jetbrains.com/toolbox/download/')" |
| 101 | |
| 102 | TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")" |
| 103 | TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")" |
| 104 | TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")" |
| 105 | [[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed." |
| 106 | log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)" |
| 107 | |
| 108 | INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox" |
| 109 | STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)" |
| 110 | trap 'rm -rf "$STAGE_DIR"' EXIT |
| 111 | |
| 112 | TARBALL="$STAGE_DIR/toolbox.tar.gz" |
| 113 | log "Downloading..." |
| 114 | run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'" |
| 115 | |
| 116 | if [[ -n "$TOOLBOX_SHA" ]]; then |
| 117 | log "Verifying SHA-256..." |
| 118 | EXPECTED_SHA="$(curl -fsSL "$TOOLBOX_SHA" | awk '{print $1}')" |
| 119 | ACTUAL_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')" |
| 120 | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then |
| 121 | die "SHA-256 mismatch! expected=$EXPECTED_SHA actual=$ACTUAL_SHA" |
| 122 | fi |
| 123 | log "SHA-256 ok." |
| 124 | else |
| 125 | warn "No checksum URL in release feed; skipping verification." |
| 126 | fi |
| 127 | |
| 128 | log "Extracting to $INSTALL_DIR..." |
| 129 | run "mkdir -p '$INSTALL_DIR'" |
| 130 | run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'" |
| 131 | |
| 132 | BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox" |
| 133 | DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop" |
| 134 | ICON_PATH="$INSTALL_DIR/bin/toolbox-tray-color.png" |
| 135 | |
| 136 | [[ -x "$BIN_PATH" || $DRY_RUN -eq 1 ]] || die "Toolbox binary missing after extract." |
| 137 | |
| 138 | # Optional ~/bin symlink |
| 139 | if [[ -d "$HOME/bin" ]]; then |
| 140 | run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'" |
| 141 | fi |
| 142 | |
| 143 | # .desktop launcher |
| 144 | APPS_DIR="$HOME/.local/share/applications" |
| 145 | run "mkdir -p '$APPS_DIR'" |
| 146 | DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop" |
| 147 | if [[ -f "$DESKTOP_SRC" ]] || (( DRY_RUN )); then |
| 148 | run "cp '$DESKTOP_SRC' '$DESKTOP_DST'" |
| 149 | run "sed -i 's|^Exec=.*|Exec=$BIN_PATH %u|' '$DESKTOP_DST'" |
| 150 | if [[ -f "$ICON_PATH" ]]; then |
| 151 | run "sed -i 's|^Icon=.*|Icon=$ICON_PATH|' '$DESKTOP_DST'" |
| 152 | fi |
| 153 | run "chmod 0755 '$DESKTOP_DST'" |
| 154 | log "Desktop entry installed: $DESKTOP_DST" |
| 155 | else |
| 156 | warn "No .desktop file in archive; skipping launcher." |
| 157 | fi |
| 158 | |
| 159 | log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR" |
| 160 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-libreoffice.sh — Install the latest stable LibreOffice from documentfoundation.org. |
| 3 | # Hardened: arch detection, version JSON when available with HTML fallback, MD5 verification, |
| 4 | # uninstalls bundled distro libreoffice* first to avoid conflicts, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | SKIP_REMOVE_DISTRO=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help] |
| 16 | |
| 17 | Detects the latest stable LibreOffice version on download.documentfoundation.org, |
| 18 | downloads the matching DEB tarball for your architecture, verifies the MD5 sum |
| 19 | published alongside it, and installs the .deb packages. |
| 20 | |
| 21 | Options: |
| 22 | --keep-distro-libreoffice Don't purge any pre-installed distro libreoffice* |
| 23 | (default: purge to avoid library conflicts). |
| 24 | --dry-run Print actions without executing. |
| 25 | --help, -h Show this help. |
| 26 | EOF |
| 27 | } |
| 28 | |
| 29 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 30 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 31 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 32 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 33 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 34 | |
| 35 | while (( $# )); do |
| 36 | case "$1" in |
| 37 | --dry-run) DRY_RUN=1 ;; |
| 38 | --keep-distro-libreoffice) SKIP_REMOVE_DISTRO=1 ;; |
| 39 | -h|--help) usage; exit 0 ;; |
| 40 | *) die "Unknown argument: $1 (try --help)" ;; |
| 41 | esac |
| 42 | shift |
| 43 | done |
| 44 | |
| 45 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 46 | |
| 47 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 48 | # shellcheck disable=SC1091 |
| 49 | . /etc/os-release |
| 50 | case "${ID:-}:${ID_LIKE:-}" in |
| 51 | *ubuntu*|*debian*) : ;; |
| 52 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;; |
| 53 | esac |
| 54 | |
| 55 | ARCH_RAW="$(uname -m)" |
| 56 | case "$ARCH_RAW" in |
| 57 | x86_64) LO_ARCH=x86_64; LO_SUFFIX=Linux_x86-64 ;; |
| 58 | aarch64) LO_ARCH=aarch64; LO_SUFFIX=Linux_aarch64 ;; |
| 59 | *) die "Unsupported architecture: $ARCH_RAW (LibreOffice ships x86_64 and aarch64)." ;; |
| 60 | esac |
| 61 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW" |
| 62 | |
| 63 | export DEBIAN_FRONTEND=noninteractive |
| 64 | |
| 65 | log "Installing prerequisites..." |
| 66 | run "apt-get update -qq" |
| 67 | run "apt-get install -y curl wget tar coreutils ca-certificates" |
| 68 | |
| 69 | log "Resolving latest stable LibreOffice version..." |
| 70 | INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)" |
| 71 | LATEST_VERSION="$(printf '%s\n' "$INDEX" \ |
| 72 | | grep -oP '(?<=href=")[0-9]+\.[0-9]+\.[0-9]+(?=/")' \ |
| 73 | | sort -V | tail -1 || true)" |
| 74 | [[ -n "$LATEST_VERSION" ]] || die "Could not detect latest version from index page." |
| 75 | log "Latest stable: $LATEST_VERSION" |
| 76 | |
| 77 | BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}" |
| 78 | ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz" |
| 79 | URL="${BASE}/${ARCHIVE_NAME}" |
| 80 | SUM_URL="${URL}.md5" |
| 81 | |
| 82 | STAGE="$(mktemp -d -t lo.XXXXXX)" |
| 83 | trap 'rm -rf "$STAGE"' EXIT |
| 84 | ARCHIVE="$STAGE/$ARCHIVE_NAME" |
| 85 | |
| 86 | log "Downloading $ARCHIVE_NAME..." |
| 87 | run "wget -q --show-progress -O '$ARCHIVE' '$URL'" |
| 88 | |
| 89 | log "Verifying MD5..." |
| 90 | if EXPECTED_MD5="$(curl -fsSL "$SUM_URL" 2>/dev/null | awk '{print $1}')" && [[ -n "$EXPECTED_MD5" ]]; then |
| 91 | ACTUAL_MD5="$(md5sum "$ARCHIVE" | awk '{print $1}')" |
| 92 | [[ "$EXPECTED_MD5" == "$ACTUAL_MD5" ]] || die "MD5 mismatch: expected=$EXPECTED_MD5 actual=$ACTUAL_MD5" |
| 93 | log "MD5 ok." |
| 94 | else |
| 95 | warn "No MD5 published for $SUM_URL; skipping checksum." |
| 96 | fi |
| 97 | |
| 98 | log "Extracting..." |
| 99 | run "tar -xzf '$ARCHIVE' -C '$STAGE'" |
| 100 | DEBS_DIR="$(find "$STAGE" -maxdepth 3 -type d -name DEBS | head -n1 || true)" |
| 101 | [[ -n "$DEBS_DIR" || $DRY_RUN -eq 1 ]] || die "DEBS/ directory not found in archive." |
| 102 | |
| 103 | if (( ! SKIP_REMOVE_DISTRO )); then |
| 104 | if dpkg -l 'libreoffice*' 2>/dev/null | awk '/^ii/ {print $2}' | grep -q .; then |
| 105 | log "Removing distro-supplied libreoffice* packages to avoid conflicts..." |
| 106 | run "apt-get remove --purge -y 'libreoffice*'" |
| 107 | fi |
| 108 | fi |
| 109 | |
| 110 | log "Installing .deb packages..." |
| 111 | run "dpkg -i -R '$DEBS_DIR'" |
| 112 | log "Resolving any missing dependencies..." |
| 113 | run "apt-get install -f -y" |
| 114 | |
| 115 | log "Done. LibreOffice $LATEST_VERSION installed." |
| 116 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-localsend.sh — Install LocalSend from the latest official GitHub release. |
| 3 | # Hardened: arch-aware, GitHub API token support, SHA-256 verification from the |
| 4 | # release asset digest, idempotent, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | REPO="localsend/localsend" |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 16 | |
| 17 | Resolves the latest release of localsend/localsend, downloads the official |
| 18 | Linux .deb package for your architecture, verifies its SHA-256 against the |
| 19 | GitHub release asset digest when available, and installs it with apt-get. |
| 20 | |
| 21 | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the |
| 22 | GitHub API request (avoids rate limits). |
| 23 | |
| 24 | Options: |
| 25 | --dry-run Print actions without executing. |
| 26 | --help, -h Show this help. |
| 27 | EOF |
| 28 | } |
| 29 | |
| 30 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 31 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 32 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 33 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 34 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 35 | |
| 36 | while (( $# )); do |
| 37 | case "$1" in |
| 38 | --dry-run) DRY_RUN=1 ;; |
| 39 | -h|--help) usage; exit 0 ;; |
| 40 | *) die "Unknown argument: $1 (try --help)" ;; |
| 41 | esac |
| 42 | shift |
| 43 | done |
| 44 | |
| 45 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 46 | |
| 47 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 48 | # shellcheck disable=SC1091 |
| 49 | . /etc/os-release |
| 50 | case "${ID:-}:${ID_LIKE:-}" in |
| 51 | *ubuntu*|*debian*) : ;; |
| 52 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 53 | esac |
| 54 | |
| 55 | ARCH="$(dpkg --print-architecture)" |
| 56 | case "$ARCH" in |
| 57 | amd64) ASSET_ARCH="x86-64" ;; |
| 58 | arm64) ASSET_ARCH="arm-64" ;; |
| 59 | *) die "LocalSend publishes Linux .deb assets for amd64 and arm64 only (detected: $ARCH)." ;; |
| 60 | esac |
| 61 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 62 | |
| 63 | export DEBIAN_FRONTEND=noninteractive |
| 64 | log "Installing prerequisites..." |
| 65 | run "apt-get update -qq" |
| 66 | run "apt-get install -y curl jq ca-certificates" |
| 67 | |
| 68 | GH_HDRS=(-H "Accept: application/vnd.github+json") |
| 69 | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") |
| 70 | |
| 71 | log "Querying GitHub API for latest release of $REPO..." |
| 72 | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" |
| 73 | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" |
| 74 | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." |
| 75 | log "Latest release: $TAG" |
| 76 | |
| 77 | ASSET_REGEX="^LocalSend-[^-]+-linux-${ASSET_ARCH}\\.deb$" |
| 78 | DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 79 | ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .name' <<<"$RELEASE_JSON" | head -n1)" |
| 80 | EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)" |
| 81 | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No $ARCH .deb asset found in latest LocalSend release." |
| 82 | |
| 83 | STAGE="$(mktemp -d -t localsend.XXXXXX)" |
| 84 | trap 'rm -rf "$STAGE"' EXIT |
| 85 | DEB="$STAGE/$ASSET_NAME" |
| 86 | |
| 87 | log "Downloading $ASSET_NAME..." |
| 88 | run "curl -fL -o '$DEB' '$DOWNLOAD_URL'" |
| 89 | |
| 90 | if (( DRY_RUN )); then |
| 91 | [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \ |
| 92 | && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \ |
| 93 | || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n' |
| 94 | printf ' DRY-RUN: apt-get install -y %s\n' "$DEB" |
| 95 | log "Done." |
| 96 | exit 0 |
| 97 | fi |
| 98 | |
| 99 | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then |
| 100 | log "Verifying SHA-256..." |
| 101 | ACTUAL_SHA="$(sha256sum "$DEB" | awk '{print $1}')" |
| 102 | [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA" |
| 103 | log "SHA-256 ok." |
| 104 | else |
| 105 | warn "No SHA-256 asset digest in GitHub release metadata; skipping verification." |
| 106 | fi |
| 107 | |
| 108 | log "Installing LocalSend..." |
| 109 | run "apt-get install -y '$DEB'" |
| 110 | |
| 111 | if command -v localsend_app >/dev/null 2>&1; then |
| 112 | log "Installed: $(localsend_app --version 2>/dev/null || echo "$TAG")" |
| 113 | else |
| 114 | log "Installed: $TAG" |
| 115 | fi |
| 116 | log "Done." |
| 117 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-network_drive.sh — Auto-mount Synology (SMB/CIFS) shares under /mnt/Synology. |
| 3 | # Hardened: safe input handling, no eval-on-username, hostname validation, |
| 4 | # fstab managed via begin/end markers (idempotent re-run), creds file 0600, |
| 5 | # GNOME dock pinning is best-effort. |
| 6 | |
| 7 | set -euo pipefail |
| 8 | IFS=$'\n\t' |
| 9 | |
| 10 | readonly SCRIPT_NAME="${0##*/}" |
| 11 | DRY_RUN=0 |
| 12 | SERVER_ADDR="" |
| 13 | SMB_USER="" |
| 14 | SMB_PASS="" |
| 15 | MASTER_DIR="/mnt/Synology" |
| 16 | |
| 17 | usage() { |
| 18 | cat <<EOF |
| 19 | Usage: sudo $SCRIPT_NAME [options] |
| 20 | |
| 21 | Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they |
| 22 | auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort) |
| 23 | pins it to the top of the GNOME Dock. |
| 24 | |
| 25 | Options: |
| 26 | --server HOST_OR_IP Synology address (skips prompt). |
| 27 | --user USERNAME SMB username (skips prompt). |
| 28 | --password-stdin Read SMB password from stdin (skips prompt). |
| 29 | (Otherwise the script prompts on the controlling tty.) |
| 30 | --mount-root DIR Override parent directory (default: $MASTER_DIR). |
| 31 | --dry-run Print actions without executing. |
| 32 | --help, -h Show this help. |
| 33 | EOF |
| 34 | } |
| 35 | |
| 36 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 37 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 38 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 39 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 40 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 41 | |
| 42 | PASS_FROM_STDIN=0 |
| 43 | while (( $# )); do |
| 44 | case "$1" in |
| 45 | --server) SERVER_ADDR="${2:?}"; shift ;; |
| 46 | --user) SMB_USER="${2:?}"; shift ;; |
| 47 | --password-stdin) PASS_FROM_STDIN=1 ;; |
| 48 | --mount-root) MASTER_DIR="${2:?}"; shift ;; |
| 49 | --dry-run) DRY_RUN=1 ;; |
| 50 | -h|--help) usage; exit 0 ;; |
| 51 | *) die "Unknown argument: $1 (try --help)" ;; |
| 52 | esac |
| 53 | shift |
| 54 | done |
| 55 | |
| 56 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 57 | |
| 58 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 59 | # shellcheck disable=SC1091 |
| 60 | . /etc/os-release |
| 61 | case "${ID:-}:${ID_LIKE:-}" in |
| 62 | *ubuntu*|*debian*) : ;; |
| 63 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 64 | esac |
| 65 | |
| 66 | ACTUAL_USER="${SUDO_USER:-${USER:-}}" |
| 67 | [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user." |
| 68 | # Use getent so we don't depend on eval/~expansion or shell glob safety. |
| 69 | USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd." |
| 70 | USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")" |
| 71 | USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")" |
| 72 | USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")" |
| 73 | |
| 74 | export DEBIAN_FRONTEND=noninteractive |
| 75 | log "Installing cifs-utils, smbclient..." |
| 76 | run "apt-get update -qq" |
| 77 | run "apt-get install -y cifs-utils smbclient" |
| 78 | |
| 79 | # --- Collect creds --- |
| 80 | [[ -n "$SERVER_ADDR" ]] || read -rp "Synology NAS address or IP: " SERVER_ADDR </dev/tty |
| 81 | SERVER_ADDR="${SERVER_ADDR#smb://}" |
| 82 | SERVER_ADDR="${SERVER_ADDR#//}" |
| 83 | SERVER_ADDR="${SERVER_ADDR%/}" |
| 84 | [[ "$SERVER_ADDR" =~ ^[A-Za-z0-9._-]+$ ]] || die "Invalid host/IP: '$SERVER_ADDR'" |
| 85 | |
| 86 | [[ -n "$SMB_USER" ]] || read -rp "Synology username: " SMB_USER </dev/tty |
| 87 | [[ "$SMB_USER" =~ ^[A-Za-z0-9._@-]+$ ]] || die "Invalid SMB username." |
| 88 | |
| 89 | if (( PASS_FROM_STDIN )); then |
| 90 | IFS= read -r SMB_PASS |
| 91 | else |
| 92 | read -rsp "Synology password: " SMB_PASS </dev/tty |
| 93 | echo |
| 94 | fi |
| 95 | [[ -n "$SMB_PASS" ]] || die "Password is empty." |
| 96 | |
| 97 | # --- Credentials file --- |
| 98 | CRED_FILE="$USER_HOME/.smbcredentials_synology" |
| 99 | log "Writing credentials to $CRED_FILE (mode 0600)..." |
| 100 | if (( DRY_RUN )); then |
| 101 | printf ' DRY-RUN: write %s\n' "$CRED_FILE" |
| 102 | else |
| 103 | umask 077 |
| 104 | { |
| 105 | printf 'username=%s\n' "$SMB_USER" |
| 106 | printf 'password=%s\n' "$SMB_PASS" |
| 107 | } >"$CRED_FILE" |
| 108 | chown "$USER_ID:$USER_GID" "$CRED_FILE" |
| 109 | chmod 0600 "$CRED_FILE" |
| 110 | fi |
| 111 | |
| 112 | # --- Query shares --- |
| 113 | log "Discovering shares on //$SERVER_ADDR..." |
| 114 | if (( DRY_RUN )); then |
| 115 | SHARE_LIST=$'home\nphoto\nvideo' |
| 116 | else |
| 117 | SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \ |
| 118 | | awk -F'|' '$1=="Disk" {print $2}' || true)" |
| 119 | fi |
| 120 | [[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network." |
| 121 | |
| 122 | # --- Parent mount dir --- |
| 123 | run "mkdir -p '$MASTER_DIR'" |
| 124 | run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'" |
| 125 | |
| 126 | # --- Backup fstab once --- |
| 127 | if [[ ! -f /etc/fstab.bak.synology ]]; then |
| 128 | run "cp /etc/fstab /etc/fstab.bak.synology" |
| 129 | log "Backed up fstab -> /etc/fstab.bak.synology" |
| 130 | fi |
| 131 | |
| 132 | MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})" |
| 133 | MARK_END="# <<< synology-master <<<" |
| 134 | # Remove any previous managed block (so re-run replaces, not appends). |
| 135 | if grep -qF "$MARK_BEGIN" /etc/fstab; then |
| 136 | if (( DRY_RUN )); then |
| 137 | printf ' DRY-RUN: strip previous managed block from /etc/fstab\n' |
| 138 | else |
| 139 | sed -i "\|$MARK_BEGIN|,\|$MARK_END|d" /etc/fstab |
| 140 | fi |
| 141 | fi |
| 142 | |
| 143 | log "Writing managed block to /etc/fstab..." |
| 144 | FSTAB_BLOCK="$MARK_BEGIN"$'\n' |
| 145 | while IFS= read -r SHARE; do |
| 146 | [[ -z "$SHARE" || "$SHARE" == "IPC\$" || "$SHARE" == "print\$" ]] && continue |
| 147 | MOUNT_POINT="$MASTER_DIR/$SHARE" |
| 148 | run "mkdir -p '$MOUNT_POINT'" |
| 149 | run "chown '$USER_ID:$USER_GID' '$MOUNT_POINT'" |
| 150 | FSTAB_BLOCK+="//${SERVER_ADDR}/${SHARE} ${MOUNT_POINT} cifs credentials=${CRED_FILE},uid=${USER_ID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.idle-timeout=60 0 0"$'\n' |
| 151 | done <<<"$SHARE_LIST" |
| 152 | FSTAB_BLOCK+="$MARK_END"$'\n' |
| 153 | |
| 154 | if (( DRY_RUN )); then |
| 155 | printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK" |
| 156 | else |
| 157 | printf '%s' "$FSTAB_BLOCK" >>/etc/fstab |
| 158 | fi |
| 159 | |
| 160 | log "Reloading systemd and mounting..." |
| 161 | run "systemctl daemon-reload" |
| 162 | run "mount -a -t cifs" |
| 163 | |
| 164 | # --- Desktop entry --- |
| 165 | APPS_DIR="$USER_HOME/.local/share/applications" |
| 166 | DESKTOP_FILENAME="synology-master.desktop" |
| 167 | DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME" |
| 168 | run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'" |
| 169 | |
| 170 | if (( DRY_RUN )); then |
| 171 | printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE" |
| 172 | else |
| 173 | cat >"$DESKTOP_FILE" <<EOF |
| 174 | [Desktop Entry] |
| 175 | Name=Synology NAS |
| 176 | Comment=Open Synology Master Directory |
| 177 | Exec=xdg-open ${MASTER_DIR} |
| 178 | Icon=folder-remote |
| 179 | Terminal=false |
| 180 | Type=Application |
| 181 | Categories=Network;FileTools; |
| 182 | EOF |
| 183 | chown "$USER_ID:$USER_GID" "$DESKTOP_FILE" |
| 184 | chmod 0755 "$DESKTOP_FILE" |
| 185 | fi |
| 186 | |
| 187 | # --- Best-effort GNOME dock pin --- |
| 188 | if (( ! DRY_RUN )) && sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings list-keys org.gnome.shell >/dev/null 2>&1; then |
| 189 | log "Pinning to top of GNOME dock..." |
| 190 | CURRENT_FAVS="$(sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings get org.gnome.shell favorite-apps 2>/dev/null || echo '[]')" |
| 191 | if [[ "$CURRENT_FAVS" != *"$DESKTOP_FILENAME"* ]]; then |
| 192 | CLEAN_FAVS="${CURRENT_FAVS#@as }" |
| 193 | if [[ "$CLEAN_FAVS" == "[]" || -z "$CLEAN_FAVS" ]]; then |
| 194 | NEW_FAVS="['$DESKTOP_FILENAME']" |
| 195 | else |
| 196 | NEW_FAVS="['$DESKTOP_FILENAME', ${CLEAN_FAVS:1}" |
| 197 | fi |
| 198 | sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ |
| 199 | gsettings set org.gnome.shell favorite-apps "$NEW_FAVS" || warn "Dock pin failed (non-fatal)." |
| 200 | else |
| 201 | log "Already pinned." |
| 202 | fi |
| 203 | else |
| 204 | log "GNOME not detected (or no active dbus session); skipping dock pin." |
| 205 | fi |
| 206 | |
| 207 | log "Done. Shares mounted under $MASTER_DIR." |
| 208 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-obsidian.sh — Install Obsidian from the latest official GitHub release. |
| 3 | # Hardened: arch-aware, GitHub API token support, SHA-256 verification from the |
| 4 | # release asset digest, idempotent, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | REPO="obsidianmd/obsidian-releases" |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 16 | |
| 17 | Resolves the latest release of obsidianmd/obsidian-releases, downloads the |
| 18 | official amd64 .deb package, verifies its SHA-256 against the GitHub release |
| 19 | asset digest when available, and installs it with apt-get. |
| 20 | |
| 21 | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the |
| 22 | GitHub API request (avoids rate limits). |
| 23 | |
| 24 | Options: |
| 25 | --dry-run Print actions without executing. |
| 26 | --help, -h Show this help. |
| 27 | EOF |
| 28 | } |
| 29 | |
| 30 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 31 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 32 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 33 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 34 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 35 | |
| 36 | while (( $# )); do |
| 37 | case "$1" in |
| 38 | --dry-run) DRY_RUN=1 ;; |
| 39 | -h|--help) usage; exit 0 ;; |
| 40 | *) die "Unknown argument: $1 (try --help)" ;; |
| 41 | esac |
| 42 | shift |
| 43 | done |
| 44 | |
| 45 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 46 | |
| 47 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 48 | # shellcheck disable=SC1091 |
| 49 | . /etc/os-release |
| 50 | case "${ID:-}:${ID_LIKE:-}" in |
| 51 | *ubuntu*|*debian*) : ;; |
| 52 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 53 | esac |
| 54 | |
| 55 | ARCH="$(dpkg --print-architecture)" |
| 56 | [[ "$ARCH" == "amd64" ]] || die "Obsidian publishes a Linux .deb for amd64 only (detected: $ARCH)." |
| 57 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 58 | |
| 59 | export DEBIAN_FRONTEND=noninteractive |
| 60 | log "Installing prerequisites..." |
| 61 | run "apt-get update -qq" |
| 62 | run "apt-get install -y curl jq ca-certificates" |
| 63 | |
| 64 | GH_HDRS=(-H "Accept: application/vnd.github+json") |
| 65 | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") |
| 66 | |
| 67 | log "Querying GitHub API for latest release of $REPO..." |
| 68 | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" |
| 69 | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" |
| 70 | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." |
| 71 | log "Latest release: $TAG" |
| 72 | |
| 73 | ASSET_REGEX='^obsidian_[^/]+_amd64\.deb$' |
| 74 | DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 75 | ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .name' <<<"$RELEASE_JSON" | head -n1)" |
| 76 | EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)" |
| 77 | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No amd64 .deb asset found in latest Obsidian release." |
| 78 | |
| 79 | STAGE="$(mktemp -d -t obsidian.XXXXXX)" |
| 80 | trap 'rm -rf "$STAGE"' EXIT |
| 81 | DEB="$STAGE/$ASSET_NAME" |
| 82 | |
| 83 | log "Downloading $ASSET_NAME..." |
| 84 | run "curl -fsSL -o '$DEB' '$DOWNLOAD_URL'" |
| 85 | |
| 86 | if (( DRY_RUN )); then |
| 87 | [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \ |
| 88 | && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \ |
| 89 | || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n' |
| 90 | printf ' DRY-RUN: apt-get install -y %s\n' "$DEB" |
| 91 | log "Done." |
| 92 | exit 0 |
| 93 | fi |
| 94 | |
| 95 | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then |
| 96 | log "Verifying SHA-256..." |
| 97 | ACTUAL_SHA="$(sha256sum "$DEB" | awk '{print $1}')" |
| 98 | [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA" |
| 99 | log "SHA-256 ok." |
| 100 | else |
| 101 | warn "No SHA-256 asset digest in GitHub release metadata; skipping verification." |
| 102 | fi |
| 103 | |
| 104 | log "Installing Obsidian..." |
| 105 | run "apt-get install -y '$DEB'" |
| 106 | |
| 107 | if (( ! DRY_RUN )) && command -v obsidian >/dev/null 2>&1; then |
| 108 | log "Installed: $(obsidian --version 2>/dev/null || echo "$TAG")" |
| 109 | fi |
| 110 | log "Done." |
| 111 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-qbittorrent.sh — Install qBittorrent from the latest official GitHub release. |
| 3 | # Hardened: arch-aware, GitHub API token support, SHA-256 verification from the |
| 4 | # release asset digest, idempotent, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | REPO="qbittorrent/qBittorrent" |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 16 | |
| 17 | Resolves the latest release of qbittorrent/qBittorrent, downloads the official |
| 18 | Linux x86_64 AppImage, verifies its SHA-256 against the GitHub release asset |
| 19 | digest when available, and installs it system-wide. |
| 20 | |
| 21 | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the |
| 22 | GitHub API request (avoids rate limits). |
| 23 | |
| 24 | Options: |
| 25 | --dry-run Print actions without executing. |
| 26 | --help, -h Show this help. |
| 27 | EOF |
| 28 | } |
| 29 | |
| 30 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 31 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 32 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 33 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 34 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 35 | |
| 36 | while (( $# )); do |
| 37 | case "$1" in |
| 38 | --dry-run) DRY_RUN=1 ;; |
| 39 | -h|--help) usage; exit 0 ;; |
| 40 | *) die "Unknown argument: $1 (try --help)" ;; |
| 41 | esac |
| 42 | shift |
| 43 | done |
| 44 | |
| 45 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 46 | |
| 47 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 48 | # shellcheck disable=SC1091 |
| 49 | . /etc/os-release |
| 50 | case "${ID:-}:${ID_LIKE:-}" in |
| 51 | *ubuntu*|*debian*) : ;; |
| 52 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 53 | esac |
| 54 | |
| 55 | ARCH="$(dpkg --print-architecture)" |
| 56 | [[ "$ARCH" == "amd64" ]] || die "qBittorrent publishes official Linux AppImages for x86_64/amd64 only (detected: $ARCH)." |
| 57 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 58 | |
| 59 | export DEBIAN_FRONTEND=noninteractive |
| 60 | log "Installing prerequisites..." |
| 61 | run "apt-get update -qq" |
| 62 | run "apt-get install -y curl jq ca-certificates desktop-file-utils shared-mime-info hicolor-icon-theme" |
| 63 | |
| 64 | GH_HDRS=(-H "Accept: application/vnd.github+json") |
| 65 | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") |
| 66 | |
| 67 | log "Querying GitHub API for latest release of $REPO..." |
| 68 | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" |
| 69 | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" |
| 70 | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." |
| 71 | VERSION="${TAG#release-}" |
| 72 | log "Latest release: $TAG" |
| 73 | |
| 74 | ASSET_REGEX='^qbittorrent-[0-9][^/]*_x86_64\.AppImage$' |
| 75 | DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | select(.name | test("_lt20_") | not) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 76 | ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | select(.name | test("_lt20_") | not) | .name' <<<"$RELEASE_JSON" | head -n1)" |
| 77 | EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | select(.name | test("_lt20_") | not) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)" |
| 78 | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No standard x86_64 AppImage asset found in latest qBittorrent release." |
| 79 | |
| 80 | STAGE="$(mktemp -d -t qbittorrent.XXXXXX)" |
| 81 | trap 'rm -rf "$STAGE"' EXIT |
| 82 | APPIMAGE="$STAGE/$ASSET_NAME" |
| 83 | |
| 84 | log "Downloading $ASSET_NAME..." |
| 85 | run "curl -fL -o '$APPIMAGE' '$DOWNLOAD_URL'" |
| 86 | |
| 87 | if (( DRY_RUN )); then |
| 88 | [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \ |
| 89 | && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \ |
| 90 | || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n' |
| 91 | printf ' DRY-RUN: install AppImage to /opt/qbittorrent/qbittorrent\n' |
| 92 | printf ' DRY-RUN: create /usr/local/bin/qbittorrent symlink\n' |
| 93 | printf ' DRY-RUN: install desktop launcher, icons, and MIME handlers\n' |
| 94 | log "Done." |
| 95 | exit 0 |
| 96 | fi |
| 97 | |
| 98 | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then |
| 99 | log "Verifying SHA-256..." |
| 100 | ACTUAL_SHA="$(sha256sum "$APPIMAGE" | awk '{print $1}')" |
| 101 | [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA" |
| 102 | log "SHA-256 ok." |
| 103 | else |
| 104 | warn "No SHA-256 asset digest in GitHub release metadata; skipping verification." |
| 105 | fi |
| 106 | |
| 107 | log "Installing qBittorrent AppImage..." |
| 108 | run "install -d -m 0755 /opt/qbittorrent" |
| 109 | run "install -m 0755 '$APPIMAGE' /opt/qbittorrent/qbittorrent" |
| 110 | run "ln -sfn /opt/qbittorrent/qbittorrent /usr/local/bin/qbittorrent" |
| 111 | |
| 112 | log "Extracting desktop metadata and icons..." |
| 113 | EXTRACT_DIR="$STAGE/extract" |
| 114 | run "install -d -m 0755 '$EXTRACT_DIR'" |
| 115 | ( |
| 116 | cd "$EXTRACT_DIR" |
| 117 | /opt/qbittorrent/qbittorrent --appimage-extract >/dev/null |
| 118 | ) |
| 119 | |
| 120 | run "install -d -m 0755 /usr/share/icons/hicolor/scalable/apps" |
| 121 | run "install -m 0644 '$EXTRACT_DIR/squashfs-root/usr/share/icons/hicolor/scalable/apps/qbittorrent.svg' /usr/share/icons/hicolor/scalable/apps/qbittorrent.svg" |
| 122 | for size in 16 22 24 32 36 48 64 72 96 128 192; do |
| 123 | src="$EXTRACT_DIR/squashfs-root/usr/share/icons/hicolor/${size}x${size}/apps/qbittorrent.png" |
| 124 | [[ -f "$src" ]] || continue |
| 125 | run "install -d -m 0755 '/usr/share/icons/hicolor/${size}x${size}/apps'" |
| 126 | run "install -m 0644 '$src' '/usr/share/icons/hicolor/${size}x${size}/apps/qbittorrent.png'" |
| 127 | done |
| 128 | |
| 129 | log "Installing desktop launcher..." |
| 130 | cat >/usr/share/applications/org.qbittorrent.qBittorrent.desktop <<EOF |
| 131 | [Desktop Entry] |
| 132 | Type=Application |
| 133 | Name=qBittorrent |
| 134 | GenericName=BitTorrent client |
| 135 | Comment=Download and share files over BitTorrent |
| 136 | Exec=/opt/qbittorrent/qbittorrent %U |
| 137 | Icon=qbittorrent |
| 138 | Terminal=false |
| 139 | Categories=Network;FileTransfer;P2P;Qt; |
| 140 | MimeType=application/x-bittorrent;x-scheme-handler/magnet; |
| 141 | StartupNotify=false |
| 142 | StartupWMClass=qbittorrent |
| 143 | Keywords=bittorrent;torrent;magnet;download;p2p; |
| 144 | SingleMainWindow=true |
| 145 | EOF |
| 146 | |
| 147 | run "desktop-file-validate /usr/share/applications/org.qbittorrent.qBittorrent.desktop" |
| 148 | run "update-desktop-database /usr/share/applications" |
| 149 | run "update-mime-database /usr/share/mime" |
| 150 | run "gtk-update-icon-cache -f -t /usr/share/icons/hicolor >/dev/null 2>&1 || true" |
| 151 | |
| 152 | if command -v xdg-mime >/dev/null 2>&1; then |
| 153 | run "xdg-mime default org.qbittorrent.qBittorrent.desktop application/x-bittorrent || true" |
| 154 | run "xdg-mime default org.qbittorrent.qBittorrent.desktop x-scheme-handler/magnet || true" |
| 155 | fi |
| 156 | |
| 157 | if command -v qbittorrent >/dev/null 2>&1; then |
| 158 | log "Installed: $(qbittorrent --version 2>/dev/null || echo "$VERSION")" |
| 159 | fi |
| 160 | log "Done." |
| 161 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-telegram.sh — Install Telegram Desktop from the latest official Linux build. |
| 3 | # Hardened: official download endpoint, arch-aware, idempotent, --dry-run. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | IFS=$'\n\t' |
| 7 | |
| 8 | readonly SCRIPT_NAME="${0##*/}" |
| 9 | DRY_RUN=0 |
| 10 | DOWNLOAD_URL="https://telegram.org/dl/desktop/linux" |
| 11 | |
| 12 | usage() { |
| 13 | cat <<EOF |
| 14 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 15 | |
| 16 | Downloads Telegram Desktop from Telegram's official latest Linux endpoint, |
| 17 | installs it to /opt/Telegram, creates /usr/local/bin/telegram-desktop, and |
| 18 | installs a desktop launcher. |
| 19 | |
| 20 | Options: |
| 21 | --dry-run Print actions without executing. |
| 22 | --help, -h Show this help. |
| 23 | EOF |
| 24 | } |
| 25 | |
| 26 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 27 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 28 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 29 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 30 | |
| 31 | while (( $# )); do |
| 32 | case "$1" in |
| 33 | --dry-run) DRY_RUN=1 ;; |
| 34 | -h|--help) usage; exit 0 ;; |
| 35 | *) die "Unknown argument: $1 (try --help)" ;; |
| 36 | esac |
| 37 | shift |
| 38 | done |
| 39 | |
| 40 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 41 | |
| 42 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 43 | # shellcheck disable=SC1091 |
| 44 | . /etc/os-release |
| 45 | case "${ID:-}:${ID_LIKE:-}" in |
| 46 | *ubuntu*|*debian*) : ;; |
| 47 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 48 | esac |
| 49 | |
| 50 | MACHINE="$(uname -m)" |
| 51 | case "$MACHINE" in |
| 52 | x86_64|amd64) : ;; |
| 53 | *) die "Telegram's official Linux desktop tarball targets x86_64 (detected: $MACHINE)." ;; |
| 54 | esac |
| 55 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $MACHINE" |
| 56 | |
| 57 | export DEBIAN_FRONTEND=noninteractive |
| 58 | log "Installing prerequisites..." |
| 59 | run "apt-get update -qq" |
| 60 | run "apt-get install -y curl ca-certificates tar xz-utils desktop-file-utils hicolor-icon-theme" |
| 61 | |
| 62 | STAGE="$(mktemp -d -t telegram.XXXXXX)" |
| 63 | trap 'rm -rf "$STAGE"' EXIT |
| 64 | ARCHIVE="$STAGE/telegram.tar.xz" |
| 65 | |
| 66 | log "Downloading latest Telegram Desktop..." |
| 67 | run "curl -fL -o '$ARCHIVE' '$DOWNLOAD_URL'" |
| 68 | |
| 69 | if (( DRY_RUN )); then |
| 70 | printf ' DRY-RUN: extract %s\n' "$ARCHIVE" |
| 71 | printf ' DRY-RUN: install Telegram to /opt/Telegram\n' |
| 72 | printf ' DRY-RUN: create /usr/local/bin/telegram-desktop symlink and desktop launcher\n' |
| 73 | log "Done." |
| 74 | exit 0 |
| 75 | fi |
| 76 | |
| 77 | log "Extracting Telegram..." |
| 78 | run "tar -xJf '$ARCHIVE' -C '$STAGE'" |
| 79 | [[ -x "$STAGE/Telegram/Telegram" ]] || die "Unexpected archive layout: Telegram/Telegram not found." |
| 80 | |
| 81 | log "Installing Telegram..." |
| 82 | run "rm -rf /opt/Telegram" |
| 83 | run "install -d -m 0755 /opt" |
| 84 | run "cp -a '$STAGE/Telegram' /opt/Telegram" |
| 85 | run "ln -sfn /opt/Telegram/Telegram /usr/local/bin/telegram-desktop" |
| 86 | |
| 87 | run "install -d -m 0755 /usr/share/applications /usr/share/icons/hicolor/256x256/apps" |
| 88 | if [[ -f /opt/Telegram/telegram.png ]]; then |
| 89 | run "install -m 0644 /opt/Telegram/telegram.png /usr/share/icons/hicolor/256x256/apps/telegram.png" |
| 90 | fi |
| 91 | |
| 92 | cat >/usr/share/applications/telegram-desktop.desktop <<'EOF' |
| 93 | [Desktop Entry] |
| 94 | Type=Application |
| 95 | Name=Telegram Desktop |
| 96 | GenericName=Messenger |
| 97 | Comment=Official Telegram desktop messaging app |
| 98 | Exec=/opt/Telegram/Telegram -- %u |
| 99 | Icon=telegram |
| 100 | Terminal=false |
| 101 | Categories=Network;InstantMessaging;Chat; |
| 102 | MimeType=x-scheme-handler/tg; |
| 103 | StartupNotify=false |
| 104 | StartupWMClass=TelegramDesktop |
| 105 | Keywords=telegram;chat;messenger; |
| 106 | EOF |
| 107 | |
| 108 | run "desktop-file-validate /usr/share/applications/telegram-desktop.desktop" |
| 109 | run "update-desktop-database /usr/share/applications" |
| 110 | run "gtk-update-icon-cache -f -t /usr/share/icons/hicolor >/dev/null 2>&1 || true" |
| 111 | if command -v xdg-mime >/dev/null 2>&1; then |
| 112 | run "xdg-mime default telegram-desktop.desktop x-scheme-handler/tg || true" |
| 113 | fi |
| 114 | |
| 115 | log "Installed: $(/opt/Telegram/Telegram --version 2>/dev/null || echo Telegram Desktop)" |
| 116 | log "Done." |
| 117 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-thunderbird.sh — Install Thunderbird. |
| 3 | # On Ubuntu: uses the Mozilla Team PPA (with APT pinning so Snap transition is bypassed). |
| 4 | # On Debian: uses the regular Debian package (no PPA available). |
| 5 | # Hardened: distro-detect, idempotent, --dry-run. |
| 6 | |
| 7 | set -euo pipefail |
| 8 | IFS=$'\n\t' |
| 9 | |
| 10 | readonly SCRIPT_NAME="${0##*/}" |
| 11 | DRY_RUN=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 16 | |
| 17 | Installs Thunderbird. |
| 18 | - Ubuntu: removes Snap version (if any), adds Mozilla Team PPA, pins it, |
| 19 | and installs the .deb. This avoids the Ubuntu snap transition wrapper. |
| 20 | - Debian: installs from the standard Debian repos. |
| 21 | |
| 22 | Options: |
| 23 | --dry-run Print actions without executing. |
| 24 | --help, -h Show this help. |
| 25 | EOF |
| 26 | } |
| 27 | |
| 28 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 29 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 30 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 31 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 32 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 33 | |
| 34 | while (( $# )); do |
| 35 | case "$1" in |
| 36 | --dry-run) DRY_RUN=1 ;; |
| 37 | -h|--help) usage; exit 0 ;; |
| 38 | *) die "Unknown argument: $1 (try --help)" ;; |
| 39 | esac |
| 40 | shift |
| 41 | done |
| 42 | |
| 43 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 44 | |
| 45 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 46 | # shellcheck disable=SC1091 |
| 47 | . /etc/os-release |
| 48 | log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})" |
| 49 | |
| 50 | export DEBIAN_FRONTEND=noninteractive |
| 51 | |
| 52 | case "${ID:-}" in |
| 53 | ubuntu) |
| 54 | log "Removing Thunderbird Snap (if present)..." |
| 55 | if command -v snap >/dev/null 2>&1; then |
| 56 | run "snap remove --purge thunderbird >/dev/null 2>&1 || true" |
| 57 | fi |
| 58 | |
| 59 | log "Installing prerequisites..." |
| 60 | run "apt-get update -qq" |
| 61 | run "apt-get install -y software-properties-common" |
| 62 | |
| 63 | log "Adding Mozilla Team PPA..." |
| 64 | PPA_LIST=/etc/apt/sources.list.d/mozillateam-ubuntu-ppa-*.list |
| 65 | # shellcheck disable=SC2086 |
| 66 | if ! ls $PPA_LIST >/dev/null 2>&1; then |
| 67 | run "add-apt-repository -y ppa:mozillateam/ppa" |
| 68 | else |
| 69 | log "Mozilla Team PPA already configured." |
| 70 | fi |
| 71 | |
| 72 | PIN=/etc/apt/preferences.d/mozillateamppa |
| 73 | log "Pinning Mozilla Team PPA so .deb wins over Snap transition wrapper..." |
| 74 | if [[ ! -f "$PIN" ]] || ! grep -q 'release o=LP-PPA-mozillateam' "$PIN"; then |
| 75 | if (( DRY_RUN )); then |
| 76 | printf ' DRY-RUN: write %s\n' "$PIN" |
| 77 | else |
| 78 | cat >"$PIN" <<'EOF' |
| 79 | Package: thunderbird* |
| 80 | Pin: release o=LP-PPA-mozillateam |
| 81 | Pin-Priority: 1001 |
| 82 | EOF |
| 83 | fi |
| 84 | fi |
| 85 | |
| 86 | log "Installing thunderbird from PPA..." |
| 87 | run "apt-get update -qq" |
| 88 | run "apt-get install -y --allow-downgrades thunderbird" |
| 89 | ;; |
| 90 | |
| 91 | debian) |
| 92 | log "Installing thunderbird from Debian repos..." |
| 93 | run "apt-get update -qq" |
| 94 | run "apt-get install -y thunderbird" |
| 95 | ;; |
| 96 | |
| 97 | *) |
| 98 | die "Unsupported distro: ${PRETTY_NAME:-unknown}. Supported: ubuntu, debian." |
| 99 | ;; |
| 100 | esac |
| 101 | |
| 102 | log "Done." |
| 103 |
| 1 | #!/usr/bin/env bash |
| 2 | # install-vscode.sh — Install Visual Studio Code from Microsoft's APT repository. |
| 3 | # Hardened: arch-aware, signed-by keyring, idempotent, --dry-run. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | IFS=$'\n\t' |
| 7 | |
| 8 | readonly SCRIPT_NAME="${0##*/}" |
| 9 | DRY_RUN=0 |
| 10 | INSIDERS=0 |
| 11 | |
| 12 | usage() { |
| 13 | cat <<EOF |
| 14 | Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help] |
| 15 | |
| 16 | Configures the official Microsoft 'vscode' APT repository (signed-by keyring) |
| 17 | and installs Visual Studio Code. Idempotent: safe to re-run. |
| 18 | |
| 19 | Options: |
| 20 | --insiders Install the 'code-insiders' build instead of stable 'code'. |
| 21 | --dry-run Print actions without executing. |
| 22 | --help, -h Show this help. |
| 23 | EOF |
| 24 | } |
| 25 | |
| 26 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 27 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 28 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 29 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 30 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 31 | |
| 32 | while (( $# )); do |
| 33 | case "$1" in |
| 34 | --dry-run) DRY_RUN=1 ;; |
| 35 | --insiders) INSIDERS=1 ;; |
| 36 | -h|--help) usage; exit 0 ;; |
| 37 | *) die "Unknown argument: $1 (try --help)" ;; |
| 38 | esac |
| 39 | shift |
| 40 | done |
| 41 | |
| 42 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 43 | |
| 44 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 45 | # shellcheck disable=SC1091 |
| 46 | . /etc/os-release |
| 47 | case "${ID:-}:${ID_LIKE:-}" in |
| 48 | *ubuntu*|*debian*) : ;; |
| 49 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 50 | esac |
| 51 | |
| 52 | ARCH="$(dpkg --print-architecture)" |
| 53 | case "$ARCH" in |
| 54 | amd64|arm64|armhf) : ;; |
| 55 | *) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;; |
| 56 | esac |
| 57 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 58 | |
| 59 | export DEBIAN_FRONTEND=noninteractive |
| 60 | |
| 61 | KEYRING=/etc/apt/keyrings/packages.microsoft.gpg |
| 62 | SOURCES=/etc/apt/sources.list.d/vscode.list |
| 63 | PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code) |
| 64 | |
| 65 | log "Installing prerequisites..." |
| 66 | run "apt-get update -qq" |
| 67 | run "apt-get install -y wget gpg apt-transport-https ca-certificates" |
| 68 | |
| 69 | log "Configuring Microsoft vscode APT repository..." |
| 70 | run "install -d -m 0755 /etc/apt/keyrings" |
| 71 | if [[ ! -s "$KEYRING" ]]; then |
| 72 | run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'" |
| 73 | run "chmod 0644 '$KEYRING'" |
| 74 | fi |
| 75 | |
| 76 | DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main" |
| 77 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 78 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 79 | fi |
| 80 | |
| 81 | log "Installing $PKG..." |
| 82 | run "apt-get update -qq" |
| 83 | run "apt-get install -y '$PKG'" |
| 84 | |
| 85 | if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then |
| 86 | log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")" |
| 87 | fi |
| 88 | log "Done." |
| 89 |
| 1 | #!/usr/bin/env bash |
| 2 | # menu.sh — Ubuntu/Debian Setup Manager |
| 3 | # |
| 4 | # Interactive menu that fetches and runs the hardened install scripts in this |
| 5 | # Opengist. Each script is downloaded to a temp file (not blindly piped to bash) |
| 6 | # and executed with the appropriate privilege level for that script: |
| 7 | # |
| 8 | # - "sudo" mode for system-wide installers (apt, /etc, /usr/local/bin) |
| 9 | # - "user" mode for per-user installers that must NOT run as root |
| 10 | # (gsettings, ~/.local, JetBrains Toolbox, fonts) |
| 11 | # |
| 12 | # Usage: |
| 13 | # bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)" |
| 14 | # |
| 15 | # Or save and run locally: |
| 16 | # curl -fsSLO https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh |
| 17 | # bash menu.sh |
| 18 | |
| 19 | # Note: NOT using `set -e` because we want the menu loop to survive a failed |
| 20 | # sub-script. We do use -u and pipefail to catch real bugs in this file. |
| 21 | set -uo pipefail |
| 22 | IFS=$'\n\t' |
| 23 | |
| 24 | readonly SCRIPT_NAME="${0##*/}" |
| 25 | readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD' |
| 26 | |
| 27 | log() { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; } |
| 28 | warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; } |
| 29 | err() { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; } |
| 30 | hr() { printf '%s\n' "------------------------------------------------------"; } |
| 31 | |
| 32 | # Refuse to run as root: per-user scripts (jetbrains, fonts, ibus) need a real |
| 33 | # desktop user. Sub-scripts elevate via sudo on their own. |
| 34 | if (( EUID == 0 )); then |
| 35 | err "Don't run $SCRIPT_NAME as root. Run as your normal user — it will call sudo for installers that need it." |
| 36 | exit 1 |
| 37 | fi |
| 38 | |
| 39 | # Sanity-check tools we depend on. |
| 40 | for tool in curl bash mktemp; do |
| 41 | command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; } |
| 42 | done |
| 43 | |
| 44 | # Distro check (warn-only; sub-scripts enforce strictly). |
| 45 | if [[ -r /etc/os-release ]]; then |
| 46 | # shellcheck disable=SC1091 |
| 47 | . /etc/os-release |
| 48 | case "${ID:-}:${ID_LIKE:-}" in |
| 49 | *ubuntu*|*debian*) : ;; |
| 50 | *) warn "Detected ${PRETTY_NAME:-unknown}. These installers target Debian/Ubuntu; some will refuse to run." ;; |
| 51 | esac |
| 52 | fi |
| 53 | |
| 54 | # Cache sudo credentials up-front so sub-scripts that elevate don't keep |
| 55 | # re-prompting in the middle of a multi-task run. |
| 56 | prime_sudo() { |
| 57 | if ! sudo -n true 2>/dev/null; then |
| 58 | log "Caching sudo credentials (you may be prompted)..." |
| 59 | sudo -v || { err "sudo authentication failed."; return 1; } |
| 60 | fi |
| 61 | # Keep sudo timestamp refreshed in the background while the menu runs. |
| 62 | ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) & |
| 63 | SUDO_KEEPALIVE_PID=$! |
| 64 | trap 'kill "${SUDO_KEEPALIVE_PID:-}" 2>/dev/null || true' EXIT |
| 65 | } |
| 66 | |
| 67 | # run_script <filename> <mode> |
| 68 | # mode: "sudo" (run as root via sudo) or "user" (run as current user) |
| 69 | # Returns: 0 on success, non-zero on failure (does NOT exit the menu). |
| 70 | run_script() { |
| 71 | local name="$1" mode="$2" |
| 72 | local url="$BASE_URL/$name" |
| 73 | local tmp |
| 74 | tmp="$(mktemp -t "${name%.sh}.XXXXXX.sh")" || { err "mktemp failed"; return 1; } |
| 75 | |
| 76 | log "Fetching $name..." |
| 77 | if ! curl -fsSL --max-time 60 --retry 2 "$url" -o "$tmp"; then |
| 78 | err "Download failed: $url" |
| 79 | rm -f "$tmp" |
| 80 | return 1 |
| 81 | fi |
| 82 | if [[ ! -s "$tmp" ]]; then |
| 83 | err "Downloaded $name is empty." |
| 84 | rm -f "$tmp" |
| 85 | return 1 |
| 86 | fi |
| 87 | # Cheap sanity: confirm it looks like a shell script. |
| 88 | if ! head -n1 "$tmp" | grep -qE '^#!.*sh'; then |
| 89 | warn "$name doesn't start with a shebang; proceeding anyway." |
| 90 | fi |
| 91 | chmod +x "$tmp" |
| 92 | |
| 93 | local rc=0 |
| 94 | if [[ "$mode" == "sudo" ]]; then |
| 95 | sudo bash "$tmp" || rc=$? |
| 96 | else |
| 97 | bash "$tmp" || rc=$? |
| 98 | fi |
| 99 | rm -f "$tmp" |
| 100 | if (( rc != 0 )); then |
| 101 | err "$name exited with status $rc" |
| 102 | fi |
| 103 | return "$rc" |
| 104 | } |
| 105 | |
| 106 | # Catalog: number | label | script-filename | mode |
| 107 | # (Edit here to add/remove options — the menu loop is data-driven.) |
| 108 | OPTIONS=( |
| 109 | "1|Install Google Chrome|install-chrome.sh|sudo" |
| 110 | "2|Install Firefox (Mozilla APT)|install-firefox.sh|sudo" |
| 111 | "3|Install Thunderbird|install-thunderbird.sh|sudo" |
| 112 | "4|Install 1Password|install-1password.sh|sudo" |
| 113 | "5|Install Espanso (text expander)|install-espanso.sh|sudo" |
| 114 | "6|Install LibreOffice (latest stable .deb)|install-libreoffice.sh|sudo" |
| 115 | "7|Install Obsidian|install-obsidian.sh|sudo" |
| 116 | "8|Install draw.io Desktop|install-drawio.sh|sudo" |
| 117 | "9|Install Visual Studio Code|install-vscode.sh|sudo" |
| 118 | "10|Install JetBrains Toolbox (per-user)|install-jetbrains-toolbox.sh|user" |
| 119 | "11|Install Bruno (API client)|install-bruno.sh|sudo" |
| 120 | "12|Install IPATool|install-ipatool.sh|sudo" |
| 121 | "13|Install qBittorrent (latest AppImage)|install-qbittorrent.sh|sudo" |
| 122 | "14|Mount Synology Network Drive|install-network_drive.sh|sudo" |
| 123 | "15|Install IBus Intelligent Pinyin (per-user)|install-ibus-pinyin.sh|user" |
| 124 | "16|Install Nerd Fonts (per-user)|install-font.sh|user" |
| 125 | "17|Fix Dual-Boot Time (RTC to UTC)|timedatectl-fix.sh|sudo" |
| 126 | "18|Install LocalSend|install-localsend.sh|sudo" |
| 127 | "19|Install Telegram Desktop|install-telegram.sh|sudo" |
| 128 | "20|Install Discord|install-discord.sh|sudo" |
| 129 | ) |
| 130 | |
| 131 | print_menu() { |
| 132 | hr |
| 133 | echo " Ubuntu / Debian Setup Manager" |
| 134 | hr |
| 135 | echo "--- [ Browsers & Mail ] ---" |
| 136 | echo " 1) Install Google Chrome" |
| 137 | echo " 2) Install Firefox" |
| 138 | echo " 3) Install Thunderbird" |
| 139 | echo "--- [ Communication ] ---" |
| 140 | echo " 18) Install LocalSend" |
| 141 | echo " 19) Install Telegram Desktop" |
| 142 | echo " 20) Install Discord" |
| 143 | echo "--- [ Productivity & Security ] ---" |
| 144 | echo " 4) Install 1Password" |
| 145 | echo " 5) Install Espanso" |
| 146 | echo " 6) Install LibreOffice" |
| 147 | echo " 7) Install Obsidian" |
| 148 | echo " 8) Install draw.io Desktop" |
| 149 | echo "--- [ Development Tools ] ---" |
| 150 | echo " 9) Install Visual Studio Code" |
| 151 | echo " 10) Install JetBrains Toolbox (runs as you, not root)" |
| 152 | echo " 11) Install Bruno" |
| 153 | echo " 12) Install IPATool" |
| 154 | echo " 13) Install qBittorrent" |
| 155 | echo "--- [ System ] ---" |
| 156 | echo " 14) Mount Synology Network Drive" |
| 157 | echo " 15) Install IBus Intelligent Pinyin (runs as you, not root)" |
| 158 | echo " 16) Install Nerd Fonts (runs as you, not root)" |
| 159 | echo " 17) Fix Dual-Boot Time (RTC to UTC)" |
| 160 | hr |
| 161 | echo " 0) Run ALL options (1-20)" |
| 162 | echo " -1) Exit" |
| 163 | hr |
| 164 | } |
| 165 | |
| 166 | # Resolve a numeric choice to its catalog entry; print "name|mode" to stdout. |
| 167 | resolve_choice() { |
| 168 | local want="$1" entry num |
| 169 | for entry in "${OPTIONS[@]}"; do |
| 170 | num="${entry%%|*}" |
| 171 | if [[ "$num" == "$want" ]]; then |
| 172 | # Strip the leading "N|label|"; what remains is "filename|mode" |
| 173 | printf '%s' "${entry#*|*|}" |
| 174 | return 0 |
| 175 | fi |
| 176 | done |
| 177 | return 1 |
| 178 | } |
| 179 | |
| 180 | ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20) |
| 181 | |
| 182 | prime_sudo || exit 1 |
| 183 | |
| 184 | while true; do |
| 185 | print_menu |
| 186 | read -rp "Enter choices separated by spaces (e.g., 1 7 12), 0 for ALL, -1 to exit: " choices </dev/tty || { |
| 187 | echo; log "Input closed; exiting." |
| 188 | break |
| 189 | } |
| 190 | |
| 191 | # Trim whitespace |
| 192 | choices="${choices##[[:space:]]}" |
| 193 | choices="${choices%%[[:space:]]}" |
| 194 | |
| 195 | if [[ -z "$choices" ]]; then |
| 196 | continue |
| 197 | fi |
| 198 | |
| 199 | if [[ "$choices" == "-1" ]]; then |
| 200 | log "Exiting." |
| 201 | break |
| 202 | fi |
| 203 | |
| 204 | # Expand "0" to all options |
| 205 | if [[ "$choices" == "0" ]]; then |
| 206 | choices="${ALL_NUMS[*]}" |
| 207 | fi |
| 208 | |
| 209 | # Validate every token first; reject the whole batch if any token is bogus, |
| 210 | # so the user sees the problem before any work starts. |
| 211 | bad="" |
| 212 | for c in $choices; do |
| 213 | if ! [[ "$c" =~ ^-?[0-9]+$ ]] || ! resolve_choice "$c" >/dev/null; then |
| 214 | bad+=" $c" |
| 215 | fi |
| 216 | done |
| 217 | if [[ -n "$bad" ]]; then |
| 218 | err "Invalid option(s):$bad" |
| 219 | sleep 1 |
| 220 | continue |
| 221 | fi |
| 222 | |
| 223 | failures=() |
| 224 | for c in $choices; do |
| 225 | spec="$(resolve_choice "$c")" |
| 226 | name="${spec%|*}" |
| 227 | mode="${spec##*|}" |
| 228 | hr |
| 229 | log "[$c] Running $name ($mode)..." |
| 230 | if ! run_script "$name" "$mode"; then |
| 231 | failures+=("$c:$name") |
| 232 | fi |
| 233 | done |
| 234 | |
| 235 | hr |
| 236 | if (( ${#failures[@]} == 0 )); then |
| 237 | log "All selected tasks completed." |
| 238 | else |
| 239 | warn "Completed with ${#failures[@]} failure(s): ${failures[*]}" |
| 240 | fi |
| 241 | echo |
| 242 | done |
| 243 |
| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | set -euo pipefail |
| 4 | |
| 5 | echo "======================================" |
| 6 | echo " Ubuntu Dual-Boot Time Fix Script" |
| 7 | echo " (UTC Method - Highly Recommended)" |
| 8 | echo "======================================" |
| 9 | echo |
| 10 | |
| 11 | # Ensure running as root |
| 12 | if [[ $EUID -ne 0 ]]; then |
| 13 | echo "Please run as root:" |
| 14 | echo "sudo bash fix-time.sh" |
| 15 | exit 1 |
| 16 | fi |
| 17 | |
| 18 | # Detect current timezone instead of hardcoding |
| 19 | CURRENT_TZ=$(timedatectl show -p Timezone --value || echo "UTC") |
| 20 | |
| 21 | echo "[1/5] Ensuring timezone is set to $CURRENT_TZ..." |
| 22 | timedatectl set-timezone "$CURRENT_TZ" |
| 23 | |
| 24 | echo "[2/5] Configuring RTC to use UTC (to prevent dual-boot conflicts)..." |
| 25 | # '0' sets the hardware clock to UTC |
| 26 | timedatectl set-local-rtc 0 --adjust-system-clock |
| 27 | |
| 28 | echo "[3/5] Enabling NTP synchronization..." |
| 29 | timedatectl set-ntp true |
| 30 | |
| 31 | echo "[4/5] Restarting time sync daemon (if applicable)..." |
| 32 | systemctl restart systemd-timesyncd 2>/dev/null || true |
| 33 | |
| 34 | echo "[5/5] Final time configuration:" |
| 35 | echo |
| 36 | timedatectl |
| 37 | |
| 38 | echo |
| 39 | echo "======================================" |
| 40 | echo " SUCCESS - LINUX CONFIGURED" |
| 41 | echo "======================================" |
| 42 | echo |
| 43 | echo "Your system is now configured with:" |
| 44 | echo " - Timezone: $CURRENT_TZ" |
| 45 | echo " - RTC stored in UTC" |
| 46 | echo " - NTP synchronization enabled" |
| 47 | echo |
| 48 | |
| 49 | cat <<'EOF' |
| 50 | -------------------------------------------------- |
| 51 | WINDOWS CONFIGURATION (REQUIRED) |
| 52 | -------------------------------------------------- |
| 53 | Linux is now correctly expecting the hardware clock to be in UTC. |
| 54 | To make Windows do the same and stop the time from resetting, |
| 55 | you MUST run the following command in Windows. |
| 56 | |
| 57 | 1. Boot into Windows. |
| 58 | 2. Open Command Prompt as Administrator. |
| 59 | 3. Run this exact command: |
| 60 | |
| 61 | reg add "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation" /v RealTimeIsUniversal /d 1 /t REG_DWORD /f |
| 62 | |
| 63 | 4. Restart Windows and sync the time in Settings one last time. |
| 64 | EOF |