Using a Makefile for your dotfiles can help simplify your dot file life!
I was originally inspired by this dotfiles
project. Using Brewfile and mackup to do most of the heavy lifting. I
really liked it, but then I ran into some problems:
Brewfileis sort of an all or nothing type system. It’s really only handy if you are on a fresh, blank computer. That doesn’t actually happen for me that often. For example, if I upgrade macOS, I need to re-install my ports and andBrewfileis not practical for this because it includes things likecasksand such.- For some reason, I was always a little paranoid about
mackupusage. Mostly it was due to how infrequently I actually used it, so I would forget how it works and what commands I should use. Plus for my particular setup, I ended up with some rather odd symlinking that would confuse the heck out of me if I had forgotten how it worked.
While Brewfile and mackup are great tools, they were not adding a lot of value
for me and I realized that what they were doing was really simple and I could
migrate to a simple Makefile.
Replacing mackup
Replacing makup was easy for me because I was only using it for dot files that
were only located in my $HOME directory. So, the goal is to create a symbolic
link between the files in my dotfiles to my $HOME directory.
Before reading the Makefile below, know this:
- The
Makefileis located indotfiles/Makefile, so commands run are relative to it. - The dot files to link to
$HOMEdirectory are all located indotfiles/homedirectory. Read this regarding the pipe in the target dependencies.
HOMEFILES := $(shell ls -A home) DOTFILES := $(addprefix $(HOME)/,$(HOMEFILES)) .PHONEY: link unlink link: | $(DOTFILES) # This will link all of our dot files into our home directory. The # magic happening in the first arg to ln is just grabbing the file name # and appending the path to dotfiles/home $(DOTFILES): @ln -sv "$(PWD)/home/$(notdir $@)" $@ # Interactively delete symbolic links. unlink: @echo "Unlinking dotfiles" @for f in $(DOTFILES); do if [ -h $$f ]; then rm -i $$f; fi ; done
What’s happening?
- First, create the
HOMEFILESvariable which is a list of our files in thedotfiles/homedirectory. A list inmakeis just space separated list, EG:foo bar bazis a list of three items. DOTFILESvariable is used to create a list of files that we want to create in our$HOMEdirectory. This is done by prepending$HOME/to each item in theHOMEFILESlist. So, if you havedotfiles/home/.zshyou will get$HOME/.zsh.- The
linktarget is there to trigger the creation of any missing dot files from your$HOMEdirectory. If you didn’t have this target, then you would have to domake $HOME/.zshfor each file, no fun. $(DOTFILES)target defines the actual creation of the dot file in your$HOMEdirectory. What is nice is that this will not overwrite any existing files in your$HOMEbecause that’s howmakeworks. It wont run the target if the file already exists. And of course, what it does is make a link, EG:ln -sv dotfiles/home/.zsh $HOME/.zsh- The
unlinktarget iterates over your dot files in your$HOMEdirectory and interactively deletes your dot files only if that file is a symbolic link.
So, perhaps I got a little verbose in my explanation, but it ends up creating
a pretty simple Makefile and the result is very safe. You can re-run make link
as you add new files to dotfiles/home. You can run make unlink anytime and
it asks you for confirmation before unlinking anything. And on top of that,
it’s all non-destructive: if you have a real file in your $HOME directory,
it will not overwritten or deleted.
Replacing Brewfile
Replacing Brewfile is a little more involved, but ends up working quite well.
BREW := /usr/local/bin/brew
PACKAGE = brew list --versions $(1) > /dev/null || brew install $(1)$(2)
CASK = brew cask list $(1) > /dev/null 2>&1 || brew cask install $(1)
.PHONEY: link install brew taps packages casks mas list unlink clean uninstall_brew uninstall_packages
install: | taps packages casks mas link clean
brew: | $(BREW)
brew update
$(BREW):
@ruby -e "$$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
taps: | brew
brew tap homebrew/cask
# and many more...
packages: | brew
$(call PACKAGE,go)
# and many more...
casks: | brew
$(call CASK,alfred)
# and many more...
mas:
mas install 587512244 # Kaleidoscope
# and many more...
# Use to update install lists.
list:
brew tap
@echo "\n"
brew leaves --full-name
@echo "\n"
brew cask list --full-name
@echo "\n"
mas list
clean:
brew cleanup
brew cask cleanup
# Use with caution, can really mess with Cask (thinks nothing is installed).
uninstall_brew:
@ruby -e "$$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"
uninstall_packages:
brew remove --force --ignore-dependencies $(shell brew list)
Still with me? Great! What is happening?
BREWvariable just defines wherebrewbinary is located.PACKAGEis a function to install a Homebrew package only when it is not already installed.CASKis a function to install a Homebrew Cask application only when it is not already installed. If I recall correctly, “already installed detection” only works if you usedbrew caskto install the application.installtarget triggers everything to install. You would run this on a fresh computer and customize it to your liking.brewtarget updates Homebrew.$(BREW)target installs Homebrew.tapstarget defines your Homebrew taps.packagestarget conditionally installs each Homebrew package.casktarget conditionally installs each Homebrew Cask application.mastarget installs applications from the App Store. Themasbinary can be installed via Homebrew.listis a handy target to list everything you have installed so you can update yourMakefilewith anything new.cleantarget is used to free up disk space.uninstall_brewtarget nukes Homebrew, use with caution.uninstall_packagestarget uninstalls all of your Homebrew packages.
So, this may look a bit complicated/intimidating, but for me it helped to simplify things and make some of these install lists more re-usable. For example, when upgrading to a new macOS, you should re-install your Homebrew packages:
$ make uninstall_packages && make packagesDone! Use multiple computers? Missing applications?
$ make caskDone!
Conclusion
dotfile ❤️ Makefile