makefile

GNU Make automation and build system guidance

SKILL.md

GitHub

Makefile Skill

Guidance for creating and maintaining GNU Make build automation.

Quick Navigation

Topic Reference
Rules, prerequisites, targets syntax.md
Variable types and assignment variables.md
Built-in functions functions.md
Special and phony targets targets.md
Recipe execution, parallel recipes.md
Implicit and pattern rules implicit.md
Common practical patterns patterns.md

Core Concepts

Rule Structure

target: prerequisites
        recipe

Critical: Recipe lines MUST start with TAB character.

File vs Phony Targets

# File target - creates/updates a file
build/app.o: src/app.c
        $(CC) -c $< -o $@

# Phony target - action, not a file
.PHONY: clean test install

clean:
        rm -rf build/

Variable Assignment

Operator Name When Expanded
:= Simple Once, at definition
?= Conditional If not already set
= Recursive Each use (late binding)
+= Append Adds to existing value
CC := gcc              # Immediate
CFLAGS ?= -O2          # Default, overridable
DEBUG = $(VERBOSE)     # Late binding
CFLAGS += -Wall        # Append

Automatic Variables

Variable Meaning
$@ Target
$< First prerequisite
$^ All prerequisites (unique)
$? Prerequisites newer than target
$* Stem in pattern rules

Essential Patterns

Self-Documenting Help

.DEFAULT_GOAL := help

help: ## Show available targets
    @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
        awk 'BEGIN {FS = ":.*?## "}; {printf "  %-15s %s\n", $$1, $$2}'

install: ## Install dependencies
    uv sync

test: ## Run tests
    uv run pytest

Platform Detection

UNAME_S := $(shell uname -s)

ifeq ($(UNAME_S),Darwin)
    OPEN := open
else ifeq ($(UNAME_S),Linux)
    OPEN := xdg-open
endif

Build Directory

BUILDDIR := build
SOURCES := $(wildcard src/*.c)
OBJECTS := $(patsubst src/%.c,$(BUILDDIR)/%.o,$(SOURCES))

$(BUILDDIR)/%.o: src/%.c | $(BUILDDIR)
    $(CC) -c $< -o $@

$(BUILDDIR):
    mkdir -p $@

Environment Export

export PYTHONPATH := $(PWD)/src
export DATABASE_URL

test:
    pytest tests/  # sees exported variables

Common Targets

Quality Checks

.PHONY: lint format check test

lint: ## Run linters
    ruff check .
    mypy src/

format: ## Format code
    ruff format .

check: format lint test ## All quality checks

Cleanup

.PHONY: clean clean-all

clean: ## Remove build artifacts
    rm -rf build/ dist/ *.egg-info
    find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

clean-all: clean ## Remove all generated files
    rm -rf .venv .pytest_cache .mypy_cache

Docker Integration

IMAGE := myapp
VERSION := $(shell git describe --tags --always)

docker-build: ## Build Docker image
    docker build -t $(IMAGE):$(VERSION) .

docker-run: ## Run container
    docker run -d -p 8000:8000 $(IMAGE):$(VERSION)

Recipe Execution

Each Line = Separate Shell

# Won't work - cd lost between lines
bad:
    cd subdir
    pwd           # Still in original dir!

# Correct - combine commands
good:
    cd subdir && pwd

# Or use line continuation
also-good:
    cd subdir && \
    pwd && \
    make

Silent and Error Handling

target:
    @echo "@ suppresses command echo"
    -rm -f maybe.txt    # - ignores errors

Parallel Execution

make -j4              # 4 parallel jobs
make -j4 lint test    # Run lint and test in parallel

Output Discipline

One line in, one line out. Avoid echo spam.

# ❌ Too chatty
start:
    @echo "Starting services..."
    docker compose up -d
    @echo "Waiting..."
    @sleep 3
    @echo "Done!"

# ✅ Concise
start: ## Start services
    @echo "Starting at http://localhost:8000 ..."
    @docker compose up -d
    @echo "Logs: docker compose logs -f"

Conditionals

DEBUG ?= 0

ifeq ($(DEBUG),1)
    CFLAGS += -g -O0
else
    CFLAGS += -O2
endif

ifdef CI
    TEST_FLAGS := --ci
endif

Including Files

# Required include (error if missing)
include config.mk

# Optional include (silent if missing)
-include local.mk
-include .env

Common Pitfalls

Pitfall Problem Solution
Spaces in recipes Recipes need TAB Use actual TAB character
Missing .PHONY make test fails if test file exists Declare .PHONY: test
cd in recipes Each line is new shell Use cd dir && command
= vs := confusion Unexpected late expansion Use := by default
Unexported vars Subprocesses don't see vars export VAR
Complex shell in make Hard to maintain Move to external script

Quick Reference

# Makefile Template
.DEFAULT_GOAL := help
SHELL := /bin/bash
.SHELLFLAGS := -ec

.PHONY: help install test lint format clean

help: ## Show this help
    @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
        awk 'BEGIN {FS = ":.*?## "}; {printf "  %-15s %s\n", $$1, $$2}'

install: ## Install dependencies
    uv sync --extra dev

test: ## Run tests
    uv run pytest tests/ -v

lint: ## Run linters
    uv run ruff check .

format: ## Format code
    uv run ruff format .

clean: ## Clean artifacts
    rm -rf build/ dist/ .pytest_cache

Links

See Also