Makefile - Old Is New Again
Posted on May 3, 2023Make is the GNU build tool for building C apps. But recently has gained popularity outside of C projects when used as a project task manager. Command task likes compile
, build
, run
or test
could be written as instructions in the README file, or written as BASH scripts to do more complex tasks. The magic of Make is its flexibility to used to organize all the tasks in one file that is capable of executing other scripts and tasks. To understand a project, just look at the Makefile and examine the “targets”, Make’s name for tasks.
Simple Example
In this simple example of Makefile you can see how to set variables and a few targets:
## VARS and ENVs ########################
EXE_NAME := myapp
VERSION := 1.0.0
## TARGETS ##############################
build:
touch dist/$(EXE_NAME)-$(VERSION)
clean:
rm -f dist/*
list:
## the `@` prefix will silent the printing of the target, and print only the output of the command
@ls dist/
When used, you will get this output:
[~/src/github.com/kyledinh/myapp]$ make build
touch dist/myapp-1.0.0
[~/src/github.com/kyledinh/myapp]$ make list
myapp-1.0.0
[~/src/github.com/kyledinh/myapp]$ make clean
rm -f dist/myapp*
[~/src/github.com/kyledinh/myapp]$ make list
[~/src/github.com/kyledinh/myapp]$
Dynamic Example
We can use shell commands to substitue variables and use targets to run scripts. Noticed in the build
target we added clean
as prerequisite by putting it on the same line as the target, then a $(MAKE)
to run another target at the end of build
. The .PHONY
annotation is required for the list
target.
## VARS and ENVs ########################
EXE_NAME := myapp
VERSION ?= $(shell head -n 1 sem-version)
GITTAG ?= $(shell git describe --tags --always --dirty)
## TARGETS ##############################
.PHONY: list
build: clean
@touch dist/$(EXE_NAME)-$(VERSION)-$(GITTAG)
$(MAKE) list
check:
./scripts/check-installed.sh
...
scripts/check-install.sh
#!/bin/bash
# Checks for installed software packages
function fn_check_docker {
if command -v docker >/dev/null 2>&1; then
docker --version
else
echo "!!! docker required, but it's not installed.";
fi
}
function fn_check_java {
if command -v java >/dev/null 2>&1; then
java --version
else
echo "!!! java required, but it's not installed.";
fi
}
## MAIN
echo "Checking installed software packages:"
echo "docker, java"
echo
fn_check_docker
fn_check_java
sem-version
1.0.1
- dynamic example
1.0.0
- simple example
When you run make check
, the check-install.sh
bash script will run and check for installed dependencies. It will warn if dependencies are missing!
*[master][~/src/github.com/kyledinh/myapp]$ make check
./scripts/check-installed.sh
Checking installed software packages:
docker, java
Docker version 20.10.17, build 100c701
openjdk 17.0.4 2022-07-19 LTS
OpenJDK Runtime Environment Corretto-17.0.4.8.1 (build 17.0.4+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.4.8.1 (build 17.0.4+8-LTS, mixed mode, sharing)
Now we can try the update make build
that will dynamically write the file with the current sem verison and git tag. Also, notice how the build target also runs the clean
target. The result is the new output file will be named with the current version and git tag: myapp-1.0.1-77aa33c-dirty
.
*[master][~/src/github.com/kyledinh/myapp]$ make build
rm -f dist/myapp*
touch dist/myapp-1.0.1-77aa33c-dirty
/Library/Developer/CommandLineTools/usr/bin/make list
myapp-1.0.1-77aa33c-dirty
From these simple patterns you can compose very complex tasks that are organized into simple Make targets. Some other targets could be setup for onboarding setup, launching and managing docker, linting and code analysis or just mundane project maintenance tasks. Just think shell commands and automation!
Examples in projects
- Scala Project - https://github.com/kyledinh/zio-blogapp/blob/main/Makefile
- Go Project - https://github.com/kyledinh/btk-go/blob/main/Makefile
Common gotchas
- Use tabs only! Instead of spaces. You will see errors like this:
Makefile:9: *** missing separator. Stop.
- Bash scripts need to be executable, use
chmod +x your-script.sh
make: ./scripts/your-script.sh: Permission denied
make: *** [check] Error 1
- Trailing whitespaces in variable assignments
VAR1 := ONE // "ONE "
VAR2 := TWO // "TWO"
echo $(VAR1)-$(VAR2) // "ONE -TWO"