Comprehensive toolkit for validating, linting, and optimizing Makefiles. Use this skill when working with Makefiles (Makefile, makefile, *.mk files), validating build configurations, checking for best practices, identifying security issues, or debugging Makefile problems.
Overall
score
100%
Does it follow best practices?
Validation for skill structure
A comprehensive guide to common mistakes in Makefiles, their consequences, and how to fix them.
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 rebuildImpact:
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."
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
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 # TABImpact: Build fails immediately with confusing error message
Detection: mbake automatically detects and fixes this issue
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 $^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 valueSolution: Proper line continuation
# CORRECT
SOURCES = main.c \
utils.c \
config.c
# Or use wildcards
SOURCES := $(wildcard src/*.c)Problem: Unmatched or incorrect quotes
# WRONG
message:
echo "Building project $(PROJECT)'
# Error: Syntax error or unexpected behaviorSolution: Match quotes properly
# CORRECT
message:
echo "Building project $(PROJECT)"
# Or use single quotes
message:
echo 'Building project $(PROJECT)'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 appEditor Configuration:
" Vim: .vimrc
autocmd FileType make setlocal noexpandtab
# VS Code: settings.json
"[makefile]": {
"editor.insertSpaces": false,
"editor.detectIndentation": false
}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)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:
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
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"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 dateSolution: 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.shProblem: 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 valueImpact: Can cause significant slowdown and inconsistent builds
Problem: Using variables without defaults
# WRONG: No default value
install:
cp app $(PREFIX)/bin/
# If PREFIX is not set, installs to /bin/ (wrong!) or failsSolution: Always provide defaults
# CORRECT: Provide sensible defaults
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
install:
mkdir -p $(DESTDIR)$(BINDIR)
cp app $(DESTDIR)$(BINDIR)/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.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 makeSolution: 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)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
exportImpact: Credentials exposed in version control, logs, and process listings
Problem: Unvalidated variables in dangerous commands
# 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)/*; \
fiProblem: 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)'"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}..."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 runSolution: 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; doneImpact: Significant speedup for large projects (40%+ in some cases)
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 filesImpact: Can reduce build times by up to 60% with proper dependencies
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 $@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 implementationsSolution: 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.cProblem: 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)Problem: Using OS-specific commands
# WRONG: Linux-specific
clean:
rm -rf build
copy:
cp -r src/* dest/
# Fails on WindowsSolution: Detect platform or use portable commands
# CORRECT: Platform detection
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
ifeq ($(UNAME_S),Windows)
RM := del /Q /S
MKDIR := mkdir
else
RM := rm -rf
MKDIR := mkdir -p
endif
clean:
$(RM) build
# Or use Go/Python for cross-platform scripts
clean:
@go run scripts/clean.goProblem: Not checking command exit codes
# WRONG: Ignoring failures
test:
go test ./pkg1
go test ./pkg2
go test ./pkg3
@echo "All tests passed!"
# If pkg1 fails, Make continues to pkg2, pkg3, and prints "passed"Solution: Use set -e or check exit codes
# CORRECT: Stop on first failure
test:
@set -e; \
go test ./pkg1; \
go test ./pkg2; \
go test ./pkg3; \
echo "All tests passed!"
# Or check explicitly
test:
@go test ./pkg1 || exit 1
@go test ./pkg2 || exit 1
@go test ./pkg3 || exit 1
@echo "All tests passed!"Problem: Unsafe parallel execution
# WRONG: Race condition with parallel builds
all: build-frontend build-backend
build-frontend:
npm install # Both may write to node_modules!
npm run build
build-backend:
npm install # Race condition!
go build
# With make -j2, both run npm install simultaneouslySolution: Use order dependencies or .NOTPARALLEL
# CORRECT: Sequential dependencies
all: build-frontend build-backend
build-frontend: node_modules
npm run build
build-backend: node_modules
go build
node_modules: package.json
npm install
@touch node_modules # Update timestamp
# Or use .NOTPARALLEL for specific target
.NOTPARALLEL: installProblem: Relying on target order without dependencies
# WRONG: Assuming build is run before test
all: build test deploy
build:
go build -o app
test:
./scripts/test.sh # Assumes app exists!
deploy:
./scripts/deploy.sh # Assumes tests passed!
# Direct "make test" or "make deploy" fails!Solution: Explicit dependencies
# CORRECT: Explicit dependencies
all: deploy
build:
go build -o app
test: build
./scripts/test.sh
deploy: test
./scripts/deploy.sh
# Now "make deploy" automatically runs build → test → deployWhen you encounter Makefile issues, check:
According to research from build system studies (2024-2025):
Install with Tessl CLI
npx tessl i pantheon-ai/makefile-validator@0.1.1