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
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)Install with Tessl CLI
npx tessl i pantheon-ai/makefile-validator@0.1.0