Criando um dotfiles


Recentemente, fiz uma troca de notebook, depois de 6 anos. E uma coisa que é muito chato quando se faz a troca é ter que configurar todas as minúcias do absoluto zero. Ter que passar dias ou até semanas procurando o programa certo, instalado os plugins e extensões, e configurar todos os detalhes daquele software acaba sendo algo muito estressante. 🤯

Uma das características que está na essência de nós programadores é querer automatizar tarefas repetitivas, nem que seja só pela diversão de saber se é possível. Por isso, decidir criar um script com toda as minhas configurações do sistema que uso no meu dia dia

Desafios

No início, eu estava pensando em criar um shell script para fazer todas as instalações e configurações dos programas que eu uso. No entanto, criar esse script me traria um trabalho enorme, e somado a isso, não tenho conhecimento aprofundado em shell. E isso estava me desmotivando muito em começar a criar o meu próprio dotfiles.

Coincidentemente, o @Dunossauro estava fazendo em live um script com suas configurações. Então, fui até o repositório do projeto para ver o que estava sendo usado e descobrir que ele estava usando o Ansible e o Dotdrop.

Após ver as ferramentas que o Dunossauro estava utilizando, comecei uma saga de 4 dias estudando e testando o Ansible e Dotdrop e tentando entender como eu poderia usá-los.

Criando os playbooks

O primeiro passo que tomei para criar o meu dotfiles foi definir os playbooks. E para isso, defini algumas categorias de aplicações e ferramentas que eu queria que o meu script instalasse. As categorias foram:

  • Ferramentas de coding.
  • Aplicações de interfaces gráficas.
  • Aplicações de work.

Os playbook usam um arquivo no formato yaml, que permite descrever um conjunto de tarefas a serem executadas. Com isso, comecei a definir todas as ferramentas de coding que uso. E o playbook ficou assim:

coding.yml

---
- name: Installation of coding tools
  hosts: localhost

  tasks:
    - name: Install pipx
      become: yes
      package:
        name: pipx
        state: present

    - name: config pipx
      become: yes
      shell: pipx ensurepath

    - name: Install poetry
      become: no
      community.general.pipx:
        name: poetry
        state: present
        install_deps: true

    - name: Install mypy
      become: no
      community.general.pipx:
        name: mypy
        state: present
        install_deps: true

    - name: Install Podman
      become: yes
      package:
        name: podman
        state: present

    - name: Install podman-compose
      become: yes
      package:
        name: podman-compose
        state: present

    - name: Clone Pyenv
      git:
        repo: https://github.com/pyenv/pyenv.git
        dest: ~/.pyenv

Mas, o que isso faz? Bom, esse script vai instalar e configurar algumas aplicações que eu gosto de usar. As ferramentas são: Pipx, Poetry, Mypy, Podman, Podman-compose e o Pyenv.

O próximo playbook que eu definir foi de aplicações de interfaces gráficas. Mas antes de instalar as aplicações de interfaces gráficas, temos que resolver um probleminha.

Limites do módulo package

No Ansible é comum usarmos o módulo package para instalar pacotes. Porém, esse modulo que vem no Ansible usa o gerenciador de pacotes do sistema em que está sendo executado. No meu caso, eu uso o Fedora Linux, então o Ansible vai usar o dnf que é gerenciador de pacotes padrão do Fedora. Mas nem todas as aplicações que uso estão disponíveis nesse gerenciador de pacotes.

Para resolver esse problema, tive que usar a collection community.general. Essa collection tem um módulo para poder instalar aplicações Flatpak. Com isso, criei um arquivo chamado requirements.yml para centralizar todas as dependências. Isso deixa o processo mais simples na hora de instalar os pacotes de terceiros no Galaxy.

requirements.yml

collections:
  - name: community.general

Agora com esse arquivo, podemos instalar a collection:

ansible-galaxy install -r requirements.yml

Com a collection instala com sucesso, podemos usar o módulo para instalar aplicações Flatpak com a seguinte estrutura:

- name: Install WaterFox          # Nome da tarefa 
  community.general.flatpak:      # Nome do módulo
      name: net.waterfox.waterfox # ID da aplicação
      state: present

Seguindo essa estrutura, definir todas as aplicações de interfaces gráficas que uso. O playbook ficou dessa forma:z

gui.yml

---
- name: Installing programs with GUI
  hosts: localhost

  tasks:
    - name: Install Peek
      become: yes
      package:
        name: peek
        state: present

    - name: Install Gnome Feeds
      become: yes
      package:
        name: gnome-feeds 
        state: present

    - name: Install Gnome Tweaks
      become: yes
      package:
        name: gnome-tweaks
        state: present

    - name: Install Okular
      become: yes
      package:
        name: okular
        state: present

    - name: Install Discord
      community.general.flatpak:
        name: com.discordapp.Discord
        state: present

    - name: Install Typora
      community.general.flatpak:
        name: io.typora.Typora
        state: present

    - name: Install ONLYOFFICE
      community.general.flatpak:
        name: org.onlyoffice.desktopeditors
        state: present

    - name: Install Obsidian
      community.general.flatpak:
        name: md.obsidian.Obsidian
        state: present

    - name: Install Solanum
      community.general.flatpak:
        name: org.gnome.Solanum
        state: present

    - name: Install Planify
      community.general.flatpak:
        name: io.github.alainm23.planify
        state: present

    - name: Install WaterFox
      community.general.flatpak:
        name: net.waterfox.waterfox
        state: present

Para finalizar, só falta fazer a instalação das aplicações de work. Seguindo a mesma lógica dos outros dois playbooks, o arquivo de work ficou assim:

work.yml

---
- name: Installation of work tools
  hosts: localhost

  tasks:
    - name: Install GNU/Emacs
      become: yes
      package:
        name: emacs
        state: present

    - name: Install Terminator
      become: yes
      package:
        name: terminator
        state: present

    - name: Install ZSH
      become: yes
      package:
        name: zsh
        state: present

    - name: Make ZSH default shell
      become: yes
      user:
        name: "{{ ansible_env.USER }}"
        shell: /bin/zsh

    - name: Download Oh My Zsh Installer
      get_url:
        url: "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh"
        dest: "/tmp/install-ohmyzsh.sh"
        mode: '0755'

    - name: Install Oh My Zsh
      shell: "/bin/sh /tmp/install-ohmyzsh.sh --unattended"
      args:
        creates: "~/.oh-my-zsh"

    - name: Install Codium
      become: yes
      community.general.flatpak:
        name: com.vscodium.codium
        state: present

O ohmyzsh segue uma instalação totalmente diferente, para eu poder instalar, preciso fazer o download do instalador no github. Para resolver isso, tive que usar o módulo get_url do Ansible com 3 argumentos.

  • url: contem a url de onde o Ansible vai fazer o Download dos arquivos.
  • dest: destino onde o Ansible vai salvar o instalador.
  • mode: o mode: 0755 vai fazer que o instalador tenha permissão de leitura e execução.

Com o instalador baixado, usei o módulo shell para executar o instalador (install-ohmyzsh.sh). Com isso, o Ansible vai poder instalar o ohmyzsh.

Para finalizar, criei um arquivo chamado tasks.yml que vai importar os 3 playbooks que criei, e fazer algumas configurações para poder executar o script a partir de um único arquivo.

tasks.yml

---
- hosts: localhost
  tasks:
    - name: Update dnf cache
      when: ansible_pkg_mgr == "dnf"
      ansible.builtin.dnf:
        update_cache: true
        cache_valid_time: 3600

    - name: Enable flatpak repository
      tags: repository,flatpak
      community.general.flatpak_remote:
        name: flathub
        flatpakrepo_url: "https://dl.flathub.org/repo/flathub.flatpakrepo"
        method: system
        state: present


- name: work
  import_playbook: work.yml

- name: gui
  import_playbook: gui.yml

- name: coding
  import_playbook: coding.yml

Veja que o script tem algumas tasks. A primeira task é responsável por fazer o update do gerenciador de pacotes do meu sistema (dnf). O segundo é responsável por ativar o repositório do Flatpak.

Estrutura do projeto:

playbooks
├── coding.yml
├── gui.yml
├── requirements.yml
├── tasks.yml
└── work.yml

Agora basta executar o script com o seguinte comando:

ansible-playbook playbook/tasks.yml --ask-become-pass

A opção --ask-become-pass-pass serve para pedir a senha de sudo para que o Ansible execute algumas tasks como root.

Pronto! Agora é so dixar o Ansible fazer todo o trabalho pesado e ser feliz. :)

imagem do ansible-playbook funcionando

Gerenciando os dotfiles com o Dotdrop

Temos os programas instalados, mas eles ainda não estão do jeito que gosto. Temos que fazer algumas configurações para que eles estejam 100% do jeito que uso. E e ter que configurar na mão é muito demorado e chatooo. 😩

Podemos usar o Dotdrop para poder gerenciar os nossos dotfiles (arquivos de configurações). Com ele, torna-se mais fácil do que nunca sincronizar e implantar arquivos de configuração em vários sistemas.

Criando os DotFiles

Para poder usar o dotdrop, comecei criando um diretorio chamado dotfiles para poder armazenar todos os arquivos de configuração das minhas ferramentas. A organização ficou da seguinte forma:

.
├── codium
│   └── extensions.txt
├── emacs
│   └── init.el
├── file-organizer
│   └── config.json
├── gnome
│   └── extensions.txt
├── obsidian
│   ├── plugins
│   └── themes
├── terminator
│   └── config
└── zsh
    └── .zshrc

Com os arquivos de configuração definidos, precisei configurar o dotdrop para ele poder sincronizar os arquivos em um novo sistema. Para isso, criei um arquivo chamado config.yml com as seguintes configurações:

config:
  backup: true
  banner: true
  create: true
  dotpath: dotfiles
  keepdot: false
  link_dotfile_default: nolink
  link_on_import: nolink
  longkey: false
dotfiles:
  f_init.el:
    src: emacs/init.el
    dst: ~/.emacs.d/init.el
  f_config:
    src: terminator/config
    dst: ~/.config/terminator/config
  f_.zshrc:
    src: zsh/.zshrc
    dst: ~/.zshrc
  d_plugins:
    src: obsidian/plugins
    dst: ~/Documents/Obsidian Vault/.obsidian/plugins
  d_themes:
    src: obsidian/themes
    dst: ~/Documents/Obsidian Vault/.obsidian/themes


profiles:
  cleverson:
    dotfiles:
    - f_init.el
    - f_config
    - f_.zshrc
    - d_plugins
    - d_themes

Não vou entrar em detalhes o que cada comando faz, para isso você pode acessar a documentação que já é rica em detalhes. Vou focar só na pate dos dotfiles.

Veja que na parte dotfiles eu defini algumas variáveis. E essas variáveis têm o prefixo f que serve para representar arquivos e d para representar diretórios. Nas variáveis que eu definir, tem alguns argumentos, que são o src (caminho do arquivo) e dst (destino aonde o arquivo será implantado).

Pronto, com o config.yml tudo configurado, podemos usar o dotdrop para que ele possa sincronizar as configurações que eu uso.

dotdrop install --profiles=cleverson

imagem da resposta do dotdrop

Agora sim todos as minhas ferramentas estão configuradas. E para finalizar, só preciso instalar as extensões que eu uso no gnome.

Automatizando as extensões do gnome.

Eu tenho algumas extensões que eu uso no Gnome para deixar a experiência no meu sistema mais legal. Uma delas é uma extensão que traz informações de memória RAM e o uso de CPU. E uma coisa que é muito chato, é ter que procurar essas extensões e instalar manualmente. Para resolver esse problema, criei um script shell que faz esse trabalho chato para mim.

Primeiro precisamos dos IDs de cada extenção. Podemos ver quais extensões estão sendo usadas com o seguinte comando:

gnome-extensions list --enabled

esse comando vai mostrar uma lista de extensões que estão ativas no sistema.

Com isso, basta salvar essas extensões em um arquivo txt. E para fazer isso, podemos usar esse comando:

gnome-extensions list --enabled > extensions.txt

Vou salvar esse arquivo que foi gerado na pasta gnome dentro do diretório dotfiles. Com a lista de extensões definida, falta só criar o script que vai instalar essas extensões.

extensions_intaller.sh

#!/bin/bash

dotfiles_dir="$PWD/dotfiles"
gnome_extensions="$dotfiles_dir/gnome/extensions.txt"

install_gnome_extensions() {
    if [ -f "$gnome_extensions" ]; then
    echo "⏳ Installing Gnome Extensions..."

    GN_CMD_OUTPUT=$(gnome-shell --version)
        GN_SHELL=${GN_CMD_OUTPUT:12:2}
        content=$(cat "$gnome_extensions")

        for ext in  $content; do
            VERSION_LIST_TAG=$(curl -Lfs "https://extensions.gnome.org/extension-query/?search=${ext}" | jq '.extensions[] | select(.uuid=="'"${ext}"'")')
            VERSION_TAG="$(echo "$VERSION_LIST_TAG" | jq '.shell_version_map |."'"${GN_SHELL}"'" | ."pk"')"
            wget -O "${ext}".zip "https://extensions.gnome.org/download-extension/${ext}.shell-extension.zip?version_tag=$VERSION_TAG"
            gnome-extensions install --force "${ext}".zip
            rm ${ext}.zip
    done
    else
        echo "Gnome extensions file not found! 🥲"
    fi
}


install_gnome_extensions


echo "🎉 Settings applied successfully!🎉"

Massa! Agora basta dar permissão e executar o script que ele vai fazer todo o trabalho.

chmod +x extensions_intaller.sh
./extensions_intaller.sh

Conclusão

Show 🎉 Com esse script, agora consigo configurar minha máquina rapidamente sem muito esforço. Só executar o script e esperar alguns minutos e vai estar tudo configurado do jeito que eu gosto. Você pode acessar o meu repositório e ver com mais detalhes o meu Dotfiles.