Don't 'Make' me repeat myself

Posted on January 13, 2020 by Niels

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. :

greet:
	@echo My name is $(shell id -un) and it is currently $(shell date)

Let’s run it:

$ make greet
My name is Spartacus and it is currently Mon Jan 13 02:29:38 JST 2020

Now let me explain the Make specific syntax that’s present.

  1. The @ suppresses the command itself in the output. If we left it out, the output would be:

    $ make greet
    echo My name is Spartacus and it is currently Mon Jan 13 02:29:38 JST 2020
    My name is Spartacus and it is currently Mon Jan 13 02:29:38 JST 2020
  2. When you want to use the $(...) form for command substitution, you instead write $(shell ...) (for more info: the shell function)

Wrapping up

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:

build:
	@cargo build

log-build: build
	@echo Build success with commit hash \
	$(shell git log -1 --format="%H") \
	on $(shell date). >> ./build.log

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’.

Rust Makefile

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.

ROOT = $(dir $(realpath $(firstword $(MAKEFILE_LIST))))

run:
	@cargo run

build:
	@cargo build

test:
	@cargo test

format:
	@cd $(ROOT) && \
	find src -name '*.rs' -exec rustfmt {} \;

lint:
	@cd $(ROOT) && \
	cargo clippy

docs:
	@cargo doc --open