makefile

SkillProductivity

Best practices for authoring clean, maintainable, and portable GNU Make Makefiles. Use when the task involves `creating or editing Makefiles`, `Makefile`, `makefile`, `*.mk`, `GNUmakefile`, `GNU Make patterns`, `build automation`, or `makefile troubleshooting`.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the makefile skill

What this skill tells your AI

The instructions your AI receives, as published by yacosta738/yacosta738.github.io in .agents/skills/makefile/SKILL.md and read by ahel’s review.

When to Use

  • Creating a new Makefile for any project (C/C++, Go, Node.js, etc.)
  • Reviewing or refactoring existing makefiles for correctness and maintainability
  • Debugging build failures related to make rules, prerequisites, or variables
  • Setting up build automation with GNU Make (pattern rules, automatic dependencies, parallel builds)
  • Ensuring portability across platforms or compliance with GNU Make conventions

Critical Patterns

  • Tabs, Not Spaces: Every recipe line MUST start with a tab character. Spaces will cause a syntax error.
  • Declare .PHONY: Always mark non-file targets (e.g., clean, all, test, install) with .PHONY to avoid conflicts with files of the same name.
  • Use := Over = When Possible: Prefer simple expansion (:=) for variables to avoid unexpected recursive expansion and improve performance.
  • Automatic Variables: Use $@ (target), $< (first prerequisite), $^ (all prerequisites), $? (newer prerequisites), and $* (stem) in recipes to keep rules generic and reusable.
  • Use $(wildcard ...) Not $(shell ls ...): Never shell out for file lists; use Make's built-in $(wildcard *.c) function instead.
  • Auto-Generate Dependencies: Use compiler flags like -MMD -MP to generate .d files rather than maintaining header dependencies manually.
  • Clean Target: Always provide a clean target and prefix destructive commands with - to ignore errors if files don't exist.
  • Variables First, Rules Second: Define all variables at the top, followed by the default goal, then build rules, then phony targets.
  • Avoid Recursive Make: Don't use $(MAKE) -C subdir unless absolutely necessary; prefer includes or non-recursive approaches.

File Structure

# -------------------------------------------------------
# Project: <name>
#
# Usage:
#   make          - Build the project
#   make clean    - Remove generated files
#   make install  - Install to $(PREFIX)
#   make test     - Run tests
#
# Variables:
#   CC       - C compiler (default: gcc)
#   PREFIX   - Installation prefix (default: /usr/local)
# -------------------------------------------------------

# --- Variables ---
CC       ?= gcc
CFLAGS   := -Wall -Wextra -O2
PREFIX   ?= /usr/local

sources  := $(wildcard src/*.c)
objects  := $(sources:.c=.o)
deps     := $(objects:.o=.d)

# --- Default Goal ---
all: program

# --- Build Rules ---
program: $(objects)
	$(CC) -o $@ $^ $(LDFLAGS)

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

-include $(deps)

# --- Phony Targets ---
.PHONY: all clean install test

clean:
	-rm -f program $(objects) $(deps)

install: program
	install -d $(PREFIX)/bin
	install -m 755 program $(PREFIX)/bin

test: program
	./run-tests.sh

Code Examples

Variables and Substitution

# Simple expansion (evaluates immediately) - PREFERRED
CC := gcc

# Recursive expansion (evaluates when used)
CFLAGS = -Wall $(EXTRA_FLAGS)

# Conditional assignment (set default, allow override)
PREFIX ?= /usr/local

# Append to variable
CFLAGS += -g

# Always reference with $() - not bare $VAR
objects := $(sources:.c=.o)

Pattern Rules and Implicit Rules

# Custom pattern rule for generic transformations
%.pdf: %.md
	pandoc $< -o $@

# Compile with auto-dependency generation
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

Order-Only Prerequisites

# The directory is created before compiling but does NOT
# trigger recompilation when its timestamp changes.
obj/%.o: %.c | obj
	$(CC) $(CFLAGS) -c $< -o $@

obj:
	mkdir -p obj

Conditional Directives

ifeq ($(OS),Windows_NT)
    EXE_EXT := .exe
else
    EXE_EXT :=
endif

program: main.o
	$(CC) -o program$(EXE_EXT) main.o

Splitting Long Lines

# Use backslash-newline to split for readability
sources := main.c \
           utils.c \
           parser.c \
           handler.c

# In recipes, backslash-newline continues the shell command
install: program
	install -d $(PREFIX)/bin && \
		install -m 755 program $(PREFIX)/bin

Including Other Makefiles

# Include shared configuration
include config.mk

# Include optional local overrides (no error if missing)
-include local.mk

Error Handling and Validation

# Validate required tools
ifeq ($(shell which gcc 2>/dev/null),)
    $(error gcc is not installed or not in PATH)
endif

# Validate required variables
ifndef VERSION
    $(error VERSION is not defined. Usage: make VERSION=1.2.3)
endif

Special Targets

# Delete targets if recipe fails (recommended)
.DELETE_ON_ERROR:

# Don't delete intermediate files
.SECONDARY:

# Preserve specific intermediate files
.PRECIOUS: %.o

Managing Multiple Programs

programs := prog1 prog2 prog3

.PHONY: all clean

all: $(programs)

prog1: prog1.o common.o
	$(CC) -o $@ $^

prog2: prog2.o common.o
	$(CC) -o $@ $^

clean:
	-rm -f $(programs) *.o

Anti-Patterns to Avoid

Anti-PatternWhy It's BadDo Instead
Spaces instead of tabs in recipesSyntax error, hard to debugUse tab characters
$(shell ls *.c) for file listsCreates subprocess, fragile$(wildcard *.c)
Missing .PHONY declarationsclean won't run if a file named clean exists.PHONY: clean
Hardcoded file listsMaintenance burden, easy to forget new files$(wildcard ...) or auto-discovery
Complex shell scripts in recipesHard to read and debugMove to a separate .sh script
Recursive make $(MAKE) -C subdirBreaks dependency tracking across dirsinclude or non-recursive make
Using = everywhereUnexpected recursive expansion, slowerUse := for simple values
Circular dependenciesInfinite loop, build failureRestructure dependency graph

Commands

# Dry run - see what would execute without running
make -n

# Force rebuild all targets
make -B

# Parallel build (use number of CPU cores)
make -j$(nproc)

# Print the database of rules and variables (debugging)
make -p

# Show which makefile and line number each rule comes from
make -d

# Override a variable from the command line
make CC=clang CFLAGS="-O3"

Resources

Signals

GitHub stars
48
Forks
7
Last commit
Sep 2026

ahel recommends instead

Advanced
Catalog kind
skill
Gateway key
makefile-yacosta738
Source
github.com/yacosta738/yacosta738.github.io