DEV Community

jorin
jorin

Posted on • Updated on • Originally published at jorin.me

Automate Your Mac Setup and Keep It Up to Date

It is rather common that companies have scripts to get their developer machines up and running. Many of us also create our own version to have our personal setup running on a new computer quickly.

A few years ago I also started doing this for my setup. One problem I came across was that my setup got out of date pretty quick and it was most definitely not matching my previous settings when using it to setup a new machine; more likely, it was completely broken and wouldn't run at all.

I wanted to fix this and make my setup script more useful. For the past year or so I have been using a new kind of setup script. It is probably even wrong to call it setup script. My setup script now is an update script instead.

The changes required are rather minimal. You simply need to make sure each command is idempotent - each command in the script has to work the same no matter how many times in a row you execute it.

The best part about this is that now you can also use this script to keep your system up to date and you have a single file that describes all of your setup in a reproducible way.

I run my setup script every week or so to have the latest software available.

What does it look like in practice?

Some changes are as simple as replacing each mkdir command with
mkdir -p to ignore errors.

Files should only be linked if they don't exist yet. My helper function to do so looks like this:

link_to() {
  if [ ! -e $2 ]
  then
    ln -s $1 $2
    printf "\nLinked $2"
  fi
}
Enter fullscreen mode Exit fullscreen mode

Homebrew should only install packages not already installed (If you are not using brew yet, do yourself a favor, stop right now and check it out; you don't want to install software without it). In my case I keep the packages in a separate brew.txt file. To install only new packages I filter them like this:

comm -23 \
  <(sort brew.txt) \
  <( \
    { \
      brew ls --full-name; \
      brew cask ls | sed -e 's#^#Caskroom/cask/#'; \
    } \
    | sort \
  ) \
  | xargs brew install
Enter fullscreen mode Exit fullscreen mode

Additionally I also remove old taps. This forces me to track all packages in the brew.txt file.

# Remove old taps
comm -13 <(sort brew.txt) <(brew leaves | sort) \
  | xargs brew rm

# Remove old cask taps
comm -13 \
  <(sort brew.txt) \
  <(brew cask ls | sed -e 's#^#Caskroom/cask/#') \
  | xargs brew cask rm
Enter fullscreen mode Exit fullscreen mode

You can use the above method also for other sources such as npm packages and RubyGems.

This might not be helpful to you unless you are using Vim, but I also setup and update Neovim and vim-plug here:

# Neovim
test -d ~/.vim || mkdir ~/.vim
if [ ! -d ~/.config/nvim ]
then
  ln -s ~/.vim ~/.config/nvim
  ln -s ~/.vimrc ~/.config/nvim/init.vim
  ln -s /usr/share/vim/vim74/spell/ ~/.config/nvim/
  printf "\nLinked Neovim config"
fi

## vim-plug
if [ ! -e ~/.config/nvim/autoload/plug.vim ]
then
  printf "\nInstalling vim-plug"
  curl -fLo ~/.config/nvim/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
fi
nvim +PlugInstall +PlugUpgrade +PlugUpdate +PlugClean! +UpdateRemotePlugins +qall
Enter fullscreen mode Exit fullscreen mode

Have a look at my dotfiles to find out more. Maybe you find some new trick to add to your Vim or Bash setup.
I would love to hear from you about some part of your setup that you automated!

Oldest comments (10)

Collapse
 
stecman profile image
Stephen Holdaway

Nice. I implemented something similar a while ago at a previous job. We had separate install (designed to run once) and update (run frequently) commands that made it a pain to add new software to existing machines without bugging people to run install tasks manually.

I took the idea a step further in our case - converting our shell scripts into a bunch of individual task classes in Python that defined dependencies so non-dependent tasks could be run in parallel. Quite a fun exercise!

Collapse
 
jorinvo profile image
jorin

I really like the simplicity of having a single file. But I also understand that not everyone has the luxury of keeping the setup so minimal. For a certain size it definitely reasonable to split things up and eventually switch to a language that is easier to reason with.

Collapse
 
caseywebb profile image
Casey Webb

I've been doing this for a while with github.com/caseywebb/laptop, and it makes setting up a new machine painless. I've got it set up to auto update whenever my .zshrc is ran which works well considering I've usually got to run refreshenv to get new bins in the PATH.

I also use keybase to sync my ssh and gpg keys across machines, so after running this script I'm pretty much set to go.

Collapse
 
jorinvo profile image
jorin

Nice setup!

I like the way you run the scripts right from your .zshrc and how you use LAST_EPOCH. I imagine it could be a little surprising if you open a new terminal and it suddenly starts to update..

Also, do you have any issues with the loading time when opening a new terminal? I try keeping my changes as minimal as possible since I open new terminals quite a bit and I like it to be as quick as possible.

Collapse
 
caseywebb profile image
Casey Webb

The naming scheme is such that the 3X-X.sh scripts are spawned in separate windows, so they don't affect the startup time at all.

That being said, I do have a small delay, but I like the tradeoffs.

1) I store my keys and credentials in Keybase, and symlink them to my local file system. It not only makes syncing across machines easier, but if I need to revoke a machine, I just revoke its keybase access.

2) The second thing is that I make a curl to api.github.com/zen. This is my uber simple way of knowing if I have internet access or not when I open the shell, considering it is where I spend most of my time.

Thread Thread
 
jorinvo profile image
jorin

Cool, missed that part about running the 3X scripts in the background. That helps for sure. Only have to make sure that they don't prompt for any user input and don't throw any errors I guess.

The syncing setup is nice too.

Thanks for sharing!

Collapse
 
stsg profile image
stsg

RC file management - rcm (github.com/thoughtbot/rcm) with post-up/pre-up script?
Works on macOS with homebrew.

Collapse
 
shanesveller profile image
Shane Sveller

A significant amount of this logic can be replaced directly by brew bundle: github.com/Homebrew/homebrew-bundle

Specifically brew bundle --global and brew bundle cleanup --global.

Collapse
 
jorinvo profile image
jorin

Nice! homebrew-bundle definitely looks like a good alternative for more complicated setups. I still like the simplicity and flexibility of managing it myself since my setup is so minimal anyways.

Collapse
 
chrischinchilla profile image
Chris Chinchilla

As we're sharing, I use something simple aliased to a command:

alias update='brew update && brew upgrade && brew cleanup && apm upgrade && mas upgrade'

Adding in Atom package manager and github.com/mas-cli/mas

Going to add some of your ideas too :)