* notes / dotfiles
GNU Stow 🤫
— Cómo migré mi configuración personal a GNU Stow, qué pierdo, qué gano y por qué los enlaces simbólicos siguen siendo la mejor abstracción para los dotfiles.
Durante años mantuve mis dotfiles con un guion de Bash artesanal: un for que recorría un directorio y disparaba ln -s con rutas absolutas, propenso a romperse cada vez que renombraba una carpeta. Luego probé chezmoi, que resolvía con plantillas problemas que yo no tenía, a costa de un modelo mental que sí tenía que aprender. La tercera parada fue GNU Stow, una herramienta de los noventa, escrita en Perl, que hace una sola cosa y la hace de manera notable. Aquí cuento cómo migré y por qué creo que es la opción más sensata para casi todo el mundo.
Contexto: qué hace Stow exactamente
Stow fue concebido como gestor de paquetes locales: dado un directorio fuente ($STOW_DIR), crea en el directorio objetivo (-t) una jerarquía de enlaces simbólicos que replica la estructura interna de cada «paquete». Si un fichero ya existe en destino y no es enlace, Stow se detiene; si es enlace propio, lo gestiona; si entra en conflicto, lo notifica.
Aplicado a dotfiles, la analogía resulta natural: cada carpeta del repositorio es un «paquete» (vim, zsh, tmux…), y al ejecutar stow -t ~ vim se materializa la configuración en el $HOME del usuario. La gracia es que el repositorio jamás se mueve: edito el fichero in situ, git lo registra al instante, y el sistema sigue viendo el archivo a través del enlace.
Topología de enlaces
Diseño del repositorio
La regla mnemotécnica es: la estructura interna de cada paquete refleja, a partir de su raíz, la estructura que aparecerá en $HOME. Por consiguiente, si Neovim espera ~/.config/nvim/init.lua, en el repositorio escribo dotfiles/nvim/.config/nvim/init.lua.
~/dotfiles
├── .stow-global-ignore
├── install.sh
├── nvim/
│ └── .config/nvim/
│ ├── init.lua
│ └── lua/
├── zsh/
│ ├── .zshrc
│ └── .zsh/
│ └── aliases.zsh
├── tmux/
│ └── .tmux.conf
├── git/
│ └── .gitconfig
└── ghostty/
└── .config/ghostty/configComandos esenciales
Tres verbos lo cubren casi todo:
# Materializar
stow -t ~ -v nvim zsh tmux git ghostty
# Inspeccionar sin tocar (modo simulación)
stow -nv nvim
# Desinstalar (elimina los enlaces, deja el repo intacto)
stow -D -t ~ nvimEl flag -v (verbose) es indispensable durante la primera puesta en marcha: cada línea describe el enlace creado, lo que facilita auditar el resultado. El flag -n (simulate) es el equivalente al --dry-run que cualquier herramienta moderna implementa, y debería ser obligatorio cuando se experimenta sobre $HOME.
Ignorar lo que no debe enlazarse
Stow respeta .stow-local-ignore por paquete y .stow-global-ignore para todo el repositorio. Es preceptivo añadir las marcas habituales:
# .stow-global-ignore
\.git
\.gitignore
\.DS_Store
README.*
LICENSE
install\.shLas expresiones son regex completas, no globs; he visto a muchos compañeros pelearse con * esperando comportamientos tipo .gitignore. No los tendrá.
Conflictos y la opción --adopt
El primer despliegue casi siempre genera colisiones: .zshrc ya existe en $HOME porque lo creó la instalación. Hay dos rutas razonables:
- Mover a mano: copio el
.zshrcprevio a~/dotfiles/zsh/.zshrc, lo borro de$HOMEy vuelvo a ejecutarstow. --adopt: Stow mueve el fichero existente al repositorio y crea el enlace. Es cómodo, pero peligroso: si el fichero del repositorio ya contenía algo, se sobrescribe con el del sistema. Lo uso solo bajogit statuslimpio y tras un commit; si el resultado no me gusta,git checkout -- .lo deshace.
cd ~/dotfiles
git add -A && git commit -m "snapshot antes de adoptar"
stow --adopt -t ~ zsh
git diff # aquí confirmo qué se ha movidoGuion de instalación
Más allá de Stow, mantengo un install.sh que instala dependencias y, finalmente, hace el stow de cada paquete. Asimismo, evito hardcodear nombres y dejo que el script descubra los paquetes solo.
#!/usr/bin/env bash
set -euo pipefail
DOTFILES="${DOTFILES:-$HOME/dotfiles}"
cd "$DOTFILES"
# Lista de paquetes = subdirectorios visibles que no sean meta.
mapfile -t pkgs < <(find . -maxdepth 1 -mindepth 1 -type d \
! -name '.*' -printf '%f\n' | sort)
echo "Paquetes detectados: ${pkgs[*]}"
for pkg in "${pkgs[@]}"; do
stow -t "$HOME" -R -v "$pkg"
doneEl flag -R (restow) equivale a desinstalar y reinstalar, lo que permite ejecutar el script de forma idempotente cada vez que añado un fichero nuevo.
Integración con Git
El repositorio de dotfiles es un repo de Git común. No obstante, recomiendo dos prácticas:
- Submódulos para plugins de Neovim que prefieras versionar (lazy.nvim los gestiona, pero a veces quiero fijar versiones de forks).
- Hooks pre-commit con
shfmtystyluapara que la configuración no se degrade con el tiempo.
Una pequeña GitHub Action ejecuta stow -n sobre una matriz Ubuntu / macOS para detectar conflictos antes de que aterricen en mi máquina:
- name: Lint stow
run: |
sudo apt-get install -y stow
cd $GITHUB_WORKSPACE
for d in */; do stow -n -t "$HOME" "${d%/}"; doneHallazgos tras la migración
- El repositorio es la verdad, el sistema es el reflejo. Sentir que ningún cambio importante vive fuera del repo cambió mi disciplina por completo.
- Stow no resuelve secretos. Para tokens y credenciales uso
pass(gpg) o1password CLI, jamás los versiono. Stow es para configuración, no para datos sensibles. - Funciona idéntico en macOS y Linux. Apenas necesité segmentar por SO algunas líneas en
.zshrc. chezmoibrilla cuando se necesitan plantillas; Stow brilla cuando no se necesitan. Mi configuración apenas requiere variables, así que ganó la simplicidad.- El
init.luade Neovim convivió con Stow sin un solo conflicto, incluso conlazy-lock.jsonmutándose constantemente.
Closure
GNU Stow respeta el sistema de archivos como interfaz. git push en una máquina y git pull && ./install.sh en la otra. Nada más.