Makefile Generator
Generate GNU Make build systems that define build targets, configure dependencies, set up phony targets, and implement parallel builds. Use when creating make/Makefile/.mk files, implementing compile rules, or building production-ready build automation for C/C++, Go, Python, and Java projects.
npx skills add github:pantheon-org/tekhne --skill development--scripting--makefile--generatorOverview
Generate production-ready Makefiles with best practices for C/C++, Python, Go, Java, and generic projects. Features GNU Coding Standards compliance, standard targets, security hardening, and automatic validation via devops-skills:makefile-validator skill.
Mindset
Treat every target as a promise about a file, not a label for a command. Make decides whether to run a recipe by comparing file timestamps, so a target that does not correspond to a real output file must always be declared .PHONY, or Make will silently skip it the day a file with that name happens to exist. Never write a recipe that depends on inherited shell state; each recipe line runs in its own subshell, so a cd or export on one line has no effect on the next unless .ONESHELL is set or the lines are joined with \. Always prefer an explicit rule over an implicit one for anything that matters to the build’s correctness — implicit rules are pattern-matched and order-dependent, which makes a broken build far harder to diagnose than an explicit prerequisite list. Verify that recipes never assume an environment variable exists; recipe shells do not always inherit the invoking shell’s exported variables, so anything the recipe needs must be declared as a Make variable instead.
When to Use
- Creating new Makefiles from scratch
- Setting up build systems for projects (C/C++, Python, Go, Java)
- Implementing build automation and CI/CD integration
- Converting manual build processes to Makefiles
- The user asks to “create”, “generate”, or “write” a Makefile
When NOT to Use
Do not use this skill for validating or linting an existing Makefile — that is the makefile-validator skill’s job, not this one. Do not use it for debugging a build that is already failing (make -d and manual tracing are the right tool there), and do not use it as a substitute for actually running the build; generating a Makefile is not the same as verifying it builds successfully.
Generation Workflow
Stage 1: Gather Requirements
Collect information for the following categories. Use AskUserQuestion when information is missing or ambiguous:
| Category | Information Needed |
|---|---|
| Project | Language (C/C++/Python/Go/Java), structure (single/multi-directory) |
| Build | Source files, output artifacts, dependencies, build order |
| Install | PREFIX location, directories (bin/lib/share), files to install |
| Targets | all, install, clean, test, dist, help (which are needed?) |
| Config | Compiler, flags, pkg-config dependencies, cross-compilation |
When to Use AskUserQuestion: MUST ask when language unspecified, project structure unclear, Docker registry unknown, multiple binaries possible, install paths unclear, or cross-compilation mentioned. Example questions: “What programming language?”, “Single or multi-directory project?”, “Which container registry?”, “Target platform/architecture?”
Skip when: User provides all info explicitly, standard project with obvious defaults (e.g., “Go project with Docker”), or user says “use defaults”.
Default Assumptions: Single-directory, PREFIX=/usr/local, standard targets (all/build/test/clean/install/help), no cross-compilation.
Stage 2: Documentation Lookup
When REQUIRED (MUST perform lookup):
- User requests integration with unfamiliar tools, frameworks, or build systems
- Complex build patterns not covered in Stage 3 examples (e.g., Bazel, Meson, custom toolchains)
- Docker/container integration (Dockerfile builds, multi-stage, registry push)
- CI/CD platform-specific integration (GitHub Actions, GitLab CI, Jenkins)
- Cross-compilation for unusual targets or embedded systems
- Package manager integration (Conan, vcpkg, Homebrew formulas)
- Multi-binary or multi-library projects
- Version embedding via ldflags or build-time variables
When OPTIONAL (may skip external lookup):
- Standard language patterns already covered in Stage 3 (C/C++, Go, Python, Java)
- Simple single-binary projects with no external dependencies
- User provides complete requirements with no ambiguity
- Internal docs already cover the required pattern comprehensively
Lookup Process (follow in order):
-
ALWAYS consult internal references/ FIRST using the Read tool (primary source of truth):
Requirement Read This Doc Docker/container targets references/patterns-guide.md(Pattern 8: Docker Integration)Multi-binary projects references/patterns-guide.md(Pattern 7: Multi-Binary Project)Go projects with version embedding references/patterns-guide.md(Pattern 5: Go Project)Parallel builds, caching, ccache references/optimization-guide.mdCredentials, secrets, API keys references/security-guide.mdComplex dependencies, pattern rules references/patterns-guide.mdOrder-only prerequisites references/optimization-guide.mdorreferences/targets-guide.mdVariables, assignment operators references/variables-guide.mdCRITICAL: You MUST explicitly use the Read tool to consult relevant docs during generation, even if you have prior knowledge. Do NOT rely on context from earlier in the conversation. This ensures patterns are always current and correctly applied.
Required Workflow Example (Docker + Go with version embedding):
# Step 1: Use Read tool to get Go pattern Read: references/patterns-guide.md (find Pattern 5: Go Project) # Step 2: Use Read tool to get Docker pattern Read: references/patterns-guide.md (find Pattern 8: Docker Integration) # Step 3: Use Read tool for security considerations Read: references/security-guide.md (credential handling for docker-push) # Step 4: Generate Makefile combining patterns # Step 5: Document which docs were consulted in Makefile headerImportant: Internal docs contain vetted, production-ready patterns. Always read the relevant docs before external lookups.
-
Try context7 for external tool documentation (when internal docs don’t cover a specific tool):
# Only needed for tools/frameworks NOT covered in internal docs mcp__context7__resolve-library-id: "<tool-name>" mcp__context7__get-library-docs: topic="<integration-topic>" # Example topics: # - For Docker: topic="dockerfile best practices" # - For Go: topic="go build ldflags" # - For specific tools: topic="<tool> makefile integration"Note: Context7 may not have GNU Make-specific documentation. Skip if internal docs provide sufficient patterns.
-
Fallback to WebSearch (only if pattern not found in internal docs OR context7):
"<specific-feature>" makefile best practices 2025 Example: "docker makefile best practices 2025" Example: "go ldflags version makefile 2025"Trigger WebSearch when: Internal docs don’t cover the specific integration AND context7 returns no relevant results.
Note: Document which internal docs you consulted in your response (add comment in generated Makefile header).
Stage 3: Generate Makefile
Header (choose one style)
Traditional (POSIX-compatible):
.DELETE_ON_ERROR:
.SUFFIXES:
Modern (GNU Make 4.0+, recommended):
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
.SUFFIXES:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
Standard Variables
# User-overridable (use ?=)
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
DESTDIR ?=
# GNU installation directories
BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/lib
INCLUDEDIR ?= $(PREFIX)/include
# Project-specific (use :=)
PROJECT := myproject
VERSION := 1.0.0
SRCDIR := src
BUILDDIR := build
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
Language-Specific Build Rules
C/C++:
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(OBJECTS:.o=.d)
Go:
$(TARGET): $(shell find . -name '*.go') go.mod
go build -o $@ ./cmd/$(PROJECT)
Python:
.PHONY: build
build:
python -m build
.PHONY: develop
develop:
pip install -e .[dev]
Java:
$(BUILDDIR)/%.class: $(SRCDIR)/%.java
@mkdir -p $(@D)
javac -d $(BUILDDIR) -sourcepath $(SRCDIR) $<
Standard Targets
.PHONY: all clean install uninstall test help
## Build all targets
all: $(TARGET)
## Install to PREFIX
install: all
install -d $(DESTDIR)$(BINDIR)
install -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/
## Remove built files
clean:
$(RM) -r $(BUILDDIR) $(TARGET)
## Run tests
test:
# Add test commands
## Show help
help:
@echo "$(PROJECT) v$(VERSION)"
@echo "Targets: all, install, clean, test, help"
@echo "Override: make CC=clang PREFIX=/opt"
Stage 4: Validate and Format
CRITICAL: Always validate using devops-skills:makefile-validator skill.
1. Generate Makefile following stages above
2. Invoke devops-skills:makefile-validator skill
3. Fix any errors identified (MUST have 0 errors)
4. Apply formatting fixes (see "Formatting Step" below)
5. Fix warnings (SHOULD fix; explain if skipped)
6. Address info items for large/production projects
7. Re-validate until checks pass
8. Output structured validation report (REQUIRED - see format below)
Formatting Step (REQUIRED)
When mbake reports formatting issues, you MUST either:
-
Auto-apply formatting (preferred for minor issues):
mbake format <Makefile> -
Explain why not applied (if formatting would break functionality):
Formatting not applied because: - [specific reason, e.g., "heredoc syntax would be corrupted"] - Manual review recommended for: [specific lines]
Formatting Decision Guide:
| mbake Report | Action |
|---|---|
| “Would reformat” with no specific issues | Auto-apply with mbake format |
| Specific whitespace/indentation issues | Auto-apply with mbake format |
| Issues in complex heredocs or multi-line strings | Skip formatting, explain in output |
Issues in # bake-format off sections |
Skip (intentionally disabled) |
Validation Pass Criteria:
| Level | Requirement | Action |
|---|---|---|
| Errors (0 required) | Syntax errors, missing tabs, invalid targets | MUST fix before completion |
| Warnings (fix if feasible) | Formatting issues, missing optimizations | SHOULD fix; explain if skipped |
| Info (address for production) | Enhancement suggestions, style preferences | SHOULD address for production Makefiles |
Known mbake False Positives (can be safely ignored):
The mbake validator may report warnings for valid GNU Make special targets. These are false positives and can be ignored:
| mbake Warning | Actual Status | Explanation |
|---|---|---|
| “Unknown special target ‘.DELETE_ON_ERROR’” | ✅ Valid | Critical GNU Make target that deletes failed build artifacts |
| “Unknown special target ‘.SUFFIXES’” | ✅ Valid | Standard GNU Make target for disabling/setting suffix rules |
| “Unknown special target ‘.ONESHELL’” | ✅ Valid | GNU Make 3.82+ feature for single-shell recipe execution |
| “Unknown special target ‘.POSIX’” | ✅ Valid | POSIX compliance declaration |
Validation Report Output (REQUIRED)
After validation completes, output a structured report with: Result (PASSED/FAILED), Errors/Warnings/Info counts, Changes applied (errors fixed, warnings addressed, formatting applied), and Remaining issues.
Common Info Items to Address:
| Info Item | When to Fix | How to Fix |
|---|---|---|
| “mkdir without order-only prerequisites” | Large projects (>10 targets) | Use target: prereqs | $(BUILDDIR) pattern |
| “recipe commands lack error handling” | Critical operations (install, deploy) | Add set -e in .SHELLFLAGS or use && chaining |
| “consider using ccache” | Long compile times | Add CC := ccache $(CC) pattern |
| “parallel-sensitive commands detected” | Docker/npm/pip targets | Add .NOTPARALLEL: for affected targets or proper dependencies |
Production-Quality Requirements (MUST address for Docker/deploy targets):
When generating Makefiles with Docker or deployment targets, you MUST apply these production patterns:
-
Error Handling for docker-push:
## Push Docker image to registry (with error handling) docker-push: docker-build @echo "Pushing $(IMAGE)..." docker push $(IMAGE) || { echo "Failed to push $(IMAGE)"; exit 1; } docker push $(IMAGE_LATEST) || { echo "Failed to push $(IMAGE_LATEST)"; exit 1; } -
Parallel Safety for Docker targets:
# Prevent parallel execution of Docker targets (race conditions) .NOTPARALLEL: docker-build docker-push docker-runOr use proper dependencies to serialize:
docker-push: docker-build # Ensures build completes before push docker-run: docker-build # Ensures build completes before run -
Install target error handling:
install: $(TARGET) install -d $(DESTDIR)$(PREFIX)/bin || exit 1 install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/ || exit 1
Note: When validation shows info items about error handling or parallel safety, you MUST address them for any Makefile containing Docker, deploy, or install targets. Explain in your response which patterns were applied.
Validation Checklist:
- Syntax correct (
make -npasses) - All non-file targets have .PHONY
- Tab indentation (not spaces)
- No hardcoded credentials
- User-overridable variables use
?= - .DELETE_ON_ERROR present
- MAKEFLAGS optimizations included (Modern header)
- Order-only prerequisites for build directories (large projects)
- Error handling in critical recipes (install, deploy, docker-push)
Best Practices
Variables
?=for user-overridable (CC, CFLAGS, PREFIX):=for project-specific (SOURCES, OBJECTS)- Use pkg-config:
CFLAGS += $(shell pkg-config --cflags lib)
Targets
- Always declare
.PHONYfor non-file targets - Default target should be
all - Use
.DELETE_ON_ERRORfor safety - Document with
##comments for help target
Directory Creation
Two approaches for creating build directories:
Simple (inline mkdir):
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
Optimized (order-only prerequisites): Prevents unnecessary rebuilds when directory timestamps change.
$(BUILDDIR):
@mkdir -p $@
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
$(CC) $(CFLAGS) -c $< -o $@
Use order-only prerequisites (|) for large projects with many targets.
Recipes
- Use tabs, never spaces
- Quote variables in shell:
$(RM) "$(TARGET)" - Use
@prefix for quiet commands - Test with
make -nfirst
Documentation
Detailed guides in references/:
- makefile-structure.md - Organization, layout, includes
- variables-guide.md - Assignment operators, automatic variables
- targets-guide.md - Standard targets, .PHONY, prerequisites
- patterns-guide.md - Pattern rules, dependencies
- optimization-guide.md - Parallel builds, caching
- security-guide.md - Safe expansion, credential handling
Anti-Patterns
NEVER declare phony targets without .PHONY
- WHY: If a file named
cleanortestexists in the directory, Make treats the target as up-to-date and silently skips it. - BAD:
clean:\n\trm -rf dist/without.PHONY: clean - GOOD: Declare all non-file targets in
.PHONY: clean test build installat the top of the Makefile.
NEVER use make recursively instead of $(MAKE)
- WHY: Bare
makein a recipe does not inherit the parent’s-jjobserver,-ndry-run, or-kkeep-going flags, causing inconsistent behavior in parallel and CI builds. - BAD:
dist:\n\tmake -C subdir all - GOOD:
dist:\n\t$(MAKE) -C subdir all
NEVER rely on shell environment variables in recipes without declaring them as Make variables
- WHY: Recipe shells are non-interactive and may not inherit all environment variables; undeclared variables expand to empty strings silently.
- BAD:
deploy:\n\techo "Deploying to $$ENVIRONMENT"expectingENVIRONMENTfrom the shell environment without a fallback. - GOOD: Declare
ENVIRONMENT ?= stagingat the top, then use$(ENVIRONMENT)in recipes.
NEVER use implicit rules for critical build targets
- WHY: Implicit rules are order-dependent and hard to debug; explicit rules make the build graph transparent and reproducible.
- BAD: Relying on the built-in
%.o: %.cimplicit rule without declaring it explicitly for project sources. - GOOD: Define explicit pattern rules or list all build targets explicitly.
NEVER put multi-line logic in recipe shells without .ONESHELL or explicit line continuations
- WHY: Each recipe line runs in a separate shell;
cdin one line does not affect the next. - BAD:
deploy:\n\tcd dist\n\tnpm publish(cdhas no effect on the second line). - GOOD: Chain commands with
&&in a single line:cd dist && npm publish, or add.ONESHELL:at the top.
References
These scripts are optional convenience tools for quick template generation.
When to Use Scripts vs Manual Generation
Use scripts/generate_makefile_template.sh for simple standard projects. Use manual generation for complex projects (Docker, multi-binary, custom patterns) or specific user requirements.
generate_makefile_template.sh
Generates a complete Makefile template for a specific project type.
bash scripts/generate_makefile_template.sh [TYPE] [NAME]
Types: c, c-lib, cpp, go, python, java, generic
Example:
bash scripts/generate_makefile_template.sh go myservice
# Creates Makefile with Go patterns, version embedding, standard targets
add_standard_targets.sh
Adds missing standard GNU targets to an existing Makefile.
bash scripts/add_standard_targets.sh [MAKEFILE] [TARGETS...]
Targets: all, install, uninstall, clean, distclean, test, check, help
Example:
bash scripts/add_standard_targets.sh Makefile install uninstall help
# Adds install, uninstall, help targets if they don't exist
Note: Manual generation following the Stage 3 patterns produces equivalent results but allows for more customization.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Makefile Generator Reference Documentation
This directory contains comprehensive reference guides for generating production-ready Makefiles.
Quick Navigation
- Makefile Structure - File organization, layout, includes
- Variables Guide - Assignment operators, automatic variables, scope
- Targets Guide - Standard targets, .PHONY declarations, prerequisites
- Patterns Guide - Pattern rules, language-specific build patterns
- Optimization Guide - Parallel builds, caching, ccache integration
- Security Guide - Safe variable expansion, credential handling
Reading Order
For New Makefiles
- Start with Makefile Structure to understand file organization
- Read Variables Guide for variable assignment and scope
- Review Targets Guide for standard GNU targets
- Check Patterns Guide for your language (C/C++, Go, Python, Java)
For Production Makefiles
- Read Optimization Guide for performance improvements
- Review Security Guide for credential handling and safe practices
Common Lookup Tasks
| Task | Reference |
|---|---|
| Docker integration | Patterns Guide § Pattern 8 |
| Multi-binary projects | Patterns Guide § Pattern 7 |
| Go with version embedding | Patterns Guide § Pattern 5 |
| Parallel builds, ccache | Optimization Guide |
| Credentials, secrets | Security Guide |
| Complex dependencies | Patterns Guide |
| Order-only prerequisites | Optimization Guide or Targets Guide |
| Variable operators (?=, :=, +=) | Variables Guide |
Usage During Generation
When generating Makefiles, ALWAYS use the Read tool to consult relevant references before writing Makefile content. This ensures patterns are current and correctly applied.
Example workflow:
1. Read references/patterns-guide.md (find your language pattern)
2. Read references/security-guide.md (for Docker/deploy targets)
3. Generate Makefile combining patterns
4. Document which references were consulted in Makefile header
Makefile Structure and Organization
Overview
This guide covers the organization and structure of well-designed Makefiles, including variable definitions, target organization, pattern rules, and modular design patterns.
Basic Makefile Structure
A well-organized Makefile follows this general structure:
# 1. Header and metadata
# 2. Special targets (.POSIX, .DELETE_ON_ERROR, .SUFFIXES)
# 3. User-overridable variables
# 4. Project-specific variables
# 5. .PHONY declarations
# 6. Default target (all)
# 7. Build rules
# 8. Install rules
# 9. Clean rules
# 10. Test rules
# 11. Help target
1. Header and Metadata
# Project: MyApp
# Description: Brief description of the project
# Author: Your Name
# License: MIT
# Version: 1.0.0
# Ensure POSIX compatibility (optional)
.POSIX:
# Delete target files if recipe fails
.DELETE_ON_ERROR:
# Disable built-in suffix rules
.SUFFIXES:
# Custom suffixes if needed
.SUFFIXES: .c .o .h
Special Targets Explained
- .POSIX: Declares intent for POSIX compliance (optional, increases portability)
- .DELETE_ON_ERROR: If a recipe fails, delete the target file (prevents corrupted builds)
- .SUFFIXES: Clear built-in suffix rules, then optionally declare custom ones
2. Variable Organization
User-Overridable Variables (use ?=)
Variables that users should be able to override from the command line or environment:
# Compiler and tools
CC ?= gcc
CXX ?= g++
LD ?= $(CC)
AR ?= ar
RANLIB ?= ranlib
INSTALL ?= install
RM ?= rm -f
MKDIR_P ?= mkdir -p
# Compiler flags
CFLAGS ?= -Wall -Wextra -O2
CXXFLAGS ?= -Wall -Wextra -O2
CPPFLAGS ?=
LDFLAGS ?=
LDLIBS ?=
# Installation paths (GNU conventions)
PREFIX ?= /usr/local
EXEC_PREFIX ?= $(PREFIX)
BINDIR ?= $(EXEC_PREFIX)/bin
LIBDIR ?= $(EXEC_PREFIX)/lib
INCLUDEDIR ?= $(PREFIX)/include
DATAROOTDIR ?= $(PREFIX)/share
DATADIR ?= $(DATAROOTDIR)
MANDIR ?= $(DATAROOTDIR)/man
# DESTDIR for staged installations
DESTDIR ?=
Why ?= instead of =:
?=only sets the variable if not already defined- Allows users to override:
make CC=clang CFLAGS="-O3 -march=native" - Respects environment variables
Project-Specific Variables (use :=)
Variables internal to the Makefile that should not be overridden:
# Project configuration
PROJECT := myapp
VERSION := 1.0.0
TARGET := $(PROJECT)
# Directory structure
SRCDIR := src
INCDIR := include
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
DEPDIR := $(BUILDDIR)/deps
# Source files (use wildcards or explicit lists)
SOURCES := $(wildcard $(SRCDIR)/*.c)
HEADERS := $(wildcard $(INCDIR)/*.h)
# Derived file lists
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
Why := instead of =:
:=performs immediate expansion (evaluated once)=performs recursive expansion (evaluated each use):=is more efficient for computed values
Variable Expansion Example
# Wrong: = causes recursive expansion
FILES = $(wildcard *.c)
# Expands every time $(FILES) is used
# Right: := evaluates once
FILES := $(wildcard *.c)
# Evaluated immediately, more efficient
3. Target Organization
.PHONY Declarations
Declare all non-file targets as .PHONY to ensure they always run:
.PHONY: all clean install uninstall test check help
.PHONY: build dist distclean format lint
Why .PHONY is critical:
- Without .PHONY, if a file named “clean” exists,
make cleanwon’t run - .PHONY tells make these targets don’t create files
- Improves make performance by skipping unnecessary stat() calls
Default Target
The first target in the Makefile is the default (run when make is called without arguments):
## Build all targets
.PHONY: all
all: $(TARGET)
Best practices:
- Name it
all - Make it the first target after variable definitions
- It should build everything but not install or clean
4. Build Rules
Explicit Rules
# Link the executable
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
Pattern Rules (Preferred)
Pattern rules use % to match multiple files:
# Compile C source files to object files
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# Alternative without directories:
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
Automatic Variables
| Variable | Meaning |
|---|---|
$@ |
Target file name |
$< |
First prerequisite |
$^ |
All prerequisites (with duplicates removed) |
$+ |
All prerequisites (with duplicates) |
$? |
Prerequisites newer than target |
$* |
Stem of pattern match |
$(@D) |
Directory part of target |
$(@F) |
File part of target |
Example using automatic variables:
# Without automatic variables (verbose):
hello: hello.o utils.o
gcc -o hello hello.o utils.o
# With automatic variables (concise):
hello: hello.o utils.o
$(CC) -o $@ $^
5. Dependency Management
Manual Dependencies
main.o: main.c common.h
utils.o: utils.c utils.h common.h
Problems:
- Tedious to maintain
- Easy to get out of sync
- Error-prone for large projects
Automatic Dependency Generation (Recommended)
# Generate dependencies automatically during compilation
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated dependency files
-include $(DEPENDS)
Flags explained:
-MMD: Generate dependency file (.d)-MP: Add phony targets for headers (prevents errors if header deleted)-include: Include files, ignoring errors if they don’t exist yet (first build)
6. VPATH and Source Organization
VPATH for Source Directories
# Search for source files in multiple directories
VPATH = src:include:lib
# Make will search these directories for prerequisites
main.o: main.c common.h
$(CC) -c $< -o $@
vpath Directive (More Specific)
# Search pattern-specific paths
vpath %.c src
vpath %.h include
vpath %.o build/obj
%.o: %.c
$(CC) -c $< -o $@
When to use VPATH:
- Multi-directory projects
- Separating source and build directories
- Organizing headers separately
7. Include Directives
Modular Makefiles
Split large Makefiles into smaller, focused files:
# Main Makefile
include config.mk
include rules.mk
include targets.mk
config.mk (variables):
CC := gcc
CFLAGS := -Wall -O2
PREFIX := /usr/local
rules.mk (pattern rules):
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
targets.mk (phony targets):
.PHONY: clean
clean:
$(RM) *.o $(TARGET)
Conditional Includes
# Include file if it exists
-include config.mk
# Include multiple files
-include $(DEPENDS)
- prefix: Suppress errors if file doesn’t exist
8. Multi-Directory Projects
Non-Recursive Make (Recommended)
Single Makefile approach:
# Directory structure:
# project/
# Makefile
# src/
# main.c
# utils.c
# lib/
# libfoo.c
SRCDIR := src
LIBDIR := lib
BUILDDIR := build
SRC_SOURCES := $(wildcard $(SRCDIR)/*.c)
LIB_SOURCES := $(wildcard $(LIBDIR)/*.c)
ALL_SOURCES := $(SRC_SOURCES) $(LIB_SOURCES)
OBJECTS := $(ALL_SOURCES:%.c=$(BUILDDIR)/%.o)
$(TARGET): $(OBJECTS)
$(CC) $^ -o $@
$(BUILDDIR)/%.o: %.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
Advantages:
- Single make invocation
- Accurate dependency tracking
- Parallel builds work correctly
- Easier to maintain
Recursive Make (Avoid if Possible)
# Top-level Makefile
SUBDIRS := src lib tests
.PHONY: all
all:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir; done
.PHONY: clean
clean:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir clean; done
Problems with recursive make:
- Incorrect dependency tracking across directories
- Slower (multiple make invocations)
- Parallel builds can break
- See: “Recursive Make Considered Harmful” paper
9. Recipe Formatting
Silent Commands
# @ prefix suppresses command echo
clean:
@echo "Cleaning build artifacts..."
@$(RM) *.o
# Without @:
clean:
echo "Cleaning..." # This line is printed
$(RM) *.o # This line is printed
Multi-Line Recipes
# Each line is a separate shell invocation
bad:
cd subdir
make all # ERROR: cd didn't persist!
# Solution 1: Use && to chain commands
good:
cd subdir && make all
# Solution 2: Use semicolons
good2:
cd subdir; make all
# Solution 3: Use backslash continuation
good3:
cd subdir && \
make all
Error Handling
# - prefix ignores errors
clean:
-$(RM) *.o # Continue even if rm fails
# Without -:
clean:
$(RM) *.o # Make stops if rm fails
10. Complete Example
# Project: example
# Description: Example project structure
.DELETE_ON_ERROR:
.SUFFIXES:
# Variables
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
PROJECT := example
VERSION := 1.0.0
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
# Phony targets
.PHONY: all clean install test help
# Default target
all: $(TARGET)
# Build rules
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
# Install
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(PROJECT)
# Clean
clean:
$(RM) -r $(BUILDDIR)
# Test
test: $(TARGET)
@echo "Running tests..."
@$(TARGET) --test
# Help
help:
@echo "$(PROJECT) v$(VERSION)"
@echo ""
@echo "Targets:"
@echo " all - Build the project (default)"
@echo " install - Install to PREFIX (default: /usr/local)"
@echo " clean - Remove build artifacts"
@echo " test - Run tests"
@echo " help - Show this message"
@echo ""
@echo "Variables:"
@echo " CC=$(CC)"
@echo " CFLAGS=$(CFLAGS)"
@echo " PREFIX=$(PREFIX)"
Best Practices Summary
- Use .DELETE_ON_ERROR to prevent corrupted builds
- Declare .PHONY for all non-file targets
- Use ?= for user-overridable variables (CC, CFLAGS, PREFIX)
- Use := for project variables (SOURCES, OBJECTS)
- Use automatic variables ($@, $<, $^) for concise rules
- Generate dependencies automatically (-MMD -MP)
- Prefer non-recursive make over recursive make
- Use pattern rules (%.o: %.c) over suffix rules
- Create directories automatically (@mkdir -p $(@D))
- Document targets with ## comments for help output
References
Makefile Optimization Guide
Overview
This guide covers techniques for optimizing Makefile performance, including parallel builds, dependency tracking, incremental builds, caching strategies, and performance profiling.
Parallel Builds
Enabling Parallel Execution
# Run with 4 parallel jobs
make -j4
# Use all CPU cores
make -j$(nproc)
# Unlimited parallel jobs (careful!)
make -j
Making Makefiles Parallel-Safe
Problem: Shared resources
# WRONG: Multiple rules write to same file
target1:
echo "data1" >> shared.log
target2:
echo "data2" >> shared.log
# With -j2, file corruption likely!
Solution: Proper dependencies
# RIGHT: Serialize access with dependencies
target2: target1
# Or use separate files
target1:
echo "data1" > target1.log
target2:
echo "data2" > target2.log
Controlling Parallelism
# Disable parallel builds for this Makefile
.NOTPARALLEL:
# Disable parallelism for specific targets
.NOTPARALLEL: install clean
# Serialize specific targets
install: build
# Install runs after build completes
GNU Make 4.4+ Parallel Control Features
GNU Make 4.4 (released October 2022) introduced new features for fine-grained parallel control. These are becoming part of the upcoming POSIX standard.
.WAIT Special Target
The .WAIT target provides explicit ordering without creating artificial dependencies:
# .WAIT ensures prerequisites to its left complete
# before starting prerequisites to its right
all: compile .WAIT link .WAIT package
# Equivalent behavior without .WAIT would require:
# link: compile
# package: link
# But .WAIT is cleaner when targets are independent conceptually
Use Cases for .WAIT:
# Build phases with explicit ordering
build: setup .WAIT compile .WAIT test .WAIT package
@echo "Build complete"
# Parallel within phases, serial between phases
ci: lint fmt .WAIT test.unit test.integration .WAIT build
# lint and fmt run in parallel
# then unit and integration tests run in parallel
# finally build runs
# Database migrations before tests
test: migrate .WAIT run-tests
@echo "Tests complete"
Important Notes:
.WAITonly affects parallel builds (make -j)- With sequential execution,
.WAIThas no effect .WAITdoesn’t create actual dependencies, just ordering- Available in GNU Make 4.4+ (check with
make --version)
.NOTPARALLEL with Prerequisites (Enhanced)
In Make 4.4+, .NOTPARALLEL can take specific targets as prerequisites:
# Traditional: Disable ALL parallel execution
.NOTPARALLEL:
# NEW in 4.4: Serialize only specific targets
.NOTPARALLEL: install deploy cleanup
# This implicitly adds .WAIT between each prerequisite
# of the listed targets
When to Use .NOTPARALLEL with Prerequisites:
# Deployment must be serial (avoid race conditions)
.NOTPARALLEL: deploy
deploy: deploy-database deploy-backend deploy-frontend
@echo "Deployment complete"
# deploy-database -> deploy-backend -> deploy-frontend (serial)
# But compilation can still be parallel
build: $(OBJECTS)
$(CC) $^ -o $(TARGET)
# Object files compile in parallel (unaffected by .NOTPARALLEL: deploy)
Version Checking for Make 4.4+ Features
Check Make version before using 4.4+ features:
# Check Make version (4.4 = 4.4, need >= 4.4)
MAKE_VERSION_MAJOR := $(word 1,$(subst ., ,$(MAKE_VERSION)))
MAKE_VERSION_MINOR := $(word 2,$(subst ., ,$(MAKE_VERSION)))
# Simple version check
ifeq ($(shell expr $(MAKE_VERSION_MAJOR) \>= 4),1)
ifeq ($(shell expr $(MAKE_VERSION_MINOR) \>= 4),1)
HAVE_WAIT := 1
endif
endif
# Alternative: Graceful degradation
ifdef HAVE_WAIT
# Use .WAIT for modern Make
all: compile .WAIT link
else
# Fall back to dependencies for older Make
link: compile
all: link
endif
Practical Version Check Pattern:
# At the top of your Makefile
MIN_MAKE_VERSION := 4.4
CURRENT_MAKE_VERSION := $(MAKE_VERSION)
# Check and warn if using older Make
ifeq ($(shell printf '%s\n' "$(MIN_MAKE_VERSION)" "$(CURRENT_MAKE_VERSION)" | sort -V | head -n1),$(MIN_MAKE_VERSION))
# Make version is sufficient
else
$(warning GNU Make $(MIN_MAKE_VERSION)+ recommended. You have $(CURRENT_MAKE_VERSION))
$(warning Some parallel control features may not work)
endif
Comparison: .WAIT vs Dependencies vs .NOTPARALLEL
| Feature | Use Case | Make Version |
|---|---|---|
Dependencies (b: a) |
Actual dependency relationship | All |
.WAIT |
Ordering without dependency | 4.4+ |
.NOTPARALLEL: (global) |
Disable all parallel | All |
.NOTPARALLEL: target |
Serialize specific target’s prereqs | 4.4+ |
Example Comparison:
# Using dependencies (works in all Make versions)
# Problem: Creates false dependency relationship
link: compile
package: link
all: package
# Using .WAIT (Make 4.4+)
# Cleaner: Explicit ordering, no false dependencies
all: compile .WAIT link .WAIT package
# Using .NOTPARALLEL with targets (Make 4.4+)
# Best for: Targets that must never run in parallel
.NOTPARALLEL: deploy
deploy: step1 step2 step3
Optimal Parallel Structure
# Good parallel structure
SOURCES := src1.c src2.c src3.c src4.c
OBJECTS := $(SOURCES:.c=.o)
# All .o files can build in parallel
program: $(OBJECTS)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $< -o $@
Parallel execution:
make -j4
# Compiles 4 .c files simultaneously
# Then links when all are done
Dependency Tracking
Accurate Dependencies
Problem: Incorrect dependencies
# WRONG: Missing header dependencies
main.o: main.c
$(CC) -c $< -o $@
# If common.h changes, main.o won't rebuild!
Solution: Automatic dependency generation
# Generate dependencies during compilation
%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated .d files
-include $(OBJECTS:.o=.d)
Generated dependency file (main.d):
main.o: main.c common.h utils.h
common.h:
utils.h:
Dependency Flags
# -MMD: Generate dependency file (.d)
# -MP: Add phony targets for headers
# -MF file: Specify dependency file name
DEPFLAGS = -MMD -MP -MF $(@:.o=.d)
%.o: %.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
Why -MP is Important
Without -MP:
# Generated main.d:
main.o: main.c utils.h
# If utils.h is deleted:
make: *** No rule to make target 'utils.h'. Stop.
With -MP:
# Generated main.d:
main.o: main.c utils.h
utils.h:
# If utils.h is deleted, make continues
# (assumes you also removed #include "utils.h")
Incremental Builds
Timestamp-Based Builds
Make rebuilds targets when prerequisites are newer:
# program rebuilt if any .o is newer
program: $(OBJECTS)
$(CC) $^ -o $@
# main.o rebuilt if main.c or headers are newer
main.o: main.c common.h
$(CC) -c main.c -o main.o
Optimizing Dependency Chains
Inefficient:
# Every source depends on config.h
# Changing config.h rebuilds EVERYTHING
main.o: main.c config.h
utils.o: utils.c config.h
helper.o: helper.c config.h
Better: Only include where needed
# Only main.c actually uses config.h
main.o: main.c config.h
utils.o: utils.c
helper.o: helper.c
Best: Use automatic dependencies
%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
# Automatically tracks which headers each file uses
Intermediate File Management
# Mark intermediate files
.INTERMEDIATE: $(OBJECTS)
# Deleted after use
# Keep important intermediate files
.SECONDARY: important.o
# Not deleted
# Never delete these files
.PRECIOUS: %.o %.d
# Protected from deletion
Avoiding Unnecessary Rebuilds
Problem: Timestamp updates without changes
# WRONG: Always updates config.h
config.h: config.h.in
sed 's/@VERSION@/$(VERSION)/g' $< > $@
# Updates timestamp even if content unchanged!
Solution: Conditional update
# RIGHT: Only update if different
config.h: config.h.in
sed 's/@VERSION@/$(VERSION)/g' $< > $@.tmp
cmp -s $@.tmp $@ || mv $@.tmp $@
rm -f $@.tmp
Build Caching
Compiler Cache (ccache)
# Use ccache for faster recompilation
CC := ccache gcc
CXX := ccache g++
# Or conditionally:
ifeq ($(shell command -v ccache 2>/dev/null),)
CC ?= gcc
else
CC ?= ccache gcc
endif
Benefits:
- Caches compilation results
- Speeds up clean rebuilds
- Useful for CI/CD and switching branches
Distcc for Distributed Compilation
# Distributed compilation across network
CC := distcc gcc
CXX := distcc g++
# Set number of jobs based on available hosts
DISTCC_HOSTS := localhost/2 build1/4 build2/4
JOBS := 10
Build Directory Caching
# Keep build artifacts between clean builds
.PHONY: clean distclean
clean:
$(RM) $(TARGET)
# Keep .o and .d files for faster rebuild
distclean: clean
$(RM) -r $(BUILDDIR)
# Complete clean
Performance Optimization Techniques
1. Use := Instead of =
# SLOW: Recursive expansion (evaluated every use)
SOURCES = $(wildcard src/*.c)
OBJECTS = $(SOURCES:.c=.o)
# $(OBJECTS) re-runs wildcard every time!
# FAST: Simple expansion (evaluated once)
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
# Evaluated once when defined
2. Minimize Shell Invocations
# SLOW: Multiple shell calls
FILES = $(shell ls *.c)
COUNT = $(shell ls *.c | wc -l)
# FAST: Single shell call
FILES := $(wildcard *.c)
COUNT := $(words $(FILES))
3. Use Static Pattern Rules
OBJECTS := main.o utils.o helper.o
# FASTER: Static pattern rule (make knows exact files)
$(OBJECTS): %.o: %.c
$(CC) -c $< -o $@
# SLOWER: Pattern rule (make searches for matches)
%.o: %.c
$(CC) -c $< -o $@
4. Reduce Makefile Parsing Time
# SLOW: Complex shell commands in variable assignment
VERSION = $(shell git describe --tags --always --dirty)
# FAST: Use := to evaluate once
VERSION := $(shell git describe --tags --always --dirty)
# FASTER: Cache in file
VERSION := $(file < VERSION.txt)
5. Avoid Recursive Make
Inefficient: Recursive Make
# Top-level Makefile
SUBDIRS := lib1 lib2 app
all:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir; done
Problems:
- Multiple make invocations (slow)
- Incorrect dependency tracking
- Parallel builds broken
Efficient: Non-Recursive Make
# Single Makefile
LIB1_SRC := $(wildcard lib1/*.c)
LIB2_SRC := $(wildcard lib2/*.c)
APP_SRC := $(wildcard app/*.c)
ALL_SRC := $(LIB1_SRC) $(LIB2_SRC) $(APP_SRC)
OBJECTS := $(ALL_SRC:.c=.o)
# Single dependency tree
# Accurate parallel builds
Reference: “Recursive Make Considered Harmful” by Peter Miller
Performance Profiling
Timing Individual Targets
# Time recipe execution
%.o: %.c
@echo "Compiling $<..."
@time $(CC) $(CFLAGS) -c $< -o $@
Build Time Measurement
# Time entire build
time make -j4
# Output:
# real 0m12.345s
# user 0m45.678s
# sys 0m3.456s
Debug Output for Performance Analysis
# Show what make is doing
make -d
# Show only remake decisions
make -d --debug=basic
# Show implicit rule search
make -d --debug=implicit
# Profile make itself
make --profile=profile.log
Finding Bottlenecks
# Add timing to critical paths
$(TARGET): $(OBJECTS)
@echo "==> Linking $(TARGET)"
@time $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.c
@echo "==> Compiling $<"
@time $(CC) $(CFLAGS) -c $< -o $@
Optimization Best Practices
1. Structure for Parallelism
# Good: Independent compilation
OBJECTS := a.o b.o c.o d.o
program: $(OBJECTS)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $< -o $@
# make -j4 compiles 4 files at once
2. Accurate Dependencies
# Use automatic dependency generation
CFLAGS += -MMD -MP
-include $(OBJECTS:.o=.d)
# Not manual maintenance
3. Minimal Clean
# Keep intermediate files by default
clean:
$(RM) $(TARGET)
# Full clean only when needed
distclean: clean
$(RM) $(OBJECTS) $(DEPENDS)
4. Efficient Variable Usage
# Use := for computed values
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
# Use ?= for user overrides
CC ?= gcc
CFLAGS ?= -O2
5. Avoid Unnecessary Work
# Don't rebuild if nothing changed
config.h: config.h.in Makefile
@sed 's/@VERSION@/$(VERSION)/g' $< > $@.tmp
@if ! cmp -s $@ $@.tmp; then \
echo " GEN $@"; \
mv $@.tmp $@; \
else \
rm -f $@.tmp; \
fi
Complete Optimized Example
# Optimized Makefile for C project
.DELETE_ON_ERROR:
.SUFFIXES:
PROJECT := optimized
VERSION := 1.0.0
# User-overridable (use ?=)
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
# Computed once (use :=)
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
# Check for ccache
ifneq ($(shell command -v ccache 2>/dev/null),)
CC := ccache $(CC)
endif
# Optimization flags for dependencies
DEPFLAGS = -MMD -MP
.PHONY: all clean distclean profile
all: $(TARGET)
# Link (serial)
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
# Compile (parallel-safe)
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# Include auto-generated dependencies
-include $(DEPENDS)
# Minimal clean (keeps .o for faster rebuild)
clean:
$(RM) $(TARGET)
# Full clean
distclean:
$(RM) -r $(BUILDDIR)
# Profile build
profile:
time $(MAKE) clean
time $(MAKE) -j$(shell nproc) all
Benchmarking Results
Example project: 100 C files
| Configuration | Build Time | Rebuild Time |
|---|---|---|
| Sequential (make) | 45s | 12s |
| Parallel -j2 | 25s | 7s |
| Parallel -j4 | 15s | 4s |
| Parallel -j8 | 12s | 3s |
| Parallel + ccache (cold) | 14s | 3s |
| Parallel + ccache (warm) | 3s | 1s |
Key takeaways:
- Parallel builds: 3-4x speedup
- ccache (warm): 10x speedup on clean builds
- Accurate dependencies: Only rebuild what changed
Advanced Optimization
Precompiled Headers
# Generate precompiled header
$(OBJDIR)/common.h.gch: $(SRCDIR)/common.h
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -x c-header $< -o $@
# Use precompiled header
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(OBJDIR)/common.h.gch
$(CC) $(CPPFLAGS) $(CFLAGS) -include $(OBJDIR)/common.h -c $< -o $@
Link-Time Optimization (LTO)
# Enable LTO for release builds
release: CFLAGS += -flto -O3
release: LDFLAGS += -flto -O3
release: $(TARGET)
Unity Builds
# Combine all sources into one compilation unit
unity.c: $(SOURCES)
@echo "Generating unity build..."
@for src in $(SOURCES); do \
echo "#include \"$$src\"" >> $@; \
done
unity.o: unity.c
$(CC) $(CFLAGS) -c $< -o $@
# Fast single-file compilation
# Trade-off: No parallel compilation
Profiling Tools
# Make's built-in profiling
make --profile=profile.log
# Analyze profile.log
# Time individual targets
make -d 2>&1 | grep -E "Considering|Must remake"
# strace for system call analysis
strace -c make 2>&1 | tail -20
# Remake (make debugger)
remake --debug
References
Makefile Patterns Guide
Overview
This guide covers pattern rules, static pattern rules, implicit rules, dependency generation, and common Makefile patterns for various project types.
Pattern Rules
Pattern rules use % to match filenames and create generic build rules.
Basic Pattern Rules
# Compile .c files to .o files
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# Generate assembly from C
%.s: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -S $< -o $@
# Preprocess C files
%.i: %.c
$(CC) $(CPPFLAGS) -E $< -o $@
Pattern Rules with Directories
# Source in src/, objects in build/obj/
build/obj/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# Multiple source directories
build/obj/%.o: src/%.c
@mkdir -p $(@D)
$(CC) -c $< -o $@
build/obj/%.o: lib/%.c
@mkdir -p $(@D)
$(CC) -c $< -o $@
Pattern Rule Variables
# Use stem ($*) in pattern rules
%.pdf: %.tex
pdflatex $*
# Runs: pdflatex report (for report.tex -> report.pdf)
# Multiple transformations
%.html: %.md
pandoc $< -o $@
%.pdf: %.md
pandoc $< -o $@
Static Pattern Rules
More efficient and explicit than pattern rules for known file lists.
Syntax
$(targets): target-pattern: prereq-pattern
recipe
Basic Example
OBJECTS := main.o utils.o helper.o
# Static pattern rule
$(OBJECTS): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Equivalent to:
# main.o: main.c
# $(CC) $(CFLAGS) -c main.c -o main.o
# utils.o: utils.c
# $(CC) $(CFLAGS) -c utils.c -o utils.o
# helper.o: helper.c
# $(CC) $(CFLAGS) -c helper.c -o helper.o
With Directories
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:src/%.c=build/obj/%.o)
$(OBJECTS): build/obj/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
Multiple Dependencies
# All objects depend on config.h
$(OBJECTS): %.o: %.c config.h
$(CC) $(CFLAGS) -c $< -o $@
# Specific objects depend on additional headers
$(NETWORK_OBJS): %.o: %.c network.h common.h
$(CC) $(CFLAGS) -c $< -o $@
Implicit Rules
GNU Make has built-in implicit rules. You can use or override them.
Common Built-in Rules
# These rules are built into make:
# %.o: %.c
# $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# %.o: %.cpp
# $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
# %: %.o
# $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
Disabling Implicit Rules
# Disable all built-in rules (recommended for explicit Makefiles)
.SUFFIXES:
# Re-enable specific patterns
.SUFFIXES: .c .o .h
# Or disable specific rules
%.o: %.c
# Empty recipe disables the built-in rule
Custom Implicit Rules
# Add your own implicit rules
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Language-specific rules
%.o: %.cpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -MMD -MP -c $< -o $@
%.o: %.s
$(AS) $(ASFLAGS) -c $< -o $@
Dependency Generation
Manual Dependencies (Avoid)
# Tedious and error-prone
main.o: main.c common.h utils.h
utils.o: utils.c utils.h common.h
helper.o: helper.c helper.h common.h
Automatic Dependency Generation
Method 1: Embedded in compilation:
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:src/%.c=build/obj/%.o)
DEPENDS := $(OBJECTS:.o=.d)
# Generate dependencies during compilation
build/obj/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated dependency files
-include $(DEPENDS)
Flags explained:
-MMD: Generate dependency file (.d)-MP: Add phony targets for headers (prevents errors if header deleted)-MF file: Specify dependency file name (optional)
Method 2: Separate dependency generation:
# Generate dependencies separately
%.d: %.c
@$(CC) $(CPPFLAGS) -MM $< | sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' > $@
-include $(DEPENDS)
Generated .d file example:
# main.d (generated from main.c)
build/obj/main.o build/obj/main.d: src/main.c include/common.h \
include/utils.h
include/common.h:
include/utils.h:
Common Project Patterns
Pattern 1: Simple Single-Directory C Project
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
TARGET := myapp
SOURCES := $(wildcard *.c)
OBJECTS := $(SOURCES:.c=.o)
DEPENDS := $(OBJECTS:.o=.d)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
clean:
$(RM) $(TARGET) $(OBJECTS) $(DEPENDS)
Pattern 2: Multi-Directory C Project
PROJECT := myapp
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2 -Iinclude
SRCDIR := src
INCDIR := include
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
clean:
$(RM) -r $(BUILDDIR)
Pattern 3: C++ Project with Libraries
PROJECT := myapp
CXX ?= g++
CXXFLAGS ?= -Wall -Wextra -std=c++17 -O2
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
# Source files
SOURCES := $(wildcard $(SRCDIR)/*.cpp)
OBJECTS := $(SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
# Library
LIBNAME := $(PROJECT)
STATIC_LIB := $(BUILDDIR)/lib$(LIBNAME).a
SHARED_LIB := $(BUILDDIR)/lib$(LIBNAME).so
# Executable
TARGET := $(BUILDDIR)/$(PROJECT)
.PHONY: all static shared executable clean
all: executable
executable: $(TARGET)
static: $(STATIC_LIB)
shared: $(SHARED_LIB)
# Link executable
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
# Create static library
$(STATIC_LIB): $(OBJECTS)
@mkdir -p $(@D)
$(AR) rcs $@ $^
# Create shared library
$(SHARED_LIB): $(OBJECTS)
@mkdir -p $(@D)
$(CXX) -shared $^ -o $@
# Compile with -fPIC for libraries
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
@mkdir -p $(@D)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -fPIC -MMD -MP -c $< -o $@
-include $(DEPENDS)
clean:
$(RM) -r $(BUILDDIR)
Pattern 4: Mixed C/C++ Project
PROJECT := mixed
CC ?= gcc
CXX ?= g++
CFLAGS ?= -Wall -O2
CXXFLAGS ?= -Wall -O2 -std=c++17
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
# Separate C and C++ sources
C_SOURCES := $(wildcard $(SRCDIR)/*.c)
CXX_SOURCES := $(wildcard $(SRCDIR)/*.cpp)
# Separate object files
C_OBJECTS := $(C_SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
CXX_OBJECTS := $(CXX_SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o)
ALL_OBJECTS := $(C_OBJECTS) $(CXX_OBJECTS)
DEPENDS := $(ALL_OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
.PHONY: all clean
all: $(TARGET)
# Link with C++ compiler (for C++ standard library)
$(TARGET): $(ALL_OBJECTS)
@mkdir -p $(@D)
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
# Compile C sources
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Compile C++ sources
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
@mkdir -p $(@D)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
clean:
$(RM) -r $(BUILDDIR)
Pattern 5: Go Project
PROJECT := myapp
GO ?= go
GOFLAGS ?=
PREFIX ?= /usr/local
# Version from git
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
LDFLAGS := -ldflags "-X main.version=$(VERSION)"
# Source files
SOURCES := $(shell find . -name '*.go' -not -path './vendor/*')
TARGET := $(PROJECT)
.PHONY: all build install test clean fmt lint mod-tidy
all: build
build: $(TARGET)
$(TARGET): $(SOURCES) go.mod go.sum
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $@ ./cmd/$(PROJECT)
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/
test:
$(GO) test -v ./...
clean:
$(RM) $(TARGET)
$(GO) clean
fmt:
$(GO) fmt ./...
lint:
golangci-lint run
mod-tidy:
$(GO) mod tidy
Pattern 6: Python Project
PROJECT := mypackage
PYTHON ?= python3
PIP ?= $(PYTHON) -m pip
.PHONY: all build install develop test lint format clean
all: build
build:
$(PYTHON) -m build
install:
$(PIP) install .
develop:
$(PIP) install -e .[dev]
test:
$(PYTHON) -m pytest tests/ -v
lint:
$(PYTHON) -m flake8 src/ tests/
$(PYTHON) -m pylint src/
format:
$(PYTHON) -m black src/ tests/
$(PYTHON) -m isort src/ tests/
clean:
$(RM) -r build/ dist/ *.egg-info/
$(RM) -r .pytest_cache/ .coverage htmlcov/
find . -type d -name '__pycache__' -exec rm -r {} +
find . -type f -name '*.pyc' -delete
Pattern 7: Multi-Binary Project
PROJECT := tools
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
# Multiple programs
PROGRAMS := tool1 tool2 tool3
TARGETS := $(addprefix $(BUILDDIR)/,$(PROGRAMS))
# Common library
LIBSRC := $(wildcard $(SRCDIR)/common/*.c)
LIBOBJ := $(LIBSRC:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
.PHONY: all clean $(PROGRAMS)
all: $(TARGETS)
# Individual program targets
tool1: $(BUILDDIR)/tool1
tool2: $(BUILDDIR)/tool2
tool3: $(BUILDDIR)/tool3
# Build each program
$(BUILDDIR)/tool1: $(OBJDIR)/tool1.o $(LIBOBJ)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(BUILDDIR)/tool2: $(OBJDIR)/tool2.o $(LIBOBJ)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(BUILDDIR)/tool3: $(OBJDIR)/tool3.o $(LIBOBJ)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
# Compile pattern
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
clean:
$(RM) -r $(BUILDDIR)
Pattern 8: Docker Integration
PROJECT := myapp
VERSION := 1.0.0
REGISTRY := docker.io
IMAGE := $(REGISTRY)/$(PROJECT):$(VERSION)
IMAGE_LATEST := $(REGISTRY)/$(PROJECT):latest
.PHONY: all build docker-build docker-push docker-run docker-clean
all: build
build:
$(MAKE) -f Makefile.app
docker-build:
docker build -t $(IMAGE) -t $(IMAGE_LATEST) .
docker-push: docker-build
docker push $(IMAGE)
docker push $(IMAGE_LATEST)
docker-run: docker-build
docker run --rm -it $(IMAGE)
docker-clean:
docker rmi $(IMAGE) $(IMAGE_LATEST) 2>/dev/null || true
Advanced Patterns
Pattern: Recursive Directory Processing
# Find all .c files recursively
SOURCES := $(shell find src -name '*.c')
# Mirror directory structure in build/
OBJECTS := $(SOURCES:src/%.c=build/obj/%.o)
# Create all necessary directories
OBJDIRS := $(sort $(dir $(OBJECTS)))
$(OBJDIRS):
@mkdir -p $@
# Order-only prerequisite: directories must exist
$(OBJECTS): | $(OBJDIRS)
build/obj/%.o: src/%.c
$(CC) $(CFLAGS) -c $< -o $@
Pattern: Multiple Build Configurations
BUILD_TYPES := debug release profile
.PHONY: all $(BUILD_TYPES) clean
all: release
# Target-specific variables
debug: CFLAGS += -g -O0 -DDEBUG
debug: TARGET := build/debug/$(PROJECT)
debug: $(TARGET)
release: CFLAGS += -O3 -DNDEBUG
release: TARGET := build/release/$(PROJECT)
release: $(TARGET)
profile: CFLAGS += -pg -O2
profile: TARGET := build/profile/$(PROJECT)
profile: $(TARGET)
# Generic build rule
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
Pattern: Parallel Sub-Builds
SUBDIRS := lib1 lib2 lib3
.PHONY: all $(SUBDIRS)
# Build subdirs in parallel: make -j4
all: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
# Dependencies between subdirs
lib2: lib1
lib3: lib1 lib2
Best Practices
- Use static pattern rules for known file lists (more efficient)
- Generate dependencies automatically (-MMD -MP)
- Create directories with order-only prerequisites
- Disable built-in rules (.SUFFIXES:) for explicit Makefiles
- Use pattern rules for generic transformations
- Mirror source structure in build directory
- Separate C and C++ compilation in mixed projects
- Use -fPIC when building shared libraries
- Include generated dependencies with -include
- Test patterns with make -n (dry run)
References
Makefile Security Guide
Overview
This guide covers security best practices for Makefiles, including secrets management, input validation, shell injection prevention, and CI/CD security considerations.
Secrets Management
Never Commit Secrets
DO NOT hardcode credentials, API keys, or passwords in Makefiles:
# WRONG: Hardcoded credentials
DB_PASSWORD := mysecretpassword
AWS_SECRET := AKIAIOSFODNN7EXAMPLE
Use Environment Variables
Pass secrets via environment variables:
# CORRECT: Environment variable with validation
DB_PASSWORD ?= $(error DB_PASSWORD is not set)
AWS_SECRET_KEY ?= $(error AWS_SECRET_KEY is not set)
deploy:
@echo "Deploying with credentials from environment..."
./deploy.sh
Use .env Files (Not in Git)
# Include .env file if it exists (never commit .env!)
-include .env
export
# Ensure .gitignore contains .env
.PHONY: check-env
check-env:
@grep -q '^\.env$$' .gitignore || echo "WARNING: Add .env to .gitignore!"
Secrets from External Sources
AWS Secrets Manager:
# Fetch secret at runtime, don't cache in Makefile variables
deploy:
@DB_PASSWORD=$$(aws secretsmanager get-secret-value \
--secret-id prod/db/password \
--query SecretString --output text) && \
./deploy.sh
HashiCorp Vault:
deploy:
@DB_PASSWORD=$$(vault kv get -field=password secret/database) && \
./deploy.sh
Shell Injection Prevention
Input Validation
Always validate user-provided variables:
# Validate PROJECT_NAME contains only safe characters
PROJECT_NAME := $(strip $(PROJECT_NAME))
ifneq ($(PROJECT_NAME),$(shell echo '$(PROJECT_NAME)' | tr -cd 'a-zA-Z0-9_-'))
$(error PROJECT_NAME contains invalid characters. Use only [a-zA-Z0-9_-])
endif
Quote Variables in Shell Commands
# WRONG: Unquoted variables - vulnerable to injection
process:
./script.sh $(USER_INPUT)
# CORRECT: Quoted variables
process:
./script.sh '$(USER_INPUT)'
Avoid Shell Expansion of User Input
# WRONG: Shell will interpret special characters
echo-input:
@echo $(MESSAGE)
# SAFER: Use printf with proper quoting
echo-input:
@printf '%s\n' '$(MESSAGE)'
Dangerous Patterns to Avoid
# NEVER do this - allows arbitrary command execution
run-command:
$(USER_COMMAND)
# NEVER pipe untrusted input to shell
execute:
echo $(INPUT) | sh
# NEVER use eval with user input
eval-input:
@eval $(USER_INPUT)
Variable Expansion Security
Simple vs Recursive Expansion
# Use := for values that shouldn't be re-evaluated
SAFE_VALUE := $(shell whoami)
# = causes re-evaluation each time - potential for injection
# if EXTERNAL_VAR changes after assignment
UNSAFE_VALUE = $(EXTERNAL_VAR)
Dollar Sign Escaping
When working with passwords containing $:
# Password with $ sign - double the $ to escape
# If password is "pa$$word", set it as:
PASSWORD := pa$$$$word
# Or read from file where $ is already escaped
PASSWORD := $(shell cat .password | sed 's/\$$/\$\$\$\$/g')
File System Security
Path Traversal Prevention
# WRONG: User can specify "../../../etc/passwd"
read-file:
cat $(FILE_PATH)
# SAFER: Validate path is within expected directory
SAFE_DIR := ./data
read-file:
@case "$(FILE_PATH)" in \
$(SAFE_DIR)/*) cat "$(FILE_PATH)" ;; \
*) echo "ERROR: Invalid path" >&2; exit 1 ;; \
esac
Secure Temporary Files
# Use mktemp for secure temporary files
process:
@TMPFILE=$$(mktemp) && \
trap 'rm -f "$$TMPFILE"' EXIT && \
./generate-config > "$$TMPFILE" && \
./process-config "$$TMPFILE"
File Permission Handling
# Set restrictive permissions on sensitive files
install-config:
install -m 600 config.secret $(DESTDIR)/etc/myapp/
# Create directories with appropriate permissions
install-dirs:
install -d -m 700 $(DESTDIR)/var/lib/myapp/secrets
CI/CD Security
Avoid Logging Secrets
# WRONG: Password visible in logs
deploy:
curl -u user:$(PASSWORD) https://api.example.com
# CORRECT: Suppress command echo
deploy:
@curl -u user:$(PASSWORD) https://api.example.com
# BEST: Use credential helper
deploy:
@curl --netrc-file ~/.netrc https://api.example.com
Fail Securely
# Use strict mode
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
# Ensure sensitive operations fail closed
deploy:
@test -n "$(API_KEY)" || { echo "ERROR: API_KEY not set" >&2; exit 1; }
@./deploy.sh
Environment Isolation
# Don't inherit all environment variables
# Only export what's needed
unexport HISTFILE
unexport AWS_SESSION_TOKEN
# Explicitly export required variables
export PATH
export HOME
Audit Logging
AUDIT_LOG := /var/log/makefile-audit.log
audit-log = @echo "$$(date -Iseconds) [$(1)] $(2)" >> $(AUDIT_LOG)
deploy: check-permissions
$(call audit-log,DEPLOY,Starting deployment by $$USER)
./deploy.sh
$(call audit-log,DEPLOY,Deployment completed)
Network Security
Secure Downloads
# Always verify downloads
CHECKSUM_FILE := checksums.sha256
download:
curl -fsSL -o package.tar.gz https://example.com/package.tar.gz
sha256sum -c $(CHECKSUM_FILE)
# Or use GPG verification
download-verified:
curl -fsSL -o package.tar.gz https://example.com/package.tar.gz
curl -fsSL -o package.tar.gz.asc https://example.com/package.tar.gz.asc
gpg --verify package.tar.gz.asc package.tar.gz
TLS/HTTPS Only
# Force HTTPS for all downloads
CURL_OPTS := --proto '=https' --tlsv1.2
download:
curl $(CURL_OPTS) -fsSL -o file.txt https://example.com/file.txt
Container Security
Don’t Build as Root
docker-build:
docker build --build-arg USER_ID=$$(id -u) --build-arg GROUP_ID=$$(id -g) -t myapp .
# In Dockerfile, create non-root user
Scan Images for Vulnerabilities
IMAGE := myapp:latest
.PHONY: docker-scan
docker-scan: docker-build
@if command -v trivy >/dev/null 2>&1; then \
trivy image --exit-code 1 --severity HIGH,CRITICAL $(IMAGE); \
else \
echo "WARNING: trivy not found, skipping security scan"; \
fi
Don’t Pass Secrets as Build Args
# WRONG: Secret visible in image layers
docker-build:
docker build --build-arg API_KEY=$(API_KEY) -t myapp .
# CORRECT: Use build secrets (BuildKit)
docker-build:
DOCKER_BUILDKIT=1 docker build \
--secret id=api_key,src=.api_key \
-t myapp .
Secure Defaults
Modern Makefile Preamble
# Secure and strict Makefile configuration
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
# Prevent accidental exposure
unexport HISTFILE
Require Explicit Targets
# Prevent running all targets by accident
.PHONY: all
all:
@echo "Please specify a target. Run 'make help' for options."
@exit 1
Security Checklist
Before committing a Makefile:
- No hardcoded credentials, API keys, or passwords
- Secrets loaded from environment or secret manager
-
.envfile listed in.gitignore - User input is validated before use
- Shell commands use proper quoting
- No use of
evalwith external input - Downloads verified with checksums or signatures
- Sensitive commands prefixed with
@to hide from logs - Temporary files created securely and cleaned up
- File permissions are appropriately restrictive
- Container builds don’t expose secrets in layers
References
Makefile Targets Guide
Overview
This guide covers target definitions, standard GNU targets, .PHONY declarations, dependencies, pattern rules, and best practices for organizing targets in Makefiles.
Target Basics
Target Syntax
target: prerequisites
recipe
recipe
...
- target: File to create or action to perform
- prerequisites: Files/targets that must exist or be up-to-date
- recipe: Shell commands to run (must be indented with TAB)
Simple Example
hello: hello.c
gcc hello.c -o hello
How it works:
- Check if
helloexists and is newer thanhello.c - If not, run the recipe
- Recipe creates
hello
.PHONY Targets
What are .PHONY Targets?
Phony targets don’t represent actual files. They represent actions:
.PHONY: clean
clean:
rm -f *.o myprogram
Without .PHONY:
- If a file named “clean” exists,
make cleanwon’t run - make thinks the target is already up-to-date
With .PHONY:
- make always runs the recipe
- Performance improvement (skips unnecessary file system checks)
Common .PHONY Targets
.PHONY: all clean install uninstall test check help
.PHONY: build dist distclean format lint docs
Multiple .PHONY Declarations
# Option 1: All at once (recommended)
.PHONY: all clean install test help
# Option 2: Separate declarations
.PHONY: all
.PHONY: clean
.PHONY: install
Standard GNU Targets
GNU Coding Standards define standard targets that users expect:
Essential Targets
all (Default Target)
## Build all targets
.PHONY: all
all: $(TARGET)
Requirements:
- Should be the first target (default)
- Should compile the entire program
- Should NOT install, clean, or run tests
- Should create the primary output (executable, library, etc.)
install
## Install built files to PREFIX
.PHONY: install
install: all
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(INSTALL_PROGRAM) $(TARGET) $(DESTDIR)$(BINDIR)/
$(INSTALL) -d $(DESTDIR)$(LIBDIR)
$(INSTALL_DATA) lib$(PROJECT).a $(DESTDIR)$(LIBDIR)/
$(INSTALL) -d $(DESTDIR)$(INCLUDEDIR)
$(INSTALL_DATA) $(PROJECT).h $(DESTDIR)$(INCLUDEDIR)/
$(INSTALL) -d $(DESTDIR)$(MAN1DIR)
$(INSTALL_DATA) docs/$(PROJECT).1 $(DESTDIR)$(MAN1DIR)/
Requirements:
- Should depend on
allto build first - Should respect
DESTDIRandPREFIX - Should create necessary directories
- Should set appropriate permissions
- Should be idempotent (safe to run multiple times)
uninstall
## Remove installed files
.PHONY: uninstall
uninstall:
$(RM) $(DESTDIR)$(BINDIR)/$(TARGET)
$(RM) $(DESTDIR)$(LIBDIR)/lib$(PROJECT).a
$(RM) $(DESTDIR)$(INCLUDEDIR)/$(PROJECT).h
$(RM) $(DESTDIR)$(MAN1DIR)/$(PROJECT).1
clean
## Remove built files (keep configuration)
.PHONY: clean
clean:
$(RM) $(OBJECTS) $(TARGET)
$(RM) -r $(BUILDDIR)
$(RM) *.o *.a *.so
Requirements:
- Remove object files, executables, libraries
- Keep configuration files and Makefile
- Should allow rebuilding without reconfiguring
distclean
## Remove all generated files (including configuration)
.PHONY: distclean
distclean: clean
$(RM) config.h config.log config.status
$(RM) Makefile
$(RM) -r autom4te.cache/
Requirements:
- Should depend on
clean - Remove configure-generated files
- Leave only source files
- After distclean, only
./configureshould work
Testing Targets
test
## Run tests
.PHONY: test
test: $(TARGET)
@echo "Running tests..."
./run_tests.sh
$(TARGET) --self-test
@echo "All tests passed!"
check
## Alias for test (GNU convention)
.PHONY: check
check: test
Distribution Targets
dist
## Create distribution tarball
.PHONY: dist
dist:
@mkdir -p dist
tar -czf dist/$(PROJECT)-$(VERSION).tar.gz \
--transform 's,^,$(PROJECT)-$(VERSION)/,' \
--exclude='.git*' \
--exclude='*.o' \
--exclude='*.a' \
--exclude='$(BUILDDIR)' \
.
distcheck
## Create and verify distribution tarball
.PHONY: distcheck
distcheck: dist
@echo "Verifying distribution..."
@mkdir -p $(BUILDDIR)/distcheck
tar -xzf dist/$(PROJECT)-$(VERSION).tar.gz -C $(BUILDDIR)/distcheck
cd $(BUILDDIR)/distcheck/$(PROJECT)-$(VERSION) && ./configure && make && make check
@echo "Distribution verified successfully!"
$(RM) -r $(BUILDDIR)/distcheck
Documentation Targets
help
## Show available targets and usage
.PHONY: help
help:
@echo "$(PROJECT) - version $(VERSION)"
@echo ""
@echo "Available targets:"
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed 's/^/ /'
@echo ""
@echo "Variables:"
@echo " PREFIX=$(PREFIX)"
@echo " CC=$(CC)"
@echo " CFLAGS=$(CFLAGS)"
@echo ""
@echo "Examples:"
@echo " make # Build the project"
@echo " make install # Install to PREFIX"
@echo " make PREFIX=/opt/local # Install to custom location"
@echo " make clean # Remove built files"
Self-documenting pattern:
## Build the application
.PHONY: build
build: $(TARGET)
## Run all tests
.PHONY: test
test:
./run_tests.sh
The ## comments are parsed by the help target.
Target Dependencies
Simple Dependencies
# program depends on main.o and utils.o
program: main.o utils.o
$(CC) $^ -o $@
# main.o depends on main.c
main.o: main.c
$(CC) -c $< -o $@
Multiple Targets
# Multiple targets with same recipe
main.o utils.o helper.o: common.h
$(CC) -c $*.c -o $@
Order-Only Prerequisites
# Normal prerequisites: update target if prerequisite changes
# Order-only prerequisites (|): only check existence, not timestamp
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) -c $< -o $@
# $(OBJDIR) must exist, but changes to it don't trigger rebuild
$(OBJDIR):
mkdir -p $@
Use cases:
- Directory creation (directory timestamp changes don’t matter)
- Lock files
- State files
Circular Dependencies
# WRONG: Circular dependency
a: b
b: a
# Error: Circular a <- b dependency dropped
# RIGHT: Break the cycle
a: c
b: c
c:
touch c
Pattern Rules
Basic Pattern Rules
# Compile .c to .o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Compile .c to .s (assembly)
%.s: %.c
$(CC) $(CFLAGS) -S $< -o $@
# Create .c from .y (yacc)
%.c: %.y
$(YACC) $(YFLAGS) -o $@ $<
Pattern Rules with Directories
# Match files in specific directories
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
# Multiple directory patterns
build/obj/%.o: src/%.c
$(CC) -c $< -o $@
build/obj/%.o: lib/%.c
$(CC) -c $< -o $@
Static Pattern Rules
More efficient than pattern rules for specific files:
OBJECTS := main.o utils.o helper.o
# Static pattern: $(targets): target-pattern: prereq-pattern
$(OBJECTS): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Equivalent to:
# main.o: main.c
# utils.o: utils.c
# helper.o: helper.c
Advantages:
- More explicit than pattern rules
- Faster (make knows exact targets)
- Easier to debug
Multiple Pattern Rules
# First matching rule is used
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
%.o: %.s
$(AS) $(ASFLAGS) -c $< -o $@
%.o: %.asm
$(NASM) $(NASMFLAGS) -f elf64 $< -o $@
Target-Specific Variables
# Set variable only for specific target and its prerequisites
debug: CFLAGS += -g -O0 -DDEBUG
debug: $(TARGET)
release: CFLAGS += -O3 -DNDEBUG
release: $(TARGET)
# Pattern-specific variables
test_%: CFLAGS += -DTESTING
test_%: LDLIBS += -lcheck
Advanced Target Patterns
Double-Colon Rules
# Double-colon allows multiple recipes for same target
install::
@echo "Installing binaries..."
$(INSTALL) $(TARGET) $(BINDIR)/
install::
@echo "Installing libraries..."
$(INSTALL) lib$(PROJECT).a $(LIBDIR)/
# Each recipe runs independently
Use cases:
- Modular Makefiles with independent install steps
- Plugin systems
- Rarely needed
Intermediate Files
# Mark files as intermediate (deleted after use)
.INTERMEDIATE: $(OBJECTS)
# Keep specific intermediate files
.SECONDARY: important.o
# Never delete these intermediate files
.PRECIOUS: %.o
Automatic Prerequisites
# Generate dependencies automatically
DEPENDS := $(OBJECTS:.o=.d)
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
# Include generated dependencies
-include $(DEPENDS)
Complete Examples
Example 1: Simple C Project
.DELETE_ON_ERROR:
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
TARGET := hello
SOURCES := hello.c
OBJECTS := $(SOURCES:.c=.o)
.PHONY: all clean install uninstall help
## Build the program (default)
all: $(TARGET)
## Compile and link
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.c
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
## Install to PREFIX
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/
## Remove installed files
uninstall:
$(RM) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
## Remove built files
clean:
$(RM) $(OBJECTS) $(TARGET)
## Show this help
help:
@echo "Available targets:"
@sed -n 's/^## //p' $(MAKEFILE_LIST)
Example 2: Multi-Directory Project
.DELETE_ON_ERROR:
PROJECT := myapp
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
.PHONY: all clean install test help
## Build everything (default)
all: $(TARGET)
## Link executable
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
## Compile source files
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
## Install to PREFIX
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(PROJECT)
## Run tests
test: $(TARGET)
@echo "Running tests..."
@$(TARGET) --test
## Remove build artifacts
clean:
$(RM) -r $(BUILDDIR)
## Show available targets
help:
@sed -n 's/^## //p' $(MAKEFILE_LIST)
Example 3: Library Project
.DELETE_ON_ERROR:
PROJECT := mylib
VERSION := 1.0.0
CC ?= gcc
AR ?= ar
RANLIB ?= ranlib
PREFIX ?= /usr/local
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
HEADERS := $(wildcard $(SRCDIR)/*.h)
STATIC_LIB := $(BUILDDIR)/lib$(PROJECT).a
SHARED_LIB := $(BUILDDIR)/lib$(PROJECT).so.$(VERSION)
.PHONY: all static shared clean install install-static install-shared
## Build both static and shared libraries
all: static shared
## Build static library
static: $(STATIC_LIB)
## Build shared library
shared: $(SHARED_LIB)
## Create static library
$(STATIC_LIB): $(OBJECTS)
@mkdir -p $(@D)
@echo " AR $@"
$(AR) rcs $@ $^
$(RANLIB) $@
## Create shared library
$(SHARED_LIB): $(OBJECTS)
@mkdir -p $(@D)
@echo " LD $@"
$(CC) -shared -Wl,-soname,lib$(PROJECT).so.1 $^ -o $@
ln -sf lib$(PROJECT).so.$(VERSION) $(BUILDDIR)/lib$(PROJECT).so.1
ln -sf lib$(PROJECT).so.1 $(BUILDDIR)/lib$(PROJECT).so
## Compile with -fPIC for shared library
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
@echo " CC $<"
$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -c $< -o $@
## Install both libraries
install: install-static install-shared
## Install static library
install-static: $(STATIC_LIB)
install -d $(DESTDIR)$(PREFIX)/lib
install -m 644 $(STATIC_LIB) $(DESTDIR)$(PREFIX)/lib/
install -d $(DESTDIR)$(PREFIX)/include/$(PROJECT)
install -m 644 $(HEADERS) $(DESTDIR)$(PREFIX)/include/$(PROJECT)/
## Install shared library
install-shared: $(SHARED_LIB)
install -d $(DESTDIR)$(PREFIX)/lib
install -m 755 $(SHARED_LIB) $(DESTDIR)$(PREFIX)/lib/
ln -sf lib$(PROJECT).so.$(VERSION) $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).so.1
ln -sf lib$(PROJECT).so.1 $(DESTDIR)$(PREFIX)/lib/lib$(PROJECT).so
ldconfig
## Clean build artifacts
clean:
$(RM) -r $(BUILDDIR)
Best Practices
- Use .PHONY for non-file targets
- Implement standard GNU targets (all, install, clean, test)
- Make ‘all’ the default target (first target)
- Use automatic variables ($@, $<, $^) for concise rules
- Create directories automatically (@mkdir -p $(@D))
- Document targets with ## comments
- Use static pattern rules for better performance
- Include .DELETE_ON_ERROR to prevent corrupted builds
- Use order-only prerequisites for directory creation
- Generate dependencies automatically (-MMD -MP)
References
Makefile Variables Guide
Overview
This guide covers variable definition, assignment operators, automatic variables, standard GNU variables, and best practices for variable management in Makefiles.
Variable Assignment Operators
Make supports four assignment operators, each with different behavior:
1. Recursive Assignment (=)
# Evaluated every time the variable is used
FILES = $(wildcard *.c)
OBJECTS = $(FILES:.c=.o)
# Each use of $(OBJECTS) re-expands $(FILES) and re-runs wildcard
target: $(OBJECTS)
$(CC) $(OBJECTS) -o target
Characteristics:
- Right-hand side evaluated every time variable is used
- Can reference variables defined later
- Can cause infinite recursion
- Less efficient for frequently-used variables
When to use:
- Variables that reference other variables that might change
- Simple string values
- When you need delayed evaluation
2. Simple Assignment (:=)
# Evaluated once when defined
FILES := $(wildcard *.c)
OBJECTS := $(FILES:.c=.o)
# $(OBJECTS) contains the fixed list from definition time
target: $(OBJECTS)
$(CC) $(OBJECTS) -o target
Characteristics:
- Right-hand side evaluated immediately
- More efficient for computed values
- Cannot reference variables defined later
- Prevents infinite recursion
When to use:
- Variables with computed values (wildcards, substitutions)
- Frequently-referenced variables
- Most project-specific variables (SOURCES, OBJECTS, TARGET)
3. Conditional Assignment (?=)
# Only assigns if variable is not already defined
CC ?= gcc
CFLAGS ?= -Wall -O2
PREFIX ?= /usr/local
# Users can override:
# make CC=clang
# export CC=clang; make
Characteristics:
- Assigns only if variable undefined or empty
- Respects environment variables
- Respects command-line overrides
- Essential for user-configurable variables
When to use:
- User-overridable variables (CC, CFLAGS, PREFIX)
- Default values that users might want to change
- All tool and configuration variables
4. Append Assignment (+=)
# Append to existing value
CFLAGS ?= -Wall -O2
CFLAGS += -I./include
CFLAGS += -DDEBUG
# Result: CFLAGS = -Wall -O2 -I./include -DDEBUG
Characteristics:
- Adds to existing variable value
- Preserves type of original assignment (= vs :=)
- Automatically adds space between values
When to use:
- Adding project-specific flags to user flags
- Building lists incrementally
- Extending default values
Comparison of Assignment Operators
# Recursive (=): Re-evaluated each use
VAR1 = $(shell date)
# VAR1 changes every time it's used!
# Simple (:=): Evaluated once
VAR2 := $(shell date)
# VAR2 is fixed to the date when defined
# Conditional (?=): Only if undefined
VAR3 ?= default
# VAR3 = "default" unless already set
# Append (+=): Add to existing
VAR4 := first
VAR4 += second
# VAR4 = "first second"
Standard GNU Variables
GNU Coding Standards define standard variable names that should be used:
Compiler and Tools
# C Compiler
CC ?= gcc
# C++ Compiler
CXX ?= g++
# Linker (usually same as CC)
LD ?= $(CC)
# Archiver (for creating .a libraries)
AR ?= ar
# Ranlib (for indexing .a libraries)
RANLIB ?= ranlib
# Install program
INSTALL ?= install
# Install program for data files
INSTALL_DATA ?= $(INSTALL) -m 644
# Install program for executables
INSTALL_PROGRAM ?= $(INSTALL) -m 755
# Remove files
RM ?= rm -f
# Yacc/Bison
YACC ?= bison -y
# Lex/Flex
LEX ?= flex
# pkg-config
PKG_CONFIG ?= pkg-config
Compiler Flags
# C Preprocessor flags (for includes, defines)
CPPFLAGS ?=
# C Compiler flags
CFLAGS ?= -Wall -Wextra -O2
# C++ Compiler flags
CXXFLAGS ?= -Wall -Wextra -O2
# Linker flags (for library paths, etc.)
LDFLAGS ?=
# Libraries to link (-lname)
LDLIBS ?=
# Yacc/Bison flags
YFLAGS ?=
# Lex/Flex flags
LFLAGS ?=
Best practices for flags:
# Preserve user-defined flags
CFLAGS ?= -Wall -Wextra -O2
# Add project-specific flags
CFLAGS += -I./include -I./src
CFLAGS += -DPROJECT_VERSION=\"$(VERSION)\"
# Use pkg-config for libraries
CFLAGS += $(shell $(PKG_CONFIG) --cflags openssl)
LDLIBS += $(shell $(PKG_CONFIG) --libs openssl)
Installation Directories
# Installation prefix
PREFIX ?= /usr/local
# Executable prefix (usually same as PREFIX)
EXEC_PREFIX ?= $(PREFIX)
# Binary directory
BINDIR ?= $(EXEC_PREFIX)/bin
# Library directory
LIBDIR ?= $(EXEC_PREFIX)/lib
# Include directory
INCLUDEDIR ?= $(PREFIX)/include
# Data root directory
DATAROOTDIR ?= $(PREFIX)/share
# Read-only data directory
DATADIR ?= $(DATAROOTDIR)
# System configuration directory
SYSCONFDIR ?= $(PREFIX)/etc
# Variable data directory
LOCALSTATEDIR ?= $(PREFIX)/var
# Man pages directory
MANDIR ?= $(DATAROOTDIR)/man
MAN1DIR ?= $(MANDIR)/man1
MAN2DIR ?= $(MANDIR)/man2
# ... etc
# Info pages directory
INFODIR ?= $(DATAROOTDIR)/info
# Documentation directory
DOCDIR ?= $(DATAROOTDIR)/doc/$(PROJECT)
# DESTDIR for staged installations (package building)
DESTDIR ?=
Usage in install target:
install: $(TARGET)
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(INSTALL_PROGRAM) $(TARGET) $(DESTDIR)$(BINDIR)/
$(INSTALL) -d $(DESTDIR)$(LIBDIR)
$(INSTALL_DATA) lib$(PROJECT).a $(DESTDIR)$(LIBDIR)/
$(INSTALL) -d $(DESTDIR)$(MAN1DIR)
$(INSTALL_DATA) docs/$(PROJECT).1 $(DESTDIR)$(MAN1DIR)/
Automatic Variables
Automatic variables are set by make for each rule:
Basic Automatic Variables
| Variable | Description | Example |
|---|---|---|
$@ |
Target file name | hello in rule for hello |
$< |
First prerequisite | hello.c in hello.o: hello.c |
$^ |
All prerequisites (no duplicates) | hello.o utils.o |
$+ |
All prerequisites (with duplicates) | Rarely needed |
$? |
Prerequisites newer than target | For conditional rebuild |
$* |
Stem of pattern match | hello in %.o: %.c |
Directory and File Components
| Variable | Description |
|---|---|
$(@D) |
Directory part of $@ |
$(@F) |
File part of $@ |
$(<D) |
Directory part of $< |
$(<F) |
File part of $< |
$(*D) |
Directory part of $* |
$(*F) |
File part of $* |
$(^D) |
Directory parts of $^ |
$(^F) |
File parts of $^ |
Examples
# Basic usage
hello: hello.o utils.o
$(CC) $(LDFLAGS) $^ -o $@
# Expands to: gcc -o hello hello.o utils.o
# Pattern rule
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# For hello.c: gcc -Wall -c hello.c -o hello.o
# Creating output directory
build/%.o: src/%.c
@mkdir -p $(@D)
$(CC) $(CFLAGS) -c $< -o $@
# $(@D) = "build"
Advanced Automatic Variables Usage
# Dependency generation with automatic variables
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) \
-MMD -MP \
-MF $(@:.o=.d) \
-c $< -o $@
# $@ = build/obj/main.o
# $< = src/main.c
# $(@:.o=.d) = build/obj/main.d
Variable Substitution and Functions
Pattern Substitution
# $(var:pattern=replacement)
SOURCES := src/main.c src/utils.c src/helper.c
OBJECTS := $(SOURCES:.c=.o)
# OBJECTS = src/main.o src/utils.o src/helper.o
OBJECTS := $(SOURCES:src/%.c=build/%.o)
# OBJECTS = build/main.o build/utils.o build/helper.o
Text Functions
# $(wildcard pattern)
SOURCES := $(wildcard src/*.c)
# $(patsubst pattern,replacement,text)
OBJECTS := $(patsubst %.c,%.o,$(SOURCES))
# $(filter pattern...,text)
C_FILES := $(filter %.c,$(SOURCES))
# $(filter-out pattern...,text)
NO_TEST := $(filter-out %_test.c,$(SOURCES))
# $(sort list)
SORTED := $(sort $(SOURCES))
# $(dir names...)
DIRS := $(dir $(SOURCES))
# $(notdir names...)
FILES := $(notdir $(SOURCES))
# $(basename names...)
NAMES := $(basename $(SOURCES))
# $(suffix names...)
EXTS := $(suffix $(SOURCES))
# $(addprefix prefix,names...)
FULL_PATHS := $(addprefix $(SRCDIR)/,$(FILES))
# $(addsuffix suffix,names...)
OBJ_FILES := $(addsuffix .o,$(NAMES))
Shell Function
# $(shell command)
GIT_VERSION := $(shell git describe --tags --always 2>/dev/null)
DATE := $(shell date +%Y-%m-%d)
CPU_COUNT := $(shell nproc 2>/dev/null || echo 1)
# Use := to evaluate once
VERSION := $(shell cat VERSION.txt)
Conditional Functions
# $(if condition,then-part,else-part)
DEBUG := 1
CFLAGS := $(if $(DEBUG),-g -O0,-O2)
# $(or conditions...)
CC := $(or $(CC),gcc)
# $(and conditions...)
BUILD_TESTS := $(and $(ENABLE_TESTS),$(HAVE_CHECK))
Environment Variables
Interaction with Environment
# Make variables override environment by default
CC = gcc # Overrides CC from environment
# Use ?= to respect environment
CC ?= gcc # Uses environment CC if set
# Export variables to recipes
export CC
export CFLAGS
# Unexport variables
unexport INTERNAL_VAR
Checking Environment Variables
# Check if variable is defined
ifndef CC
CC := gcc
endif
# Check if variable is empty
ifeq ($(strip $(CC)),)
$(error CC is not defined)
endif
Target-Specific Variables
# Variables can be set for specific targets
debug: CFLAGS += -g -O0 -DDEBUG
debug: $(TARGET)
release: CFLAGS += -O3 -DNDEBUG
release: $(TARGET)
# Pattern-specific variables
tests/%: CFLAGS += -DTESTING
tests/%: LDLIBS += -lcheck
Best Practices
1. Variable Naming
# Use UPPERCASE for user-overridable variables
CC ?= gcc
PREFIX ?= /usr/local
# Use lowercase or mixed case for internal variables
sources := $(wildcard src/*.c)
target_name := myapp
# Use descriptive names
SOURCES := $(wildcard src/*.c) # Good
S := $(wildcard src/*.c) # Bad
2. Variable Organization
# Group related variables
# ============================================
# User Configuration
# ============================================
CC ?= gcc
CFLAGS ?= -Wall -O2
PREFIX ?= /usr/local
# ============================================
# Project Configuration
# ============================================
PROJECT := myapp
VERSION := 1.0.0
SOURCES := $(wildcard src/*.c)
3. Preserve User Flags
# WRONG: Overwrites user flags
CFLAGS = -Wall -O2
# RIGHT: Provides default, respects user override
CFLAGS ?= -Wall -O2
# Add project-specific flags
CFLAGS += -I./include
4. Use pkg-config
# WRONG: Hardcoded paths
CFLAGS += -I/usr/include/openssl
LDLIBS += -L/usr/lib -lssl -lcrypto
# RIGHT: Use pkg-config
PKG_CONFIG ?= pkg-config
CFLAGS += $(shell $(PKG_CONFIG) --cflags openssl)
LDLIBS += $(shell $(PKG_CONFIG) --libs openssl)
5. Use := for Computed Values
# WRONG: Re-computes every use (slow)
SOURCES = $(wildcard src/*.c)
OBJECTS = $(SOURCES:.c=.o)
# RIGHT: Computes once (fast)
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
Complete Example
# ============================================
# User-Overridable Variables
# ============================================
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
LDFLAGS ?=
LDLIBS ?=
PREFIX ?= /usr/local
DESTDIR ?=
# ============================================
# pkg-config Dependencies
# ============================================
PKG_CONFIG ?= pkg-config
PACKAGES := openssl zlib
CFLAGS += $(shell $(PKG_CONFIG) --cflags $(PACKAGES))
LDLIBS += $(shell $(PKG_CONFIG) --libs $(PACKAGES))
# ============================================
# Project Configuration
# ============================================
PROJECT := myapp
VERSION := 1.0.0
SRCDIR := src
BUILDDIR := build
# Computed values (use :=)
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
# Git version
GIT_VERSION := $(shell git describe --tags --always 2>/dev/null || echo "unknown")
CFLAGS += -DVERSION=\"$(GIT_VERSION)\"
# ============================================
# Build Rules
# ============================================
all: $(TARGET)
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(BUILDDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
# ============================================
# Target-Specific Variables
# ============================================
debug: CFLAGS += -g -O0 -DDEBUG
debug: $(TARGET)
release: CFLAGS += -O3 -DNDEBUG -s
release: $(TARGET)