Itā€™s been a while since I last opened a shiny new laptop or set up a fresh system. Yet, occasionally, I find myself indulging in my mad scientist tendencies, creating a virtual machine (VM) for some wild experiment before wiping it off the face of the earth. My usual partners in crime are Vagrant or VirtualBox, each hosting a fresh installation of my preferred Linux distro.

However, each time I embark on this journey, I find myself slightly handicapped. My seasoned hands miss their trusted tools like fish, spaceship, brew, and my beloved keyboard shortcuts. Transitioning back to Gnome or ā€” šŸ˜± ā€” using a mouse feels as unnatural as a fish learning to tap dance. An inconvenience, sure, but not yet at the ā€œpull your hair outā€ level of frustration.

Ansible

Now, fast forward to earlier this week. I finally decided to construct a new rig for myself. Besides partaking in a bit of casual gaming, I needed a dedicated workstation to tackle the ever-growing mountain of side projects thatā€™s been gathering dust. After installing Ubuntu, I realized that my long-suppressed curiosity will inevitably lead me to explore other distros. The pain of setting up each new shell quickly loomed over me like a dark cloud. So, I rolled up my sleeves and dove deep into my toolbox to make life easier for the future Sumit.

I love automation! And having used Ansible to build some sturdy servers in the past, I couldnā€™t help but think: ā€œIf it can configure servers, why canā€™t it do the same for my desktop environment?ā€ So, armed with my usual resolve (and a few cups of chai), I started crafting some Ansible playbooks for my bare minimum development setup.

But thereā€™s a twist. Despite Ansibleā€™s expectations for an inventory, I found a neat little loophole. You can make Ansible run on localhostā€”like a self-hosted party. And voila! I had a playbook that would install my basic CLI and GUI packages, such as fish-shell, LinuxBrew, podman, VLC, Chrome, and VS Code. I know, I use IntelliJ Idea more than VS Code, but I havenā€™t yet found a way to invite it to this automation party. Itā€™s on the to-do list, I promise!

To get started, I have setup a few variable files to list down the packages that I need to install:

packages:
	core:
		- git
		- stow
		- fish
	system:
		- wget
		- curl
		- fuse
		- jq
		- libfuse2
		- zip
		- unzip
		- unrar
	desktop:
	# Audio and video
		- vlc
	fonts:
		- fonts-powerline
	virtualization:
		- podman

With that set, I ended up creating a series of tasks thatā€™d upgrade the packages and run a series of tasks to setup my system. Here are some example tasks that I ended up writing:

- name: Upgrade system packages
  become: true
  ansible.builtin.apt:
    update_cache: yes
    upgrade: yes
 
- name: Install core packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: latest
    update_cache: no
  loop: "{{ packages.core }}"
 
- name: Install system packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: latest
    update_cache: no
  loop: "{{ packages.system }}"
 
- name: Install desktop packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: latest
    update_cache: no
  loop: "{{ packages.desktop }}"
 
 

The task for installing the Jetbrain Mono Nerd font:

- name: Install fonts packages
  become: true
  ansible.builtin.apt:
    name: "{{ item }}"
    state: latest
    update_cache: no
  loop: "{{ packages.fonts }}"
  
# Install Nerd font
- name: Install Jetbrains Nerd Font
  block:
    - name: Create Directory
      ansible.builtin.file:
        path: "/home/{{ user }}/.fonts/JetBrainsMonoNerd"
        state: directory
        mode: "0755"
        owner: "{{ user }}"
        group: "{{ user }}"
    - name: Download
      ansible.builtin.unarchive:
        src: https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/JetBrainsMono.zip
        dest: "/home/{{ user }}/.fonts/JetBrainsMonoNerd"
        remote_src: true
        owner: "{{ user }}"
        group: "{{ user }}"
 
    - name: Font cache update
      action: command fc-cache -fv
 

I have been a fan of HomeBrew and have missed that in the Linux world. Thankfully it does support Linux very well now - albeit with a fairly anaemic list of packages. So I ended up using a task to install that as well:

---
# Installs [HomeBrew](https://brew.sh/) prompt
- name: Check if HomeBrew is installed
  ansible.builtin.stat:
    path: /home/linuxbrew/.linuxbrew/bin/brew
  register: brew
 
- name: Download & Install HomeBrew
  when: not brew.stat.exists
  block:
    - name: Download Installer Script
      ansible.builtin.get_url:
        url: https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh
        dest: /tmp/brew_install.sh
        mode: '775'
    - name: Run Installer Script
      ansible.builtin.shell: NONINTERACTIVE=1 /tmp/brew_install.sh 
 
- name: Set LinuxBrew Executable Path
  ansible.builtin.set_fact:
    linuxbrew_executable: /home/linuxbrew/.linuxbrew/bin/brew
    linuxbrew_path: /home/linuxbrew/.linuxbrew/bin
 

Now I can install Starship easily. If you have used tools like oh-my-zsh built by Robby Russell or the fish equivalent oh-my-fish, Starship is similar, but is cross platform by default and can run on multiple shells. Besides this I also use fisherwhich makes it easier to install some tools without having to port them into fish from bash. Here are the tasks to install them:

---
# Installs [Starship](https://starship.rs/) prompt
- name: Check if Starship is installed
  ansible.builtin.stat:
    path: /home/{{ user }}/.config/starship.toml
  register: starship
 
- name: Install
  when: not starship.stat.exists
  ansible.builtin.command: "{{ linuxbrew_executable }} install starship"
  environment: 
    HOME: "/home/{{ user }}"
  args:
    executable: /bin/bash
 
# Installing [Fisher](https://github.com/jorgebucaran/fisher)
- name: Install Fisher
  ansible.builtin.shell: curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher
  environment:
    HOME:  "/home/{{ user }}"
  args:
    executable: /bin/fish
 

I have been using i3wm for about 4 years now, but havenā€™t been too fond of the high degree of learning required for configuring the desktop environment. Regolith has done an amazing job to make i3 truly accessible for the masses. I am one of the masses :) It did take me more time than the other tasks to get it right, because of the step of adding the signing key. I hope this helps:

---
- name: regolith ~> configure apt-repository for {{ ansible_distribution }} {{ ansible_distribution_release }}
  become: true
  block:
    - name: Regolith |no apt key
      ansible.builtin.get_url:
        url: https://regolith-desktop.org/regolith.key
        dest: /usr/share/keyrings/regolith-archive-keyring.asc
 
    - name: Regolith | apt source
      ansible.builtin.apt_repository:
        repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/regolith-archive-keyring.asc] https://regolith-desktop.org/release-ubuntu-jammy-amd64 jammy main"
        state: present
 
- name: Regolith ~> installing
  become: true
  ansible.builtin.apt:
    name:
      - regolith-desktop
      - regolith-compositor-picom-glx
      - regolith-look-blackhole
      - regolith-look-default
      - i3xrocks-focused-window-name
      - i3xrocks-rofication
      - i3xrocks-info
      - i3xrocks-app-launcher
      - i3xrocks-memory
    state: present

For VS Code I am using a role I found on Ansible Galaxy. It does the job well. Hereā€™s quick view. Besides these I also ended up installing Google Chrome.

Dotfiles

While installing packages is pretty straightforward, recreating the look and feel, themes, and configurations of my tools becomes a bit challenging if done by Ansible. These configuration files are generally known as dotfiles, and thereā€™s a passionate community around efficient dotfile management. You can go to awesome-dotfilesand find numerous tools and articles on how to manage dotfiles effectively. I felt chezmoi would be a pretty good fit for my setup, as it comes with git and password-manager support. This makes it significantly easier for me to set things up.

Setting up packages with Ansible is as straightforward as a Roman road, but replicating the configurations, themes, and overall feel of my favorite tools requires a different approach. These settings live in the world of dotfiles. And thereā€™s an entire legion of folks just as obsessed with efficient dotfile management as I am.

For my setup, chezmoi seemed like the perfect candidate. It comes with git and password-manager support, simplifying the process. But as the saying goes, ā€œEvery rose has its thorns.ā€ I discovered that Ubuntu runs on Wayland while Regolith leans on the X11 display server. This created some issues - such as regolith-refresh or regolith-look require Wayland specific tools. Copying over the dotfiles sometimes resulted in a broken Regolith desktop. Annoying? Absolutely. The end of the world? Not quite.

Currently, I have a workaround using Ansible to install all the packages in the Ubuntu Gnome Shell environment. I then log out and back in with Regolith, which runs on X11. On the first startup, it sets the default theme. At this point, I sync over the dotfiles using chezmoi (with Ansible tasks). Hereā€™s how it looks:

- hosts: localhost
 
  vars_files:
    - vars/packages.yml
    - vars/common.yml
 
  tasks:
    - ansible.builtin.import_tasks: tasks/packages.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') != "x11"
    - ansible.builtin.import_tasks: tasks/fonts.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') != "x11"
    - ansible.builtin.import_tasks: tasks/brew.yml
    - ansible.builtin.import_tasks: tasks/shell.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') != "x11"
    - ansible.builtin.import_tasks: tasks/regolith.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') != "x11"
    - ansible.builtin.import_tasks: tasks/chrome.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') != "x11"
    - ansible.builtin.import_tasks: tasks/programming.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') != "x11"
    - name: Copy dot files
      ansible.builtin.include_tasks: tasks/dotfiles.yml
      when: lookup('ansible.builtin.env', 'XDG_SESSION_TYPE') == "x11"
 

And hereā€™s how the dotfiles sync setup looks:

#Installing [Chezmoi](https://www.chezmoi.io/)
- name: Install & Initialize Chezmoi
  block:
    - name: Install Chezmoi
      ansible.builtin.command: "{{ linuxbrew_executable }} install chezmoi"
      environment: 
        HOME: "/home/{{ user }}"
      args:
        executable: /bin/bash
    - name: Initialize Chezmoi
      ansible.builtin.command: "{{ linuxbrew_path }}/chezmoi init --apply https://github.com/{{ github_username }}/dotfiles.git"
      environment: 
        HOME: "/home/{{ user }}"
      args:
        executable: /bin/bash
 
# Configuring gnome-shell to copy over the colors that I use
- name: Configure gnome-terminal
  block:
    - name: Copy dconf settings
      ansible.builtin.copy:
        src: gnome-terminal.conf
        dest: /tmp/gnome-terminal.conf
    - name: Apply dconf
      ansible.builtin.shell: dconf load /org/gnome/terminal/ < /tmp/gnome-terminal.conf
      environment:
        HOME:  "/home/{{ user }}"
      args:
        executable: /bin/bash
 

Itā€™s not an ideal situation, but for now, itā€™s a peace treaty Iā€™m willing to sign. In the future, I plan to refine the playbook and dotfiles, and hopefully, concoct a one-step solution.

With this setup, all Iā€™d need to do on a fresh Ubuntu installation is install Ansible. No other tooling required, no hair-pulling, no keyboard smashing. Just automation bliss.

Check out the GitHub repositories for both projects:

  1. Dotfiles: sumitsarkar/dotfiles
  2. Ansible Playbook: sumitsarkar/dev-box-setup

Hereā€™s to smoother setups and fewer headaches! Until next time, keep automating and keep exploring!