diff --git a/README.md b/README.md index 6554629..d231b90 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ Dos variantes de escritorio: **GNOME** y **KDE Plasma**. ``` arch-setup/ -├── gnome/ archinstall-config.json + setup.sh (GNOME, gdm) -├── kde/ archinstall-config.json + setup.sh (KDE Plasma, sddm) -├── shared/ packages-common.sh (paquetes comunes a ambos) +├── gnome/ archinstall-config.json + setup.sh (GNOME, gdm) +├── kde/ archinstall-config.json + setup.sh (KDE Plasma, sddm) +├── shared/ packages-common.sh (paquetes comunes a ambos) +├── pkgsync/ pkgsync.sh + systemd/ (sync de paquetes entre máquinas) └── README.md ``` @@ -77,3 +78,106 @@ Remmina+FreeRDP, Apostrophe, guvcview, KVIrc, Zen Browser, VSCode, DavMail, Betterbird, OnlyOffice, Spotify, ungoogled-chromium, Karere, HelpWire (vía `.deb`→`debtap`). GNOME añade gnome-calculator/tweaks; KDE añade kcalc/ark/spectacle/kdeconnect/kinfocenter. + +## pkgsync — sincronización de paquetes entre las dos máquinas + +Mismo patrón que chezmoi para los dotfiles (`chezmoi-boot-sync.service` / +`chezmoi-shutdown-sync.service` en el repo `arch-configs`), pero para qué +paquetes hay instalados de verdad (pacman + AUR + flatpak). Fuente de verdad: +los arrays `PACMAN_COMMON`/`AUR_COMMON`/`FLATPAK_COMMON` de +`shared/packages-common.sh`. Motor de diff/aplicación: +[metapac](https://github.com/ripytide/metapac) (AUR), con `paru` como backend +para que cubra repos oficiales y AUR en una sola pasada. + +``` +pkgsync/ +├── pkgsync.sh CLI: bootstrap/push/pull/diff/apply/log/incoming/audit +└── systemd/ + ├── pkgsync-push.service captura+sube al cerrar sesión/apagar + └── pkgsync-pull.service trae+compara al iniciar sesión (nunca instala/desinstala) +``` + +### Cómo funciona + +- **`pkgsync push`** (dispara `pkgsync-push.service` al cerrar sesión, mismo + truco `ExecStop` en unidad oneshot `RemainAfterExit` que usa + `chezmoi-shutdown-sync.service`): captura el inventario real + (`pacman -Qqen`/`pacman -Qqem`/`flatpak list`), parchea los tres arrays de + `packages-common.sh` en un clon dedicado (`~/.local/share/arch-setup`) — + añade lo nuevo, quita lo que ya no está instalado — y hace `git commit` + + `push` best-effort con timeout, igual que chezmoi. El parcheo respeta los + comentarios de agrupación existentes: lo nuevo se añade en un bloque + marcador al final del array, listo para que lo reorganices a mano cuando + quieras. +- **`pkgsync pull`** (dispara `pkgsync-pull.service` al iniciar sesión): `git + pull` best-effort con timeout (nunca bloquea el login), calcula el diff + entre lo declarado y lo instalado, y **solo notifica** (notificación de + escritorio + detalle en `~/.local/share/pkgsync.log`). No instala ni + desinstala nada por sí solo. +- **`pkgsync apply`** (manual, cuando tú decidas revisar el diff): corre + `metapac sync` (instala lo que falte) y `metapac clean` (desinstala lo + sobrante), **ambos con la confirmación interactiva nativa de metapac** — + nunca se pasa `--no-confirm`, así que siempre ves y confirmas cada borrado + antes de que ocurra. +- **`pkgsync diff`**: igual que el cálculo de `pull` pero solo imprime, sin + tocar git ni notificar. +- **`pkgsync incoming`**: commits nuevos en `origin/main` aún no traídos + (equivalente a `git log HEAD..origin/main`). +- **`pkgsync log [--patch]`**: historial legible de cambios de paquetes + (`git log` sobre `shared/packages-common.sh`). +- **`pkgsync audit`**: paquetes instalados localmente que no están declarados + en ningún array ni en el override local — drift no capturado. Nota: los + paquetes específicos de KDE/GNOME hardcodeados en `/setup.sh` + (`plasma-desktop`, `sddm`, temas AUR, etc.) siempre van a aparecer aquí — + no viven en los arrays por diseño, no es drift real. + +### Paquetes críticos (nunca se borran automáticamente) + +`pkgsync.sh` lleva una lista `CRITICAL_PACKAGES` hardcodeada (kernel, +bootloader, drivers gráficos, red de rollback btrfs, y todo lo que sea +soporte de hardware: CPU/energía, audio, bluetooth, red física, USB) que es +independiente del array — sobrevive aunque el array se corrompa por error +humano. Dos capas de protección: `push` nunca la quita del array aunque +detecte que un paquete crítico no está instalado localmente (solo avisa en el +log), y el group file que se genera para metapac siempre fuerza la unión con +esta lista, así que `metapac clean` nunca puede verlos como "no declarados". +La lista completa está al principio de `pkgsync.sh`. + +### Overrides locales (divergencias legítimas entre máquinas) + +`~/.config/metapac/groups/overrides.local.toml` — mismo formato de group file +que usa metapac. No vive en ningún repo git (fuera de `arch-setup`, en +`~/.config/metapac/`), así que ya queda fuera del control de versiones sin +necesidad de nada más. `pkgsync` lo lee solo para no proponer esos paquetes +como "nuevos" en `push` ni marcarlos como drift en `audit`; nunca lo escribe +ni lo sobreescribe. `bootstrap` deja un scaffold comentado si no existe. + +### Instalación + +**Máquinas nuevas**: automático, `install_pkgsync()` (en +`packages-common.sh`) se llama desde `/setup.sh` justo después de +`chezmoi init --apply`. + +**Las dos máquinas ya instaladas** (retrofit, sin reinstalar): + +```bash +mkdir -p ~/.local/bin ~/.config/systemd/user +git clone https://gitea.josevi.ovh/josevi/arch-setup.git ~/.local/share/arch-setup +cp ~/.local/share/arch-setup/pkgsync/pkgsync.sh ~/.local/bin/ && chmod +x ~/.local/bin/pkgsync.sh +cp ~/.local/share/arch-setup/pkgsync/systemd/*.service ~/.config/systemd/user/ +pkgsync.sh bootstrap +systemctl --user enable --now pkgsync-push.service pkgsync-pull.service +``` + +`loginctl enable-linger` ya está activo por el `run_once` de chezmoi, no hace +falta repetirlo. Verifica una vez con `flatpak list +--columns=installation,application` que tu instalación de Flatpak es +`system` (si es `user`, cambia `FLATPAK_INSTALLATION` al principio de +`pkgsync.sh`). + +### Auto-instalar sin supervisión (opt-in, apagado por defecto) + +`pkgsync pull` respeta la variable de entorno `PKGSYNC_AUTO_INSTALL=1`: si +está activada, corre `metapac sync --no-confirm` justo después de calcular el +diff (solo instala lo que falte). **Nunca** toca `metapac clean` — los +borrados siguen requiriendo `pkgsync apply` manual, pase lo que pase. diff --git a/gnome/setup.sh b/gnome/setup.sh index 06568f6..b462cb4 100644 --- a/gnome/setup.sh +++ b/gnome/setup.sh @@ -65,8 +65,7 @@ install_pacman "${PACMAN_COMMON[@]}" \ xdg-desktop-portal-gnome xdg-user-dirs-gtk gvfs \ nautilus gnome-console gnome-text-editor gnome-calculator loupe \ gnome-tweaks gnome-themes-extra gnome-shell-extension-appindicator \ - gnome-backgrounds power-profiles-daemon \ - mesa vulkan-icd-loader vulkan-radeon vulkan-intel intel-media-driver + gnome-backgrounds power-profiles-daemon echo ">> Bootloader (GRUB)..." install_bootloader @@ -150,6 +149,9 @@ REMOTE="$GITEA/arch-configs.git" as_user "export GIT_NAME='$GIT_NAME' GIT_EMAIL='$GIT_EMAIL'; \ chezmoi init --apply '$REMOTE'" +# --- pkgsync: sincronización de paquetes (repo arch-setup) ----------------- +install_pkgsync + # quitar el NOPASSWD temporal rm -f /etc/sudoers.d/99-setup-nopasswd echo ">> Configuración de GNOME completada." diff --git a/kde/setup.sh b/kde/setup.sh index 0ef05f8..ea04cfd 100644 --- a/kde/setup.sh +++ b/kde/setup.sh @@ -67,7 +67,6 @@ install_pacman "${PACMAN_COMMON[@]}" \ konsole dolphin kate ark spectacle kcalc gwenview okular kdeconnect kdegraphics-thumbnailers \ plasma-systemmonitor \ adwaita-fonts \ - mesa vulkan-icd-loader vulkan-radeon vulkan-intel intel-media-driver \ packagekit-qt6 # Sin discover: se usa pamac-all + pamac-tray-plasma-git como gestor gráfico. # packagekit-qt6 lo requiere pamac-tray-plasma-git. @@ -121,5 +120,8 @@ REMOTE="$GITEA/arch-configs.git" as_user "export GIT_NAME='$GIT_NAME' GIT_EMAIL='$GIT_EMAIL'; \ chezmoi init --apply '$REMOTE'" +# --- pkgsync: sincronización de paquetes (repo arch-setup) ----------------- +install_pkgsync + rm -f /etc/sudoers.d/99-setup-nopasswd echo ">> Configuración de KDE completada." diff --git a/pkgsync/pkgsync.sh b/pkgsync/pkgsync.sh new file mode 100644 index 0000000..8e84000 --- /dev/null +++ b/pkgsync/pkgsync.sh @@ -0,0 +1,466 @@ +#!/usr/bin/env bash +# pkgsync: sincroniza qué paquetes (pacman + AUR + flatpak) hay instalados +# entre las dos máquinas, usando shared/packages-common.sh (en un clon propio +# del repo arch-setup) como fuente de verdad y metapac como motor de +# diff/aplicación. Mismo patrón que chezmoi-boot-sync.sh/chezmoi-shutdown-sync.sh +# (repo arch-configs) pero para paquetes en vez de dotfiles. +# +# pkgsync.sh bootstrap instala metapac + config, primera vez o retrofit +# pkgsync.sh push captura inventario real -> arrays -> commit+push (pkgsync-push.service) +# pkgsync.sh pull git pull + diff + notifica, NO instala/desinstala (pkgsync-pull.service) +# pkgsync.sh diff como pull pero solo imprime, sin tocar git ni notificar +# pkgsync.sh apply metapac sync + metapac clean, ambos con confirmación interactiva +# pkgsync.sh log [--patch] +# pkgsync.sh incoming +# pkgsync.sh audit +set -uo pipefail + +REPO_URL="https://gitea.josevi.ovh/josevi/arch-setup.git" +REPO_DIR="$HOME/.local/share/arch-setup" +LOG="$HOME/.local/share/pkgsync.log" + +METAPAC_CONFIG_DIR="$HOME/.config/metapac" +METAPAC_GROUP="$METAPAC_CONFIG_DIR/groups/arch-setup.toml" +OVERRIDES_FILE="$METAPAC_CONFIG_DIR/groups/overrides.local.toml" + +# flatpak sin --user/--system instala en "system" con la config actual de +# install_flatpaks() en packages-common.sh. Si tu máquina usa --user, cambia +# esto (o verifica con: flatpak list --columns=installation,application). +FLATPAK_INSTALLATION="system" + +# Paquetes que NUNCA se borran automáticamente, pase lo que pase en el array +# en un momento dado -- red de seguridad independiente, para que sobreviva +# aunque el array se corrompa por error humano. Confirmada con el usuario +# 2026-08-18 (kernel/arranque, gráficos, red de rollback btrfs, y todo lo que +# sea soporte de hardware: CPU/energía, audio, bluetooth, red física, USB). +CRITICAL_PACKAGES=( + linux linux-lts linux-lts-headers linux-firmware grub efibootmgr amd-ucode + mesa vulkan-icd-loader vulkan-radeon vulkan-intel intel-media-driver + btrfs-progs snapper snap-pac grub-btrfs zram-generator + cpupower power-profiles-daemon + pipewire pipewire-alsa pipewire-jack pipewire-pulse wireplumber libpulse + gst-plugin-pipewire gst-plugins-good gst-plugins-bad gst-libav sof-firmware + bluez bluez-utils + networkmanager wpa_supplicant + smartmontools usbutils gvfs-mtp +) + +# --- inventario real --------------------------------------------------------- +installed_pacman_native() { pacman -Qqen 2>/dev/null; } # explícitos de repos oficiales +installed_pacman_foreign() { pacman -Qqem 2>/dev/null; } # explícitos AUR/locales +installed_flatpak() { + command -v flatpak &>/dev/null || return 0 + flatpak list --app --columns=application 2>/dev/null +} + +# --- arrays declarados (leídos del clon en $REPO_DIR) ----------------------- +# Sourcea packages-common.sh en un subshell: solo lee los arrays, nunca llama +# a los install_*. +declared_array() { + local name="$1" + ( source "$REPO_DIR/shared/packages-common.sh" 2>/dev/null + case "$name" in + PACMAN_COMMON) printf '%s\n' "${PACMAN_COMMON[@]}" ;; + AUR_COMMON) printf '%s\n' "${AUR_COMMON[@]}" ;; + FLATPAK_COMMON) printf '%s\n' "${FLATPAK_COMMON[@]}" ;; + esac ) +} + +# Paquetes en el override local (parser mínimo: basta para sacar los +# packages = [...] de [arch] y [flatpak], ignorando líneas comentadas). +overrides_packages() { + [[ -f "$OVERRIDES_FILE" ]] || return 0 + awk ' + /^[[:space:]]*#/ { next } + /packages[[:space:]]*=[[:space:]]*\[/ { inarr=1 } + inarr { + while (match($0, /"[^"]+"/)) { + print substr($0, RSTART+1, RLENGTH-2) + $0 = substr($0, RSTART+RLENGTH) + } + } + /\]/ { inarr=0 } + ' "$OVERRIDES_FILE" +} + +# --- parcheo del array en packages-common.sh sin tocar comentarios ---------- +# Quita de $array_name los tokens listados en $remove_list (uno por línea) y +# añade los de $add_list en un bloque marcador al final, dejando el resto de +# líneas (incluidos los comentarios de agrupación) intactas. +patch_array_file() { + local file="$1" array_name="$2" remove_list="$3" add_list="$4" + local marker=" # --- pkgsync: nuevos detectados (revisa y reorganiza a mano) ---" + + local -A to_remove=() + local pkg + while IFS= read -r pkg; do + [[ -n "$pkg" ]] && to_remove["$pkg"]=1 + done <<<"$remove_list" + + local -a lines + mapfile -t lines <"$file" + + local start_idx=-1 end_idx=-1 i + for i in "${!lines[@]}"; do + if [[ "${lines[$i]}" == "${array_name}=(" ]]; then start_idx=$i; break; fi + done + if (( start_idx < 0 )); then + echo "!! no encuentro el array $array_name en $file" >&2 + return 1 + fi + for ((i = start_idx + 1; i < ${#lines[@]}; i++)); do + if [[ "${lines[$i]}" == ")" ]]; then end_idx=$i; break; fi + done + if (( end_idx < 0 )); then + echo "!! no encuentro el cierre de $array_name en $file" >&2 + return 1 + fi + + # Ojo: las líneas de paquetes que quedaron bajo el marcador de una pasada + # anterior NO se descartan aquí -- pasan por el mismo filtro de to_remove + # que cualquier otra línea, así que lo que ya se añadió sigue en el array + # aunque esta pasada no tenga nada nuevo que añadir (add_list vacío). Solo + # se quita la línea de comentario del marcador en sí, para no acumular + # comentarios duplicados; si hay contenido nuevo se vuelve a escribir abajo. + local -a new_body=() + for ((i = start_idx + 1; i < end_idx; i++)); do + local line="${lines[$i]}" + if [[ "$line" == "$marker" ]]; then continue; fi + if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then + new_body+=("$line") + continue + fi + local indent="${line%%[^[:space:]]*}" + local -a tokens=($line) + local -a kept=() + local t + for t in "${tokens[@]}"; do + [[ -n "${to_remove[$t]:-}" ]] || kept+=("$t") + done + (( ${#kept[@]} > 0 )) && new_body+=("${indent}${kept[*]}") + done + + if [[ -n "$add_list" ]]; then + new_body+=("$marker") + local p + while IFS= read -r p; do + [[ -n "$p" ]] && new_body+=(" $p") + done <<<"$add_list" + fi + + local tmp + tmp="$(mktemp)" + printf '%s\n' "${lines[@]:0:start_idx+1}" "${new_body[@]}" "${lines[@]:end_idx}" >"$tmp" + mv "$tmp" "$file" +} + +# --- group file de metapac --------------------------------------------------- +# Regenerado en bootstrap/push/pull. Fuerza la unión con CRITICAL_PACKAGES: es +# la segunda capa de protección, para que metapac nunca vea esos paquetes como +# "no declarados" aunque el array se corrompiera. +write_group_file() { + mkdir -p "$(dirname "$METAPAC_GROUP")" + { + echo "# generado por pkgsync -- no editar a mano, se sobreescribe en cada bootstrap/push/pull" + echo "[arch]" + echo "packages = [" + { declared_array PACMAN_COMMON; declared_array AUR_COMMON; printf '%s\n' "${CRITICAL_PACKAGES[@]}"; } \ + | sort -u | while IFS= read -r p; do [[ -n "$p" ]] && printf ' "%s",\n' "$p"; done + echo "]" + echo + echo "[flatpak]" + echo "packages = [" + declared_array FLATPAK_COMMON | sort -u | while IFS= read -r p; do + [[ -n "$p" ]] && printf ' { name = "%s:%s", options = { remote = "flathub" } },\n' "$FLATPAK_INSTALLATION" "$p" + done + echo "]" + } >"$METAPAC_GROUP" +} + +# --- diff declarado vs instalado -------------------------------------------- +# Deja el resultado en arrays globales: TO_INSTALL_* (declarado, no instalado) +# y TO_REVIEW_REMOVE_* (instalado, no declarado). +compute_diff() { + local work + work="$(mktemp -d)" + trap 'rm -rf "$work"' RETURN + + { declared_array PACMAN_COMMON; declared_array AUR_COMMON; printf '%s\n' "${CRITICAL_PACKAGES[@]}"; } | sort -u >"$work/declared_arch" + declared_array FLATPAK_COMMON | sort -u >"$work/declared_flatpak" + overrides_packages | sort -u >"$work/overrides" + { installed_pacman_native; installed_pacman_foreign; } | sort -u >"$work/installed_arch" + installed_flatpak | sort -u >"$work/installed_flatpak" + + mapfile -t TO_INSTALL_ARCH < <(comm -23 "$work/declared_arch" "$work/installed_arch" | comm -23 - "$work/overrides") + mapfile -t TO_REVIEW_REMOVE_ARCH < <(comm -13 "$work/declared_arch" "$work/installed_arch" | comm -23 - "$work/overrides") + mapfile -t TO_INSTALL_FLATPAK < <(comm -23 "$work/declared_flatpak" "$work/installed_flatpak" | comm -23 - "$work/overrides") + mapfile -t TO_REVIEW_REMOVE_FLATPAK < <(comm -13 "$work/declared_flatpak" "$work/installed_flatpak" | comm -23 - "$work/overrides") +} + +print_list() { + if (( $# == 0 )); then echo " (nada)"; return; fi + printf ' %s\n' "$@" +} + +print_diff() { + echo "== pendiente de instalar (declarado, no instalado) ==" + print_list "${TO_INSTALL_ARCH[@]}" "${TO_INSTALL_FLATPAK[@]}" + echo + echo "== instalado pero no declarado (candidato a revisar/borrar con 'pkgsync apply') ==" + print_list "${TO_REVIEW_REMOVE_ARCH[@]}" "${TO_REVIEW_REMOVE_FLATPAK[@]}" +} + +notify() { + command -v notify-send &>/dev/null && notify-send --app-name=pkgsync "$1" "$2" +} + +# --- subcomandos -------------------------------------------------------------- +cmd_bootstrap() { + echo ">> pkgsync bootstrap" + + if ! command -v metapac &>/dev/null; then + echo ">> instalando metapac (AUR)..." + if command -v paru &>/dev/null; then + paru -S --needed --noconfirm metapac + else + echo "!! no encuentro paru; instala metapac a mano (helper AUR) y vuelve a correr 'pkgsync bootstrap'." >&2 + return 1 + fi + fi + + mkdir -p "$METAPAC_CONFIG_DIR/groups" "$(dirname "$REPO_DIR")" "$(dirname "$LOG")" + + if [[ ! -f "$METAPAC_CONFIG_DIR/config.toml" ]]; then + cat >"$METAPAC_CONFIG_DIR/config.toml" <<'EOF' +enabled_backends = ["arch", "flatpak"] + +[arch] +package_manager = "paru" +EOF + echo ">> $METAPAC_CONFIG_DIR/config.toml creado" + fi + + if [[ ! -f "$OVERRIDES_FILE" ]]; then + cat >"$OVERRIDES_FILE" <<'EOF' +# Paquetes que SOLO tienen sentido en ESTA máquina. +# +# pkgsync nunca lee ni escribe este fichero de forma automática: no vive en +# el repo arch-setup, no viaja a la otra máquina. metapac sí lo combina con +# el resto de group files de ~/.config/metapac/groups/ para sus propios +# sync/clean, así que un paquete puesto aquí queda protegido de 'pkgsync +# apply' igual que si estuviera en el array compartido. +# +# Descomenta y edita, por ejemplo: +# +# [arch] +# packages = ["algun-paquete-solo-de-esta-maquina"] +# +# [flatpak] +# packages = ["system:com.algo.SoloAqui"] +EOF + echo ">> scaffold de $OVERRIDES_FILE creado (vacío, edítalo a mano)" + fi + + if [[ ! -d "$REPO_DIR/.git" ]]; then + echo ">> clonando arch-setup en $REPO_DIR..." + git clone "$REPO_URL" "$REPO_DIR" + fi + + write_group_file + echo ">> bootstrap completo. Activa los servicios con:" + echo " systemctl --user enable --now pkgsync-push.service pkgsync-pull.service" +} + +cmd_push() { + mkdir -p "$(dirname "$LOG")" + exec >>"$LOG" 2>&1 + echo "=== push $(date -Iseconds) ===" + + if [[ ! -d "$REPO_DIR/.git" ]]; then + echo "!! no existe $REPO_DIR, corre 'pkgsync bootstrap' primero" + return 0 + fi + + local work + work="$(mktemp -d)" + trap 'rm -rf "$work"' RETURN + + installed_pacman_native | sort -u >"$work/inst_native" + installed_pacman_foreign | sort -u >"$work/inst_foreign" + cat "$work/inst_native" "$work/inst_foreign" | sort -u >"$work/inst_arch_any" + installed_flatpak | sort -u >"$work/inst_flatpak" + overrides_packages | sort -u >"$work/overrides" + printf '%s\n' "${CRITICAL_PACKAGES[@]}" | sort -u >"$work/critical" + + declared_array PACMAN_COMMON | sort -u >"$work/decl_pacman" + declared_array AUR_COMMON | sort -u >"$work/decl_aur" + declared_array FLATPAK_COMMON | sort -u >"$work/decl_flatpak" + cat "$work/decl_pacman" "$work/decl_aur" | sort -u >"$work/decl_arch_any" + + comm -23 "$work/critical" "$work/inst_arch_any" | while IFS= read -r c; do + [[ -n "$c" ]] && echo "!! AVISO: paquete crítico '$c' no está instalado localmente; se mantiene declarado (no se toca)" + done + + local remove_pacman add_pacman remove_aur add_aur remove_flatpak add_flatpak + remove_pacman="$(comm -23 "$work/decl_pacman" "$work/inst_arch_any" | comm -23 - "$work/overrides" | comm -23 - "$work/critical")" + add_pacman="$(comm -23 "$work/inst_native" "$work/decl_arch_any" | comm -23 - "$work/overrides")" + remove_aur="$(comm -23 "$work/decl_aur" "$work/inst_arch_any" | comm -23 - "$work/overrides" | comm -23 - "$work/critical")" + add_aur="$(comm -23 "$work/inst_foreign" "$work/decl_arch_any" | comm -23 - "$work/overrides")" + remove_flatpak="$(comm -23 "$work/decl_flatpak" "$work/inst_flatpak" | comm -23 - "$work/overrides" | comm -23 - "$work/critical")" + add_flatpak="$(comm -23 "$work/inst_flatpak" "$work/decl_flatpak" | comm -23 - "$work/overrides")" + + local pc="$REPO_DIR/shared/packages-common.sh" + patch_array_file "$pc" PACMAN_COMMON "$remove_pacman" "$add_pacman" + patch_array_file "$pc" AUR_COMMON "$remove_aur" "$add_aur" + patch_array_file "$pc" FLATPAK_COMMON "$remove_flatpak" "$add_flatpak" + + write_group_file + + cd "$REPO_DIR" || return 0 + git add shared/packages-common.sh + if git diff --cached --quiet; then + echo "sin cambios de paquetes que subir" + return 0 + fi + + git commit -m "pkgsync: actualiza inventario de paquetes $(date '+%Y-%m-%d %H:%M')" + if timeout 20 git push origin main; then + echo "push OK" + else + echo "push falló (¿sin red?), el commit queda en local para el próximo intento" + fi +} + +cmd_pull() { + mkdir -p "$(dirname "$LOG")" + exec >>"$LOG" 2>&1 + echo "=== pull $(date -Iseconds) ===" + + if ! timeout 15 git ls-remote --exit-code "$REPO_URL" &>/dev/null; then + echo "sin conexión con el remoto, me quedo con el estado local" + elif [[ ! -d "$REPO_DIR/.git" ]]; then + echo "clonando arch-setup en $REPO_DIR..." + mkdir -p "$(dirname "$REPO_DIR")" + timeout 30 git clone "$REPO_URL" "$REPO_DIR" && echo "clon inicial OK" || echo "clone falló" + else + if timeout 30 git -C "$REPO_DIR" pull --ff-only origin main; then + echo "git pull OK" + else + echo "git pull falló (conflicto local o sin red); sigo con el estado que haya" + fi + fi + + if [[ ! -d "$REPO_DIR/.git" ]]; then + echo "no hay clon de arch-setup, no puedo calcular diff" + return 0 + fi + + write_group_file + compute_diff + + local n_install=$(( ${#TO_INSTALL_ARCH[@]} + ${#TO_INSTALL_FLATPAK[@]} )) + local n_remove=$(( ${#TO_REVIEW_REMOVE_ARCH[@]} + ${#TO_REVIEW_REMOVE_FLATPAK[@]} )) + + print_diff + + if (( n_install == 0 && n_remove == 0 )); then + echo "sin cambios pendientes" + else + notify "pkgsync: cambios de paquetes pendientes" \ + "$n_install a instalar, $n_remove a revisar para borrar. Detalle: pkgsync diff (o $LOG)" + fi + + if [[ "${PKGSYNC_AUTO_INSTALL:-0}" == "1" ]] && (( n_install > 0 )); then + echo "PKGSYNC_AUTO_INSTALL=1: instalando automáticamente lo que falta (nunca borra)" + metapac sync --no-confirm + fi +} + +cmd_apply() { + echo ">> pkgsync apply: metapac sync (instalar) + metapac clean (borrar, con confirmación)" + [[ -d "$REPO_DIR/.git" ]] || { echo "!! no existe $REPO_DIR, corre 'pkgsync bootstrap' primero"; return 1; } + write_group_file + metapac sync + metapac clean +} + +cmd_log() { + [[ -d "$REPO_DIR/.git" ]] || { echo "!! no hay clon en $REPO_DIR, corre 'pkgsync bootstrap' primero"; return 1; } + if [[ "${1:-}" == "--patch" ]]; then + git -C "$REPO_DIR" log --follow -p -- shared/packages-common.sh + else + git -C "$REPO_DIR" log --follow --stat -- shared/packages-common.sh + fi +} + +cmd_incoming() { + [[ -d "$REPO_DIR/.git" ]] || { echo "!! no hay clon en $REPO_DIR, corre 'pkgsync bootstrap' primero"; return 1; } + git -C "$REPO_DIR" fetch --quiet origin + local n + n="$(git -C "$REPO_DIR" log HEAD..origin/main --oneline | wc -l)" + if (( n == 0 )); then + echo "sin commits nuevos en origin/main" + else + echo "$n commit(s) nuevos en origin/main:" + git -C "$REPO_DIR" log HEAD..origin/main --oneline + fi +} + +cmd_audit() { + local work + work="$(mktemp -d)" + trap 'rm -rf "$work"' RETURN + + { declared_array PACMAN_COMMON; declared_array AUR_COMMON; printf '%s\n' "${CRITICAL_PACKAGES[@]}"; overrides_packages; } | sort -u >"$work/declared_arch" + { declared_array FLATPAK_COMMON; overrides_packages; } | sort -u >"$work/declared_flatpak" + + installed_pacman_native | sort -u >"$work/inst_native" + installed_pacman_foreign | sort -u >"$work/inst_foreign" + installed_flatpak | sort -u >"$work/inst_flatpak" + + echo "== pacman (oficiales) instalados, no declarados en ningún array ==" + comm -23 "$work/inst_native" "$work/declared_arch" + echo + echo "== AUR/foráneos instalados, no declarados en ningún array ==" + comm -23 "$work/inst_foreign" "$work/declared_arch" + echo + echo "== flatpaks instalados, no declarados ==" + comm -23 "$work/inst_flatpak" "$work/declared_flatpak" + echo + echo "(nota: los paquetes específicos de KDE/GNOME hardcodeados en /setup.sh" + echo " -- plasma-desktop, sddm, temas AUR, etc. -- siempre van a salir aquí: no" + echo " viven en los arrays de packages-common.sh por diseño, no es drift real)" +} + +usage() { + cat >&2 <<'EOF' +uso: pkgsync.sh + + bootstrap instala metapac + config + clon local (primera vez / retrofit) + push captura el inventario real y lo sube al repo (usa pkgsync-push.service) + pull git pull + calcula diff + notifica; NO instala ni desinstala nada (usa pkgsync-pull.service) + diff como pull pero solo imprime el diff, sin tocar git ni notificar + apply aplica lo pendiente con metapac (sync + clean), pidiendo confirmación + log [--patch] historial de shared/packages-common.sh + incoming commits nuevos en origin/main aún no traídos + audit paquetes instalados localmente que no están declarados en ningún sitio +EOF +} + +main() { + local cmd="${1:-}" + [[ $# -gt 0 ]] && shift + case "$cmd" in + bootstrap) cmd_bootstrap ;; + push) cmd_push ;; + pull) cmd_pull ;; + diff) compute_diff; print_diff ;; + apply) cmd_apply ;; + log) cmd_log "$@" ;; + incoming) cmd_incoming ;; + audit) cmd_audit ;; + *) usage; return 1 ;; + esac +} + +main "$@" diff --git a/pkgsync/systemd/pkgsync-pull.service b/pkgsync/systemd/pkgsync-pull.service new file mode 100644 index 0000000..757104e --- /dev/null +++ b/pkgsync/systemd/pkgsync-pull.service @@ -0,0 +1,12 @@ +[Unit] +Description=pkgsync: trae y compara el estado de paquetes declarado al iniciar sesión (no instala/desinstala nada) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=%h/.local/bin/pkgsync.sh pull +TimeoutStartSec=60 + +[Install] +WantedBy=default.target diff --git a/pkgsync/systemd/pkgsync-push.service b/pkgsync/systemd/pkgsync-push.service new file mode 100644 index 0000000..f525553 --- /dev/null +++ b/pkgsync/systemd/pkgsync-push.service @@ -0,0 +1,14 @@ +[Unit] +Description=pkgsync: captura paquetes instalados y sube el estado a arch-setup al cerrar sesión/apagar +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/true +ExecStop=%h/.local/bin/pkgsync.sh push +TimeoutStopSec=30 + +[Install] +WantedBy=default.target diff --git a/shared/packages-common.sh b/shared/packages-common.sh index c28cac3..8f52f92 100644 --- a/shared/packages-common.sh +++ b/shared/packages-common.sh @@ -37,6 +37,13 @@ PACMAN_COMMON=( linux linux-lts linux-lts-headers linux-firmware # rollback de actualizaciones atómicas sobre btrfs snapper snap-pac grub-btrfs + # bootloader (antes hardcodeado en cada /setup.sh, install_bootloader() + # también los instala explícito -- duplicado inofensivo con --needed, pero + # ahora viven aquí para que pkgsync pueda declararlos/protegerlos) + grub efibootmgr + # drivers gráficos open-source (antes hardcodeados en cada /setup.sh; + # mismo hardware físico en ambas máquinas, ver PROJECT_CONTEXT.md) + mesa vulkan-icd-loader vulkan-radeon vulkan-intel intel-media-driver ) # --- Flatpak --------------------------------------------------------------- @@ -162,6 +169,25 @@ install_flatpaks() { return 0 } +# pkgsync: sincroniza qué paquetes hay instalados (pacman+AUR+flatpak) entre +# las dos máquinas, igual que chezmoi sincroniza los dotfiles. Instala metapac +# (motor de diff/aplicación), despliega pkgsync.sh y sus dos servicios de +# usuario, y deja el group file de metapac listo. Ver pkgsync/ en este repo. +install_pkgsync() { + echo ">> Instalando pkgsync (metapac + servicios de sincronización de paquetes)..." + install_aur metapac + + as_user "mkdir -p ~/.local/bin ~/.config/systemd/user ~/.config/metapac/groups" + as_user "cp '$REPO_DIR/pkgsync/pkgsync.sh' ~/.local/bin/pkgsync.sh && chmod +x ~/.local/bin/pkgsync.sh" + as_user "cp '$REPO_DIR/pkgsync/systemd/'*.service ~/.config/systemd/user/" + as_user "~/.local/bin/pkgsync.sh bootstrap" + as_user "systemctl --user enable pkgsync-push.service pkgsync-pull.service" + + # ya lo activa el run_once de chezmoi, pero por si este DE se instala algún + # día sin chezmoi: idempotente, no hace nada si ya está activo. + loginctl enable-linger "$INSTALL_USER" 2>/dev/null || true +} + # Configura snapper + snap-pac + grub-btrfs para rollback de actualizaciones. # snap-pac crea snapshots automaticos antes/después de cada 'pacman -Syu'. # grub-btrfs añade las entradas de snapshots al menú de GRUB.