Complete makefile toolkit with generation and validation capabilities
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Comprehensive guide to writing professional, maintainable, and efficient Makefiles.
For modern, robust Makefiles, start with this recommended preamble from Jacob Davis-Hansson:
# Modern Makefile Header
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rulesExplanation:
| Setting | Purpose |
|---|---|
SHELL := bash | Use bash instead of /bin/sh for modern shell features |
.ONESHELL: | Run entire recipe in single shell (enables multi-line scripts) |
.SHELLFLAGS := -eu -o pipefail -c | Stop on errors (-e), undefined vars (-u), pipe failures |
.DELETE_ON_ERROR: | Delete target on recipe failure (prevents corrupt builds) |
--warn-undefined-variables | Alert on undefined Make variable references |
--no-builtin-rules | Disable built-in implicit rules for faster builds |
Note: This preamble is for GNU Make 4.0+. For maximum portability, use a simpler header.
GNU Make provides several special targets that should be used in professional Makefiles.
Always include .DELETE_ON_ERROR: at the top of your Makefile. This ensures partially built targets are deleted when a recipe fails, preventing corrupt builds.
# CRITICAL: Delete target on recipe failure
.DELETE_ON_ERROR:
# Rest of Makefile follows...Why it matters:
make run sees the file exists and skips rebuildingFrom GNU Make Manual: "This is almost always what you want make to do, but it is not historical practice; so for compatibility, you must explicitly request it."
Exception: Use .PRECIOUS to protect specific targets that should be preserved even on error:
.DELETE_ON_ERROR:
.PRECIOUS: expensive-to-rebuild.datDeclare non-file targets as phony to avoid conflicts and improve performance:
.PHONY: all build clean test installRun entire recipe in a single shell invocation:
.ONESHELL:
deploy:
set -e
echo "Deploying..."
cd /app
git pull
./restart.shWithout .ONESHELL, each line runs in a separate shell, so cd has no effect on subsequent lines.
Clear built-in suffix rules to speed up builds:
# Disable all built-in suffix rules
.SUFFIXES:
# Only keep rules you need (optional)
.SUFFIXES: .c .oWhy: GNU Make has ~90 built-in implicit rules. Clearing them speeds up rule resolution.
# Modern Makefile Header
.DELETE_ON_ERROR:
.SUFFIXES:
.PHONY: all build clean test install deploy
# Your targets follow...# Organized Makefile structure
.PHONY: all clean test install
# Variables section
PROJECT := myapp
VERSION := 1.0.0
BUILD_DIR := build
SRC_DIR := src
# Include external makefiles
include config.mk
include rules/*.mk
# Default target (should be first)
all: build test
# Build targets
build: $(BUILD_DIR)/$(PROJECT)
# ... more targetsUse include for large projects:
# Main Makefile
include config/variables.mk
include rules/build.mk
include rules/test.mk
include rules/deploy.mk
.PHONY: all
all: build testUse / as delimiter for namespaced targets:
# Good: Namespaced targets
.PHONY: docker/build docker/push docker/clean
docker/build:
docker build -t $(IMAGE) .
docker/push:
docker push $(IMAGE)
docker/clean:
docker rmi $(IMAGE)
# Avoid: Flat namespace
.PHONY: docker-build docker-push docker-cleanDeclare targets that don't create files as phony:
# GOOD: Proper .PHONY declarations
.PHONY: all clean test install build deploy
all: build test
clean:
rm -rf $(BUILD_DIR)
test:
go test ./...
# BAD: Missing .PHONY - causes issues if files named 'clean' or 'test' exist
clean:
rm -rf build
test:
go test ./...# Group related phony targets
.PHONY: all build clean
.PHONY: test test-unit test-integration
.PHONY: install uninstall
.PHONY: docker/build docker/push docker/clean
# Or use a single declaration (mbake can organize this)
.PHONY: all build clean test test-unit test-integration install uninstallFirst target is the default (or use .DEFAULT_GOAL):
# Method 1: First target is default
.PHONY: all
all: build test
# Method 2: Explicit default goal
.DEFAULT_GOAL := build
.PHONY: build test
build:
go build -o app
test:
go test ./...Choose the right operator for your use case:
# Simple assignment (=) - Recursive expansion (evaluated when used)
CFLAGS = -Wall $(OPTIMIZE)
OPTIMIZE = -O2
# CFLAGS will expand to: -Wall -O2 (recursive)
# Immediate assignment (:=) - Expanded immediately (RECOMMENDED for most cases)
BUILD_TIME := $(shell date +%Y%m%d-%H%M%S)
VERSION := 1.0.0
# Evaluated once, avoids repeated shell calls
# Conditional assignment (?=) - Set only if not already defined
CC ?= gcc
PREFIX ?= /usr/local
# Allows environment variable override
# Append (+=) - Add to existing value
CFLAGS := -Wall
CFLAGS += -Wextra
CFLAGS += -O2
# CFLAGS = -Wall -Wextra -O2# GOOD: Immediate expansion (predictable, faster)
BUILD_DIR := build
SRC_FILES := $(wildcard src/*.c)
TIMESTAMP := $(shell date +%s)
# AVOID: Recursive expansion (unpredictable, slower)
BUILD_DIR = build
SRC_FILES = $(wildcard src/*.c) # Re-evaluated every time!
TIMESTAMP = $(shell date +%s) # Shell called multiple times!# Allow user/environment override
CC ?= gcc
CXX ?= g++
PREFIX ?= /usr/local
DESTDIR ?=
VERBOSE ?= 0
# Usage:
# make # Uses defaults
# make CC=clang # Override CC
# PREFIX=/opt make # Override via environment# GOOD: Clear, consistent naming
PROJECT_NAME := myapp
BUILD_DIR := build
SOURCE_FILES := $(wildcard src/*.c)
COMPILER_FLAGS := -Wall -Wextra -O2
# AVOID: Unclear abbreviations
PROJ := myapp
BDIR := build
SRCS := $(wildcard src/*.c)
FLAGS := -Wall# GOOD: Tab character (required)
build:
@echo "Building..."
go build -o app
# BAD: Spaces (will fail)
build:
@echo "Building..."
go build -o appNote: Makefiles require TAB characters for recipes. Configure your editor to use tabs for Makefiles.
# Method 1: Prefix with @ to suppress echo, - to ignore errors
clean:
@echo "Cleaning build artifacts..."
-rm -rf $(BUILD_DIR)
@echo "Done!"
# Method 2: Use || for conditional error handling
build:
mkdir -p $(BUILD_DIR) || exit 1
go build -o $(BUILD_DIR)/app || exit 1
# Method 3: Use set -e for strict error handling
test:
@set -e; \
echo "Running tests..."; \
go test ./...; \
echo "All tests passed!"
# Method 4: Check exit codes explicitly
deploy:
@./scripts/deploy.sh
@if [ $$? -ne 0 ]; then \
echo "Deployment failed!"; \
exit 1; \
fi# Use backslash for line continuation
build: $(SOURCES)
@echo "Building $(PROJECT)..."; \
mkdir -p $(BUILD_DIR); \
$(CC) $(CFLAGS) -o $(BUILD_DIR)/$(PROJECT) $(SOURCES); \
echo "Build complete!"
# Or use .ONESHELL for easier multi-line scripts
.ONESHELL:
test:
echo "Running tests..."
for file in tests/*.sh; do
bash $$file
done
echo "All tests passed!"# Use @ to suppress command echo
.PHONY: build
build:
@echo "Building..."
@$(CC) $(CFLAGS) -o app $(SOURCES)
# Optional verbose mode
VERBOSE ?= 0
ifeq ($(VERBOSE),1)
Q :=
else
Q := @
endif
build:
$(Q)echo "Building..."
$(Q)$(CC) $(CFLAGS) -o app $(SOURCES)
# Usage:
# make build # Silent
# make build VERBOSE=1 # Verbose# GOOD: Proper dependency chain
app: $(OBJECTS)
$(CC) -o $@ $^
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
# BAD: Missing dependencies - app won't rebuild when headers change
app: $(OBJECTS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@# Automatic dependency generation
DEPDIR := .deps
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
%.o: %.c $(DEPDIR)/%.d | $(DEPDIR)
$(CC) $(DEPFLAGS) $(CFLAGS) -c $< -o $@
$(DEPDIR):
@mkdir -p $@
# Include generated dependency files
-include $(patsubst %,$(DEPDIR)/%.d,$(basename $(SOURCES)))Use | for prerequisites that shouldn't trigger rebuilds:
# Regular prerequisites trigger rebuild
$(BUILD_DIR)/app: $(SOURCES)
$(CC) -o $@ $^
# Order-only prerequisites (directories) don't trigger rebuild
$(BUILD_DIR)/app: $(SOURCES) | $(BUILD_DIR)
$(CC) -o $@ $^
$(BUILD_DIR):
mkdir -p $@
# Without |, updating BUILD_DIR timestamp would trigger app rebuild
# With |, app only rebuilds when SOURCES change# Search for prerequisites in multiple directories
VPATH = src:include:tests
# Or use vpath for specific patterns
vpath %.c src
vpath %.h include
vpath %.test tests
# Now Make will find files in these directories
app: main.o utils.o
$(CC) -o $@ $^
# Make will find src/main.c and src/utils.c automatically# GOOD: Phony targets skip implicit rule search
.PHONY: clean test install
clean:
rm -rf $(BUILD_DIR)
# BAD: Without .PHONY, Make checks for file existence
clean:
rm -rf $(BUILD_DIR)# Enable parallel builds (use -j flag)
# make -j8 build # 8 parallel jobs
# For sequential targets, use .NOTPARALLEL
.NOTPARALLEL: deploy
deploy: build test
./scripts/deploy.sh
# Or use order-only prerequisites for partial ordering
build-frontend: | build-backend
npm run build# Mark intermediate files for auto-deletion
.INTERMEDIATE: $(OBJECTS)
# Or mark files to keep through one build
.SECONDARY: $(OBJECTS)
# Delete on error (recommended)
.DELETE_ON_ERROR:
# Example: .o files cleaned after linking
app: main.o utils.o
$(CC) -o $@ $^
# main.o and utils.o auto-deleted after successful build# BAD: Shell called every time variable is used
DATE = $(shell date +%Y%m%d)
VERSION = $(shell git describe --tags)
target1:
echo $(DATE) # Shell called here
target2:
echo $(DATE) # Shell called again!
# GOOD: Use := for one-time evaluation
DATE := $(shell date +%Y%m%d)
VERSION := $(shell git describe --tags)
target1:
echo $(DATE) # Expands to cached value
target2:
echo $(DATE) # Same cached value# GOOD: POSIX-compatible commands
.PHONY: install
install:
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp -f app $(DESTDIR)$(PREFIX)/bin/
chmod 755 $(DESTDIR)$(PREFIX)/bin/app
# AVOID: Bashisms or GNU-specific features
install:
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp app $(DESTDIR)$(PREFIX)/bin/ # Missing -f for portability# Detect operating system
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
PLATFORM := linux
EXE_EXT :=
endif
ifeq ($(UNAME_S),Darwin)
PLATFORM := macos
EXE_EXT :=
endif
ifeq ($(OS),Windows_NT)
PLATFORM := windows
EXE_EXT := .exe
endif
# Use platform-specific settings
APP := app$(EXE_EXT)# BAD: Hard-coded paths
install:
cp app /usr/local/bin/
cp docs/app.1 /usr/share/man/man1/
# GOOD: Use variables for paths
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man
install:
install -d $(DESTDIR)$(BINDIR)
install -m 755 app $(DESTDIR)$(BINDIR)/
install -d $(DESTDIR)$(MANDIR)/man1
install -m 644 docs/app.1 $(DESTDIR)$(MANDIR)/man1/# Project: MyApp
# Description: Build system for MyApp project
# Author: Your Name
# Version: 1.0.0
# Configuration variables
PROJECT := myapp
VERSION := $(shell git describe --tags 2>/dev/null || echo "dev")
# Build directories
BUILD_DIR := build
SRC_DIR := src
# Compiler settings
CC := gcc
CFLAGS := -Wall -Wextra -O2
# Default target: Build and test the application
.PHONY: all
all: build test
# Build the main application binary
.PHONY: build
build: $(BUILD_DIR)/$(PROJECT)
@echo "Build complete: $(BUILD_DIR)/$(PROJECT)"
# Run all test suites
.PHONY: test
test:
@echo "Running tests..."
@./scripts/run-tests.sh# Provide a help target
.PHONY: help
help:
@echo "Available targets:"
@echo " make build - Build the application"
@echo " make test - Run tests"
@echo " make clean - Remove build artifacts"
@echo " make install - Install to $(PREFIX)"
@echo ""
@echo "Variables:"
@echo " PREFIX=$(PREFIX)"
@echo " CC=$(CC)"
# Or auto-generate from comments
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
build: ## Build the application
@go build -o app
test: ## Run all tests
@go test ./...
clean: ## Remove build artifacts
@rm -rf $(BUILD_DIR)# BAD: Hardcoded secrets
deploy:
curl -H "Authorization: Bearer sk-1234567890" https://api.example.com/deploy
# GOOD: Use environment variables
deploy:
@if [ -z "$$API_TOKEN" ]; then \
echo "Error: API_TOKEN not set"; \
exit 1; \
fi
curl -H "Authorization: Bearer $$API_TOKEN" https://api.example.com/deploy# Validate critical variables
.PHONY: deploy
deploy:
@if [ -z "$(ENV)" ]; then \
echo "Error: ENV not specified (prod|staging|dev)"; \
exit 1; \
fi
@if [ "$(ENV)" != "prod" ] && [ "$(ENV)" != "staging" ] && [ "$(ENV)" != "dev" ]; then \
echo "Error: Invalid ENV=$(ENV)"; \
exit 1; \
fi
@echo "Deploying to $(ENV)..."
./scripts/deploy.sh $(ENV)# BAD: Unsafe variable expansion
clean:
rm -rf $(BUILD_DIR)/* # Dangerous if BUILD_DIR is empty or /
# GOOD: Validate before dangerous operations
.PHONY: clean
clean:
@if [ -z "$(BUILD_DIR)" ] || [ "$(BUILD_DIR)" = "/" ]; then \
echo "Error: Invalid BUILD_DIR"; \
exit 1; \
fi
rm -rf $(BUILD_DIR)/*
# BETTER: Use safer patterns
BUILD_DIR := build # Never empty
clean:
@test -d $(BUILD_DIR) && rm -rf $(BUILD_DIR)/* || true# Pattern rule for object files
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Multiple pattern rules
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
# Static pattern rules
$(OBJECTS): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@# $@ - Target name
# $< - First prerequisite
# $^ - All prerequisites
# $? - Prerequisites newer than target
# $* - Stem of pattern rule
build/%.o: src/%.c
@mkdir -p $(dir $@) # Directory of target
$(CC) -c $< -o $@ # First prereq to target
@echo "Built $@" # Target name
# Example:
# build/main.o: src/main.c
# $@ = build/main.o
# $< = src/main.c
# $* = main# Built-in functions
SOURCES := $(wildcard src/*.c)
OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES))
HEADERS := $(shell find include -name '*.h')
# String manipulation
UPPERCASE := $(shell echo $(PROJECT) | tr '[:lower:]' '[:upper:]')
VERSION_MAJOR := $(word 1,$(subst ., ,$(VERSION)))
# Custom functions
define compile_template
$(1): $(2)
$(CC) $(CFLAGS) -c $$< -o $$@
endef
$(foreach src,$(SOURCES),$(eval $(call compile_template,$(patsubst %.c,%.o,$(src)),$(src))))# Debug vs Release builds
DEBUG ?= 0
ifeq ($(DEBUG),1)
CFLAGS := -g -O0 -DDEBUG
BUILD_TYPE := debug
else
CFLAGS := -O2 -DNDEBUG
BUILD_TYPE := release
endif
build:
@echo "Building $(BUILD_TYPE) version..."
$(CC) $(CFLAGS) -o app $(SOURCES)