Seriously do this right now, it's amazing.

This post is about how I wrote a fantastically useful script. If you have multiple computers, you will be thanking me by the end of it. Bold words, but what I’m going to describe here is that good.

The problem

The problem is that, like many of you, I have multiple computers. I have two desktops and two laptops (I just like keeping my old computers, they work), and installing a program on one was always a hassle, because I’d then have to remember to install it on the others and configure it the same way. Not only that, but, when I sometimes had to reformat (for performance, or to solve a problem, or whatever), I had to spend ages getting all the programs and their preferences working just the way I wanted them again.

I needed a better way to do this, and this is the post where I describe that way and how you can do it too. Here it is:

The solution

The solution actually consists of two steps. First, I need to create a git repository to hold the various preferences and assorted dotfiles in my home directory. The term “dotfiles” refers to the hidden files in each user’s home directory in UNIX, where programs store their preferences, settings, and other user-specific information. They are called that because they are hidden files, and hidden files in UNIX start with a dot. Storing them in a repository will allow me to save a history of all my application settings and sync them between computers by just pulling and merging. Programmers usually think git is the answer to everything, so this is very stereotypical of me.

The second step is to create a script to install programs and perform system-level tasks. This script will be stored in the repository above, for ease of use.

Of course, this entire solution kind of assumes you aren’t using Windows. If you are, I’m sorry, but at least you have AAA games available, so it’s not all bad.

A repository for dotfiles

Adding our dotfiles to a git repository is pretty simple, although there are helpers for you to get as fancy as you like. I’ve opted to just create a git repository in my home directory, which can sometimes cumbersome, so I would recommend that you pick a utility that suits your needs from the page above and go with it.

You should keep a few things in mind:

  • Never run git clean on your home dir, as it will delete everything (your home is now a git repository, after all). Using one of the utilities above will help with this, since those don’t just turn everything into one huge repository.
  • Be judicious in choosing which files you add to the repository. Don’t just add everything that’s in your home directory, or even most of it, because that will just require unnecessary commits and make your repository larger. It does take a bit more time to figure out where programs store their preferences, but it’s nothing you can’t find out in two minutes, and it will ensure you only sync the things you really need to.
  • If some files contain sensitive information, you can use the excellent git-crypt to encrypt them with your GPG key. If you’re unfamiliar with GPG, here’s a [good introduction](https://www.madboa.com/geek/gpg-quickstart/), including details on how you can generate your key. This will ensure that nobody but you will be able to read the sensitive information (not even your git host), but be careful not to commit decrypted files, which can sometimes inadvertently happen.

After (some of) your dotfiles are sitting safely in a repository, all you need to do is remember to commit and push the changes every so often. When you switch computers, just git pull, and all your updated preferences will be available there as well!

We are now ready to proceed to phase “awesome”.

Writing a provisioning script

The role of the provisioning script is to bring the system into a specified state, from any starting state. This usually means using the system’s package manager to install packages, as well as checking if things are missing and only then installing them. The way I achieved this is with a series of small scripts, but you should feel free to use whatever tool you feel comfortable with. In my case, I used Ansible just because it’s very easy to write such a simple script, but anything else (even a simple and portable bash script) would work just as well.

The main script

First, let’s start with the main Ansible script:

# This playbook is meant to provision a new computer.
# Run with: ansible-playbook -i , provision.yml

---
- name: Provision a machine.
  hosts: 127.0.0.1
  connection: local
  tasks:

  - name: Add Virtualbox server.
    apt_repository: repo='deb http://download.virtualbox.org/virtualbox/debian {{ hostvars[inventory_hostname]["ansible_distribution_release"] }} contrib' state=present
    register: ppa
    sudo: yes

  - name: Add repository keys.
    apt_key: keyserver=keyserver.ubuntu.com id={{ item }}
    register: ppa
    sudo: yes
    with_items:
      - 54422A4B98AB5139  # Virtualbox

  - name: Add PPAs.
    apt_repository: repo='{{ item }}'
    sudo: yes
    register: ppa
    with_items:
      - ppa:webupd8team/java
      - ppa:fish-shell/release-2
      - ppa:tmate.io/archive

  - name: Update the apt cache.
    apt: update_cache=yes
    sudo: yes
    when: ppa.changed

  - name: Install Mac packages.
    apt: name={{ item }}
    sudo: yes
    with_items:
     - macfanctld
    when: '"Apple" in hostvars[inventory_hostname]["ansible_system_vendor"]'

  - name: Install packages.
    apt: name={{ item }}
    sudo: yes
    with_items:
     - adobe-flashplugin
     - aptitude
     - aria2
     - build-essential

  - name: Run bootstrap script.
    shell: ~/bin/provisioning/scripts/bootstrap

  - name: Run settings script.
    shell: ~/bin/provisioning/scripts/settings

  - name: Change shell to fish.
    sudo: yes
    user: name={{ hostvars[inventory_hostname]["ansible_user_id"] }} shell=/usr/bin/fish groups=dialout,disk append=yes

That’s most of the actual file I use. I’ve omitted some packages for brevity, but you can see that what I’m doing is adding some non-PPA apt repositories, adding their keys, then adding the PPAs, updating the cache to refresh everything above, installing Mac-specific packages (one of my computers is a Macbook Air), installing all the packages I use, running two scripts that I’ll get to shortly, and changing my shell to the best shell in existence, fish.

Just copy and paste that thing right into a file. I have a ~/bin/ directory I put executable scripts in (and add some of them to my dotfiles repo above), but you can also customize this. I put the script above in ~/bin/provisioning/provision.yml and use the following little bash script (~/bin/provision) to run it:

#!/bin/sh

/usr/local/bin/ansible-playbook -K -i , ~/bin/provisioning/provision.yml

This lets me just type provision into a terminal, and my computer is soon synchronized to the latest state.

The bootstrap script

Some changes don’t fit (or aren’t practical to do) with the above Ansible script, so I wrote another short bash script to perform those. It mainly clones repositories and builds the software in them:

#!/bin/bash

# Install git-crypt.
if [ ! -f ~/bin/git-crypt ]; then
    echo "Installing git-crypt..."

    cd /tmp/
    git clone https://github.com/AGWA/git-crypt.git
    cd /tmp/git-crypt/
    make
    cp /tmp/git-crypt/git-crypt ~/bin/

    cd ~
    ~/bin/git-crypt unlock
fi

# Install Vundle.
if [ ! -d ~/.vim/bundle/vundle ]; then
    echo "Installing Vundle..."
    git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/vundle
fi
vim +PluginInstall +qall

# Install Powerline fonts.
if [ ! -f ~/.fonts/DejaVu\ Sans\ Mono\ for\ Powerline.ttf ]; then
    echo "Installing Powerline fonts..."
    git clone https://github.com/powerline/fonts.git /tmp/fonts/
    cd /tmp/fonts
    ./install.sh
fi

# Install virtualfish.
if [ ! -d ~/.config/fish/virtualfish ]; then
    echo "Installing virtualfish..."
    git clone https://github.com/adambrenecki/virtualfish.git /tmp/virtualfish/
    mv /tmp/virtualfish/virtualfish/ ~/.config/fish/
fi

You can just download and run the above script now, and it will set up various useful programs for you, including git-crypt.

The configuration script

The final piece of the puzzle is the configuration script. This sets various options in gconf that can’t easily be set elsewhere.

To discover these settings, I used dconf to watch the root key (with dconf watch / and toggle the option I wanted to store. dconf then printed the key and value, which I just stuck into this file.

The following script will also swap your Caps Lock and Ctrl keys, which I highly recommend, and which you will love, once you get past the first half day of discomfort.

#!/bin/bash

# Set various system options.
gsettings set org.gnome.desktop.session idle-delay "uint32 300"
gsettings set org.gnome.settings-daemon.plugins.power idle-dim true
gsettings set org.gnome.desktop.screensaver lock-enabled true
gsettings set org.gnome.desktop.screensaver lock-delay "uint32 0"
gsettings set org.gnome.desktop.screensaver ubuntu-lock-on-suspend true
gsettings set org.gnome.desktop.input-sources xkb-options "['ctrl:swapcaps']"
gsettings set org.gnome.settings-daemon.plugins.xsettings overrides "{'Gtk/EnablePrimaryPaste': <0>}"
gsettings set org.gnome.desktop.wm.preferences resize-with-right-button true
gsettings set org.gnome.settings-daemon.plugins.media-keys terminal '<Super>t'
gsettings set org.gnome.settings-daemon.plugins.media-keys home '<Super>e'
gsettings set org.compiz.integrated show-hud "['']"
gsettings set com.canonical.indicator.sound visible true
dconf write /org/compiz/profiles/unity/plugins/unityshell/show-launcher "'<Control><Super>'"

# Change default file manager to Nemo.
xdg-mime default nemo.desktop inode/directory application/x-gnome-saved-search

# Gnome-Do
gconftool-2 --type bool --set /apps/gnome-do/preferences/Do/CorePreferences/QuietStart true
gconftool-2 --type string --set /apps/gnome-do/preferences/Do/Platform/Common/AbstractKeyBindingService/Summon_Do "<Super>space"

# Clock tray
dconf write /com/canonical/indicator/datetime/show-day true
dconf write /com/canonical/indicator/datetime/show-date true
dconf write /com/canonical/indicator/datetime/time-format "'24-hour'"
dconf write /com/canonical/indicator/datetime/show-seconds true
dconf write /com/canonical/indicator/datetime/show-auto-detected-location true

Setting up a new machine from scratch

Now that everything above is in place, a great benefit we reap immediately is that we can now set it up to our exact preferences with a single command. The one simple step that we need to take to perform this amazing, single-command feat is:

  1. Clone the dotfiles repository to the new home directory (or just download the zip file). This will probably require that we have copied our SSH keys to this machine in step 0.5.
  2. Decrypt the repository, if it’s encrypted with git-crypt. This will require that we copy our GPG keys over to the new machine in step 1.5.
  3. Run the provision command, which will download all the programs, whose settings have been put in place by step 1.
  4. That’s it! Everything is now working as if we’ve always used this computer, with one very easy step.
  5. We’ve always been at war with Eurasia.

Epilogue

I’ve been using this for around a year now, and I’m kicking myself for not having done it earlier. This setup makes it trivial to format any of my PCs and have it ready to go again within the hour. The biggest benefit, however, is that I don’t have to spend time remembering how I set up one or the other computer, or have slightly different preferences from one machine to the next. All my computers have the exact same configuration without me needing to perform any setup twice, and everything stays up to date as I make any change on any of them.

I highly recommend that you try it out, if only for a few days, as it’s pretty simple to get started. I’m sure you’ll love it enough that you’ll be wondering how you lived without it, like I did.

If you have any feedback, or if the way that I jump from past to present tense or first person singular to first person plural in my writing bothers you, please send me a tweet or leave a comment below. I’ve always wondered how annoying and/or noticeable it is to readers.