Последняя активность 4 days ago

Hardened install scripts with an interactive menu — Chrome, Firefox, Thunderbird, 1Password, Espanso, LibreOffice, Obsidian, draw.io Desktop, VS Code, JetBrains Toolbox, Bruno, IPATool, Synology NAS auto-mount, IBus Pinyin, Nerd Fonts, qBittorrent LocalSend Telegram Discord.

Версия 6188864856442e07f7654ce2f60ece9c2ea83974

READMD.md Исходник

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 (no apt-key), is idempotent (safe to re-run), and supports --help and --dry-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
  • 0 runs every option in order
  • -1 exits

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, Ubuntu Sans Nerd Font).

Available scripts

# Script Runs as What it does
1 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.
2 install-thunderbird.sh sudo Ubuntu: removes the Snap, adds the Mozilla Team PPA, and pins it. Debian: installs from the standard repos.
3 install-1password.sh sudo Configures the 1Password APT repo with debsig signature policy. Arch-aware (amd64 / arm64).
4 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).
5 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).
6 install-vscode.sh sudo Microsoft's official code APT repo, signed-by keyring. --insiders flag installs code-insiders instead.
7 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.
8 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.
9 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.
10 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.
11 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.
12 install_font.sh user Installs the latest Ubuntu Sans Nerd Font to ~/.local/share/fonts/UbuntuSans and refreshes the font cache. No sudo needed.

"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, 8, 9, 10) — pipe through sudo bash
curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-firefox.sh | sudo bash

# user scripts (7, 11, 12) — DO NOT use sudo; they install per-user
bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)"

Every script accepts --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

All scripts share the same robustness baseline:

  • set -euo pipefail + IFS hygiene + ERR trap reporting the failing line number
  • /etc/os-release distro auto-detection (no hardcoded codenames)
  • dpkg --print-architecture / uname -m for 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 .desktop files
  • signed-by keyrings in /etc/apt/keyrings (no deprecated apt-key add)
  • Checksum verification where the upstream publishes one (JetBrains SHA-256, LibreOffice MD5, ipatool 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.sh takes a different code path since Debian has no PPAs)

Other distros are rejected up-front rather than failing later in unpredictable ways.

install-1password.sh Исходник
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
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10
11usage() {
12 cat <<EOF
13Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
14
15Configures the official 1Password APT repository (with debsig policy and
16keyring) and installs the 1Password desktop application. Idempotent: safe to
17re-run.
18
19Options:
20 --dry-run Print actions without executing.
21 --help, -h Show this help.
22EOF
23}
24
25log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
26warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
27die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
28run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
29trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
30
31while (( $# )); 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
38done
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
45case "${ID:-}:${ID_LIKE:-}" in
46 *ubuntu*|*debian*) : ;;
47 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
48esac
49
50# 1Password publishes builds for amd64 and arm64.
51ARCH="$(dpkg --print-architecture)"
52case "$ARCH" in
53 amd64|arm64) : ;;
54 *) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;;
55esac
56log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
57
58export DEBIAN_FRONTEND=noninteractive
59
60KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg
61SOURCES=/etc/apt/sources.list.d/1password.list
62DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22
63DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y curl gpg ca-certificates"
68
69log "Configuring 1Password APT repository..."
70run "install -d -m 0755 /etc/apt/keyrings"
71if [[ ! -s "$KEYRING" ]]; then
72 run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'"
73 run "chmod 0644 '$KEYRING'"
74fi
75
76DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main"
77if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
78 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
79fi
80
81log "Installing debsig policy (verifies package signatures on install)..."
82run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'"
83if [[ ! -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'"
85fi
86if [[ ! -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'"
88fi
89
90log "Installing 1Password..."
91run "apt-get update -qq"
92run "apt-get install -y 1password"
93
94if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then
95 log "1Password binary available at: $(command -v 1password)"
96fi
97log "Done."
98
install-bruno.sh Исходник
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
15
16Configures the official Bruno APT repository (signed-by keyring fetched from
17keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to
18re-run.
19
20Options:
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
28die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
29run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
30trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
31
32while (( $# )); 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
39done
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
46case "${ID:-}:${ID_LIKE:-}" in
47 *ubuntu*|*debian*) : ;;
48 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
49esac
50
51ARCH="$(dpkg --print-architecture)"
52[[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)."
53log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
54
55export DEBIAN_FRONTEND=noninteractive
56
57KEYRING=/etc/apt/keyrings/bruno.gpg
58SOURCES=/etc/apt/sources.list.d/bruno.list
59KEY_ID="9FA6017ECABE0266"
60DESKTOP=/usr/share/applications/bruno.desktop
61
62log "Installing prerequisites..."
63run "apt-get update -qq"
64run "apt-get install -y curl gpg ca-certificates"
65
66log "Configuring Bruno APT repository..."
67run "install -d -m 0755 /etc/apt/keyrings"
68
69if [[ ! -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'"
88fi
89
90DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable"
91if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
92 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
93fi
94
95log "Installing Bruno..."
96run "apt-get update -qq"
97run "apt-get install -y bruno"
98
99log "Writing desktop entry..."
100if (( DRY_RUN )); then
101 printf ' DRY-RUN: write %s\n' "$DESKTOP"
102else
103 cat >"$DESKTOP" <<'EOF'
104[Desktop Entry]
105Name=Bruno
106Comment=Open-source API Client
107Exec=bruno %U
108Terminal=false
109Type=Application
110Icon=bruno
111Categories=Development;Utility;
112StartupNotify=true
113EOF
114 chmod 0644 "$DESKTOP"
115fi
116
117log "Done. Bruno installed."
118
install-espanso.sh Исходник
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: session detection, registers service as the invoking user, idempotent.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11FORCE_VARIANT="" # "wayland" | "x11"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help]
16
17Installs Espanso from the latest GitHub release. Auto-detects whether to use
18the 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
21Options:
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.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
31die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
32run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
33trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
34
35while (( $# )); 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
44done
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
51case "${ID:-}:${ID_LIKE:-}" in
52 *ubuntu*|*debian*) : ;;
53 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
54esac
55
56ARCH="$(dpkg --print-architecture)"
57[[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)."
58
59ACTUAL_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."
61USER_ID="$(id -u "$ACTUAL_USER")"
62
63# Detect session type from the invoking user's environment, fallback to env, fallback to wayland
64if [[ -n "$FORCE_VARIANT" ]]; then
65 VARIANT="$FORCE_VARIANT"
66else
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
74fi
75log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT"
76
77export DEBIAN_FRONTEND=noninteractive
78TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)"
79trap 'rm -f "$TEMP_DEB"' EXIT
80
81DEB_NAME="espanso-debian-${VARIANT}-amd64.deb"
82URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}"
83
84log "Installing prerequisites..."
85run "apt-get update -qq"
86run "apt-get install -y wget libcap2-bin"
87
88log "Downloading ${DEB_NAME}..."
89run "wget -qO '$TEMP_DEB' '$URL'"
90
91log "Installing package..."
92run "apt-get install -y '$TEMP_DEB'"
93
94ESPANSO_BIN="$(command -v espanso || true)"
95[[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install."
96
97if [[ "$VARIANT" == "wayland" ]]; then
98 log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..."
99 run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'"
100fi
101
102log "Registering & starting espanso service for $ACTUAL_USER..."
103# Register may fail if already registered — treat that as success.
104if (( DRY_RUN )); then
105 printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso service register || true\n' "$ACTUAL_USER" "$USER_ID"
106 printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso start || true\n' "$ACTUAL_USER" "$USER_ID"
107else
108 sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso service register || true
109 sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso restart || \
110 sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso start || true
111fi
112
113log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER."
114if [[ "$VARIANT" == "wayland" ]]; then
115 log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml"
116fi
117
install-firefox.sh Исходник
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11ASSUME_YES=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help]
16
17Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the
18official Mozilla APT repository (signed-by keyring + APT pin), then installs
19Firefox so it auto-updates from Mozilla.
20
21Options:
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.
25EOF
26}
27
28log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
29warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
32
33on_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; }
34trap 'on_err $LINENO' ERR
35
36while (( $# )); 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
44done
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
51case "${ID:-}:${ID_LIKE:-}" in
52 *ubuntu*|*debian*) : ;;
53 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
54esac
55log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})"
56
57APT_YES=()
58(( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use
59export DEBIAN_FRONTEND=noninteractive
60
61KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc
62SOURCES=/etc/apt/sources.list.d/mozilla.list
63PIN=/etc/apt/preferences.d/mozilla
64
65log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..."
66if 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"
69fi
70run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true"
71run "rm -f /usr/bin/firefox"
72
73log "Installing prerequisites (wget, gpg, ca-certificates)..."
74run "apt-get update -qq"
75run "apt-get install ${APT_YES[*]} wget gpg ca-certificates"
76
77log "Configuring Mozilla APT repository..."
78run "install -d -m 0755 /etc/apt/keyrings"
79if [[ ! -s "$KEYRING" ]]; then
80 run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg"
81 run "chmod 0644 '$KEYRING'"
82else
83 log "Keyring already present at $KEYRING (skipping download)."
84fi
85
86# Verify key fingerprint matches Mozilla's published fingerprint
87EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3"
88ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')"
89if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then
90 warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually."
91else
92 log "Mozilla key fingerprint verified."
93fi
94
95DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main'
96if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
97 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
98fi
99
100log "Pinning Mozilla repo to priority 1000..."
101if [[ ! -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'
106Package: *
107Pin: origin packages.mozilla.org
108Pin-Priority: 1000
109EOF
110 fi
111fi
112
113log "Installing Firefox from Mozilla repo..."
114run "apt-get update -qq"
115run "apt-get install ${APT_YES[*]} firefox"
116
117if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then
118 log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')"
119fi
120
121log "Done. Firefox installed from Mozilla APT and will auto-update."
122
install-font.sh Исходник
1#!/bin/bash
2
3# Exit immediately if a command exits with a non-zero status
4set -e
5
6FONT_NAME="UbuntuSans"
7FONT_ZIP="${FONT_NAME}.zip"
8# Simplified directory: removed the "NerdFonts" subfolder
9FONT_DIR="$HOME/.local/share/fonts/${FONT_NAME}"
10TMP_DIR=$(mktemp -d)
11
12echo "Searching for the latest release of $FONT_NAME Nerd Font..."
13
14# Use GitHub API to find the latest release download URL for UbuntuSans.zip
15DOWNLOAD_URL=$(curl -s https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \
16 | grep "browser_download_url.*${FONT_ZIP}" \
17 | cut -d '"' -f 4)
18
19if [ -z "$DOWNLOAD_URL" ]; then
20 echo "Error: Could not find the download URL for $FONT_NAME. Check your internet connection or GitHub API limits."
21 rm -rf "$TMP_DIR"
22 exit 1
23fi
24
25echo "Latest version found!"
26echo "Downloading from: $DOWNLOAD_URL"
27
28# Download the zip file to the temporary directory
29curl -L -q "$DOWNLOAD_URL" -o "$TMP_DIR/$FONT_ZIP"
30
31echo "Extracting fonts..."
32# Ensure unzip is installed (will fail gracefully if not)
33if ! command -v unzip &> /dev/null; then
34 echo "Error: 'unzip' is not installed. Please install it using 'sudo apt install unzip' and try again."
35 rm -rf "$TMP_DIR"
36 exit 1
37fi
38
39unzip -q -o "$TMP_DIR/$FONT_ZIP" -d "$TMP_DIR/extracted"
40
41echo "Installing fonts to $FONT_DIR..."
42# Create the simplified font directory if it doesn't exist
43mkdir -p "$FONT_DIR"
44
45# Move only the TrueType (.ttf) or OpenType (.otf) files to the fonts directory
46find "$TMP_DIR/extracted" -name '*.[ot]tf' -type f -exec cp {} "$FONT_DIR/" \;
47
48echo "Updating the system font cache..."
49fc-cache -f "$FONT_DIR"
50
51echo "Cleaning up temporary files..."
52rm -rf "$TMP_DIR"
53
54echo "Done! $FONT_NAME Nerd Font has been successfully installed."
install-ibus-pinyin.sh Исходник
1#!/usr/bin/env bash
2# install-ibus-pinyin.sh — Install IBus + libpinyin and add Intelligent Pinyin
3# to GNOME's Input Sources. Tuned for Ubuntu 25.10 (GNOME 49 / 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
9set -euo pipefail
10IFS=$'\n\t'
11
12readonly SCRIPT_NAME="${0##*/}"
13DRY_RUN=0
14SKIP_GSETTINGS=0
15
16usage() {
17 cat <<EOF
18Usage: $SCRIPT_NAME [--dry-run] [--skip-gsettings] [--help]
19
20Installs ibus-libpinyin (plus Simplified Chinese language packs) and registers
21'Intelligent Pinyin' as a GNOME input source. Idempotent.
22
23Do NOT run with sudo: gsettings is per-user and must run as the desktop user.
24The script invokes sudo internally only for apt-get steps.
25
26Options:
27 --skip-gsettings Install packages but don't touch GNOME input sources
28 (useful on non-GNOME desktops; configure manually afterwards).
29 --dry-run Print actions without executing.
30 --help, -h Show this help.
31
32Switch input methods at runtime with: Super + Space
33EOF
34}
35
36log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
37warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
38die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
39run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
40trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
41
42while (( $# )); do
43 case "$1" in
44 --dry-run) DRY_RUN=1 ;;
45 --skip-gsettings) SKIP_GSETTINGS=1 ;;
46 -h|--help) usage; exit 0 ;;
47 *) die "Unknown argument: $1 (try --help)" ;;
48 esac
49 shift
50done
51
52# Resolve the desktop user. If invoked via sudo, gsettings must target SUDO_USER.
53if (( EUID == 0 )); then
54 DESKTOP_USER="${SUDO_USER:-}"
55 [[ -n "$DESKTOP_USER" && "$DESKTOP_USER" != "root" ]] \
56 || die "Run as a normal user (the script will call sudo itself for apt). gsettings can't run as root."
57else
58 DESKTOP_USER="$USER"
59fi
60
61# Look up the user's UID for the DBus session bus.
62USER_ENTRY="$(getent passwd "$DESKTOP_USER")" || die "User '$DESKTOP_USER' not found in passwd."
63USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")"
64
65[[ -r /etc/os-release ]] || die "/etc/os-release not found."
66# shellcheck disable=SC1091
67. /etc/os-release
68case "${ID:-}:${ID_LIKE:-}" in
69 *ubuntu*|*debian*) : ;;
70 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
71esac
72log "Detected: ${PRETTY_NAME:-unknown}, desktop user: $DESKTOP_USER"
73
74# Helper: invoke a command as $DESKTOP_USER with a working DBus address.
75run_as_user() {
76 local cmd=("$@")
77 if (( DRY_RUN )); then
78 printf ' DRY-RUN (as %s): %s\n' "$DESKTOP_USER" "${cmd[*]}"
79 return 0
80 fi
81 if (( EUID == 0 )); then
82 sudo -u "$DESKTOP_USER" \
83 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
84 XDG_RUNTIME_DIR="/run/user/$USER_ID" \
85 "${cmd[@]}"
86 else
87 "${cmd[@]}"
88 fi
89}
90
91# Helper: invoke a command with sudo when the caller is non-root.
92sudo_run() {
93 if (( DRY_RUN )); then
94 printf ' DRY-RUN (sudo): %s\n' "$*"
95 return 0
96 fi
97 if (( EUID == 0 )); then "$@"; else sudo "$@"; fi
98}
99
100export DEBIAN_FRONTEND=noninteractive
101
102log "Installing IBus Pinyin + Simplified Chinese language packs..."
103sudo_run apt-get update -qq
104sudo_run apt-get install -y \
105 ibus \
106 ibus-libpinyin \
107 language-pack-zh-hans \
108 language-pack-gnome-zh-hans
109
110# Make sure ibus-daemon picks up the new engines for the user.
111if command -v ibus >/dev/null 2>&1; then
112 log "Restarting ibus-daemon for $DESKTOP_USER..."
113 # `ibus exit` will fail if no daemon is running — treat as non-fatal.
114 run_as_user ibus exit >/dev/null 2>&1 || true
115 # Start fresh in the background; -drx replaces a running daemon.
116 if (( ! DRY_RUN )); then
117 sudo -u "$DESKTOP_USER" \
118 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
119 XDG_RUNTIME_DIR="/run/user/$USER_ID" \
120 sh -c 'nohup ibus-daemon -drx >/dev/null 2>&1 &' || \
121 warn "ibus-daemon restart returned non-zero (non-fatal)."
122 sleep 1
123 fi
124fi
125
126if (( SKIP_GSETTINGS )); then
127 log "Skipping GNOME input-sources update (--skip-gsettings)."
128 log "Done. Add 'Chinese (Intelligent Pinyin)' manually under Settings → Keyboard → Input Sources."
129 exit 0
130fi
131
132# Only manage gsettings if GNOME schemas are present.
133if ! run_as_user gsettings list-schemas 2>/dev/null | grep -q '^org.gnome.desktop.input-sources$'; then
134 warn "GNOME schema org.gnome.desktop.input-sources not found; skipping gsettings update."
135 log "Done. Add 'Chinese (Intelligent Pinyin)' manually in your DE's input settings."
136 exit 0
137fi
138
139log "Adding 'Intelligent Pinyin' to GNOME Input Sources (idempotent)..."
140
141CURRENT_SOURCES="$(run_as_user gsettings get org.gnome.desktop.input-sources sources 2>/dev/null || echo '[]')"
142# Strip the optional "@as " type annotation gvariant sometimes prepends.
143CLEAN_SOURCES="${CURRENT_SOURCES#@as }"
144
145if [[ "$CLEAN_SOURCES" == *"'ibus', 'libpinyin'"* ]]; then
146 log "Intelligent Pinyin already present in input sources — nothing to do."
147else
148 if [[ -z "$CLEAN_SOURCES" || "$CLEAN_SOURCES" == "[]" || "$CLEAN_SOURCES" == "@as []" ]]; then
149 NEW_SOURCES="[('xkb', 'us'), ('ibus', 'libpinyin')]"
150 else
151 # Insert the libpinyin tuple before the closing bracket of the existing list.
152 NEW_SOURCES="${CLEAN_SOURCES%]*}, ('ibus', 'libpinyin')]"
153 fi
154 run_as_user gsettings set org.gnome.desktop.input-sources sources "$NEW_SOURCES"
155 log "Added: ('ibus', 'libpinyin')"
156fi
157
158log "Done. Switch input methods with: Super + Space"
159log "If 'Chinese (Intelligent Pinyin)' doesn't appear in the top bar, log out and back in."
160
install-ipatool.sh Исходник
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="majd/ipatool"
12INSTALL_PATH="/usr/local/bin/ipatool"
13
14usage() {
15 cat <<EOF
16Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
17
18Resolves the latest release of majd/ipatool, downloads the tarball for your
19architecture, verifies its SHA-256 against the published checksums.txt, and
20installs the binary to /usr/local/bin/ipatool atomically.
21
22If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
23GitHub API request (avoids rate limits).
24
25Options:
26 --dry-run Print actions without executing.
27 --help, -h Show this help.
28EOF
29}
30
31log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
32warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
33die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
34run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
35trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
36
37while (( $# )); 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
44done
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
51log "Detected: ${PRETTY_NAME:-unknown}"
52
53ARCH_RAW="$(uname -m)"
54case "$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" ;;
59esac
60ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz"
61
62export DEBIAN_FRONTEND=noninteractive
63log "Installing prerequisites..."
64run "apt-get update -qq"
65run "apt-get install -y curl jq tar ca-certificates libsecret-1-0"
66
67GH_HDRS=(-H "Accept: application/vnd.github+json")
68[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
69
70log "Querying GitHub API for latest release of $REPO..."
71RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
72TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
73[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
74log "Latest release: $TAG"
75
76DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
77CHECKSUM_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
80STAGE="$(mktemp -d -t ipatool.XXXXXX)"
81trap 'rm -rf "$STAGE"' EXIT
82TARBALL="$STAGE/ipatool.tar.gz"
83
84log "Downloading $(basename "$DOWNLOAD_URL")..."
85run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'"
86
87if [[ -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."
95else
96 warn "No checksums.txt in release; skipping SHA-256 verification."
97fi
98
99log "Extracting..."
100run "tar -xzf '$TARBALL' -C '$STAGE'"
101BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)"
102if [[ -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)"
105fi
106[[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive."
107
108log "Installing to $INSTALL_PATH..."
109run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'"
110
111if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then
112 log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)"
113fi
114log "Done."
115
install-jetbrains-toolbox.sh Исходник
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11ASSUME_YES=0
12
13usage() {
14 cat <<EOF
15Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help]
16
17Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and
18creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is
19verified against the SHA-256 published in JetBrains' release feed.
20
21Do NOT run with sudo: Toolbox is a per-user install. If a missing system
22package (curl/jq/tar/libfuse2) needs installing, the script will call sudo
23just for that step.
24
25Options:
26 --yes, -y Auto-confirm prompts for installing missing system packages.
27 --dry-run Print actions without executing.
28 --help, -h Show this help.
29EOF
30}
31
32log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
33warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
34die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
35run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
36trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
37
38while (( $# )); 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
46done
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
53case "${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}." ;;
58esac
59log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)"
60
61confirm() {
62 (( ASSUME_YES )) && return 0
63 read -rp "$1 [y/N] " ans
64 [[ "$ans" =~ ^[Yy] ]]
65}
66
67ensure_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
81ensure_pkg curl "command -v curl"
82ensure_pkg jq "command -v jq"
83ensure_pkg tar "command -v tar"
84case "$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" ;;
88esac
89
90ARCH_RAW="$(uname -m)"
91case "$ARCH_RAW" in
92 x86_64) TOOLBOX_ARCH_KEY=linux ;;
93 aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;;
94 *) die "Unsupported architecture: $ARCH_RAW" ;;
95esac
96
97log "Querying JetBrains release feed..."
98RELEASE_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
102TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")"
103TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")"
104TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")"
105[[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed."
106log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)"
107
108INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox"
109STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)"
110trap 'rm -rf "$STAGE_DIR"' EXIT
111
112TARBALL="$STAGE_DIR/toolbox.tar.gz"
113log "Downloading..."
114run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'"
115
116if [[ -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."
124else
125 warn "No checksum URL in release feed; skipping verification."
126fi
127
128log "Extracting to $INSTALL_DIR..."
129run "mkdir -p '$INSTALL_DIR'"
130run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'"
131
132BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox"
133DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop"
134ICON_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
139if [[ -d "$HOME/bin" ]]; then
140 run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'"
141fi
142
143# .desktop launcher
144APPS_DIR="$HOME/.local/share/applications"
145run "mkdir -p '$APPS_DIR'"
146DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop"
147if [[ -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"
155else
156 warn "No .desktop file in archive; skipping launcher."
157fi
158
159log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR"
160
install-libreoffice.sh Исходник
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
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11SKIP_REMOVE_DISTRO=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help]
16
17Detects the latest stable LibreOffice version on download.documentfoundation.org,
18downloads the matching DEB tarball for your architecture, verifies the MD5 sum
19published alongside it, and installs the .deb packages.
20
21Options:
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.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
31die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
32run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
33trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
34
35while (( $# )); 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
43done
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
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;;
53esac
54
55ARCH_RAW="$(uname -m)"
56case "$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)." ;;
60esac
61log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW"
62
63export DEBIAN_FRONTEND=noninteractive
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y curl wget tar coreutils ca-certificates"
68
69log "Resolving latest stable LibreOffice version..."
70INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)"
71LATEST_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."
75log "Latest stable: $LATEST_VERSION"
76
77BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}"
78ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz"
79URL="${BASE}/${ARCHIVE_NAME}"
80SUM_URL="${URL}.md5"
81
82STAGE="$(mktemp -d -t lo.XXXXXX)"
83trap 'rm -rf "$STAGE"' EXIT
84ARCHIVE="$STAGE/$ARCHIVE_NAME"
85
86log "Downloading $ARCHIVE_NAME..."
87run "wget -q --show-progress -O '$ARCHIVE' '$URL'"
88
89log "Verifying MD5..."
90if 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."
94else
95 warn "No MD5 published for $SUM_URL; skipping checksum."
96fi
97
98log "Extracting..."
99run "tar -xzf '$ARCHIVE' -C '$STAGE'"
100DEBS_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
103if (( ! 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
108fi
109
110log "Installing .deb packages..."
111run "dpkg -i -R '$DEBS_DIR'"
112log "Resolving any missing dependencies..."
113run "apt-get install -f -y"
114
115log "Done. LibreOffice $LATEST_VERSION installed."
116
install-network_drive.sh Исходник
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
7set -euo pipefail
8IFS=$'\n\t'
9
10readonly SCRIPT_NAME="${0##*/}"
11DRY_RUN=0
12SERVER_ADDR=""
13SMB_USER=""
14SMB_PASS=""
15MASTER_DIR="/mnt/Synology"
16
17usage() {
18 cat <<EOF
19Usage: sudo $SCRIPT_NAME [options]
20
21Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they
22auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort)
23pins it to the top of the GNOME Dock.
24
25Options:
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.
33EOF
34}
35
36log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
37warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
38die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
39run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
40trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
41
42PASS_FROM_STDIN=0
43while (( $# )); 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
54done
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
61case "${ID:-}:${ID_LIKE:-}" in
62 *ubuntu*|*debian*) : ;;
63 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
64esac
65
66ACTUAL_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.
69USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd."
70USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")"
71USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")"
72USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")"
73
74export DEBIAN_FRONTEND=noninteractive
75log "Installing cifs-utils, smbclient..."
76run "apt-get update -qq"
77run "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
81SERVER_ADDR="${SERVER_ADDR#smb://}"
82SERVER_ADDR="${SERVER_ADDR#//}"
83SERVER_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
89if (( PASS_FROM_STDIN )); then
90 IFS= read -r SMB_PASS
91else
92 read -rsp "Synology password: " SMB_PASS </dev/tty
93 echo
94fi
95[[ -n "$SMB_PASS" ]] || die "Password is empty."
96
97# --- Credentials file ---
98CRED_FILE="$USER_HOME/.smbcredentials_synology"
99log "Writing credentials to $CRED_FILE (mode 0600)..."
100if (( DRY_RUN )); then
101 printf ' DRY-RUN: write %s\n' "$CRED_FILE"
102else
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"
110fi
111
112# --- Query shares ---
113log "Discovering shares on //$SERVER_ADDR..."
114if (( DRY_RUN )); then
115 SHARE_LIST=$'home\nphoto\nvideo'
116else
117 SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \
118 | awk -F'|' '$1=="Disk" {print $2}' || true)"
119fi
120[[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network."
121
122# --- Parent mount dir ---
123run "mkdir -p '$MASTER_DIR'"
124run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'"
125
126# --- Backup fstab once ---
127if [[ ! -f /etc/fstab.bak.synology ]]; then
128 run "cp /etc/fstab /etc/fstab.bak.synology"
129 log "Backed up fstab -> /etc/fstab.bak.synology"
130fi
131
132MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})"
133MARK_END="# <<< synology-master <<<"
134# Remove any previous managed block (so re-run replaces, not appends).
135if 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
141fi
142
143log "Writing managed block to /etc/fstab..."
144FSTAB_BLOCK="$MARK_BEGIN"$'\n'
145while 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'
151done <<<"$SHARE_LIST"
152FSTAB_BLOCK+="$MARK_END"$'\n'
153
154if (( DRY_RUN )); then
155 printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK"
156else
157 printf '%s' "$FSTAB_BLOCK" >>/etc/fstab
158fi
159
160log "Reloading systemd and mounting..."
161run "systemctl daemon-reload"
162run "mount -a -t cifs"
163
164# --- Desktop entry ---
165APPS_DIR="$USER_HOME/.local/share/applications"
166DESKTOP_FILENAME="synology-master.desktop"
167DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME"
168run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'"
169
170if (( DRY_RUN )); then
171 printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE"
172else
173 cat >"$DESKTOP_FILE" <<EOF
174[Desktop Entry]
175Name=Synology NAS
176Comment=Open Synology Master Directory
177Exec=xdg-open ${MASTER_DIR}
178Icon=folder-remote
179Terminal=false
180Type=Application
181Categories=Network;FileTools;
182EOF
183 chown "$USER_ID:$USER_GID" "$DESKTOP_FILE"
184 chmod 0755 "$DESKTOP_FILE"
185fi
186
187# --- Best-effort GNOME dock pin ---
188if (( ! 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
203else
204 log "GNOME not detected (or no active dbus session); skipping dock pin."
205fi
206
207log "Done. Shares mounted under $MASTER_DIR."
208
install-thunderbird.sh Исходник
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
7set -euo pipefail
8IFS=$'\n\t'
9
10readonly SCRIPT_NAME="${0##*/}"
11DRY_RUN=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Installs 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
22Options:
23 --dry-run Print actions without executing.
24 --help, -h Show this help.
25EOF
26}
27
28log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
29warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
32trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
33
34while (( $# )); 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
41done
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
48log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})"
49
50export DEBIAN_FRONTEND=noninteractive
51
52case "${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'
79Package: thunderbird*
80Pin: release o=LP-PPA-mozillateam
81Pin-Priority: 1001
82EOF
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 ;;
100esac
101
102log "Done."
103
install-vscode.sh Исходник
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
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10INSIDERS=0
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help]
15
16Configures the official Microsoft 'vscode' APT repository (signed-by keyring)
17and installs Visual Studio Code. Idempotent: safe to re-run.
18
19Options:
20 --insiders Install the 'code-insiders' build instead of stable 'code'.
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
28die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
29run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
30trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
31
32while (( $# )); 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
40done
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
47case "${ID:-}:${ID_LIKE:-}" in
48 *ubuntu*|*debian*) : ;;
49 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
50esac
51
52ARCH="$(dpkg --print-architecture)"
53case "$ARCH" in
54 amd64|arm64|armhf) : ;;
55 *) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;;
56esac
57log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
58
59export DEBIAN_FRONTEND=noninteractive
60
61KEYRING=/etc/apt/keyrings/packages.microsoft.gpg
62SOURCES=/etc/apt/sources.list.d/vscode.list
63PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code)
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y wget gpg apt-transport-https ca-certificates"
68
69log "Configuring Microsoft vscode APT repository..."
70run "install -d -m 0755 /etc/apt/keyrings"
71if [[ ! -s "$KEYRING" ]]; then
72 run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'"
73 run "chmod 0644 '$KEYRING'"
74fi
75
76DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main"
77if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
78 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
79fi
80
81log "Installing $PKG..."
82run "apt-get update -qq"
83run "apt-get install -y '$PKG'"
84
85if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then
86 log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")"
87fi
88log "Done."
89
menu.sh Исходник
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.
21set -uo pipefail
22IFS=$'\n\t'
23
24readonly SCRIPT_NAME="${0##*/}"
25readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD'
26
27log() { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; }
28warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; }
29err() { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; }
30hr() { 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.
34if (( 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
37fi
38
39# Sanity-check tools we depend on.
40for tool in curl bash mktemp; do
41 command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; }
42done
43
44# Distro check (warn-only; sub-scripts enforce strictly).
45if [[ -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
52fi
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.
56prime_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).
70run_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.)
108OPTIONS=(
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 Visual Studio Code|install-vscode.sh|sudo"
116 "8|Install JetBrains Toolbox (per-user)|install-jetbrains-toolbox.sh|user"
117 "9|Install Bruno (API client)|install-bruno.sh|sudo"
118 "10|Install IPATool|install-ipatool.sh|sudo"
119 "11|Mount Synology Network Drive|install-network_drive.sh|sudo"
120 "12|Install IBus Intelligent Pinyin (per-user)|install-ibus-pinyin.sh|user"
121 "13|Install Ubuntu Sans Nerd Font (per-user)|install-font.sh|user"
122 "14|Fix Dual-Boot Time (RTC to UTC)|timedatectl-fix.sh|sudo"
123)
124
125print_menu() {
126 hr
127 echo " Ubuntu / Debian Setup Manager"
128 hr
129 echo "--- [ Browsers & Mail ] ---"
130 echo " 1) Install Google Chrome"
131 echo " 2) Install Firefox"
132 echo " 3) Install Thunderbird"
133 echo "--- [ Productivity & Security ] ---"
134 echo " 4) Install 1Password"
135 echo " 5) Install Espanso"
136 echo " 6) Install LibreOffice"
137 echo "--- [ Development Tools ] ---"
138 echo " 7) Install Visual Studio Code"
139 echo " 8) Install JetBrains Toolbox (runs as you, not root)"
140 echo " 9) Install Bruno"
141 echo " 10) Install IPATool"
142 echo "--- [ System ] ---"
143 echo " 11) Mount Synology Network Drive"
144 echo " 12) Install IBus Intelligent Pinyin (runs as you, not root)"
145 echo " 13) Install Ubuntu Sans Nerd Font (runs as you, not root)"
146 echo " 14) Fix Dual-Boot Time (RTC to UTC)"
147 hr
148 echo " 0) Run ALL options (1-14)"
149 echo " -1) Exit"
150 hr
151}
152
153# Resolve a numeric choice to its catalog entry; print "name|mode" to stdout.
154resolve_choice() {
155 local want="$1" entry num
156 for entry in "${OPTIONS[@]}"; do
157 num="${entry%%|*}"
158 if [[ "$num" == "$want" ]]; then
159 # Strip the leading "N|label|"; what remains is "filename|mode"
160 printf '%s' "${entry#*|*|}"
161 return 0
162 fi
163 done
164 return 1
165}
166
167ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12 13 14)
168
169prime_sudo || exit 1
170
171while true; do
172 print_menu
173 read -rp "Enter choices separated by spaces (e.g., 1 7 12), 0 for ALL, -1 to exit: " choices </dev/tty || {
174 echo; log "Input closed; exiting."
175 break
176 }
177
178 # Trim whitespace
179 choices="${choices##[[:space:]]}"
180 choices="${choices%%[[:space:]]}"
181
182 if [[ -z "$choices" ]]; then
183 continue
184 fi
185
186 if [[ "$choices" == "-1" ]]; then
187 log "Exiting."
188 break
189 fi
190
191 # Expand "0" to all options
192 if [[ "$choices" == "0" ]]; then
193 choices="${ALL_NUMS[*]}"
194 fi
195
196 # Validate every token first; reject the whole batch if any token is bogus,
197 # so the user sees the problem before any work starts.
198 bad=""
199 for c in $choices; do
200 if ! [[ "$c" =~ ^-?[0-9]+$ ]] || ! resolve_choice "$c" >/dev/null; then
201 bad+=" $c"
202 fi
203 done
204 if [[ -n "$bad" ]]; then
205 err "Invalid option(s):$bad"
206 sleep 1
207 continue
208 fi
209
210 failures=()
211 for c in $choices; do
212 spec="$(resolve_choice "$c")"
213 name="${spec%|*}"
214 mode="${spec##*|}"
215 hr
216 log "[$c] Running $name ($mode)..."
217 if ! run_script "$name" "$mode"; then
218 failures+=("$c:$name")
219 fi
220 done
221
222 hr
223 if (( ${#failures[@]} == 0 )); then
224 log "All selected tasks completed."
225 else
226 warn "Completed with ${#failures[@]} failure(s): ${failures[*]}"
227 fi
228 echo
229done
timedatectl-fix.sh Исходник
1#!/usr/bin/env bash
2
3set -euo pipefail
4
5echo "======================================"
6echo " Ubuntu Dual-Boot Time Fix Script"
7echo " (UTC Method - Highly Recommended)"
8echo "======================================"
9echo
10
11# Ensure running as root
12if [[ $EUID -ne 0 ]]; then
13 echo "Please run as root:"
14 echo "sudo bash fix-time.sh"
15 exit 1
16fi
17
18# Detect current timezone instead of hardcoding
19CURRENT_TZ=$(timedatectl show -p Timezone --value || echo "UTC")
20
21echo "[1/5] Ensuring timezone is set to $CURRENT_TZ..."
22timedatectl set-timezone "$CURRENT_TZ"
23
24echo "[2/5] Configuring RTC to use UTC (to prevent dual-boot conflicts)..."
25# '0' sets the hardware clock to UTC
26timedatectl set-local-rtc 0 --adjust-system-clock
27
28echo "[3/5] Enabling NTP synchronization..."
29timedatectl set-ntp true
30
31echo "[4/5] Restarting time sync daemon (if applicable)..."
32systemctl restart systemd-timesyncd 2>/dev/null || true
33
34echo "[5/5] Final time configuration:"
35echo
36timedatectl
37
38echo
39echo "======================================"
40echo " SUCCESS - LINUX CONFIGURED"
41echo "======================================"
42echo
43echo "Your system is now configured with:"
44echo " - Timezone: $CURRENT_TZ"
45echo " - RTC stored in UTC"
46echo " - NTP synchronization enabled"
47echo
48
49cat <<'EOF'
50--------------------------------------------------
51WINDOWS CONFIGURATION (REQUIRED)
52--------------------------------------------------
53Linux is now correctly expecting the hardware clock to be in UTC.
54To make Windows do the same and stop the time from resetting,
55you MUST run the following command in Windows.
56
571. Boot into Windows.
582. Open Command Prompt as Administrator.
593. Run this exact command:
60
61reg add "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation" /v RealTimeIsUniversal /d 1 /t REG_DWORD /f
62
634. Restart Windows and sync the time in Settings one last time.
64EOF