Don't 'Make' me repeat myself
tldr: click here for the Rust Makefile
When working in a terminal you’re often typing the same commands, over and over. This is fine if they’re short, but when passing arguments things can get clunky fast. At best you hit Ctrl-r (reverse-i-search), attempting to remember part of the command and crossing your fingers it’s still in your history. At worst you have to search the internet each time your shell’s history expired.
To combat this I used to write separate shell scripts for each command I didn’t want to lose. Then a colleague introduced me to (GNU) Make and now I often think: “How did I get anything done without a Makefile?!” Mind you I was very late to this party because according to Wikipedia, Make first appeared in… April 1976!
What is Make?
In short: Make is used to run shell commands which you define by name, in a Makefile.
What does a Makefile look like?
Below is a Makefile. There’s a little syntax going on but I’ll explain that shortly. :
Let’s run it:
Now let me explain the Make specific syntax that’s present.
@suppresses the command itself in the output. If we left it out, the output would be:
When you want to use the
$(...)form for command substitution, you instead write
$(shell ...)(for more info: the shell function)
Abstracting away commands is handy, especially if you switch between programming languages or build-tools a lot. When these repetitive tasks aren’t broken, you usually don’t care which tools are being used in the background. So, to make my life easier I use the same names for my Make targets regardless of which programming language the project is written in. This way I can just run
make build in any language, because I set it up once.
There’s a lot of nice features in Make I haven’t discussed because I rarely use them. The one I do want to mention now is “chaining targets”. With very little syntax you can tell Make that your target depends on another target’s success. Imagine something like: first build, then if that succeeds test, then if that succeeds make a release etc. Here’s an arbitrary example:
The only line of interest here is
log-build: build. The syntax is short and clear. We define that when we run
make log-build we first run the ‘build’ target, and only if that succeeds do we run ‘log-build’.
Without further ado here’s the Makefile I start with when creating a new Rust project. It’s little more than a thin wrapper around cargo, but the ‘format’ and ‘docs’ commands contain just enough syntax to make me forget or mistype them.