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

Updated

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 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 a GitHub account, and the tools below:

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 my Brewfile:

chezmoi add --template ~/Brewfile

If you’re not familiar with Brewfile, it’s a file where you can specify all the dev tools, and even Mac apps and fonts, that you want to install on your Mac with Homebrew. Read more about it in my article about installing Xcode with Homebrew.

The advantage of turning it into a chezmoi template is you can now specify different setups for different computers in a single file. Without chezmoi, if you wanted to have a different setup for your personal computer versus your work computer, you would need to maintain two or more different Brewfiles. For example, Brewfile.personal and Brewfile.work.

With chezmoi, you can use conditionals based on various variables, such as the name of the computer, or a variable that you define yourself. Here’s an excerpt from my Brewfile.tmpl:

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

# Install these on iMac only
{{- if eq .chezmoi.hostname "Moncefs-iMac" }}
cask 'beatport-pro'
cask 'farrago'
cask 'screenflow'
{{- 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 with the GitHub CLI, as shown below.

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. If you prefer to skip the manual setup and use a fully automated solution, check out my Ruby on Mac Ultimate product. In addition to installing all the essential tools for modern Ruby web development, you can customize it to install additional tools, Mac apps, fonts, Git preferences, macOS preferences, and GitHub repos.

As mentioned earlier, I like to use Homebrew, Homebrew Cask, and mas to install all my dev tools, Mac apps, and fonts. I can define everything in a single file and they will all be installed or updated at once.

Ruby on Mac Ultimate comes with a Brewfile with hundreds of popular dev tools, Mac apps, and fonts. That way, you don’t have to spend time looking up the proper name of the tool/app and then add it manually to your Brewfile.

If you already have dev tools and Mac apps installed with Homebrew, and you want to install the same things on a different Mac, you can easily generate a Brewfile with homebrew-bundle:

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 into a chezmoi template as mentioned earlier.

To install an additional Mac app, you can check if it’s available through Homebrew Cask by searching the Homebrew Formulae, and if you find it, you can install it with the cask command in Brewfile. For example:

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, 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. If you already had Mac apps installed with mas, they get automatically included in the Brewfile that you generate with brew bundle dump.

In addition to my Brewfile, the other things I want to set up on a new machine are my macOS prefs, my GitHub repos, and my Git preferences. These are links to the YAML files in my public dotfiles repo. However, they only work with Ruby on Mac Ultimate. Ruby on Mac makes it easy to customize your preferences without knowing how to code, or spending hundreds of hours automating everything. Once you have your preferences set, Ruby on Mac takes care of setting everything up.

If you want to write your own automations for setting up macOS preferences, the macOS defaults site is a good place to start.

Now let’s take a look at my .laptop.local bash script. One of the first things that needs to happen when setting up a new Mac is to install chezmoi, and then initialize it from my dotfiles GitHub repo. That will copy over all my setup files to the new Mac. These files need to be on the Mac before I run the Ruby on Mac script. I’m working on automating this part as well in Ruby on Mac Ultimate.

To avoid being prompted for all my chezmoi variables every time I run the script, it only runs the chezmoi initialization if ~/.config/chezmoi/chezmoi.toml doesn’t already exist. 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 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" ]; then
  fancy_echo "Installing tools and apps from Brewfile ..."
  if brew bundle --file="$HOME/Brewfile"; then
    fancy_echo "All items in Brewfile were installed successfully."
  else
    fancy_echo "Some items in Brewfile were not installed successfully."
  fi
fi

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.

This is an affiliate link for Backblaze, which gets me 1 free month if you sign up. I only share products I use myself and genuinely believe in. Thank you for supporting me!