feat: скрипт чистки orphan-пакетов и старых бэкендов LM Studio
cleanup-packages.sh с подкомандами check/plan/apply/rollback. Дефолт — ничего не делать. apply: план → подтверждение 'yes' → бэкап в ~/.local/state/cleanup-packages/<ts>/ → удаление. Защищённый список: linux-meta, linux-nvidia-meta, linux618-nvidia, python-pkg_resources, boost. Бэкенды — по фиксированному белому списку, vendor/ и cuda12-avx2-2.67.0 не трогаются.
This commit is contained in:
Executable
+633
@@ -0,0 +1,633 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# cleanup-packages.sh — удаление orphan-пакетов pacman и старых
|
||||||
|
# бэкендов LM Studio (Manjaro/Arch)
|
||||||
|
#
|
||||||
|
# Подкоманды:
|
||||||
|
# check — состояние: что найдено, что исключено, прогноз (read-only)
|
||||||
|
# plan — точный список того, что будет удалено (read-only)
|
||||||
|
# apply — бэкап → план → подтверждение → удаление
|
||||||
|
# rollback — переустановить пакеты, удалённые последним apply
|
||||||
|
#
|
||||||
|
# Дефолтное поведение — НИЧЕГО НЕ ДЕЛАТЬ.
|
||||||
|
# Без аргументов скрипт печатает usage и выходит с кодом 2.
|
||||||
|
#
|
||||||
|
# Скрипт НЕ трогает: кэши huggingface/npm/Code, journal, перенос файлов.
|
||||||
|
# Только две группы:
|
||||||
|
# A — orphan-пакеты pacman (минус защищённый список из 5 пакетов)
|
||||||
|
# B — 13 старых бэкендов LM Studio из фиксированного белого списка
|
||||||
|
#
|
||||||
|
# Требования к pacman (проверено на pacman 7.1.0):
|
||||||
|
# * `pacman -R --print` печатает цели в stdout; stderr — только ошибки.
|
||||||
|
# Перехват через 2>&1 работает в обоих случаях.
|
||||||
|
# * `-n` (=--nosave) несовместим с `--print` → в плане только `-R`
|
||||||
|
# * реальное удаление: `sudo pacman -Rns` (конфиги уходят в .pacsave)
|
||||||
|
# ============================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_NAME="${0##*/}"
|
||||||
|
|
||||||
|
# Каталог с бэкапами apply (используется и в rollback).
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/cleanup-packages"
|
||||||
|
|
||||||
|
# Корневой каталог бэкендов LM Studio.
|
||||||
|
BACKENDS_ROOT="$HOME/.lmstudio/extensions/backends"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Константы предметной области
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Пакеты-исключения: orphan'ы, которые удалять НЕЛЬЗЯ.
|
||||||
|
# linux-meta, linux-nvidia-meta — переключатели ядра по умолчанию (0 байт)
|
||||||
|
# linux618-nvidia — модули nvidia для LTS-ядра 6.18.55
|
||||||
|
# (иначе чёрный экран при загрузке с 6.18)
|
||||||
|
# python-pkg_resources — AUR-пакет, нужен pip
|
||||||
|
# boost — нужен для сборки AUR-пакетов
|
||||||
|
PKG_EXCLUDE_RE='^(linux-meta|linux-nvidia-meta|linux618-nvidia|python-pkg_resources|boost)$'
|
||||||
|
|
||||||
|
# ГРУППА B — белый список бэкендов LM Studio к удалению.
|
||||||
|
# Зашит именно список (безопаснее, чем «оставить N самых свежих»).
|
||||||
|
# НЕ удаляются: vendor/ (CUDA-рантайм ~1.5G) и
|
||||||
|
# llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.67.0 (свежая рабочая).
|
||||||
|
LM_BACKEND_NAMES=(
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.13.0"
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.14.4"
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda-avx2-2.20.1"
|
||||||
|
"llama.cpp-linux-x86_64-avx2-2.13.0"
|
||||||
|
"llama.cpp-linux-x86_64-avx2-2.14.4"
|
||||||
|
"llama.cpp-linux-x86_64-avx2-2.20.1"
|
||||||
|
"llama.cpp-linux-x86_64-vulkan-avx2-2.13.0"
|
||||||
|
"llama.cpp-linux-x86_64-vulkan-avx2-2.14.4"
|
||||||
|
"llama.cpp-linux-x86_64-vulkan-avx2-2.20.1"
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.37.0"
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.51.0"
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.55.0"
|
||||||
|
"llama.cpp-linux-x86_64-nvidia-cuda12-avx2-2.64.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод и утилиты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
|
||||||
|
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||||
|
else
|
||||||
|
C_RESET=''; C_BOLD=''; C_DIM=''; C_RED=''; C_GREEN=''; C_YELLOW=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
section() { printf '\n%s%s%s\n' "$C_BOLD" "$1" "$C_RESET"; }
|
||||||
|
ok() { printf '%s✔%s %s\n' "$C_GREEN" "$C_RESET" "$1"; }
|
||||||
|
warn() { printf '%s! %s%s\n' "$C_YELLOW" "$1" "$C_RESET"; }
|
||||||
|
fail() { printf '%s✖ %s%s\n' "$C_RED" "$1" "$C_RESET"; }
|
||||||
|
skip() { printf '%s— %s%s\n' "$C_DIM" "$1" "$C_RESET"; }
|
||||||
|
note() { printf ' %s%s%s\n' "$C_DIM" "$1" "$C_RESET"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<EOF
|
||||||
|
Использование: $SCRIPT_NAME <команда>
|
||||||
|
|
||||||
|
check Состояние: найденные кандидаты, исключения, прогноз (read-only)
|
||||||
|
plan Что ИМЕННО будет удалено + прогноз освобождения (read-only)
|
||||||
|
apply Бэкап → план → подтверждение → удаление
|
||||||
|
rollback Переустановить пакеты, удалённые последним apply
|
||||||
|
|
||||||
|
По умолчанию ничего не выполняется.
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Байты → человекочитаемый размер.
|
||||||
|
human_size() {
|
||||||
|
local b="${1:-0}"
|
||||||
|
awk -v b="$b" 'BEGIN{
|
||||||
|
split("B KiB MiB GiB TiB", u, " "); i = 1
|
||||||
|
while (b >= 1024 && i < 5) { b = b / 1024; i = i + 1 }
|
||||||
|
if (i == 1) printf "%d %s", b, u[i]; else printf "%.2f %s", b, u[i]
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Размер каталога/файла в байтах (реально занятое место, без перехода по ФС).
|
||||||
|
path_bytes() {
|
||||||
|
local p="$1" out
|
||||||
|
out="$(du -sx -B1 -- "$p" 2>/dev/null | awk '{print $1}' | head -n1 || true)"
|
||||||
|
[[ "$out" =~ ^[0-9]+$ ]] || out=0
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Свободное место на корневом разделе в байтах.
|
||||||
|
root_free_bytes() {
|
||||||
|
local out
|
||||||
|
out="$(df -B1 --output=avail / 2>/dev/null | tail -n1 | tr -d ' ' || true)"
|
||||||
|
[[ "$out" =~ ^[0-9]+$ ]] || out=0
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Причина, по которой пакет исключён из удаления.
|
||||||
|
exclusion_reason() {
|
||||||
|
case "$1" in
|
||||||
|
linux-meta) printf '%s' "переключатель ядра по умолчанию (0 байт)" ;;
|
||||||
|
linux-nvidia-meta) printf '%s' "переключатель ядра по умолчанию (0 байт)" ;;
|
||||||
|
linux618-nvidia) printf '%s' "драйвер nvidia для LTS-ядра 6.18 (без него чёрный экран)" ;;
|
||||||
|
python-pkg_resources) printf '%s' "нужен для pip (AUR-пакет)" ;;
|
||||||
|
boost) printf '%s' "нужен для сборки AUR-пакетов" ;;
|
||||||
|
*) printf '%s' "исключён защитным списком" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сбор данных (read-only)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Все orphan-пакеты (как выдаёт pacman) — для отчёта.
|
||||||
|
ALL_ORPHANS=()
|
||||||
|
# Orphan'ы к удалению (минус защищённый список).
|
||||||
|
ORPHAN_PKGS=()
|
||||||
|
# Исключённые пакеты.
|
||||||
|
EXCLUDED_PKGS=()
|
||||||
|
|
||||||
|
collect_orphan_packages() {
|
||||||
|
ALL_ORPHANS=(); ORPHAN_PKGS=(); EXCLUDED_PKGS=()
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
ALL_ORPHANS+=("$line")
|
||||||
|
if [[ "$line" =~ $PKG_EXCLUDE_RE ]]; then
|
||||||
|
EXCLUDED_PKGS+=("$line")
|
||||||
|
else
|
||||||
|
ORPHAN_PKGS+=("$line")
|
||||||
|
fi
|
||||||
|
done < <(pacman -Qdtq 2>/dev/null || true)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Реальный вывод `pacman -R --print` (цели в формате имя-версия).
|
||||||
|
# Внимание: pacman печатает цели в stdout, а в stderr — ошибки.
|
||||||
|
# Перехват через 2>&1 корректен для обоих потоков.
|
||||||
|
# `-n`/--nosave с --print несовместимы, поэтому тут только `-R`.
|
||||||
|
print_targets() {
|
||||||
|
((${#ORPHAN_PKGS[@]} == 0)) && return 0
|
||||||
|
LC_ALL=C pacman -R --print "${ORPHAN_PKGS[@]}" 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Размер каждого orphan-пакета в байтах: "имя<TAB>байты".
|
||||||
|
# LC_ALL=C → английское поле "Installed Size" и точка как десятичный разделитель
|
||||||
|
# (в русской локали там «Установленный размер : 151,16 MiB» с запятой).
|
||||||
|
package_sizes() {
|
||||||
|
((${#ORPHAN_PKGS[@]} == 0)) && return 0
|
||||||
|
LC_ALL=C pacman -Qi "${ORPHAN_PKGS[@]}" 2>/dev/null | awk '
|
||||||
|
/^Name[ \t]*:/ { n = $3 }
|
||||||
|
/^Installed Size/ {
|
||||||
|
u = $5; v = $4 + 0
|
||||||
|
if (u == "KiB") b = v * 1024
|
||||||
|
else if (u == "MiB") b = v * 1048576
|
||||||
|
else if (u == "GiB") b = v * 1073741824
|
||||||
|
else if (u == "B") b = v
|
||||||
|
else b = 0
|
||||||
|
printf "%s\t%d\n", n, b
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Суммарный установленный размер orphan-пакетов (байты).
|
||||||
|
packages_bytes() {
|
||||||
|
package_sizes | awk '{ t += $2 } END { printf "%d", t + 0 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Бэкенды LM Studio к удалению (полные пути) и их суммарный размер.
|
||||||
|
LM_BACKEND_PATHS=()
|
||||||
|
LM_BACKEND_BYTES=0
|
||||||
|
LM_BACKEND_MISSING=()
|
||||||
|
|
||||||
|
collect_lmstudio_backends() {
|
||||||
|
LM_BACKEND_PATHS=(); LM_BACKEND_BYTES=0; LM_BACKEND_MISSING=()
|
||||||
|
local name path b
|
||||||
|
for name in "${LM_BACKEND_NAMES[@]}"; do
|
||||||
|
path="$BACKENDS_ROOT/$name"
|
||||||
|
if [[ ! -e "$path" ]]; then
|
||||||
|
LM_BACKEND_MISSING+=("$name")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$path" || -L "$path" ]]; then
|
||||||
|
warn "пропущен (не каталог или symlink): $path"
|
||||||
|
LM_BACKEND_MISSING+=("$name")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
b="$(path_bytes "$path")"
|
||||||
|
LM_BACKEND_PATHS+=("$path")
|
||||||
|
LM_BACKEND_BYTES=$((LM_BACKEND_BYTES + b))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Проверки безопасности перед rm -rf
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
# Путь разрешён к удалению, только если:
|
||||||
|
# 1) basename входит в белый список LM_BACKEND_NAMES;
|
||||||
|
# 2) путь — каталог (не symlink, не файл);
|
||||||
|
# 3) родитель — ровно $BACKENDS_ROOT (никаких вложенных путей).
|
||||||
|
backend_allowed() {
|
||||||
|
local path="$1" base parent name
|
||||||
|
[[ -e "$path" ]] || return 1
|
||||||
|
[[ -d "$path" ]] || return 1
|
||||||
|
[[ ! -L "$path" ]] || return 1
|
||||||
|
|
||||||
|
base="$(basename -- "$path")"
|
||||||
|
parent="$(dirname -- "$path")"
|
||||||
|
[[ "$parent" == "$BACKENDS_ROOT" ]] || return 1
|
||||||
|
|
||||||
|
for name in "${LM_BACKEND_NAMES[@]}"; do
|
||||||
|
if [[ "$base" == "$name" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# PPID процесса по /proc (comm может содержать пробелы — режем по последней ')').
|
||||||
|
proc_ppid() {
|
||||||
|
local pid="$1" stat rest
|
||||||
|
[[ -r "/proc/$pid/stat" ]] || return 1
|
||||||
|
stat="$(cat "/proc/$pid/stat" 2>/dev/null)" || return 1
|
||||||
|
rest="${stat#*) }"
|
||||||
|
awk '{ print $2 }' <<< "$rest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Запущен ли LM Studio? (процесс не убиваем — только предупреждаем)
|
||||||
|
# Отсекаем: сам скрипт, его предков и любой процесс, чей cmdline содержит
|
||||||
|
# имя этого скрипта — иначе pgrep ловит сам вызов.
|
||||||
|
lmstudio_running() {
|
||||||
|
local pid ppid cmdline comm guard=0
|
||||||
|
local -a ancestors=() candidates=() extra=()
|
||||||
|
|
||||||
|
ppid="$PPID"
|
||||||
|
while [[ -n "$ppid" && "$ppid" != "0" && "$ppid" != "1" && $guard -lt 32 ]]; do
|
||||||
|
ancestors+=("$ppid")
|
||||||
|
ppid="$(proc_ppid "$ppid" || true)"
|
||||||
|
[[ -z "$ppid" ]] && break
|
||||||
|
guard=$((guard + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Точное имя процесса + полный cmdline (вспомогательные процессы LM Studio).
|
||||||
|
mapfile -t candidates < <(pgrep -x 'LM-Studio' 2>/dev/null || true)
|
||||||
|
mapfile -t extra < <(pgrep -f 'lm-studio|LM-Studio' 2>/dev/null || true)
|
||||||
|
candidates+=("${extra[@]:-}")
|
||||||
|
|
||||||
|
for pid in "${candidates[@]:-}"; do
|
||||||
|
[[ -z "$pid" ]] && continue
|
||||||
|
[[ "$pid" == "$$" ]] && continue
|
||||||
|
|
||||||
|
local is_ancestor=0 a
|
||||||
|
for a in "${ancestors[@]:-}"; do
|
||||||
|
if [[ -n "$a" && "$pid" == "$a" ]]; then is_ancestor=1; break; fi
|
||||||
|
done
|
||||||
|
[[ "$is_ancestor" == "1" ]] && continue
|
||||||
|
|
||||||
|
cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
|
||||||
|
[[ "$cmdline" == *"$SCRIPT_NAME"* ]] && continue
|
||||||
|
|
||||||
|
comm="$(cat "/proc/$pid/comm" 2>/dev/null || true)"
|
||||||
|
case "$comm" in
|
||||||
|
LM-Studio*|lm-studio*|LMStudio*|lmstudio*)
|
||||||
|
printf '%s\n' "$pid"; return 0 ;;
|
||||||
|
esac
|
||||||
|
case "$cmdline" in
|
||||||
|
*LM-Studio*|*lm-studio*)
|
||||||
|
printf '%s\n' "$pid"; return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Подтверждение: только точное слово "yes".
|
||||||
|
confirm() {
|
||||||
|
local prompt="$1" reply=""
|
||||||
|
printf '%s' "$prompt"
|
||||||
|
if ! IFS= read -r reply; then reply=""; fi
|
||||||
|
[[ "$reply" == "yes" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Отчёт: check / plan
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print_excluded() {
|
||||||
|
((${#EXCLUDED_PKGS[@]} == 0)) && return 0
|
||||||
|
printf ' %sИсключено (не удаляем):%s\n' "$C_BOLD" "$C_RESET"
|
||||||
|
local p
|
||||||
|
for p in "${EXCLUDED_PKGS[@]}"; do
|
||||||
|
printf ' %-24s %s%s%s\n' "$p" "$C_DIM" "$(exclusion_reason "$p")" "$C_RESET"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
print_plan_section() {
|
||||||
|
local total_bytes="$1"
|
||||||
|
|
||||||
|
section "A. Orphan-пакеты pacman (${#ORPHAN_PKGS[@]} шт.)"
|
||||||
|
if ((${#ORPHAN_PKGS[@]} == 0)); then
|
||||||
|
skip "orphan-пакетов к удалению нет"
|
||||||
|
else
|
||||||
|
printf ' %sЦели (реальный вывод `pacman -R --print`):%s\n' "$C_BOLD" "$C_RESET"
|
||||||
|
print_targets | sed 's/^/ /'
|
||||||
|
printf '\n %sРазмеры:%s\n' "$C_BOLD" "$C_RESET"
|
||||||
|
package_sizes | sort -t$'\t' -k2 -rn | awk -F'\t' '
|
||||||
|
{ printf " %-42s %10.2f MiB\n", $1, $2 / 1048576 }'
|
||||||
|
fi
|
||||||
|
print_excluded
|
||||||
|
|
||||||
|
section "B. Старые бэкенды LM Studio (${#LM_BACKEND_PATHS[@]} шт.)"
|
||||||
|
if ((${#LM_BACKEND_PATHS[@]} == 0)); then
|
||||||
|
skip "бэкендов к удалению нет (уже удалены или не установлены)"
|
||||||
|
else
|
||||||
|
local path
|
||||||
|
for path in "${LM_BACKEND_PATHS[@]}"; do
|
||||||
|
printf ' %s\n' "$path"
|
||||||
|
printf ' %s%s%s\n' "$C_DIM" "$(human_size "$(path_bytes "$path")")" "$C_RESET"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if ((${#LM_BACKEND_MISSING[@]} > 0)); then
|
||||||
|
printf '\n %sОтсутствуют (пропущены, идемпотентность):%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
local m
|
||||||
|
for m in "${LM_BACKEND_MISSING[@]}"; do
|
||||||
|
printf ' %s%s%s\n' "$C_DIM" "$m" "$C_RESET"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
printf '\n %sНе трогаем:%s vendor/ (CUDA-рантайм) и ...-nvidia-cuda12-avx2-2.67.0 (свежая)\n' \
|
||||||
|
"$C_DIM" "$C_RESET"
|
||||||
|
|
||||||
|
section "Прогноз"
|
||||||
|
printf ' пакеты pacman : %s\n' "$(human_size "$PACKAGES_BYTES")"
|
||||||
|
printf ' бэкенды LM : %s\n' "$(human_size "$LM_BACKEND_BYTES")"
|
||||||
|
printf ' %sИТОГО : %s%s\n' "$C_BOLD" "$(human_size "$total_bytes")" "$C_RESET"
|
||||||
|
printf ' %sСвободно на / : %s (было %s)%s\n' \
|
||||||
|
"$C_DIM" "$(human_size "$(root_free_bytes)")" "$(human_size "$FREE_BEFORE")" "$C_RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Команды
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
cmd_check() {
|
||||||
|
section "Проверка (read-only)"
|
||||||
|
|
||||||
|
printf ' orphan-пакетов найдено всего : %s\n' "${#ALL_ORPHANS[@]}"
|
||||||
|
printf ' из них исключено : %s\n' "${#EXCLUDED_PKGS[@]}"
|
||||||
|
printf ' к удалению (группа A) : %s\n' "${#ORPHAN_PKGS[@]}"
|
||||||
|
print_excluded
|
||||||
|
printf '\n старых бэкендов LM Studio : %s (из %s в белом списке)\n' \
|
||||||
|
"${#LM_BACKEND_PATHS[@]}" "${#LM_BACKEND_NAMES[@]}"
|
||||||
|
printf ' бэкендов отсутствует (ок) : %s\n' "${#LM_BACKEND_MISSING[@]}"
|
||||||
|
|
||||||
|
section "Прогноз освобождения"
|
||||||
|
printf ' пакеты pacman : %s\n' "$(human_size "$PACKAGES_BYTES")"
|
||||||
|
printf ' бэкенды LM : %s\n' "$(human_size "$LM_BACKEND_BYTES")"
|
||||||
|
printf ' %sИТОГО : %s%s\n' "$C_BOLD" "$(human_size "$TOTAL_BYTES")" "$C_RESET"
|
||||||
|
printf ' %sСвободно на / : %s%s\n' "$C_DIM" "$(human_size "$(root_free_bytes)")" "$C_RESET"
|
||||||
|
printf '\n %sНичего не изменено.%s Подробности: %s plan\n' "$C_DIM" "$C_RESET" "$SCRIPT_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_plan() {
|
||||||
|
section "План удаления (read-only)"
|
||||||
|
print_plan_section "$TOTAL_BYTES"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# apply
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
cmd_apply() {
|
||||||
|
section "APPLY — удаление orphan-пакетов и старых бэкендов LM Studio"
|
||||||
|
|
||||||
|
if ((${#ORPHAN_PKGS[@]} == 0 && ${#LM_BACKEND_PATHS[@]} == 0)); then
|
||||||
|
skip "нечего удалять — всё уже чисто"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1) Полный план.
|
||||||
|
print_plan_section "$TOTAL_BYTES"
|
||||||
|
|
||||||
|
# 2) Предупреждение про запущенный LM Studio (не убиваем процесс).
|
||||||
|
if lmstudio_running >/dev/null 2>&1; then
|
||||||
|
warn "LM Studio запущен — закройте его перед удалением бэкендов."
|
||||||
|
note "Процесс LM Studio не будет убит этим скриптом."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3) Подтверждение.
|
||||||
|
printf '\n'
|
||||||
|
if ! confirm "Удалить перечисленное? Введите 'yes' для подтверждения: "; then
|
||||||
|
printf '\n'
|
||||||
|
fail "Отменено пользователем. Ничего не удалено."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
# 4) Бэкап.
|
||||||
|
local stamp backup_dir
|
||||||
|
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
backup_dir="$STATE_DIR/$stamp"
|
||||||
|
local n=1
|
||||||
|
while [[ -e "$backup_dir" ]]; do
|
||||||
|
backup_dir="$STATE_DIR/$stamp-$n"
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
mkdir -p -- "$backup_dir"
|
||||||
|
|
||||||
|
printf '%s\n' "${ORPHAN_PKGS[@]:-}" > "$backup_dir/packages-removed.txt"
|
||||||
|
local bnames=()
|
||||||
|
local path
|
||||||
|
for path in "${LM_BACKEND_PATHS[@]:-}"; do
|
||||||
|
bnames+=("$(basename -- "$path")")
|
||||||
|
done
|
||||||
|
printf '%s\n' "${bnames[@]:-}" > "$backup_dir/lmstudio-backends-removed.txt"
|
||||||
|
# Пустые строки от "${arr[@]:-}" на пустом массиве — убираем.
|
||||||
|
sed -i '/^$/d' "$backup_dir/packages-removed.txt" "$backup_dir/lmstudio-backends-removed.txt"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'cleanup-packages.sh — отчёт об удалении\n'
|
||||||
|
printf 'Дата (UTC): %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
printf 'Дата (лок): %s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
printf 'Хост: %s\n' "$(hostname)"
|
||||||
|
printf '\n=== df ДО ===\n'
|
||||||
|
df -h / 2>/dev/null || true
|
||||||
|
printf '\n=== пакеты (имя, установленный размер) ===\n'
|
||||||
|
package_sizes | awk -F'\t' '{ printf "%-42s %s\n", $1, $2 }'
|
||||||
|
printf '\n=== бэкапы ===\n'
|
||||||
|
printf 'packages-removed.txt — %s пакет(ов)\n' "${#ORPHAN_PKGS[@]}"
|
||||||
|
printf 'lmstudio-backends-removed.txt — %s каталог(ов)\n' "${#LM_BACKEND_PATHS[@]}"
|
||||||
|
printf '\n=== df ПОСЛЕ ===\n'
|
||||||
|
} > "$backup_dir/manifest.txt"
|
||||||
|
chmod 600 -- "$backup_dir"/* 2>/dev/null || true
|
||||||
|
|
||||||
|
ok "бэкап создан: $backup_dir"
|
||||||
|
|
||||||
|
# 5) Удаление бэкендов LM Studio (от пользователя, sudo не нужен).
|
||||||
|
local removed_backends=0
|
||||||
|
local -a removed_backend_names=()
|
||||||
|
if ((${#LM_BACKEND_PATHS[@]} > 0)); then
|
||||||
|
section "Удаление бэкендов LM Studio"
|
||||||
|
for path in "${LM_BACKEND_PATHS[@]}"; do
|
||||||
|
if backend_allowed "$path"; then
|
||||||
|
if rm -rf -- "$path"; then
|
||||||
|
ok "удалён: $path"
|
||||||
|
removed_backends=$((removed_backends + 1))
|
||||||
|
removed_backend_names+=("$(basename -- "$path")")
|
||||||
|
else
|
||||||
|
fail "не удалось удалить: $path"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "ПРОПУЩЕН (не прошёл проверку безопасности): $path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6) Удаление пакетов pacman (нужен sudo).
|
||||||
|
if ((${#ORPHAN_PKGS[@]} > 0)); then
|
||||||
|
section "Удаление orphan-пакетов pacman"
|
||||||
|
if ! command -v sudo >/dev/null 2>&1; then
|
||||||
|
warn "sudo не найден — пакеты пропущены (бэкап сохранён, откат возможен)."
|
||||||
|
elif ! sudo -v; then
|
||||||
|
warn "sudo недоступен (не удалось получить права) — пакеты пропущены."
|
||||||
|
note "Бэкап: $backup_dir — откат возможен командой: $SCRIPT_NAME rollback"
|
||||||
|
else
|
||||||
|
# -n (--nosave) оставляет конфиги в .pacsave; --noconfirm — мы уже спросили.
|
||||||
|
if sudo pacman -Rns --noconfirm "${ORPHAN_PKGS[@]}"; then
|
||||||
|
ok "pacman завершил удаление пакетов"
|
||||||
|
else
|
||||||
|
fail "pacman завершился с ошибкой — часть пакетов могла остаться"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7) Итог: df после + уточнение бэкапа (только реально удалённое).
|
||||||
|
local free_after
|
||||||
|
free_after="$(root_free_bytes)"
|
||||||
|
|
||||||
|
# Уточняем списки: оставляем только то, что действительно исчезло с системы.
|
||||||
|
local -a removed_pkg_names=()
|
||||||
|
local p
|
||||||
|
for p in "${ORPHAN_PKGS[@]:-}"; do
|
||||||
|
[[ -z "$p" ]] && continue
|
||||||
|
if ! pacman -Q "$p" >/dev/null 2>&1; then
|
||||||
|
removed_pkg_names+=("$p")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '%s\n' "${removed_pkg_names[@]:-}" > "$backup_dir/packages-removed.txt"
|
||||||
|
printf '%s\n' "${removed_backend_names[@]:-}" > "$backup_dir/lmstudio-backends-removed.txt"
|
||||||
|
sed -i '/^$/d' "$backup_dir/packages-removed.txt" "$backup_dir/lmstudio-backends-removed.txt"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '\n=== df ПОСЛЕ (факт) ===\n'
|
||||||
|
df -h / 2>/dev/null || true
|
||||||
|
printf '\nУдалено пакетов: %s\n' "${#removed_pkg_names[@]}"
|
||||||
|
printf 'Удалено бэкендов: %s\n' "${#removed_backend_names[@]}"
|
||||||
|
printf 'Свободно до: %s\n' "$(human_size "$FREE_BEFORE")"
|
||||||
|
printf 'Свободно после: %s\n' "$(human_size "$free_after")"
|
||||||
|
printf '\n=== фактически удалённые пакеты ===\n'
|
||||||
|
printf '%s\n' "${removed_pkg_names[@]:-}" | sed '/^$/d'
|
||||||
|
printf '\n=== фактически удалённые бэкенды ===\n'
|
||||||
|
printf '%s\n' "${removed_backend_names[@]:-}" | sed '/^$/d'
|
||||||
|
} >> "$backup_dir/manifest.txt"
|
||||||
|
|
||||||
|
section "Готово"
|
||||||
|
printf ' удалено пакетов : %s\n' "${#removed_pkg_names[@]}"
|
||||||
|
printf ' удалено бэкендов : %s\n' "${#removed_backend_names[@]}"
|
||||||
|
printf ' свободно на / : %s → %s\n' \
|
||||||
|
"$(human_size "$FREE_BEFORE")" "$(human_size "$free_after")"
|
||||||
|
printf ' бэкап : %s\n' "$backup_dir"
|
||||||
|
printf '\n %sОткат: %s rollback%s\n' "$C_DIM" "$SCRIPT_NAME" "$C_RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# rollback
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
cmd_rollback() {
|
||||||
|
section "ROLLBACK — восстановление пакетов из последнего apply"
|
||||||
|
|
||||||
|
local latest
|
||||||
|
latest="$(find "$STATE_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -n1 || true)"
|
||||||
|
if [[ -z "$latest" ]]; then
|
||||||
|
fail "бэкапов не найдено в $STATE_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "последний бэкап: $latest"
|
||||||
|
|
||||||
|
local pkgs=()
|
||||||
|
if [[ -f "$latest/packages-removed.txt" ]]; then
|
||||||
|
mapfile -t pkgs < "$latest/packages-removed.txt"
|
||||||
|
fi
|
||||||
|
local backends=()
|
||||||
|
if [[ -f "$latest/lmstudio-backends-removed.txt" ]]; then
|
||||||
|
mapfile -t backends < "$latest/lmstudio-backends-removed.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#pkgs[@]} == 0)); then
|
||||||
|
skip "в бэкапе нет списка пакетов"
|
||||||
|
else
|
||||||
|
printf ' %sПакеты к переустановке (%s шт.):%s\n' "$C_BOLD" "${#pkgs[@]}" "$C_RESET"
|
||||||
|
local p
|
||||||
|
for p in "${pkgs[@]}"; do
|
||||||
|
[[ -z "$p" ]] && continue
|
||||||
|
printf ' %s\n' "$p"
|
||||||
|
done
|
||||||
|
printf '\n %sКоманда восстановления:%s\n' "$C_BOLD" "$C_RESET"
|
||||||
|
printf ' sudo pacman -S %s\n' "${pkgs[*]}"
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
if confirm "Выполнить восстановление пакетов? Введите 'yes': "; then
|
||||||
|
if sudo -v && sudo pacman -S --noconfirm "${pkgs[@]}"; then
|
||||||
|
ok "пакеты переустановлены"
|
||||||
|
else
|
||||||
|
fail "восстановление не удалось"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
skip "восстановление не выполнялось (команда выше — скопируйте вручную)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#backends[@]} > 0)); then
|
||||||
|
printf '\n %sБэкенды LM Studio (%s шт.) — перекачиваются автоматически:%s\n' \
|
||||||
|
"$C_BOLD" "${#backends[@]}" "$C_RESET"
|
||||||
|
local b
|
||||||
|
for b in "${backends[@]}"; do
|
||||||
|
[[ -z "$b" ]] && continue
|
||||||
|
printf ' %s\n' "$b"
|
||||||
|
done
|
||||||
|
note "LM Studio сам скачает нужную версию при следующем запуске."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Точка входа
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local cmd="${1:-}"
|
||||||
|
case "$cmd" in
|
||||||
|
check|plan|apply|rollback) ;;
|
||||||
|
-h|--help|help) usage ;;
|
||||||
|
"") usage ;;
|
||||||
|
*) printf 'Неизвестная команда: %s\n\n' "$cmd" >&2; usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v pacman >/dev/null 2>&1; then
|
||||||
|
fail "pacman не найден — этот скрипт рассчитан на Manjaro/Arch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
collect_orphan_packages
|
||||||
|
collect_lmstudio_backends
|
||||||
|
|
||||||
|
PACKAGES_BYTES="$(packages_bytes)"
|
||||||
|
TOTAL_BYTES=$((PACKAGES_BYTES + LM_BACKEND_BYTES))
|
||||||
|
FREE_BEFORE="$(root_free_bytes)"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
check) cmd_check ;;
|
||||||
|
plan) cmd_plan ;;
|
||||||
|
apply) cmd_apply ;;
|
||||||
|
rollback) cmd_rollback ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user