Añade pkgsync: sincronización de paquetes entre máquinas (mismo patrón que chezmoi)

Nuevo pkgsync/pkgsync.sh + servicios systemd --user (push al cerrar sesión,
pull+diff+notifica al iniciar sesión, sin aplicar cambios solo) usando
metapac como motor de diff/aplicación sobre los arrays de
packages-common.sh. Lista de paquetes críticos protegidos de borrado
automático, fichero de overrides local para divergencias entre máquinas, y
comandos de introspección (diff/log/incoming/audit).

De paso, grub/efibootmgr y los drivers gráficos pasan de estar hardcodeados
en cada <de>/setup.sh a vivir en PACMAN_COMMON, para que pkgsync pueda
declararlos y protegerlos.
This commit is contained in:
Josevi
2026-08-18 10:46:23 +02:00
parent f19b6f0f93
commit c074095d68
7 changed files with 632 additions and 6 deletions
+104
View File
@@ -9,6 +9,7 @@ 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)
├── 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 `<de>/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 `<de>/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.
+4 -2
View File
@@ -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."
+3 -1
View File
@@ -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."
+466
View File
@@ -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 <de>/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|push|pull|diff|apply|log|incoming|audit>
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 "$@"
+12
View File
@@ -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
+14
View File
@@ -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
+26
View File
@@ -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 <de>/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 <de>/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.