Now that the hardware is here, and the OS is up and ready, It is time to install the packages we need and configure them to our likings.

First let me tell you what I used to do:

I used to keep the list of all the packages I need in csv files. For dotfiles I stored them in a plain directory tree, one folder per package. On new installs I use a shell script to filter the csv files and install the packages, and then copy the dotfiles from backup (HD or git).

This solution was good and simple but I needed more:

  • Bug-free shell scripts are hard to write. Although I still write a lot of shell scrips I try to avoid them whenever I can.
  • Shell scripts are imperative and it is very hard to introduce idempotence into them.
  • Profiles: Besides my main machine, I use linux on a lot of other machines, including machine in the cloud, VMs and machine containers, and I want to be able to choose what to install and what not.
  • When something changes especially configuration wise, it is so hard to apply those changes in a clean way with shell scripts.
  • Undoing things is also hard using the shell script approch.

Ansible + Stow = perfect dotfiles

The solution that I came out with is using ansible to install the packages I need from the Arch repos, and clone my dotfiles from gitlab. Then I use stow to farm all my dotfiles as symbolic links into my home directory.

Ansible solves many of my previous problems, since it is more declarative than shell scripts, and provides idempotence most of the times. Also it allow me to skip things and only include what I need to be installed/configured.

On the other hand GNU Stow is so useful when it comes to dotfiles. It eliminates the bajillion cp commands and manages the symlinks seamlessly. Here are some examples on how it works.

# Symlink all files in source_dir recursively to a given directory:
stow --target=path/to/target_directory source_dir

# You can also simulate its behavior if you are not sure how it works:
stow --simulate --target=path/to/target_directory file1 directory1 file2 directory2

To start with I created an ansible playbook that uses 6 roles, and runs on my machine (can be run on a remote machine).

---
- hosts: localhost
  connection: localhost
  roles:
    - setup
    - core
    - desktop
    - media
    - virtualization
    - configuration

The first role setup is pretty simple, it ensures that git and stow are installed, then it clone the dotfiles from Gitlab into my home directory.

---
- name: Install core packages using Pacman
  become: true
  community.general.pacman:
    name: "{{ item }}"
    state: latest
    update_cache: yes
  loop:
    - git
    - stow

- name: Clone dotfiles to home directory
  ansible.builtin.git:
    repo: {{ dotfiles_git_repository }}
    dest: "/home/{{ ansible_user }}/dotfiles"

2nd to 5th roles, are installer roles. I used them to install all the packages that I need to have in my desktop. Each role is responsible for installing a category of packages .e.g. the core role installs cli and tui packages I need to have in the machine in order to be productive, while desktop install all the required packages to run my desktop environment/window manager.

### defaults/main.yml
packages:
  core:
    - alacritty
  system:
    - tlp
  fs:
    - udisks2
  utils:
    - man
    - jq
  archive:
    - zip
    - atool
  network:
    - openssh

### tasks/main.yml
- name: Install core packages
  become: true
  community.general.pacman:
    name: "{{ item }}"
    state: latest
    update_cache: yes
  loop: "{{ packages.core }}"

- name: Install System related packages
  become: true
  community.general.pacman:
    name: "{{ item }}"
    state: latest
    update_cache: yes
  loop: "{{ packages.system }}"

- name: Install FS related packages
  become: true
  community.general.pacman:
    name: "{{ item }}"
    state: latest
    update_cache: yes
  loop: "{{ packages.fs }}"
...

The last role is configuration. I use it to populate my home directory with the dotfiles. This roles created the required folders in the home then uses stow to create symlinks from my dotfiles into the home directory. In addition to that the configuration role installs oh-my-zsh and neovim plugins.

...
- name: Create home folder structure
  ansible.builtin.file:
    path: "/home/guru/{{ item }}"
    state: directory
  loop: "{{ folders.home }}"

- name: stow dotfiles
  ansible.builtin.shell:
    cmd: "stow -d $HOME/dotfiles -t ~/ {{ item }}"
  loop: "{{ stow }}"
...

Finally you can check the code for all of this here.

I should note that although this works for me, it is still far from perfect. For that any ideas, questions, issues or contributions are very welcome and much needed.