Common Makefile Mistakes
A comprehensive guide to common mistakes in Makefiles, their consequences, and how to fix them.
Overview
Common Makefile Mistakes
A comprehensive guide to common mistakes in Makefiles, their consequences, and how to fix them.
Table of Contents
- Critical Missing Declarations
- Syntax Errors
- Indentation Issues
- Target and Dependency Problems
- Variable Issues
- Security Vulnerabilities
- Performance Problems
- Portability Issues
- Build Logic Errors
Critical Missing Declarations
0. Missing .DELETE_ON_ERROR
Problem: Not declaring .DELETE_ON_ERROR (most common critical mistake)
# WRONG: Missing .DELETE_ON_ERROR
.PHONY: all clean
all: app.bin
app.bin: app.c
$(CC) -o $@ $<
# If compilation fails partway through, a partial/corrupt app.bin may exist
# Next "make" sees the file and thinks target is up-to-date!
Solution: Always add .DELETE_ON_ERROR: at the top
# CORRECT: Always include .DELETE_ON_ERROR
.DELETE_ON_ERROR:
.PHONY: all clean
all: app.bin
app.bin: app.c
$(CC) -o $@ $<
# Now if build fails, the partial file is deleted
# Next "make" will properly rebuild
Impact:
- Corrupt/partial files left behind after failed builds
- Subsequent builds silently use corrupt files
- Very difficult to debug ("it worked yesterday!")
GNU Make Manual Quote: "This is almost always what you want make to do, but it is not historical practice; so for compatibility, you must explicitly request it."
0b. Not Clearing .SUFFIXES
Problem: Built-in suffix rules slow down large projects
# Slow: Make checks ~90 built-in suffix rules
%.o: %.c
$(CC) -c $< -o $@
Solution: Clear .SUFFIXES for faster builds
# Fast: Disable built-in suffix rules
.SUFFIXES:
%.o: %.c
$(CC) -c $< -o $@
Impact: Up to 40% faster rule resolution on large projects
Syntax Errors
1. Spaces Instead of Tabs
Problem: Using spaces for recipe indentation
# WRONG: Spaces (will fail!)
build:
echo "Building..." # 4 spaces
go build -o app # 4 spaces
# Error: Makefile:2: *** missing separator. Stop.
Solution: Use TAB characters
# CORRECT: Tab characters
build:
echo "Building..." # TAB
go build -o app # TAB
Impact: Build fails immediately with confusing error message
Detection: mbake automatically detects and fixes this issue
2. Missing Colon After Target
Problem: Forgetting colon in target definition
# WRONG
build $(SOURCES)
$(CC) -o app $^
# Error: Makefile:1: *** missing separator. Stop.
Solution: Always include colon
# CORRECT
build: $(SOURCES)
$(CC) -o app $^
3. Incorrect Line Continuation
Problem: Missing backslash or space after backslash
# WRONG: Missing backslash
SOURCES = main.c
utils.c
config.c
# WRONG: Space after backslash
SOURCES = main.c \\
utils.c \\
config.c
# Error: Unexpected token or incorrect variable value
Solution: Proper line continuation
# CORRECT
SOURCES = main.c \\
utils.c \\
config.c
# Or use wildcards
SOURCES := $(wildcard src/*.c)
4. Mismatched Quotes
Problem: Unmatched or incorrect quotes
# WRONG
message:
echo "Building project $(PROJECT)'
# Error: Syntax error or unexpected behavior
Solution: Match quotes properly
# CORRECT
message:
echo "Building project $(PROJECT)"
# Or use single quotes
message:
echo 'Building project $(PROJECT)'
Indentation Issues
5. Mixed Tabs and Spaces
Problem: Mixing tabs and spaces in recipes
# WRONG: First line has tab, second has spaces
build:
@echo "Starting..."
go build -o app # Spaces!
# Error: Makefile:3: *** missing separator. Stop.
Solution: Use tabs consistently
# CORRECT: All tabs
build:
@echo "Starting..."
go build -o app
Editor Configuration:
" Vim: .vimrc
autocmd FileType make setlocal noexpandtab
# VS Code: settings.json
"[makefile]": {
"editor.insertSpaces": false,
"editor.detectIndentation": false
}
6. Tab Width Confusion
Problem: Assuming tab width instead of using actual tabs
# WRONG: Looks like tab but is 8 spaces
build:
echo "Building..." # 8 spaces, not a tab!
Solution: Configure editor to show whitespace and use real tabs
# CORRECT: Actual tab character
build:
echo "Building..." # TAB (shows as single character)
Target and Dependency Problems
7. Missing .PHONY Declarations
Problem: Not declaring non-file targets as phony
# WRONG: Missing .PHONY
clean:
rm -rf build
test:
go test ./...
# If files named 'clean' or 'test' exist, targets won't run!
# $ touch clean # Create a file named 'clean'
# $ make clean
# make: 'clean' is up to date.
Solution: Always declare non-file targets
# CORRECT: Declare .PHONY targets
.PHONY: clean test all install
clean:
rm -rf build
test:
go test ./...
Impact:
- 35%+ of developers face issues due to missing .PHONY
- Targets may not run if files with same names exist
- Performance degradation (implicit rule search)
8. Incorrect Dependency Specification
Problem: Missing or incomplete dependencies
# WRONG: Missing header dependencies
app: main.o utils.o
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $<
# If headers change, .o files won't rebuild!
Solution: Include all dependencies
# CORRECT: Include header dependencies
app: main.o utils.o
$(CC) -o $@ $^
main.o: main.c main.h common.h
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c utils.h common.h
$(CC) $(CFLAGS) -c utils.c
# BETTER: Auto-generate dependencies
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
%.o: %.c
$(CC) $(DEPFLAGS) $(CFLAGS) -c $<
-include $(DEPS)
Impact: Over 60% reduction in unnecessary recompilation with proper dependencies
9. Circular Dependencies
Problem: Targets depending on each other
# WRONG: Circular dependency
A: B
@echo "Target A"
B: A
@echo "Target B"
# Error: Makefile:1: *** Circular A <- B dependency dropped.
Solution: Break the cycle
# CORRECT: Proper dependency chain
A: B
@echo "Target A depends on B"
B: C
@echo "Target B depends on C"
C:
@echo "Target C has no dependencies"
10. Phony Target as Prerequisite of Real Target
Problem: Using phony target as dependency of file target
# WRONG: Phony prerequisite causes always-rebuild
.PHONY: generate
app.o: app.c generate
$(CC) -c app.c -o app.o
generate:
./gen-config.sh
# app.o rebuilds EVERY time because 'generate' is always out of date
Solution: Use real file dependencies
# CORRECT: Depend on actual generated file
app.o: app.c config.h
$(CC) -c app.c -o app.o
config.h:
./gen-config.sh
Variable Issues
11. Using = Instead of :=
Problem: Recursive expansion causing performance issues
# WRONG: Recursive expansion (re-evaluated every time)
BUILD_TIME = $(shell date +%Y%m%d-%H%M%S)
GIT_HASH = $(shell git rev-parse HEAD)
target1:
echo $(BUILD_TIME) # Shell called here
target2:
echo $(BUILD_TIME) # Shell called AGAIN with different time!
echo $(GIT_HASH) # Shell called here
target3:
echo $(GIT_HASH) # Shell called AGAIN!
Solution: Use := for immediate expansion
# CORRECT: Immediate expansion (evaluated once)
BUILD_TIME := $(shell date +%Y%m%d-%H%M%S)
GIT_HASH := $(shell git rev-parse HEAD)
target1:
echo $(BUILD_TIME) # Uses cached value
target2:
echo $(BUILD_TIME) # Same cached value
echo $(GIT_HASH) # Cached value
target3:
echo $(GIT_HASH) # Same cached value
Impact: Can cause significant slowdown and inconsistent builds
12. Undefined Variables
Problem: Using variables without defaults
# WRONG: No default value
install:
cp app $(PREFIX)/bin/
# If PREFIX is not set, installs to /bin/ (wrong!) or fails
Solution: Always provide defaults
# CORRECT: Provide sensible defaults
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
install:
mkdir -p $(DESTDIR)$(BINDIR)
cp app $(DESTDIR)$(BINDIR)/
13. Incorrect Variable Expansion
Problem: Using wrong expansion syntax
# WRONG: Shell variable vs Make variable confusion
build:
for file in *.c; do \\
echo "Compiling $file"; \\
$(CC) -c $file; \\
done
# $file expands as Make variable (empty!), not shell variable
# Output: Compiling (nothing)
Solution: Escape shell variables
# CORRECT: Escape $ for shell variables
build:
for file in *.c; do \\
echo "Compiling $$file"; \\
$(CC) -c $$file; \\
done
# Output: Compiling main.c, Compiling utils.c, etc.
14. Variable Naming Conflicts
Problem: Overriding special Make variables
# WRONG: Overriding built-in variable
MAKEFLAGS = -j4 # This overrides Make's internal flags!
# AVOID: Using reserved names
MAKE = my-build-tool # Breaks recursive make
Solution: Use unique names
# CORRECT: Use custom names for your variables
BUILD_FLAGS := -j4
MY_BUILD_TOOL := custom-builder
build:
$(MAKE) -f sub.mk $(BUILD_FLAGS)
Security Vulnerabilities
15. Hardcoded Credentials
Problem: Secrets in Makefile
# WRONG: Hardcoded secrets
API_KEY = sk-1234567890abcdef
DB_PASSWORD = super_secret_123
deploy:
curl -H "Authorization: Bearer $(API_KEY)" https://api.example.com/
psql -U admin -p $(DB_PASSWORD) -c "SELECT version();"
Solution: Use environment variables
# CORRECT: Load from environment
deploy:
@if [ -z "$$API_KEY" ]; then \\
echo "Error: API_KEY not set"; \\
exit 1; \\
fi
curl -H "Authorization: Bearer $$API_KEY" https://api.example.com/
# Or use a .env file (not committed)
include .env
**Impact**: Credentials exposed in version control, logs, and process listings
### 16. Unsafe Variable Expansion
**Problem**: Unvalidated variables in dangerous commands
```makefile
# WRONG: Unsafe rm command
BUILD_DIR = $(USER_INPUT)
clean:
rm -rf $(BUILD_DIR)/*
# If BUILD_DIR is empty or "/", this is catastrophic!
# $ make clean BUILD_DIR=/
# rm -rf /* # Disaster!
Solution: Validate before dangerous operations
# CORRECT: Validate variables
BUILD_DIR := build # Default value
clean:
@if [ -z "$(BUILD_DIR)" ] || [ "$(BUILD_DIR)" = "/" ]; then \\
echo "Error: Invalid BUILD_DIR=$(BUILD_DIR)"; \\
exit 1; \\
fi
@if [ -d "$(BUILD_DIR)" ]; then \\
rm -rf $(BUILD_DIR)/*; \\
fi
17. Command Injection
Problem: Unsanitized input in shell commands
# WRONG: User input directly in command
deploy:
ssh user@$(SERVER) "cd /app && git pull origin $(BRANCH)"
# Malicious input: BRANCH="; rm -rf /"
# Executes: git pull origin ; rm -rf /
Solution: Validate and quote input
# CORRECT: Validate input
ALLOWED_BRANCHES := main develop staging
BRANCH ?= main
deploy:
@if ! echo "$(ALLOWED_BRANCHES)" | grep -wq "$(BRANCH)"; then \\
echo "Error: Invalid branch $(BRANCH)"; \\
exit 1; \\
fi
ssh user@$(SERVER) "cd /app && git pull origin '$(BRANCH)'"
18. Logging Sensitive Information
Problem: Echoing secrets in build output
# WRONG: Secrets visible in logs
deploy:
echo "Deploying with token: $(API_TOKEN)"
curl -H "Authorization: Bearer $(API_TOKEN)" https://api.example.com/
Solution: Suppress sensitive output
# CORRECT: Hide sensitive information
deploy:
@echo "Deploying to production..."
@curl -s -H "Authorization: Bearer $$API_TOKEN" https://api.example.com/
@echo "Deployment complete"
# Or mask partial value
@echo "Using token: $${API_TOKEN:0:8}..."
Performance Problems
19. Inefficient Wildcards
Problem: Repeated wildcard evaluation
# WRONG: wildcard called every time
build:
$(CC) -o app $(wildcard src/*.c)
test:
for file in $(wildcard tests/*.sh); do bash $$file; done
# wildcard searches filesystem every time these targets run
Solution: Evaluate once with :=
# CORRECT: Evaluate wildcard once
SOURCES := $(wildcard src/*.c)
TESTS := $(wildcard tests/*.sh)
build:
$(CC) -o app $(SOURCES)
test:
for file in $(TESTS); do bash $$file; done
Impact: Significant speedup for large projects (40%+ in some cases)
20. Missing Incremental Build Support
Problem: Always rebuilding everything
# WRONG: No incremental build
build:
rm -rf build
mkdir -p build
$(CC) -o build/app $(SOURCES)
# Rebuilds from scratch every time!
Solution: Proper dependency tracking
# CORRECT: Incremental build
OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES))
build: build/app
build/app: $(OBJECTS)
$(CC) -o $@ $^
build/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
# Only rebuilds changed files
Impact: Can reduce build times by up to 60% with proper dependencies
21. Not Using Pattern Rules
Problem: Duplicated rules for similar targets
# WRONG: Repetitive rules
main.o: main.c
$(CC) $(CFLAGS) -c main.c -o main.o
utils.o: utils.c
$(CC) $(CFLAGS) -c utils.c -o utils.o
config.o: config.c
$(CC) $(CFLAGS) -c config.c -o config.o
# Lots of duplication!
Solution: Use pattern rules
# CORRECT: Single pattern rule
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Or with directories
build/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
Portability Issues
22. Assuming GNU Make
Problem: Using GNU Make-specific features
# WRONG: GNU Make specific
SOURCES := $(shell find src -name '*.c')
build: $(SOURCES:.c=.o)
$(CC) -o app $^
# Fails with BSD make or other Make implementations
Solution: Use portable constructs
# CORRECT: More portable (though still uses shell)
SOURCES != find src -name '*.c' || find src -name '*.c'
# Or manually list sources for maximum portability
SOURCES = src/main.c src/utils.c src/config.c
23. Hard-Coded Tools
Problem: Assuming specific tool paths
# WRONG: Hard-coded tool paths
CC = /usr/bin/gcc
PYTHON = /usr/bin/python3
build:
$(CC) -o app $(SOURCES)
Solution: Use which or allow override
# CORRECT: Allow override with defaults
CC ?= gcc
PYTHON ?= python3
INSTALL ?= install
# Or detect at runtime
CC := $(shell command -v gcc || command -v clang)
24. Platform-Specific Commands
Problem: Using OS-specific commands
# WRONG: Linux-specific
clean:
rm -rf build
copy:
cp -r src/* dest/
# Fails on Windows
Solution: Detect platform or use portable commands
`