4 Levels of How To Use Makefile
Tags: devops, bash, tooling, makefile
Published: 2022-10-31 14:56:06

I love Makefile. Today, I use Makefile for most of the projects that I am working on. You may have seen `Makefile` in many open-source projects on GitHub ([e.g. this](https://github.com/scikit-learn/scikit-learn/blob/main/Makefile)). Probably like me, you have wondered what Makefiles are and what they do.

There is a bazillion of incredible Makefile tutorials out there. My goal is to get you interested enough to start using Makefile in under 5 minutes. By the end, you should know enough to start using and self-learn about Makefile.

_TL;DR: read Level 1 & Level 2_

## What Are Make and Makefile

In short, `Makefile` is a special format file with _rules_ that tells the GNU Make utility tool (i.e. `make`) how to execute commands that run on \*nix. Typically, Make is used to compile, build, or install the software.

While Makefile is commonly used to compile C or C++, it is NOT limited to any particular programming language. You can use Make for all sorts of stuff:

-   Execute a chain of commands to set up your dev environment
-   Automate build
-   Run test suites
-   Deployments, etc.

## Why Use Makefile

Compiling source code can be cumbersome, especially when you have to include multiple source files.

At its core, `Makefile` is a utility for writing and executing a series of command-line instructions for things like compiling code, testing code, formatting code, running code, etc.

Basically, it helps to automate your development workflows into simple commands (`make build`, `make test`, `make format`, `make run`).

Also:

-   `make` is preinstalled at most \*nix systems that you can find
-   `make` is programming language/framework agnostic

Alright enough talking, let’s get to the real deal.

---

## A Quick Guide to Makefile

While going through the levels below, I highly encourage you to create a `Makefile` (note: always name it `Makefile`) and try things out yourself.

### Level 1: “Tell me all I need to know”

At this level, you’ll learn the basics of Makefile; probably all you ever need to know to benefit from it.

Suppose you have a project using that uses Docker. To build and run your app iteratively using a Docker container, you would typically:

1. Run `docker build`
2. Make sure there are no running container
3. Run `docker run`
4. Repeat

To do this yourself:

```bash
docker build -t image-name . --build-arg ENV_VAR=foo
docker stop container-name || true && docker rm container-name || true
docker run -d -e ANOTHER_VAR=bar--name container-name image-name
```

What a hassle! There is a lot to remember and type. On top of that, many chances to make silly mistakes.

Sure, you could just run all 3 commands every time you make changes. That would work. However, it is just so inefficient. Instead, we could write a `Makefile` just like the below:

```makefile
all: build stop run # build -> stop -> run

build:
	@docker build -t image-name . --build-arg ENV_VAR=foo

stop:
	@docker stop container-name || true && docker rm container-name || true

run:
	@docker run -d -e ANOTHER_VAR=bar--name container-name image-name
```

Now to build and run a new Docker image, all you need is a single `make all` command. In the case above, we can just call `make` because `all` is the first rule (note: the first rule is selected by default).

To summarize, a rule within a `Makefile` generally looks like this:

```makefile
# run `make <target>` to run this rule
<target>:	<prerequisite 1> <prerequisite 2> <prerequisite N> # a comment block is prefixed with a "#"
	<command 1>
	<command 2>
	<command N>
```

**Takeaways for Level 1:**

1. `<target>` — typically file name; could be any name
2. `<command>` — typically \*nix commands/steps used to make the `<target>`. These MUST start with a TAB character
3. `<prerequisite>` — _optional._ This tells `make` that each prerequisite must exist before the commands are run. Therefore, prerequisites run in order of 1 to N (in the example above).
4. What is the `@` syntax for? If a command line starts with `@`, the echoing of that command is suppressed ([reference](https://www.gnu.org/software/make/manual/html_node/Echoing.html))
5. The first `<target>` is selected by default when running just `make`

That’s it. Simple right?

Now go forth and _make_ a Makefile at work!

### Level 2: “Cool, but I need a little bit more”

The use of variable substitution is rather common when it comes to all aspects of programming. Makefile is not exempted.

So, how to use environment variables (with default)?

Suppose you want to `docker build` your app with different build arguments (e.g. `ENV_VAR`), here’s how your `Makefile` would look like:

```makefile
NAME := my-app
DOCKER := $(shell command -v docker 2> /dev/null)
ENV_VAR := $(shell echo $${ENV_VAR-development}) # NOTE: double $ for escaping
ENV_ANOTHER_VAR ?= bar

.PHONY: build
build:	## build docker image based on shell ENV_VAR
	@if [ -z $(DOCKER) ]; then echo "docker is missing."; exit 2; fi # tip
	@docker build -t $(NAME) . --build-arg ENV_VAR=$(ENV_VAR)
```

**Takeaways for Level 2:**

1. `.PHONY` — by default `make` assumes your `<target>` is a file. So if they are not, just mark them with `.PHONY` in case you have a filename that is the same as `<target>` ([reference](https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html) and [read this](https://makefiletutorial.com/#phony) for more info)
2. Declare a Makefile variable with `=` or `:=` syntax ([reference](https://stackoverflow.com/questions/4879592/whats-the-difference-between-and-in-makefile))
3. `:=` — to execute a statement once
4. `=` — to execute a statement every time. An example use case is when you need a new `date` value every time you call a function.
5. `?=` — set the _Makefile variable_ only if it's not set or doesn't have a value ([reference](https://www.gnu.org/software/make/manual/html_node/Setting.html))
6. Finally, you can use an environment variable to check if a command exists too as shown in the `if` statement above
7. Be careful about adding in-line comments as below. You may end up adding extra space to your variable:

    ```sh
    FOO = /my/path/to # <- a white space!
    ```

**Bonus:**

-   `echo $${ENV_VAR-development}` sets `ENV_VAR` _shell variable_ based on your current shell environment with a default value to `development`.
-   Alternatively, `make` allows you to pass variables and environment variables from the command line, e.g. `ENV_VAR=development make build`

By now, you probably have everything you need to create a Makefile for small and medium-sized projects.

Now go forth and _make_ awesomeness with Makefile!

### Level 3: “Now, show me the fancy stuff”

This is probably my favorite part. Everyone loves a good `help` message right? [Right…?](https://knowyourmeme.com/memes/for-the-better-right)

Let’s create a useful `help` target in case our users need help on how to use Make in our project:

```makefile
.DEFAULT_GOAL := help

.PHONY: help
help:
	@echo "Welcome to $(NAME)!"
	@echo "Use 'make <target>' where <target> is one of:"
	@echo ""
	@echo "  all	run build -> stop -> run"
	@echo "  build	build docker image based on shell ENV_VAR"
	@echo "  stop	stop docker container"
	@echo "  run	run docker container"
	@echo ""
	@echo "Go forth and make something great!"

all: build stop run

.PHONY: build
build:
	@docker build -t image-name . --build-arg ENV_VAR=foo

.PHONY: stop
stop:
	@docker stop container-name || true && docker rm container-name || true

.PHONY: run
run:
	@docker run -d -e ANOTHER_VAR=bar--name container-name image-name
```

-   `.DEFAULT_GOAL` — remember how I said the first rule is selected by default when you run just `make`? You can override that with this
-   With this, you can simply run `make` to display the error message every time

However, every time you add a new target to your `Makefile`, you’ll need to add a new line of echo.

Now, how does a self-documenting Makefile help message sound to you?

Modify your `help` target as follow (modify accordingly):

```makefile
.DEFAULT_GOAL := help

.PHONY: help
help:  ## display this help message
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
```

Next, add comments with the `##` tag to print the comments as part of the `help` message:

```makefile
all: build stop run	## run build -> stop -> run

.PHONY: build
build:	## build docker image
	@docker build -t image-name . --build-arg ENV_VAR=foo

.PHONY: stop
stop:	## stop running container
	@docker stop container-name || true && docker rm container-name || true

.PHONY: run
run:	## run docker container
	@docker run -d -e ANOTHER_VAR=bar--name container-name image-name
```

Bam! Here you have it, a nice self-documenting `help` message:

```bash
$ make

Usage:
  make <target>

Targets:
  help        display this help
  all         run build -> stop -> run
  build       build docker image
  stop        stop running container
  run         run docker container
```

_Credits: I would like to thank the author of [make help - Well documented Makefiles](https://www.thapaliya.com/en/writings/well-documented-makefiles/) for this._

### Level 4: “Your article does not provide the level of details I need!”

Here are some Makefile references that I’ve curated that I highly recommend you to check out; I owe my thanks to all of them:

-   [Learn Makefiles With the tastiest examples](https://makefiletutorial.com/) — a really good general Makefile tutorial; I prefer to rather than going through the 180+ pages long manual
-   [Tips for your Makefile with Python](https://blog.mathieu-leplatre.info/tips-for-your-makefile-with-python.html) — if you’re working on a Python project
-   Need a decent example? Here’s a [Makefile with 1.8k+ Stars on GitHub](https://gist.github.com/isaacs/62a2d1825d04437c6f08)
-   If all else fails — [GNU Make manual](https://www.gnu.org/software/make/manual/make.html) (you probably won’t need most of it)

---

# Final Thoughts

Even if you’re writing good documentation, chances are Makefile is much more reliable. The beauty of Makefile is that it’s simply a rigorous way of documenting what you’re already doing.

Contributing to an open-source project? Refer to the `Makefile` to identify the development workflow.

## Current State of Makefile

First appearing in 1976, the use of Makefile is known to have its quirks.

Some of the most common complaints about Make is that it has rather complicated syntax. As a result, you’ll find people who [absolutely love](https://news.ycombinator.com/item?id=21566530) or [hate](https://news.ycombinator.com/item?id=7463048) Makefile.

I think this kind of utility gets very personal. Everyone has their favorite. I for one am less interested in having to download yet another fancy program/tool/framework/library for many other reasons.

Makefile “just works”.