Parsia-Clone

'Documentation is a love letter that you write to your future self.' - Damian Conway

Nov 30, 2022 - 4 minute read - Random

Makefile Notes

Github Link

Most of this is about GNU Make but should work with others.

If you want to find someone who likes to use both tabs and spaces as different separators in a text file, talk to the guy who invented Makefiles!

  • Me

A makefile has a bunch of rules. Each rule has a target, some prerequisites (e.g., files) and a recipe. A recipe is a series of commands.

target: prereqs
[tab]   command1
        command2
        command3

The recipe should start with a tab. But you also need to indent some other things (e.g., conditionals) with space. This will be a mess.

Use RECIPEPREFIX

To make your life easier, add this to the beginning of your makefile to replace tab with another character.

.RECIPEPREFIX := > # use `>` instead of `\t`

Note: Macs come with GNU Make version 3.81 from 2006 that does not support this. Either replace ^> with \t after debugging and figuring out what it does or just not support Macs :).

Now, your file looks like this:

target: prereqs
> command1
> command2
> command3

PHONY

You can run each target with make target. However, if target also exists as a file in the target directory, it will interfere. See Phony Targets.

.PHONY: target
target: prereqs
> command1
> command2
> command3

Conditionals

Conditionals are different from programming languages. They do not necessarily control the program flow but they hide/unhide instructions. Think of them as IFDEFs in C. Let's use an example.

Detect if Command Exists

I have tested this on Linux. It's supposedly POSIX compliant but I don't know if it works on other OS yet. For example, if we want to check Python3 exists.

target:
# check if python3 exists
ifeq ($(shell command -v python3 2> /dev/null), )
> $(error python3 not installed)
endif

error will terminate the makefile with an error.

Let's say you want to check if Semgrep exists and if not, install it.

target:
# install Semgrep if it's not installed
ifeq ($(shell command -v semgrep 2> /dev/null), )
> python3 -m pip install semgrep
> $(info you might have to add ~/.local/bin to PATH)
endif

info prints some information.

Parallel Tasks

You can parallelize the commands in the recipe instead of sequentially running them. This can be done with -j (infinite) or -j number-of-parallel-jobs.

This is useful when each command is different. For example, building Go binaries for different platforms. Note the use of the variable BINARY_NAME.

BINARY_NAME := myapp

.PHONY: build-all
build-all:
> GOARCH=arm64 GOOS=darwin go build -o ${BINARY_NAME}-darwin-arm64 -ldflags "-w -s" main.go
> GOARCH=amd64 GOOS=darwin go build -o ${BINARY_NAME}-darwin-amd64 -ldflags "-w -s" main.go
> GOARCH=arm64 GOOS=linux  go build -o ${BINARY_NAME}-linux-arm64  -ldflags "-w -s" main.go
> GOARCH=amd64 GOOS=linux  go build -o ${BINARY_NAME}-linux-amd64  -ldflags "-w -s" main.go

Run Multiple Targets

You can have targets that run other ones. A popular target is all.

.PHONY: all
all: clean build deploy

This is an alternate way of writing a recipe. This is the same as below:

.PHONY: all
all:
> clean
> build
> deploy

Now we can run make all to run all these targets.

The Help Target

If no argument is provided, make will run the first target. It's a good idea for the first target to be the help/usage. We can print something manually:

.PHONY: help
help:
> $(info Usage)
> $(info make help     print help)
> $(info make all      clean, test and build)
> $(info make install  install dependencies)

We can also do it automatically. I forgot where I copied the code from but there are multiple versions all over the internet.

.PHONY: help
help:   ## print help
> @sed -ne '/@sed/!s/## //p' $(MAKEFILE_LIST)

.PHONY: all
all:    ## clean, deploy, and build
> clean
> build
> deploy

If we run make help or make (help is the first target) we will see:

help:   ## print help
all:    ## clean, deploy, and build

This is an issue if we have prerequisites. One of the other similar commands might fix this but I did not investigate. Note, we have to manually fix the whitespace between the target and the comment.