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.