Skip to content
All Skills

Makefile Development Instructions

Best practices for authoring GNU Make Makefiles

Education & Writing|v1|Updated 7/14/2026|GitHub source
MCP get_skill({ skillId: "makefile-development-instructions-2b02a347" })

Use this skill with your agent

Create a free account and connect via MCP

Get Started Free
# Makefile Development Instructions

Instructions for writing clean, maintainable, and portable GNU Make Makefiles. These instructions are based on the [GNU Make manual](https://www.gnu.org/software/make/manual/).

## General Principles

- Write clear and maintainable makefiles that follow GNU Make conventions
- Use descriptive target names that clearly indicate their purpose
- Keep the default goal (first target) as the most common build operation
- Prioritize readability over brevity when writing rules and recipes
- Add comments to explain complex rules, variables, or non-obvious behavior

## Naming Conventions

- Name your makefile `Makefile` (recommended for visibility) or `makefile`
- Use `GNUmakefile` only for GNU Make-specific features incompatible with other make implementations
- Use standard variable names: `objects`, `OBJECTS`, `objs`, `OBJS`, `obj`, or `OBJ` for object file lists
- Use uppercase for built-in variable names (e.g., `CC`, `CFLAGS`, `LDFLAGS`)
- Use descriptive target names that reflect their action (e.g., `clean`, `install`, `test`)

## File Structure

- Place the default goal (primary build target) as the first rule in the makefile
- Group related targets together logically
- Define variables at the top of the makefile before rules
- Use `.PHONY` to declare targets that don't represent files
- Structure makefiles with: variables, then rules, then phony targets

```makefile
# Variables
CC = gcc
CFLAGS = -Wall -g
objects = main.o utils.o

# Default goal
all: program

# Rules
program: $(objects)
	$(CC) -o program $(objects)

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

# Phony targets
.PHONY: clean all
clean:
	rm -f program $(objects)
```

## Variables and Substitution

- Use variables to avoid duplication and improve maintainability
- Define variables with `:=` (simple expansion) for immediate evaluation, `=` for recursive expansion
- Use `?=` to set default values that can be overridden
- Use `+=` to append to existing variables
- Reference variables with `$(VARIABLE)` not `$VARIABLE` (unless single character)
- Use automatic variables (`$@`, `$<`, `$^`, `$?`, `$*`) in recipes to make rules more generic

```makefile
# Simple expansion (evaluates immediately)
CC := gcc

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

# Conditional assignment
PREFIX ?= /usr/local

# Append to variable
CFLAGS += -g
```

## Rules and Prerequisites

- Separate targets, prerequisites, and recipes clearly
- Use implicit rules for standard compilations (e.g., `.c` to `.o`)
- List prerequisites in logical order (normal prerequisites before order-only)
- Use order-only prerequisites (after `|`) for directories and dependencies that shouldn't trigger rebuilds
- Include all actual dependencies to ensure correct rebuilds
- Avoid circular dependencies between targets
- Remember that order-only prerequisites are omitted from automatic variables like `$^`, so reference them explicitly if needed

The example below shows a pattern rule that compiles objects into an `obj/` directory. The directory itself is listed as an order-only prerequisite so it is created before compiling but does not force recompilation when its timestamp changes.

```makefile
# Normal prerequisites
program: main.o utils.o
	$(CC) -o $@ $^

# Order-only prerequisites (directory creation)
obj/%.o: %.c | obj
	$(CC) $(CFLAGS) -c $< -o $@

obj:
	mkdir -p obj
```

## Recipes and Commands

- Start every recipe line with a **tab character** (not spaces) unless `.RECIPEPREFIX` is changed
- Use `@` prefix to suppress command echoing when appropriate
- Use `-` prefix to ignore errors for specific commands (use sparingly)
- Combine related commands with `&&` or `;` on the same line when they must execute together
- Keep recipes readable; break long commands across multiple lines with backslash continuation
- Use shell conditionals and loops within recipes when needed

```makefile
# Silent command
clean:
	@echo "Cleaning up..."
	@rm -f $(objects)

# Ignore errors
.PHONY: clean-all
clean-all:
	-rm -rf build/
	-rm -rf dist/

# Multi-line recipe with proper continuation
install: program
	install -d $(PREFIX)/bin && \
		install -m 755 program $(PREFIX)/bin
```

## Phony Targets

- Always declare phony targets with `.PHONY` to avoid conflicts with files of the same name
- Use phony targets for actions like `clean`, `install`, `test`, `all`
- Place phony target declarations near their rule definitions or at the end of the makefile

```makefile
.PHONY: all clean test install

all: program

clean:
	rm -f program $(objects)

test: program
	./run-tests.sh

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

## Pattern Rules and Implicit Rules

- Use pattern rules (`%.o: %.c`) for generic transformations
- Leverage built-in implicit rules when appropriate (GNU Make knows how to compile `.c` to `.o`)
- Override implicit rule variables (like `CC`, `CFLAGS`) rather than rewriting the rules
- Define custom pattern rules only when built-in rules are insufficient

```makefile
# Use built-in implicit rules by setting variables
CC = gcc
CFLAGS = -Wall -O2

# Custom pattern rule for special cases
%.pdf: %.md
	pandoc $< -o $@
```

## Splitting Long Lines

- Use backslash-newline (`\`) to split long lines for readability
- Be aware that backslash-newline is converted to a single space in non-recipe contexts
- In recipes, backslash-newline preserves the line continuation for the shell
- Avoid trailing whitespace after backslashes

### Splitting Without Adding Whitespace

If you need to split a line without adding whitespace, you can use a special technique: insert `$ ` (dollar-space) followed by a backslash-newline. The `$ ` refers to a variable with a single-space name, which doesn't exist and expands to nothing, effectively joining the lines without inserting a space.

```makefile
# Concatenate strings without adding whitespace
# The following creates the value "oneword"
var := one$ \
       word

# This is equivalent to:
# var := oneword
```

```makefile
# Variable definition split across lines
sources = main.c \
          utils.c \
          parser.c \
          handler.c

# Recipe with long command
build: $(objects)
	$(CC) -o program $(objects) \
	      $(LDFLAGS) \
	      -lm -lpthread
```

## Including Other Makefiles

- Use `include` directive to share common definitions across makefiles
- Use `-include` (or `sinclude`) to include optional makefiles without errors
- Place `include` directives after variable definitions that may affect included files
- Use `include` for shared variables, pattern rules, or common targets

```makefile
# Include common settings
include config.mk

# Include optional local configuration
-include local.mk
```

## Conditional Directives

- Use conditional directives (`ifeq`, `ifneq`, `ifdef`, `ifndef`) for platform or configuration-specific rules
- Place conditionals at the makefile level, not within recipes (use shell conditionals in recipes)
- Keep conditionals simple and well-documented

```makefile
# Platform-specific settings
ifeq ($(OS),Windows_NT)
    EXE_EXT = .exe
else
    EXE_EXT =
endif

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

## Automatic Prerequisites

- Generate header dependencies automatically rather than maintaining them manually
- Use compiler flags like `-MMD` and `-MP` to generate `.d` files with dependencies
- Include generated dependency files with `-include $(deps)` to avoid errors if they don't exist

```makefile
objects = main.o utils.o
deps = $(objects:.o=.d)

# Include dependency files
-include $(deps)

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

## Error Handling and Debugging

- Use `$(error text)` or `$(warning text)` functions for build-time diagnostics
- Test makefiles with `make -n` (dry run) to see commands without executing
- Use `make -p` to print the database of rules and variables for debugging
- Validate required variables and tools at the beginning of the makefile

```makefile
# Check for required tools
ifeq ($(shell which gcc),)
    $(error "gcc is not installed or not in PATH")
endif

# Validate required variables
ifndef VERSION
    $(error VERSION is not defined)
endif
```

## Clean Targets

- Always provide a `clean` target to remove generated files
- Declare `clean` as phony to avoid conflicts with a file named "clean"
- Use `-` prefix with `rm` commands to ignore errors if files don't exist
- Consider separate `clean` (removes objects) and `distclean` (removes all generated files) targets

```makefile
.PHONY: clean distclean

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

distclean: clean
	-rm -f program config.mk
```

## Portability Considerations

- Avoid GNU Make-specific features if portability to other make implementations is required
- Use standard shell commands (prefer POSIX shell constructs)
- Test with `make -B` to force rebuild all targets
- Document any platform-specific requirements or GNU Make extensions used

## Performance Optimization

- Use `:=` for variables that don't need recursive expansion (faster)
- Avoid unnecessary use of `$(shell ...)` which creates subprocesses
- Order prerequisites efficiently (most frequently changing files last)
- Use parallel builds (`make -j`) safely by ensuring targets don't conflict

## Documentation and Comments

- Add a header comment explaining the makefile's purpose
- Document non-obvious variable settings and their effects
- Include usage examples or targets in comments
- Add inline comments for complex rules or platform-specific workarounds

```makefile
# Makefile for building the example application
#
# Usage:
#   make          - Build the program
#   make clean    - Remove generated files
#   make install  - Install to $(PREFIX)
#
# Variables:
#   CC       - C compiler (default: gcc)
#   PREFIX   - Installation prefix (default: /usr/local)

# Compiler and flags
CC ?= gcc
CFLAGS = -Wall -Wextra -O2

# Installation directory
PREFIX ?= /usr/local
```

## Special Targets

- Use `.PHONY` for non-file targets
- Use `.PRECIOUS` to preserve intermediate files
- Use `.INTERMEDIATE` to mark files as intermediate (automatically deleted)
- Use `.SECONDARY` to prevent deletion of intermediate files
- Use `.DELETE_ON_ERROR` to remove targets if recipe fails
- Use `.SILENT` to suppress echoing for all recipes (use sparingly)

```makefile
# Don't delete intermediate files
.SECONDARY:

# Delete targets if recipe fails
.DELETE_ON_ERROR:

# Preserve specific files
.PRECIOUS: %.o
```

## Common Patterns

### Standard Project Structure

```makefile
CC = gcc
CFLAGS = -Wall -O2
objects = main.o utils.o parser.o

.PHONY: all clean install

all: program

program: $(objects)
	$(CC) -o $@ $^

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

clean:
	-rm -f program $(objects)

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

### Managing Multiple Programs

```makefile
programs = prog1 prog2 prog3

.PHONY: all clean

all: $(programs)

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

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

prog3: prog3.o
	$(CC) -o $@ $^

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

## Anti-Patterns to Avoid

- Don't start recipe lines with spaces instead of tabs
- Avoid hardcoding file lists when they can be generated with wildcards or functions
- Don't use `$(shell ls ...)` to get file lists (use `$(wildcard ...)` instead)
- Avoid complex shell scripts in recipes (move to separate script files)
- Don't forget to declare phony targets as `.PHONY`
- Avoid circular dependencies between targets
- Don't use recursive make (`$(MAKE) -C subdir`) unless absolutely necessary
#github-copilot#code#documentation

Related Skills

More skills in Education & Writing

Academic Plotting

Generates publication-quality figures for ML papers from research context. Given a paper section or description, extracts system components and relationships to generate architecture diagrams via Gemini. Given experiment results or data, auto-selects chart type and generates data-driven figures via matplotlib/seaborn. Use when creating any figure for a conference paper.

#broad-capability#ai-researchMIT

Add Educational Comments

Add educational comments to the file specified, or prompt asking for file to comment if one is not provided.

#github-copilot#educationMIT

AI Prompt Engineering & Safety Best Practices

Comprehensive best practices for AI prompt engineering, safety frameworks, bias mitigation, and responsible AI usage for Copilot and LLMs.

#github-copilot#awesome-copilotMIT

Anti Slop

Comprehensive toolkit for detecting and eliminating "AI slop" - generic, low-quality AI-generated patterns in natural language, code, and design. Use when reviewing or improving content quality, preventing generic AI patterns, cleaning up existing content, or enforcing quality standards in writing, code, or design work.

#broad-capability#mathMIT

Apple Notes

Create, view, edit, delete, search, move, or export Apple Notes via the memo CLI on macOS.

#broad-capability#browserMIT

Asc Whats New Writer

Generate engaging, localized App Store release notes (What's New) from git log, bullet points, or free text using canonical metadata under `./metadata`. Optionally pairs with promotional text updates.

#rudrankriyam-app-store-connect#iosMIT