Automating the Setup of a New Mac With All Your Apps, Preferences, and Development Tools

It was March 21, 2019. I was sitting in our comfy lounge chair working on the last and most complicated part of our taxes on my laptop, and all of a sudden, it started acting weird. I rebooted, and got the dreaded flashing question mark. I panicked because I didn’t have a full backup process in place. I was only using Dropbox, but only for certain files. And I was using the desktop version of TurboTax, and so my many hours of work weren’t backed up in the cloud. So I frantically ran downstairs to the basement to find an external hard drive, then ran back up, plugged it in with shaky hands, got the computer to reboot successfully, and started transferring all the important files.

You know that feeling when you’re watching the suspenseful part in a movie where the file transfer progress bar takes forever? Yeah, that’s how I felt. Luckily, I was able to save everything that was important, moments before the hard drive died!

The hard drive that failed was not the original one from Apple. I had upgraded to a 480 GB SSD from OWC in November 2014. Once I recovered all the files, I had several options for getting a working computer back:

I ended up ordering a new 2018 MacBook Air in Gold (the 2019 model didn’t come out until July). My reasoning was that 7 years is a decent run for a laptop, I could afford it, and I could use the extra speed for development. I didn’t sell the old laptop right away, which came in handy during the pandemic for the kids’ virtual sessions.

When I get a new computer, I prefer a fresh start, and add things as I need them. After 7 years, there are inevitably apps that I don’t need anymore. However, much of the initial setup stays the same. To remind me of all the steps needed, I created a checklist out of everything I did while setting up my new MacBook Air. Then I thought about how I could automate as much as possible for the next time.

The first thing I looked into was how to manage my dotfiles. I’m amazed I went that long without having a system in place. One of the best resources I found was https://dotfiles.github.io. The vast amount of options made it hard to get started. I read several tutorials, and looked through some of the most popular utilities, but couldn’t settle on anything. I ended up placing it on the backburner because it wasn’t an immediate need.

Then the pandemic happened, and I needed a more comfortable working space at home, and ended up ordering an iMac. That got the ball rolling again, and when I checked the dotfiles website, chezmoi had risen to the #2 spot in terms of stars on GitHub. I watched a video of Tom Payne (the creator) going over it, and it seemed promising. I found the templating feature particularly appealing, so I went for it. Let’s dive in to see how it works.

Prerequisites

To follow along, you’ll need:

On the primary machine

Dotfiles management

Install chezmoi:

brew install chezmoi

Create the chezmoi Git repo:

chezmoi init

This creates a repo in ~/.local/share/chezmoi.

Add files you want to manage, for example:

chezmoi add ~/.zshrc

You can also add entire folders:

chezmoi add -r ~/.config

If you want to add a file as a template, use --template, like this:

chezmoi add --template ~/.gitconfig

This will create a file called dot_gitconfig.tmpl in the chezmoi repo. Files that start with a dot are renamed with the dot_ prefix in chezmoi to make it clear which dotfiles are managed by chezmoi.

Templates are useful because you can create different configurations on different machines using a single file. For example, you might use your work email address for Git commits on your work machine, and another email address for your personal projects.

To have chezmoi automatically populate your .gitconfig with the correct email address, you set the email to a template variable in dot_gitconfig.tmpl:

[user]
    name = Moncef Belyamani
    email = "{{ .email }}"

For this to work, chezmoi needs to know about the email variable. chezmoi looks for variables in ~/.config/chezmoi/chezmoi.toml under the [data] section. For example:

[data]
  email = "sarah@home.org"

But we don’t want to have to create this file and populate it manually on every machine. To automate this, we can create a template for this data file in the chezmoi repo:

chezmoi cd
touch .chezmoi.toml.tmpl

Then open the file:

open .chezmoi.toml.tmpl

and paste the following in it:

{{- $email := promptString "email" -}}
[data]
    email = "{{ $email }}"

This tells chezmoi to prompt for the email variable whenever you initialize chezmoi. Let’s test it:

chezmoi init

You should see a prompt for your email:

email?

For testing purposes, let’s enter foo@bar.com. After you enter the email, the data file should now be created in ~/.config/chezmoi/chezmoi.toml, and it should look like this:

[data]
  email = "foo@bar.com"

You can also verify this by viewing all variables chezmoi knows about, including the default ones it creates:

chezmoi data

This alone won’t also update the ~/.gitconfig with the email we entered when prompted. We need to apply the changes:

chezmoi apply

Now if you open your ~/.gitconfig, you’ll see it now shows “foo@bar.com” for the user email.

Another file I wanted to turn into a template was Brewfile.local:

chezmoi add --template ~/Brewfile.local

This allowed me to tell chezmoi which tools and apps should be installed on both my home and work machines, and which ones should only be installed on one or the other. We’ll go over how this Brewfile gets called in the next section. Here’s an excerpt from my Brewfile.local.tmpl:

# Install these on work machines only
{{- if eq .location "work" }}
cask 'goland'
cask 'harvest'
cask 'microsoft-teams'
{{- end }}

This also required that I add the location variable to my .chezmoi.toml.tmpl:

{{- $email := promptString "email" -}}
{{- $location := promptString "location" -}}
[data]
    email = "{{ $email }}"
    location = "{{ $location }}"

Once we’ve added an initial set of dotfiles and templates, we can commit them to the chezmoi repo:

chezmoi cd
git add .
git commit -m "Add initial dotfiles with chezmoi"

Then we can push this repo to the cloud, so we can pull it down and apply the dotfiles on other machines. You can use any service you want, but in this guide, we’ll use GitHub. To create the repo, you can either do it on the GitHub website, and follow their instructions for pushing the local chezmoi repo to your new GitHub repo, or you can do it all from the command line.

First, we need to log in to GitHub with the proper scopes:

gh auth login --scopes repo

Follow the instructions in the terminal, picking the default options each time, unless you know you need a different option. If it says that you’re already logged into github.com, say no and skip to creating the repo below.

If not, keep going, and once you see “Congratulations, you’re all set!” on the GitHub site, go back to the terminal, and you should see this:

✓ Authentication complete. Press Enter to continue...

Keep going through the rest of the flow. When it asks if you want to authenticate Git with your GitHub credentials, say yes.

Now we can create our new repo. In the example below, the name of the repo is dotfiles, it will be a public repo, and its description is “My dotfiles”.

gh repo create dotfiles --public -d "My dotfiles" -y

And finally we can push our dotfiles to this new repo:

git push origin main

If you’re still using master as your default branch, replace main above with master. To make main your default branch name for new repos, read my guide on configuring Git.

At some point, you’ll want to add a README.md, and when you do, you’ll want to ignore it from chezmoi by adding it to a file called .chezmoiignore.

Mac setup automation

Now that we have a way to manage dotfiles, we can start scripting the rest of the Mac setup. The bulk of my development environment comes from my popular laptop script. It installs all the essential tools for modern Ruby web development, and if you need anything extra, you can add them in ~/.laptop.local, where you can add additional shell scripts and commands, and in ~/Brewfile.local, where you can specify additional tools and Mac apps to install with Homebrew.

To find out if a Mac app is available through Homebrew Cask, search the Homebrew Formulae, and if you find it, you can install it with the cask command in Brewfile.local:

cask 'alfred'

You can also search for casks from the command line:

brew search --casks alfred

And if you want more details about the app:

brew info alfred

The Brewfile also supports installing apps from the Mac App Store through the mas command line interface, for example:

mas '1Password', id: 1333542190

Since mas is installed by Homebrew, you’ll also need to add brew 'mas' to your Brewfile.local, and make sure that it comes before any lines that start with mas.

To find out which apps you’ve installed from the App Store on your primary machine, install mas, then use it to list the apps:

brew install mas
mas list

This will show the id of the app, which you need to specify in the Brewfile, as shown above. The cool thing is that homebrew-bundle can automatically generate a Brewfile for you, including your App Store apps, and their id:

brew bundle dump

This will create a Brewfile in the directory you ran that command from, and you can use it as a starting point. If you need to have different apps installed on different machines, turn the Brewfile.local into a chezmoi template as we talked about earlier. Here’s my entire Brewfile.local.tmpl for reference.

Now let’s take a look at my .laptop.local. The first thing that needs to happen is to install chezmoi, and then initialize it from my dotfiles GitHub repo, but only if ~/.config/chezmoi/chezmoi.toml doesn’t already exist. I don’t want to be prompted for all my variables every time I run the script. I should be able to run the script multiple times without any issues, and without it repeating things that have already been done.

Because I have sensitive tokens in my chezmoi.toml, I also need to change the permissions to 0600, as mentioned in the chezmoi documentation.

brew bundle --file=- <<EOF
    brew 'chezmoi'
EOF
if [ ! -f "$HOME/.config/chezmoi/chezmoi.toml" ]; then
  chezmoi init --apply --verbose https://github.com/monfresh/dotfiles.git
  chmod 0600 "$HOME/.config/chezmoi/chezmoi.toml"
fi

When chezmoi does its thing, it will create the proper Brewfile.local in my home directory based on the value I provided for the location variable. Then we can install the apps and tools:

if [ -f "$HOME/Brewfile.local" ]; then
  fancy_echo "Installing tools and apps from Brewfile.local ..."
  if brew bundle --file="$HOME/Brewfile.local"; then
    fancy_echo "All items in Brewfile.local were installed successfully."
  else
    fancy_echo "Some items in Brewfile.local were not installed successfully."
  fi
fi

Then, .laptop.local calls some other scripts, which are also managed by chezmoi, hence needing to initialize chezmoi first to have access to these scripts. Originally, I had everything in the .laptop.local file, to make sure everything worked. And only then did I start thinking about putting things in separate files for better organization and ease of maintenance. As of today, I have three files: one to clone my GitHub repos with the GitHub CLI, one to set up my macOS preferences, and one to set up my fish shell.

The more interesting one of those three is the macOS preferences, but that deserves a guide of its own. I’m also planning to record a video version of this guide, which will go over the macOS preferences in detail, including how to find out the name of the setting you want to change, and how to change it. This video will only be available to my newsletter subscribers, so join now if you’re interested!

On the new machine

Now that we have a working script, we can run it on our new machine, but before we can do that, there are some manual steps that need to happen. You can see the exact steps I follow in my dotfiles README.

Once all your machines are set up, keeping the dotfiles in sync between them is very easy. If you make any changes to the dotfiles on one machine, you push them to GitHub, and then all you have to do on the other machines is:

chezmoi update

I hope this inspired you to automate your own Mac setup, or maybe you got some ideas for your existing script. I’d love to hear your thoughts and tips of your own.

P.S.

For backups, I now use Backblaze (including external hard drives), iCloud for most files, and I still have my free Dropbox account.