Tutorial Beginner 10 min read

Makefile Tutorial: Build Automation for Any Project

Learn Makefile basics for build automation. Understand targets, dependencies, variables, and patterns. Practical examples for C, Node.js, and general automation.

OceanSoft Solutions
makefilebuildautomationdevops
make@build:~

Basic Structure

A Makefile consists of rules. Each rule has three parts:

target: dependencies
	command
	command
  • target — Usually a file name to create, or a phony target name
  • dependencies — Files that the target depends on
  • command — Shell commands to run (MUST be indented with TAB, not spaces)

Important: Commands must be indented with a TAB character, not spaces!


Your First Makefile

# Simple Makefile
hello:
	echo "Hello, World!"

clean:
	rm -f *.o
# Run the default (first) target
make

# Run a specific target
make hello
make clean

Phony Targets

Phony targets are commands, not files. Declare them with .PHONY to avoid conflicts with files of the same name:

.PHONY: clean test build deploy

clean:
	rm -rf build/ dist/

test:
	npm test

build:
	npm run build

deploy: build
	./deploy.sh

Variables

# Define variables
CC = gcc
CFLAGS = -Wall -g
SRC_DIR = src
BUILD_DIR = build

# Use variables with $(NAME) or ${NAME}
compile:
	$(CC) $(CFLAGS) -o $(BUILD_DIR)/app $(SRC_DIR)/main.c

# Override variables from command line
# make compile CC=clang

Automatic Variables

# $@ = target name
# $< = first dependency
# $^ = all dependencies
# $* = stem of pattern match

%.o: %.c
	$(CC) -c $< -o $@
# This compiles file.c to file.o

Dependencies

Make only rebuilds targets when dependencies are newer:

app: main.o utils.o
	gcc -o app main.o utils.o

main.o: main.c header.h
	gcc -c main.c

utils.o: utils.c header.h
	gcc -c utils.c

# If header.h changes, both .o files rebuild
# If only utils.c changes, only utils.o rebuilds

Pattern Rules

Use % as a wildcard to create generic rules:

# Compile any .c file to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Build any executable from its .o file
%: %.o
	$(CC) $< -o $@

Practical Examples

Node.js Project

.PHONY: install dev build test clean deploy

# Default target
all: install build

install:
	npm ci

dev:
	npm run dev

build:
	npm run build

test:
	npm test

lint:
	npm run lint

clean:
	rm -rf node_modules dist build

deploy: build
	rsync -avz dist/ user@server:/var/www/app/

Python Project

.PHONY: venv install test lint clean

PYTHON = python3
VENV = venv
PIP = $(VENV)/bin/pip

venv:
	$(PYTHON) -m venv $(VENV)

install: venv
	$(PIP) install -r requirements.txt

install-dev: install
	$(PIP) install -r requirements-dev.txt

test:
	$(VENV)/bin/pytest

lint:
	$(VENV)/bin/flake8 src/

clean:
	rm -rf $(VENV) __pycache__ .pytest_cache

Docker Project

.PHONY: build run stop logs clean

IMAGE_NAME = myapp
CONTAINER_NAME = myapp-container

build:
	docker build -t $(IMAGE_NAME) .

run: build
	docker run -d --name $(CONTAINER_NAME) -p 3000:3000 $(IMAGE_NAME)

stop:
	docker stop $(CONTAINER_NAME)
	docker rm $(CONTAINER_NAME)

logs:
	docker logs -f $(CONTAINER_NAME)

shell:
	docker exec -it $(CONTAINER_NAME) /bin/sh

clean: stop
	docker rmi $(IMAGE_NAME)

C/C++ Project

CC = gcc
CFLAGS = -Wall -Wextra -g
SRC_DIR = src
BUILD_DIR = build
SOURCES = $(wildcard $(SRC_DIR)/*.c)
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
TARGET = $(BUILD_DIR)/myapp

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJECTS) | $(BUILD_DIR)
	$(CC) $(OBJECTS) -o $@

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR):
	mkdir -p $(BUILD_DIR)

clean:
	rm -rf $(BUILD_DIR)

Self-Documenting Help

Add a help target that lists available commands:

.PHONY: help

help: ## Show this help message
	@echo "Available targets:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "  %-15s %s\n", $$1, $$2}'

build: ## Build the application
	npm run build

test: ## Run tests
	npm test

deploy: ## Deploy to production
	./deploy.sh
$ make help
Available targets:
  help           Show this help message
  build          Build the application
  test           Run tests
  deploy         Deploy to production

Tips & Best Practices

  • Always use .PHONY for non-file targets
  • Use @ prefix to suppress command echo: @echo "quiet"
  • Use - prefix to ignore errors: -rm file.txt
  • Use $(MAKE) instead of make for recursive calls
  • Keep it simple—complex logic belongs in shell scripts
# Suppress command output
quiet:
	@echo "This command is not printed"

# Ignore errors (continue if rm fails)
clean:
	-rm -f *.o
	-rm -f app

# Recursive make
subdirs:
	$(MAKE) -C subdir

Resources

Need help with build automation? Contact us for consulting.