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:
Brewfile
is 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 andBrewfile
is not practical for this because it includes things likecasks
and such.- For some reason, I was always a little paranoid about
mackup
usage. 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
Makefile
is located indotfiles/Makefile
, so commands run are relative to it. - The dot files to link to
$HOME
directory are all located indotfiles/home
directory. 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
HOMEFILES
variable which is a list of our files in thedotfiles/home
directory. A list inmake
is just space separated list, EG:foo bar baz
is a list of three items. DOTFILES
variable is used to create a list of files that we want to create in our$HOME
directory. This is done by prepending$HOME/
to each item in theHOMEFILES
list. So, if you havedotfiles/home/.zsh
you will get$HOME/.zsh
.- The
link
target is there to trigger the creation of any missing dot files from your$HOME
directory. If you didn’t have this target, then you would have to domake $HOME/.zsh
for each file, no fun. $(DOTFILES)
target defines the actual creation of the dot file in your$HOME
directory. What is nice is that this will not overwrite any existing files in your$HOME
because that’s howmake
works. 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
unlink
target iterates over your dot files in your$HOME
directory 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?
BREW
variable just defines wherebrew
binary is located.PACKAGE
is a function to install a Homebrew package only when it is not already installed.CASK
is 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 cask
to install the application.install
target triggers everything to install. You would run this on a fresh computer and customize it to your liking.brew
target updates Homebrew.$(BREW)
target installs Homebrew.taps
target defines your Homebrew taps.packages
target conditionally installs each Homebrew package.cask
target conditionally installs each Homebrew Cask application.mas
target installs applications from the App Store. Themas
binary can be installed via Homebrew.list
is a handy target to list everything you have installed so you can update yourMakefile
with anything new.clean
target is used to free up disk space.uninstall_brew
target nukes Homebrew, use with caution.uninstall_packages
target 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 packages
Done! Use multiple computers? Missing applications?
$ make cask
Done!
Conclusion
dotfile
❤️ Makefile