Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

Makefiles

Build automation tool that uses dependency tracking to execute commands and manage project workflows.

Makefiles

Build automation tool that uses dependency tracking to execute commands and manage project workflows.

Overview

Make is a build automation utility that automatically determines which pieces of a program need to be recompiled and issues commands to recompile them. Beyond compilation, Make is widely used for task running, deployment scripts, and workflow automation. A Makefile defines targets (tasks), their dependencies, and recipes (commands) to execute.

YesNoMakefileParse TargetsDependenciesChanged?Execute RecipeSkip TargetUpdate TimestampsNext TargetYesNoMakefileParse TargetsDependenciesChanged?Execute RecipeSkip TargetUpdate TimestampsNext Target

Basic Structure

A Makefile consists of rules that define targets, their prerequisites (dependencies), and recipes (commands to execute).

Key Concepts

  • Target - The file to create or task to run (left of colon)
  • Prerequisites/Dependencies - Files that must exist before the target can be built
  • Recipe - Shell commands to execute (must be indented with a tab)
  • Rule - A target with its prerequisites and recipe combined
Makefile Rule Structuretarget:prerequisitesrecipe line 1recipe line 2Makefile Rule Structuretarget:prerequisitesrecipe line 1recipe line 2

Common Patterns

# Basic rule structure
target: prerequisites
	recipe

# Simple compilation example
main.o: main.c main.h
	gcc -c main.c -o main.o

# Multiple prerequisites
program: main.o utils.o config.o
	gcc main.o utils.o config.o -o program

# Multiple targets with same recipe
clean distclean:
	rm -rf build/

# First target is the default
all: build test

Examples

# Complete C project example
CC = gcc
CFLAGS = -Wall -g

# Default target
all: myapp

# Build the application
myapp: main.o utils.o
	$(CC) $(CFLAGS) -o myapp main.o utils.o

# Compile source files
main.o: main.c utils.h
	$(CC) $(CFLAGS) -c main.c

utils.o: utils.c utils.h
	$(CC) $(CFLAGS) -c utils.c

# Clean build artifacts
clean:
	rm -f *.o myapp

Variables and Functions

Variables store values for reuse; functions transform text and perform operations.

Key Concepts

  • Recursive Assignment (=) - Recursively expanded when used
  • Immediate Assignment (:=) - Expanded once when defined
  • Conditional Assignment (?=) - Set only if not already defined
  • Append (+=) - Add to existing variable
  • Automatic Variables - Special variables set by Make for each rule

Common Patterns

# Variable assignment types
CC = gcc                    # Recursive expansion
CFLAGS := -Wall -g          # Immediate expansion
PREFIX ?= /usr/local        # Only if not set
LDFLAGS += -lm              # Append to existing

# Using variables
program: main.o
	$(CC) $(CFLAGS) -o $@ $^

# Automatic variables
# $@ - Target name
# $< - First prerequisite
# $^ - All prerequisites
# $* - Stem of pattern rule match
# $? - Prerequisites newer than target

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Multi-line variables
define HELP_TEXT
Usage: make [target]

Targets:
  all      Build everything
  clean    Remove build artifacts
endef
export HELP_TEXT

help:
	@echo "$$HELP_TEXT"

# Environment variables
# Make imports environment variables automatically
export PATH := $(PATH):./bin
export DEBUG = 1

Built-in Functions

# Text functions
FILES = foo.c bar.c baz.c
OBJS = $(patsubst %.c,%.o,$(FILES))      # foo.o bar.o baz.o
BASE = $(basename $(FILES))               # foo bar baz
DIR = $(dir src/main.c)                   # src/
FILE = $(notdir src/main.c)               # main.c
SUFFIX = $(suffix main.c)                 # .c

# String substitution (suffix-only)
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)                      # main.o utils.o
OBJS2 = $(patsubst %.c,%.o,$(SRCS))       # Equivalent long form
# Note: $(subst .c,.o,...) is NOT equivalent — it replaces ".c"
# anywhere in the string, not just as a suffix

# Filtering
SOURCES = main.c utils.h config.c data.h
C_FILES = $(filter %.c,$(SOURCES))        # main.c config.c
NOT_H = $(filter-out %.h,$(SOURCES))      # main.c config.c

# Wildcard and shell
SRCS = $(wildcard src/*.c)                # All .c files in src/
VERSION = $(shell git describe --tags)    # Run shell command

# Conditionals in functions
DEBUG_FLAG = $(if $(DEBUG),-g,)
ITEMS = $(or $(VAR1),$(VAR2),default)

# Word functions
WORDS = one two three four
FIRST = $(firstword $(WORDS))             # one
LAST = $(lastword $(WORDS))               # four
COUNT = $(words $(WORDS))                 # 4
SECOND = $(word 2,$(WORDS))               # two

# Foreach loop
DIRS = src lib test
CLEAN_TARGETS = $(foreach dir,$(DIRS),clean-$(dir))

# Call function
reverse = $(2) $(1)
RESULT = $(call reverse,a,b)              # b a

Examples

# Real-world variable usage
PROJECT_NAME := myproject
VERSION := $(shell git describe --tags 2>/dev/null || echo "dev")
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

# Source file discovery
SRC_DIR := src
BUILD_DIR := build
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))

# Compiler settings
CC := gcc
CFLAGS := -Wall -Wextra -std=c11
LDFLAGS := -lm

# Debug/Release builds
ifdef DEBUG
    CFLAGS += -g -DDEBUG
else
    CFLAGS += -O2 -DNDEBUG
endif

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

$(BUILD_DIR):
	mkdir -p $@

Pattern Rules

Pattern rules define implicit rules using wildcards to match multiple files.

Key Concepts

  • Pattern - Uses % as a wildcard to match any string
  • Stem - The part matched by %, accessible via $*
  • Implicit Rules - Built-in rules for common operations
  • Static Pattern Rules - Apply patterns to a specific list of targets

Common Patterns

# Generic pattern rule
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Pattern with directories
build/%.o: src/%.c
	$(CC) $(CFLAGS) -c $< -o $@

# Multiple patterns
%.html: %.md
	pandoc -o $@ $<

%.pdf: %.md
	pandoc -o $@ $<

# Static pattern rules (apply to specific files only)
OBJECTS = foo.o bar.o baz.o

$(OBJECTS): %.o: %.c
	$(CC) -c $< -o $@

# Cancel implicit rule
%.o: %.c

# Pattern with multiple prerequisites
%.o: %.c %.h
	$(CC) $(CFLAGS) -c $< -o $@

Examples

# Build system with pattern rules
SRC_DIR := src
OBJ_DIR := obj
BIN_DIR := bin

SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRCS))

# Pattern rule for object files
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# Include generated dependencies
-include $(OBJS:.o=.d)

# Directory creation
$(OBJ_DIR) $(BIN_DIR):
	mkdir -p $@

# Documentation generation
docs/%.html: docs/%.md
	mkdir -p $(dir $@)
	pandoc --standalone -o $@ $<

# Multiple language support
%.js: %.ts
	tsc $<

%.css: %.scss
	sass $< $@

Phony Targets

Phony targets represent actions rather than files, ensuring they always execute.

Key Concepts

  • Phony Target - A target that doesn't represent a file
  • .PHONY - Special target declaring targets as phony
  • Always Runs - Phony targets execute even if a file with that name exists
  • Common Phonies - all, clean, install, test, help

Common Patterns

# Declare phony targets
.PHONY: all clean install test help

# Standard targets
all: build

clean:
	rm -rf build/ dist/

install: build
	cp -r build/* /usr/local/

test:
	pytest tests/

# Help target
help:
	@echo "Available targets:"
	@echo "  all      - Build the project"
	@echo "  clean    - Remove build artifacts"
	@echo "  install  - Install to system"
	@echo "  test     - Run tests"

# Group phony targets by category
.PHONY: build test lint format

# Build targets
build: compile link

# Quality targets
lint:
	flake8 src/

format:
	black src/

Examples

# Comprehensive phony targets
.PHONY: all build clean test lint format docs install deploy help

# Default target
all: build test lint

# Build targets
build: deps
	@echo "Building project..."
	go build -o bin/app ./cmd/app

deps:
	go mod download

# Clean targets
clean:
	rm -rf bin/ dist/ coverage/

clean-all: clean
	go clean -cache -testcache

# Test targets
test:
	go test -v ./...

test-coverage:
	go test -coverprofile=coverage.out ./...
	go tool cover -html=coverage.out -o coverage.html

# Quality targets
lint:
	golangci-lint run

format:
	gofmt -w .

# Documentation
docs:
	godoc -http=:6060

# Deployment
deploy: build test
	./scripts/deploy.sh

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

build: ## Build the application
test: ## Run tests
clean: ## Remove build artifacts

Conditional Execution

Conditionals allow different behaviour based on variable values or system state.

Key Concepts

  • ifdef/ifndef - Check if variable is defined
  • ifeq/ifneq - Compare values for equality
  • Nesting - Conditionals can be nested
  • Scope - Conditionals can wrap rules or variable assignments

Common Patterns

# Check if variable is defined
ifdef DEBUG
    CFLAGS += -g -DDEBUG
endif

ifndef PREFIX
    PREFIX = /usr/local
endif

# Compare values
ifeq ($(OS),Windows_NT)
    RM = del /Q
    EXE = .exe
else
    RM = rm -f
    EXE =
endif

# Compare with empty
ifeq ($(VERBOSE),)
    Q = @
else
    Q =
endif

# Nested conditionals
ifdef RELEASE
    ifeq ($(ARCH),arm64)
        CFLAGS += -march=armv8-a
    else
        CFLAGS += -march=native
    endif
    CFLAGS += -O3
endif

# String comparison (built-in CC has origin "default", not "undefined")
ifeq "$(origin CC)" "default"
    CC = gcc
endif

# Using $(filter)
ifneq ($(filter debug,$(MAKECMDGOALS)),)
    DEBUG = 1
endif

Examples

# Cross-platform Makefile
UNAME_S := $(shell uname -s)

ifeq ($(UNAME_S),Linux)
    PLATFORM := linux
    LDFLAGS += -lpthread
endif
ifeq ($(UNAME_S),Darwin)
    PLATFORM := macos
    LDFLAGS += -framework CoreFoundation
endif
ifeq ($(OS),Windows_NT)
    PLATFORM := windows
    EXE_EXT := .exe
endif

# Build configuration
BUILD_TYPE ?= release

ifeq ($(BUILD_TYPE),debug)
    CFLAGS += -g -O0 -DDEBUG
    BUILD_DIR := build/debug
else ifeq ($(BUILD_TYPE),release)
    CFLAGS += -O3 -DNDEBUG
    BUILD_DIR := build/release
else
    $(error Unknown BUILD_TYPE: $(BUILD_TYPE))
endif

# Feature flags
ifdef ENABLE_SSL
    CFLAGS += -DUSE_SSL
    LDFLAGS += -lssl -lcrypto
endif

ifdef SANITIZE
    CFLAGS += -fsanitize=address,undefined
    LDFLAGS += -fsanitize=address,undefined
endif

# Verbose mode
V ?= 0
ifeq ($(V),1)
    Q =
    ECHO = @true
else
    Q = @
    ECHO = @echo
endif

%.o: %.c
	$(ECHO) "CC $<"
	$(Q)$(CC) $(CFLAGS) -c $< -o $@

Common Use Cases

Build Automation

# C/C++ Project
CC := gcc
CXX := g++
CFLAGS := -Wall -Wextra -std=c11
CXXFLAGS := -Wall -Wextra -std=c++17

SRC_DIR := src
BUILD_DIR := build
TARGET := $(BUILD_DIR)/app

SRCS := $(wildcard $(SRC_DIR)/*.c $(SRC_DIR)/**/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)

.PHONY: all clean rebuild

all: $(TARGET)

$(TARGET): $(OBJS) | $(BUILD_DIR)
	$(CC) $(LDFLAGS) -o $@ $^

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

$(BUILD_DIR):
	mkdir -p $@

clean:
	rm -rf $(BUILD_DIR)

rebuild: clean all

-include $(DEPS)

Task Running

# Project task runner
.PHONY: dev prod test lint format db-migrate db-seed docker-build docker-run

# Development
dev:
	npm run dev

prod:
	npm run build
	npm run start

# Testing
test:
	npm test

test-watch:
	npm test -- --watch

test-coverage:
	npm test -- --coverage

# Code quality
lint:
	npm run lint

format:
	npm run format

lint-fix:
	npm run lint -- --fix

# Database
db-migrate:
	npx prisma migrate dev

db-seed:
	npx prisma db seed

db-reset:
	npx prisma migrate reset

# Docker
docker-build:
	docker build -t myapp:latest .

docker-run:
	docker run -p 3000:3000 myapp:latest

docker-push:
	docker push registry.example.com/myapp:latest

# Combined workflows
ci: lint test build
deploy: ci docker-build docker-push

Python Project

.PHONY: install dev test lint format docs clean

PYTHON := python3
VENV := .venv
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
BLACK := $(VENV)/bin/black
FLAKE8 := $(VENV)/bin/flake8
MYPY := $(VENV)/bin/mypy

# Setup
install: $(VENV)
	$(PIP) install -r requirements.txt

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

$(VENV):
	$(PYTHON) -m venv $(VENV)
	$(PIP) install --upgrade pip

# Testing
test:
	$(PYTEST) tests/ -v

test-coverage:
	$(PYTEST) tests/ --cov=src --cov-report=html

# Code quality
lint:
	$(FLAKE8) src/ tests/
	$(MYPY) src/

format:
	$(BLACK) src/ tests/

format-check:
	$(BLACK) --check src/ tests/

# Documentation
docs:
	cd docs && make html

# Cleanup
clean:
	rm -rf $(VENV) dist/ build/ *.egg-info
	find . -type d -name __pycache__ -exec rm -rf {} +
	find . -type f -name "*.pyc" -delete

Go Project

.PHONY: build run test lint clean docker

# Variables
BINARY := myapp
VERSION := $(shell git describe --tags --always --dirty)
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME)"

# Build
build:
	go build $(LDFLAGS) -o bin/$(BINARY) ./cmd/$(BINARY)

build-all:
	GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-linux-amd64 ./cmd/$(BINARY)
	GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-darwin-amd64 ./cmd/$(BINARY)
	GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-windows-amd64.exe ./cmd/$(BINARY)

# Run
run:
	go run ./cmd/$(BINARY)

# Test
test:
	go test -v ./...

test-coverage:
	go test -coverprofile=coverage.out ./...
	go tool cover -func=coverage.out

# Lint
lint:
	golangci-lint run

# Clean
clean:
	rm -rf bin/ coverage.out

# Docker
docker:
	docker build -t $(BINARY):$(VERSION) .

Debugging Makefiles

Techniques for troubleshooting and understanding Makefile behaviour.

Key Concepts

  • Dry Run - Show commands without executing
  • Debug Mode - Print detailed Make internals
  • Warning/Error - Print messages during parsing
  • Origin - Determine where a variable was defined

Common Commands

# Dry run - show commands without executing
make -n target
make --dry-run target

# Print database of rules and variables
make -p
make --print-data-base

# Debug mode
make -d target                    # Full debug output
make --debug=basic target         # Basic debugging
make --debug=verbose target       # Verbose debugging
make --debug=implicit target      # Show implicit rule search
make --debug=jobs target          # Show job processing
make --debug=all target           # All debugging

# Show why target is being rebuilt (GNU Make 4.4+)
make --debug=why target

# Print working directory
make -w target
make --print-directory target

# Trace execution
make --trace target

# Show Makefile being used
make -f Makefile.custom target

Debugging Patterns

# Print variable value
debug:
	@echo "CC = $(CC)"
	@echo "CFLAGS = $(CFLAGS)"
	@echo "SRCS = $(SRCS)"
	@echo "OBJS = $(OBJS)"

# Show variable origin
print-origin:
	@echo "CC origin: $(origin CC)"
	@echo "SHELL origin: $(origin SHELL)"

# Warning messages during parsing
$(warning Current directory: $(CURDIR))
$(warning Building: $(TARGET))

# Error with message (stops Make)
ifndef REQUIRED_VAR
    $(error REQUIRED_VAR is not set)
endif

# Info messages (less intrusive than warning)
$(info Compiling for $(PLATFORM))

# Debugging function calls
DEBUG_FUNC = $(warning Calling $(0) with args: $(1), $(2))
my_func = $(DEBUG_FUNC)$(1)-$(2)

# Verbose target information
VERBOSE ?= 0
ifeq ($(VERBOSE),1)
    define log
        @echo "$(1)"
    endef
else
    define log
    endef
endif

build:
	$(call log,Building target: $@)
	$(call log,Prerequisites: $^)
	$(CC) -o $@ $^

# Trace rule execution
TRACE ?= 0
ifeq ($(TRACE),1)
    OLD_SHELL := $(SHELL)
    SHELL = $(warning Running: $@)$(OLD_SHELL) -x
endif

Examples

# Comprehensive debugging Makefile
.PHONY: debug-vars debug-rules debug-env

# Print all project variables
debug-vars:
	@echo "=== Build Variables ==="
	@echo "CC        = $(CC)"
	@echo "CFLAGS    = $(CFLAGS)"
	@echo "LDFLAGS   = $(LDFLAGS)"
	@echo ""
	@echo "=== Paths ==="
	@echo "SRC_DIR   = $(SRC_DIR)"
	@echo "BUILD_DIR = $(BUILD_DIR)"
	@echo "SRCS      = $(SRCS)"
	@echo "OBJS      = $(OBJS)"
	@echo ""
	@echo "=== Configuration ==="
	@echo "DEBUG     = $(DEBUG)"
	@echo "PLATFORM  = $(PLATFORM)"
	@echo "VERSION   = $(VERSION)"

# Show Make's automatic variables for a target
debug-auto: main.o
	@echo "Target:         $@"
	@echo "First prereq:   $<"
	@echo "All prereqs:    $^"
	@echo "Newer prereqs:  $?"
	@echo "Stem:           $*"
	@echo "Dir of target:  $(@D)"
	@echo "File of target: $(@F)"

# Print environment
debug-env:
	@echo "=== Environment ==="
	@echo "SHELL     = $(SHELL)"
	@echo "MAKE      = $(MAKE)"
	@echo "MAKEFLAGS = $(MAKEFLAGS)"
	@echo "CURDIR    = $(CURDIR)"
	@echo "PATH      = $(PATH)"

# Check prerequisites
.PHONY: check-deps
check-deps:
	@echo "Checking dependencies..."
	@which $(CC) > /dev/null || (echo "ERROR: $(CC) not found" && exit 1)
	@which pkg-config > /dev/null || (echo "WARNING: pkg-config not found")
	@echo "All required dependencies found."

Task, Just, and Other Task Runners

Modern alternatives to Make for task running and build automation.

Just

Just is a command runner focused on simplicity and convenience for project-specific commands.

# justfile

# Set shell
set shell := ["bash", "-uc"]

# Variables
project := "myapp"
version := `git describe --tags`

# Default recipe
default: build

# Build the project
build:
    cargo build --release

# Run tests
test:
    cargo test

# Format code
fmt:
    cargo fmt

# Lint code
lint:
    cargo clippy

# Clean build artifacts
clean:
    cargo clean

# Recipe with arguments
greet name:
    echo "Hello, {{name}}!"

# Recipe with default argument
serve port="8080":
    python -m http.server {{port}}

# Private recipe (not shown in --list)
_setup:
    mkdir -p build

# Recipe dependencies
ci: fmt lint test build

# Conditional recipe
docker:
    #!/usr/bin/env bash
    if command -v docker &> /dev/null; then
        docker build -t {{project}}:{{version}} .
    else
        echo "Docker not installed"
        exit 1
    fi

# Multi-line recipe with different shell
python-script:
    #!/usr/bin/env python3
    import sys
    print(f"Python {sys.version}")

Task (go-task)

Task is a task runner written in Go, using YAML syntax.

# Taskfile.yml
version: '3'

vars:
  PROJECT: myapp
  VERSION:
    sh: git describe --tags

tasks:
  default:
    deps: [build]

  build:
    desc: Build the application
    cmds:
      - go build -o bin/{{.PROJECT}} ./cmd/{{.PROJECT}}
    sources:
      - ./**/*.go
    generates:
      - bin/{{.PROJECT}}

  test:
    desc: Run tests
    cmds:
      - go test -v ./...

  lint:
    desc: Run linter
    cmds:
      - golangci-lint run

  clean:
    desc: Clean build artifacts
    cmds:
      - rm -rf bin/

  docker:
    desc: Build Docker image
    cmds:
      - docker build -t {{.PROJECT}}:{{.VERSION}} .

  ci:
    desc: Run CI pipeline
    deps: [lint, test, build]

  serve:
    desc: Run development server
    cmds:
      - go run ./cmd/{{.PROJECT}}
    env:
      DEBUG: "true"

  db:migrate:
    desc: Run database migrations
    cmds:
      - migrate -path migrations -database $DATABASE_URL up

  # Task with parameters
  greet:
    desc: Greet someone
    cmds:
      - echo "Hello, {{.CLI_ARGS}}!"

Comparison Table

Feature Make Just Task
Syntax Custom Custom YAML
File-based deps Yes No Yes
Cross-platform Yes Yes Yes
Variables Yes Yes Yes
Parameters Limited Yes Yes
Private recipes No Yes Yes
Shell selection Limited Yes Yes
Tab requirement Yes No No

When to Use Each

YesNoYesNoYesNoYesNoNeed Task Runner?File Dependencies?Complex Build?Team Familiar withYAML?Use MakeNeed Parameters?Use TaskUse JustUse Just or TaskYesNoYesNoYesNoYesNoNeed Task Runner?File Dependencies?Complex Build?Team Familiar withYAML?Use MakeNeed Parameters?Use TaskUse JustUse Just or Task

Migration from Make to Just

# Makefile
.PHONY: build test clean

CC = gcc
CFLAGS = -Wall

build:
	$(CC) $(CFLAGS) -o app main.c

test:
	./test.sh

clean:
	rm -f app
# Equivalent justfile

cc := "gcc"
cflags := "-Wall"

build:
    {{cc}} {{cflags}} -o app main.c

test:
    ./test.sh

clean:
    rm -f app

Migration from Make to Task

# Equivalent Taskfile.yml
version: '3'

vars:
  CC: gcc
  CFLAGS: -Wall

tasks:
  build:
    cmds:
      - "{{.CC}} {{.CFLAGS}} -o app main.c"

  test:
    cmds:
      - ./test.sh

  clean:
    cmds:
      - rm -f app

Quick Reference

Common Targets

Target Purpose
all Default target, typically builds everything
build Compile/build the project
test Run tests
clean Remove build artifacts
install Install to system
uninstall Remove from system
dist Create distribution package
help Show available targets
lint Run linters
format Format code

Automatic Variables

Variable Description
$@ Target filename
$< First prerequisite
$^ All prerequisites
$? Prerequisites newer than target
$* Stem of pattern match
$(@D) Directory of target
$(@F) Filename of target

Common Functions

Function Example Result
$(wildcard *.c) Find files main.c utils.c
$(patsubst %.c,%.o,$(SRCS)) Pattern replace main.o utils.o
$(filter %.c,$(FILES)) Keep matching Only .c files
$(filter-out %.h,$(FILES)) Remove matching Without .h files
$(shell cmd) Run shell Command output
$(dir path) Extract directory src/
$(notdir path) Extract filename file.c
$(basename file.c) Remove suffix file
$(suffix file.c) Get suffix .c
$(if cond,then,else) Conditional Result based on condition

Command-Line Options

Option Description
-n / --dry-run Print commands without executing
-f FILE Use FILE as Makefile
-j N Run N jobs in parallel
-k Continue after errors
-B Unconditionally make all targets
-d Print debugging information
-p Print database of rules
-w Print working directory
--trace Trace target execution

Common Issues and Solutions

Issue Cause Solution
missing separator Recipe not indented with tab Use tab (not spaces) for recipes
No rule to make target Missing prerequisite file Check file paths and names
Nothing to be done Target is up to date Use -B to force rebuild
Circular dependency A depends on B which depends on A Restructure dependencies
Recipe not executing Target file already exists Add to .PHONY if it's not a file
Variables not expanding Using $ instead of $$ Use $$ for shell variables
Command not found PATH not set correctly Export PATH or use full path
Parallel build fails Missing dependencies Add missing prerequisites
Wrong shell used Default shell is /bin/sh Set SHELL := /bin/bash
Pattern rule not matching Pattern doesn't match file names Check % placement and paths

Debugging Tips

# Check why target is being rebuilt (GNU Make 4.4+)
make --debug=why target

# See what Make thinks it needs to do
make -n target

# Print all variables and rules
make -p | less

# Run with maximum verbosity
make -d target 2>&1 | less

# Check variable value
make -p | grep "^VAR_NAME"

# Trace with timestamps
time make target

Recipe Best Practices

# Suppress command echo
clean:
	@rm -rf build/

# Continue on error
clean:
	-rm -f *.o

# Use bash features
SHELL := /bin/bash
target:
	[[ -d build ]] || mkdir build

# Multi-line commands
target:
	for f in *.c; do \
		echo "Processing $$f"; \
		$(CC) -c "$$f"; \
	done

Related Topics

The following topics would complement this Makefiles cheatsheet:

  1. Bash - Shell scripting fundamentals for writing Make recipes
  2. Docker - Container builds often use Makefiles for workflow automation
  3. CI/CD Patterns - Integrating Make targets into continuous integration pipelines
  4. CMake - More advanced build system generator for cross-platform C/C++ projects
  5. Go - Go projects commonly use Makefiles for build automation
  6. Python - Python project automation with Make for testing, linting, and packaging