Makefile - Old Is New Again
May 3, 2023

Makefile - Old Is New Again

Posted on May 3, 2023

Make 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


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"