Makefiles for Everyone
Published 2014-02-08
A little over two years ago I got a MacBook from work. It was my first full-time unix machine, and while I'd played with Linux in the past I always used the package manager to get software. After getting frustrated with some of the Mac installers for php and mysql, one of my colleagues recommended compiling my own software from source.
One evening I went home and tried it and found it suprisingly easy. Before long I was downloading tarballs and git cloning and compiling code from source and beginning to understand what made Linux and open source so powerful. Any time I wanted to try something out, whether it was a new nosql database or the bleeding edge of PHP, it was a three-step process:
git clone ./configure make
I don't know C. I can pretend, sure. But not actually knowing how to write it doesn't stop me from downloading C code, compiling it, and getting it running. Sometimes make
blew up. So I googled the error messages, fixed a few bugs in the code I checked out, and recompiled the code. And it worked.
make
became the great equalizer. But it wasn't until later that I learned how to wield it myself.
The Easy Path is the Right Path
The key to adoption of any new software, open source or otherwise, is a value proposition: Using this software will save you time. No one uses software because the icon looks nice on their desktop. They use software because it helps them accomplish some other objective, faster and more pleasantly than they could otherwise.
As a creator and evangelist of that software, it's your job to make your software as easy to use as possible. Easy to learn. Easy to install. Easy to maintain. Easy for other developers to contribute back and improve the software, so your value proposition grows over time.
If you lead a software project and you don't have a Makefile
, you're missing an opportunity. It doesn't matter whether you're writing C code. It doesn't matter whether your project is open source or not. If your peers can git clone
and make
and try out your project in a few minutes, they're only a few edits and a pull request away from becoming a contributor. And the benefits for your core team are huge, too.
Make it a Team Habit
Remember those 15 steps to setup your project? Only 10 of which are actually in the readme? Replace them with a Makefile
. The Makefile
becomes your build documentation. It becomes your installer. It becomes the mentor for a new developer when you aren't around. When you are around. It runs your tests, it resolves your dependencies, and it enforces your code quality standards.
But wait! I'm using [ node.js | ruby | php | java ] and we have [ npm | gem | composer | maven].
That's fine, but why should I have to know that? I work on projects written in php, python, ruby, node.js, java, go, erlang, and scala and switch between them every day. I use a dozen different test frameworks. Some projects build into tarballs. Some into jars. Some into VMs. Do I remember which flags I need to pass to test each project I work on? Do I remember to update the readme when the test framework changes? Do my colleagues actually read the readme? The answer is "No." to all those questions.
make
doesn't replace npm
or mvn
or your test runner. It just wraps their long, domain-specific commands in simple phrases. make
. make test
. make install
. The commands are the same for every project. This communicates intent to your team. It quickly standardizes the way your team works. Your team can read the makefile to learn the dirty details when they need to, but they don't need to do it on day one. The added level of comfort and familiarity makes it easy to jump into a new project, and just as easy to jump into a project you last worked on 6 months ago as one you worked on yesterday.
Make the Makefile
I'd like to propose a simple convention that I've developed over a dozen or so projects, and liberally stolen from a few python guys. I've organized it more or less in the order of the software development lifecycle.
all: init test docs package init: # - Install your dependencies via gem, maven, etc. # - Download sql dumps or copy configuration templates # that a dev needs to get up and running. # - Install git hooks (more below) test: # Run unit tests, code coverage, and linters docs: # Generate your API documentation (you do have some, don't you?) package: # Build a release tarball or jar or executable dev: # Start up a development server or process e.g. `vagrant up` or `node server.js` # Bonus: open that page in the user's browser automatically install: # If your project builds an executable, place it in the `$PATH`. # E.g. copy or symlink it into `/usr/local/bin` deploy: # If you have a simple deployment mechanism, like `rsync` or `s3cmd`, let # the Makefile take care of it. .PHONY: test docs
I'm sure you can come up with other verbs that may be more relevant or useful to your project, like make debug
or make me a sandwich
.
Some Technical Notes about Makefiles
- The filename
Makefile
is case-sensitive. - Makefiles must be indented using hard tabs. Spaces don't work.
- You can use bash in your Makefile for conditionals, variables, and more.
- When you run
make
with no arguments it pulls the target at the top of the file. - Make targets can include other make targets, on the same line. e.g.
all: init test
. Use this to run a wrapper that doesinit test package
in one command. - If a make target overlaps with a file or folder in your project, make will say there's nothing to do. You'll need to use
.PHONY
to fix it. e.g..PHONY: test
You can learn more about writing Makefiles in the make
documentation. You can also look at some examples.
Seal the Deal with Git Hooks
What if someone forgets to run the unit tests before they commit code? What if they can't forget? What if the tests run each time they commit something? Use make init
to install a git pre-commit hook that runs your linter and unit tests. It's now impossible to check in broken code.
Are your colleagues seeing errors after they pull down new code? Install a post-merge
hook that runs make
to resolve any new dependencies that came in from an update, and runs unit tests to verify the merge succeeded.
Read more about git hooks on the git site.