Tour pelos meus dotfiles: arquitetura modular pra setup rápido em Arch
Toda vez que eu reinstalava o sistema ou subia uma VM nova, perdia algumas horas reaplicando configs. Resolvi isso com um repo de dotfiles construído em torno de uma ideia simples: um comando, um sistema pronto — desde que a base seja Arch (ou Manjaro) com recursos equivalentes.
Esse post é um tour por dentro dele: a estrutura, as decisões de design, e por que Arch é a base certa pra esse tipo de automação.
Por que Arch como base
A escolha não é estética. Arch traz três coisas que tornam dotfiles automatizados muito mais previsíveis:
- Pacotes atomizados na base oficial. Quase tudo que eu uso (
docker,tmux,neovim,zsh,firefox) está empacmandireto, na versão atual. Sem PPAs, sem repositórios extras, semapt update && apt upgradeesperando 10 minutos. - AUR cobre o resto. O que não está no oficial (
visual-studio-code-bin,slack-desktop,spotify,postman-bin) está no AUR e oyayresolve dependências automaticamente. Isso elimina a necessidade de.deb,.AppImageousnapespalhados pelo sistema. - Sistema limpo de fábrica. Arch sobe sem desktop, sem serviços supérfluos, sem snapd nem flatpakd rodando. O que eu instalo eu sei que é meu. Isso simplifica scripts: posso assumir um estado base mínimo e construir em cima.
A comunidade ativa fecha o pacote: pra qualquer pacote do AUR, em geral existe gente mantendo, mesmo de tools obscuras. O que reduz o risco de eu depender de um binário que vai morrer no próximo upgrade.
A árvore do projeto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dotfiles/
├── _setup.sh # bootstrap remoto: clona o repo e dispara install
├── install.sh # orquestrador principal
├── helpers.sh # funções compartilhadas (echo_*, _update, _install, _symlink)
├── packages.sh # arrays PKG (pacman) e AUR (yay)
├── docker-compose.yaml
├── README.md
├── LICENSE.txt
│
├── albert/ # launcher
├── asdf/ # version manager (erlang, elixir, node, ruby)
├── config/ # fontes, hosts
├── docker/
├── git/
├── heroku/
├── lscripts/ # scripts utilitários (recolocar janela, reconectar headset)
├── node/
├── ruby/
├── ssh/
├── terminator/
├── tmux/
├── xfce/ # desktop env
├── yay/ # AUR helper
└── zsh/ # oh-my-zsh + p10k + plugins
Cada pasta na raiz é um módulo com responsabilidade única. Todas têm a mesma assinatura: um install.sh que sabe se instalar sozinho.
A entrada: _setup.sh
O ponto de entrada é um one-liner pra rodar em uma máquina nova:
1
curl -sL https://raw.githubusercontent.com/luciotbc/dotfiles/master/_setup.sh | bash
O _setup.sh faz três coisas:
1
2
3
4
DOTFILES=${DOTFILES:-~/.dotfiles}
REPO=${REPO:-luciotbc/dotfiles}
REMOTE=${REMOTE:-https://github.com/${REPO}.git}
BRANCH=${BRANCH:-master}
Variáveis com default e override por env (DOTFILES=/tmp/df bash _setup.sh funciona). Em seguida ele:
- Verifica se git existe; se não, aborta com erro colorido.
- Faz
git clone --depth=1em~/.dotfiles, com flags pra não bagunçar EOLs (core.eol=lf,core.autocrlf=false) — importante porque scripts shell quebram com CRLF. - Entra no diretório clonado e roda
./install.sh.
Detalhe pequeno mas útil: ele não permite reinstalar por cima — se ~/.dotfiles já existe, ele aborta em vez de tentar mesclar.
O orquestrador: install.sh + helpers.sh
O install.sh da raiz tem 20 linhas e é deliberadamente burro:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
. packages.sh
. helpers.sh
echo_info "Updating packages..."
_update
echo_info "Installing core packages..."
_install core
echo_info "Configure settings..."
_symlink
echo_info "Installing aur packages..."
_install aur
A inteligência fica em helpers.sh. As três funções principais:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function _update() {
sudo pacman-mirrors --geoip
sudo pacman -Syyu --needed --noconfirm
}
function _install() {
if [[ $1 == "core" ]]; then
for pkg in "${PKG[@]}"; do
sudo pacman -Sy "$pkg" --needed --noconfirm
echo_done "${pkg} installed!"
done
elif [[ $1 == "aur" ]]; then
for aur in "${AUR[@]}"; do
yay -S "$aur" --needed --noconfirm
done
fi
}
function _symlink() {
dirs=$(find . -maxdepth 1 -mindepth 1 -type d -not -name '.git')
for dir in $dirs; do
cd "$dir" || exit
./install.sh
cd ..
done
}
_update usa pacman-mirrors --geoip pra escolher o mirror mais rápido pra geolocalização atual — na prática, faz uma diferença gigante quando você sobe uma máquina em outro país.
_install é polimórfica: passa core pra rodar sobre ${PKG[@]} com pacman, ou aur pra rodar sobre ${AUR[@]} com yay. As flags --needed --noconfirm garantem idempotência (pacote já instalado é ignorado, sem prompt).
_symlink é o coração da modularidade: itera por toda pasta na raiz e roda o install.sh que estiver lá. Adicionar um módulo novo é só criar uma pasta com install.sh — o orquestrador descobre sozinho.
A lista declarativa: packages.sh
Em vez de espalhar pacman -S pelos scripts, tudo que vem do oficial vira uma lista Bash:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export PKG=(
albert
base-devel
blueman
chromium
cmake
docker
docker-compose
firefox
fzf
htop
neovim
noto-fonts
openssh
postgresql-libs
terminator
tmux
zsh
# ... ~80 pacotes
)
export AUR=(
brave-bin
ctop
espanso
git-delta
google-chrome
insync
postman-bin
slack-desktop
spotify
visual-studio-code-bin
zeal
)
Vantagens dessa separação:
- Comentar = desinstalar do template. Não preciso decidir entre Brave e Chrome pra sempre — comento a linha numa máquina específica.
- Diff limpa entre máquinas. Se eu fizer fork pra desktop e pra notebook, o diff fica concentrado em
packages.sh, sem mudar shell scripts. - Source única da verdade. Tudo que entra no sistema via gerenciador de pacote está num lugar só.
O padrão dos módulos
Cada <modulo>/install.sh segue um esqueleto mínimo. Exemplo do git/install.sh:
1
2
3
4
5
6
7
8
9
#!/bin/bash
# shellcheck source=helpers.sh
. ../helpers.sh
echo_info "Symlink ~/.gitconfig"
ln -sfT "$HOME/.dotfiles/git/gitconfig" "$HOME/.gitconfig"
echo_done "Git configuration!"
A estratégia é sempre a mesma: o arquivo de config mora versionado dentro do repo (git/gitconfig), e o install só cria um symlink em $HOME apontando pra ele. Isso quer dizer que:
- Um
git pullno~/.dotfilesatualiza a config em todas as máquinas. - Editar
~/.gitconfigna verdade edita o arquivo do repo, então mudanças locais ficam rastreáveis no git. - Pra “desinstalar” um módulo, basta apagar o symlink — o arquivo original continua no repo.
Os módulos que precisam de mais coisa que symlink fazem mais — mas mantêm a mesma porta de entrada install.sh.
zsh/install.sh — clone de plugins + symlink
1
2
3
4
5
6
7
8
9
10
11
12
13
sh -c "$(curl -fsSL .../oh-my-zsh/.../install.sh)" -s --batch
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions \
"$HOME/.oh-my-zsh/custom/plugins/zsh-autosuggestions"
git clone --depth=1 https://github.com/zsh-users/zsh-syntax-highlighting.git \
"$HOME/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \
"$HOME/.oh-my-zsh/themes/powerlevel10k"
ln -sfT "$HOME/.dotfiles/zsh/.p10k.zsh" "$HOME/.p10k.zsh"
ln -sfT "$HOME/.dotfiles/zsh/zshrc" "$HOME/.zshrc"
chsh -s $(which zsh)
sudo chsh -s $(which zsh)
Instala oh-my-zsh em modo batch (sem prompt interativo), clona os três plugins/themes que eu uso (autosuggestions, syntax-highlighting, powerlevel10k), faz symlink dos meus .zshrc e .p10k.zsh, e troca o shell padrão pra zsh — tanto pro usuário quanto pro root.
asdf/install.sh — version manager + linguagens
1
2
3
4
5
6
7
8
9
10
11
12
13
14
git clone https://github.com/asdf-vm/asdf.git "$HOME/.asdf"
cd "$HOME/.asdf" && git checkout "$(git describe --abbrev=0 --tags)"
ln -sfT "$HOME/.dotfiles/asdf/asdfrc" "$HOME/.asdfrc"
ln -sfT "$HOME/.dotfiles/asdf/tool-versions" "$HOME/.tool-versions"
~/.asdf/bin/asdf plugin-add erlang
~/.asdf/bin/asdf plugin-add elixir
~/.asdf/bin/asdf plugin-add nodejs
~/.asdf/bin/asdf plugin-add ruby
~/.asdf/bin/asdf install ruby 2.7.1
~/.asdf/bin/asdf global ruby 2.7.1
# ... mesma coisa pra erlang, elixir, node
asdf clona em ~/.asdf e checa out na última tag estável. Os plugins de linguagem viram instalação declarativa via .tool-versions versionado.
yay/install.sh — bootstrap do AUR
1
2
3
4
5
git clone https://aur.archlinux.org/yay.git
cd yay
makepkg -si
cd ..
rm -rf yay
Esse é especial: precisa rodar antes do _install aur, porque yay é o helper que vai instalar tudo do AUR depois. Por isso ele aparece entre o _install core e o _install aur no fluxo principal — é a parte “manual” do bootstrap.
O fluxo completo, do curl até a máquina pronta
Sequência do que acontece quando você roda o one-liner numa Arch zerada:
_setup.shbaixa-se via curl, validagit, clona o repo em~/.dotfiles.install.shda raiz começa._updateajusta mirror viageoipe rodapacman -Syyu._install corepercorre${PKG[@]}instalando tudo do oficial._symlinkitera pelas pastas e dispara cadainstall.sh(incluindo o doyay).- Cada módulo cuida do próprio dotfile: zsh muda shell, asdf compila Ruby/Elixir, git linka
.gitconfig, etc. - Por último,
_install aurpercorre${AUR[@]}viayay.
No fim, login, p10k já está bonito, ruby -v retorna 2.7.1, docker --version responde, e o VS Code está no menu.
Reaplicando em uma máquina remota
O caso pra qual o projeto foi pensado: VPS Arch nova, ou Manjaro num notebook reinstalado. O setup mínimo do meu lado é:
1
2
3
4
5
6
7
8
9
10
11
# 1. Como root, criar usuário e dar sudo
useradd -m -G wheel -s /bin/bash lucio
passwd lucio
visudo # descomentar %wheel
# 2. Logar como o usuário e rodar o one-liner
curl -sL https://raw.githubusercontent.com/luciotbc/dotfiles/master/_setup.sh | bash
# 3. Configurar SSH/GPG (instruções no README)
ssh-keygen -t rsa -b 4096 -C "hi@lucio.app"
gpg --default-new-key-algo rsa4096 --gen-key
A premissa é que a máquina tem recursos equivalentes — memória pra compilar Erlang/Ruby via asdf, espaço pra o conjunto de pacotes (uns 6–8 GB com tudo) e rede decente pra o initial pull. Se for uma máquina mais magra, dá pra comentar pacotes em packages.sh ou módulos pesados antes de rodar.
O que eu mudaria hoje
Olhando com calma, três coisas que melhoraria:
- Idempotência mais forte. Hoje
install.shdos módulos assume estado limpo. Rodar duas vezes pode falhar nosgit clone(asdf, oh-my-zsh) porque a pasta já existe. Um check do tipo[ -d ~/.asdf ] || git clone ...resolve. - Versões pinadas das linguagens em arquivo separado. Hoje as versões (Ruby 2.7.1, Node 10.16.0) estão hardcoded em
asdf/install.sh. Mover pra.tool-versionse usar sóasdf installsem argumentos seria mais limpo. - Testes em container. O
.dockerdev/Dockerfileque existe no repo dá a base pra rodar o setup completo num container Arch e validar que ele termina sem erro — bom pra CI.
Resumo do que eu acho que esse projeto acerta
A coisa que mais me serve depois de quase um ano usando esse layout:
- Modularidade por pasta elimina a necessidade de um framework de dotfiles (Dotbot, GNU Stow, chezmoi). Cada módulo é independente, lê como código de configuração, não como configuração declarativa abstrata.
packages.shseparado torna o “o que está instalado” auditável num arquivo só.- Symlink-first garante que minha config vive no git, não num backup esporádico.
- Bootstrap remoto torna a máquina nova um problema de 10 minutos, não de uma tarde.
E principalmente: aproveitar Arch + AUR como repositório universal de software resolve o que em outras distros vira solução com 4 ferramentas diferentes (apt + snap + flatpak + AppImage). Isso reduz drasticamente o que eu preciso codar nos dotfiles — quase tudo é “adicionar nome do pacote no array”.
Repo público em github.com/luciotbc/dotfiles se quiser fazer fork.