
Makefile Generator
- 391 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
makefile-generator is an agent skill that generates production-ready Makefiles with GNU-standard install, test, lint, build, and deploy targets for developers who need consistent local and CI build automation across C, G
About
makefile-generator is an akin-ozer/cc-devops-skills agent skill that creates production-ready Makefiles with .PHONY targets, GNU Coding Standards compliance, security hardening, and optional validation via the companion makefile-validator skill. It supports C/C++, Python, Go, and Java projects through a three-stage workflow: gather language and target requirements, consult six internal docs (structure, variables, targets, patterns, optimization, security guides), then generate the Makefile with standard all, install, clean, test, dist, and help targets. Optional helper scripts include generate_makefile_template.sh and add_standard_targets.sh for fast paths. Docker integration, multi-binary layouts, cross-compilation, and ldflags version embedding are covered in patterns-guide.md. Developers reach for makefile-generator when asked to create, generate, or write a Makefile—not to debug builds or validate existing files.
- Common target scaffolding
- CI-friendly recipes
- Consistent dev CLI surface
- Lint, test, deploy shortcuts
Makefile Generator by the numbers
- 391 all-time installs (skills.sh)
- Ranked #305 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill makefile-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 391 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you generate a standard Makefile for CI pipelines?
Generate a repo Makefile standardizing install, test, lint, build, and deploy commands for local developers and CI pipelines.
Who is it for?
DevOps and application engineers bootstrapping build automation for new or legacy repos who want GNU-standard targets shared between local development and CI.
Skip if: Validating or linting an existing Makefile (use makefile-validator), debugging make -d output, or projects standardized on Bazel, Meson, or language-native build tools only.
When should I use this skill?
The user asks to create, generate, scaffold, or write a Makefile with install, test, lint, build, or deploy targets for a project.
What you get
Production-ready Makefile with .PHONY targets, documented header comments, and standard install, test, clean, build, and help commands.
- Makefile with standard targets
- Optional helper-script generated template
By the numbers
- Supports four project languages: C/C++, Python, Go, and Java
- References six internal documentation guides during generation
- Pairs with devops-skills:makefile-validator for post-generation validation
Files
Makefile Generator
Overview
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.
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
Do NOT use for: Validating existing Makefiles (use devops-skills:makefile-validator), debugging (use make -d), or running builds.
Trigger Phrases
Use this skill when prompts look like:
- "Generate a Makefile for a Go service"
- "Create a production Makefile with install/test/help targets"
- "Write a Makefile for a C project with dependency tracking"
- "Add standard GNU targets to this existing Makefile"
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 if any apply):
| Condition | Example Question |
|---|---|
| Language not specified | "What programming language is this project? (C/C++/Go/Python/Java)" |
| Project structure unclear | "Is this a single-directory or multi-directory project?" |
| Docker requested but registry unknown | "Which container registry should be used? (docker.io/ghcr.io/custom)" |
| Multiple binaries possible | "Should this build a single binary or multiple executables?" |
| Install targets needed but paths unclear | "Where should binaries be installed? (default: /usr/local/bin)" |
| Cross-compilation mentioned | "What is the target platform/architecture?" |
When to Skip AskUserQuestion (proceed with defaults):
- User explicitly provides all required information
- Standard project type with obvious defaults (e.g., "Go project with Docker" → use standard Go+Docker patterns)
- User says "use defaults" or "standard setup"
Default Assumptions (when not asking):
- Single-directory project structure
- 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):
1. ALWAYS consult internal docs first using explicit file-open commands (primary source of truth):
Full doc path map (prefer full paths for deterministic access):
| Doc | Full Path |
|---|---|
| Structure guide | devops-skills-plugin/skills/makefile-generator/docs/makefile-structure.md |
| Variables guide | devops-skills-plugin/skills/makefile-generator/docs/variables-guide.md |
| Targets guide | devops-skills-plugin/skills/makefile-generator/docs/targets-guide.md |
| Patterns guide | devops-skills-plugin/skills/makefile-generator/docs/patterns-guide.md |
| Optimization guide | devops-skills-plugin/skills/makefile-generator/docs/optimization-guide.md |
| Security guide | devops-skills-plugin/skills/makefile-generator/docs/security-guide.md |
| Requirement | Read This Doc |
|---|---|
| Docker/container targets | .../docs/patterns-guide.md (Pattern 8: Docker Integration) |
| Multi-binary projects | .../docs/patterns-guide.md (Pattern 7: Multi-Binary Project) |
| Go projects with version embedding | .../docs/patterns-guide.md (Pattern 5: Go Project) |
| Parallel builds, caching, ccache | .../docs/optimization-guide.md |
| Credentials, secrets, API keys | .../docs/security-guide.md |
| Complex dependencies, pattern rules | .../docs/patterns-guide.md |
| Order-only prerequisites | .../docs/optimization-guide.md or .../docs/targets-guide.md |
| Variables, assignment operators | .../docs/variables-guide.md |
Deterministic open/read commands:
# From repository root:
sed -n '1,220p' devops-skills-plugin/skills/makefile-generator/docs/patterns-guide.md
rg -n "Pattern 5|Pattern 8" devops-skills-plugin/skills/makefile-generator/docs/patterns-guide.md
# From skill directory:
sed -n '1,220p' docs/security-guide.mdIf shell commands are unavailable, use the environment's file-open/read capability on the same paths.
Required Workflow Example (Docker + Go with version embedding):
# Step 1: Read Go pattern
rg -n "Pattern 5" devops-skills-plugin/skills/makefile-generator/docs/patterns-guide.md
# Step 2: Read Docker pattern
rg -n "Pattern 8" devops-skills-plugin/skills/makefile-generator/docs/patterns-guide.md
# Step 3: Read security guidance
sed -n '1,220p' devops-skills-plugin/skills/makefile-generator/docs/security-guide.mdThen generate Makefile and list consulted docs in a header comment.
2. 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__query-docs: query="<integration-topic>"
# Example queries:
# - For Docker: query="dockerfile best practices"
# - For Go: query="go build ldflags"
# - For specific tools: query="<tool> makefile integration"Fallback: If context7 is unavailable or returns nothing useful, record that and continue to Step 3.
3. 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
Optional helper-script fast path (for standard layouts):
# Generate template: TYPE NAME OUTPUT
bash scripts/generate_makefile_template.sh go myservice Makefile
# Add only selected standard targets
bash scripts/add_standard_targets.sh Makefile install clean helpUse manual authoring when requirements are complex (Docker release flow, multi-binary matrices, custom toolchains).
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-rulesStandard 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
Validation is required for every generated Makefile.
Validation Tool Preflight (default + fallback)
1. Preferred path: run devops-skills:makefile-validator. 2. If validator skill is unavailable: run local fallback checks:
# Required fallback check (if make exists)
make -f <Makefile> -n --dry-run
# Structural fallback checks
rg -n '^ {1,}\S' <Makefile> # suspicious space-indented recipe lines
rg -n '^\.PHONY:' <Makefile>3. If `make` is unavailable: run structural checks only, report "partial validation due to missing make binary", and request user confirmation before claiming production readiness.
Required Validation Loop
1. Generate Makefile following stages above
2. Run validator skill (or fallback checks if unavailable)
3. Fix all errors (MUST have 0 errors before completion)
4. Apply formatting fixes (see "Formatting Step" below)
5. Fix warnings when feasible (SHOULD fix; explain if skipped)
6. Address info items for large/production projects
7. Re-run validation until checks pass
8. Output structured validation report (REQUIRED - see format below)Formatting Step (REQUIRED)
When mbake reports formatting issues, you MUST either:
1. Auto-apply formatting (preferred for minor issues):
mbake format <Makefile>2. 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]If mbake is not installed or not executable, skip formatter execution and record: Formatting skipped: mbake unavailable in current environment.
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) |
mbake command unavailable | Skip formatting, record tool-unavailable reason |
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, you MUST output a structured report in the following format. This is not optional.
Required Report Format:
## Validation Report
**Result:** [PASSED / PASSED with warnings / FAILED]
**Errors:** [count]
**Warnings:** [count]
**Info:** [count]
### Errors Fixed
- [List each error and how it was fixed, or "None" if 0 errors]
### Warnings Addressed
- [List each warning that was fixed]
### Warnings Skipped (with reasons)
- [List each warning that was NOT fixed and explain why]
- Example: "mbake reports '.DELETE_ON_ERROR' as unknown - this is a valid GNU Make
special target (false positive)"
### Formatting Applied
- [Yes/No] - [If No, explain why formatting was skipped]
### Info Items Addressed
- [List info items that were addressed for production Makefiles]
- [Or "N/A - simple project" if not applicable]
### Remaining Issues (if any)
- [List any issues requiring user attention]
- [Or "None - Makefile is production-ready"]Example Complete Report:
## Validation Report
**Result:** PASSED with warnings
**Errors:** 0
**Warnings:** 2
**Info:** 1
### Errors Fixed
- None
### Warnings Addressed
- Fixed: Added error handling to install target (|| exit 1)
### Warnings Skipped (with reasons)
- mbake reports ".DELETE_ON_ERROR" as unknown - this is a valid and critical
GNU Make special target that ensures failed builds don't leave corrupt files.
See: https://www.gnu.org/software/make/manual/html_node/Special-Targets.html
### Formatting Applied
- Yes - Applied `mbake format` to fix whitespace issues
### Info Items Addressed
- Added .NOTPARALLEL for Docker targets (parallel safety)
- Added error handling for docker-push target
### Remaining Issues
- None - Makefile is production-readyCommon Info Items to Address:
| Info Item | When to Fix | How to Fix |
|---|---|---|
| "mkdir without order-only prerequisites" | Large projects (>10 targets) | Use `target: prereqs \ |
| "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:
1. 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; }2. 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 run3. Install target error handling:
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin || exit 1
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/ || exit 1Note: 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
Helper Scripts (Optional)
These scripts are optional convenience tools for quick template generation.
When to Use Scripts vs Manual Generation
| Scenario | Recommendation |
|---|---|
| Simple, standard project (single binary, no special features) | ✅ Use generate_makefile_template.sh for speed |
| Complex project (Docker, multi-binary, custom patterns) | ❌ Use manual generation for full control |
| Adding targets to existing Makefile | ✅ Use add_standard_targets.sh |
| User has specific formatting/style requirements | ❌ Use manual generation |
| Rapid prototyping / proof-of-concept | ✅ Use scripts, customize later |
| Production-ready Makefile | ⚠️ Start with script, then customize manually |
generate_makefile_template.sh
Generates a complete Makefile template for a specific project type. Script path: scripts/generate_makefile_template.sh
bash scripts/generate_makefile_template.sh [TYPE] [NAME] [OUTPUT_FILE]
Types: c, c-lib, cpp, go, python, java, genericExample:
bash scripts/generate_makefile_template.sh go myservice
# Creates Makefile with Go patterns, version embedding, standard targets
bash scripts/generate_makefile_template.sh go myservice build/Makefile
# Writes template to build/Makefile (TYPE NAME OUTPUT)add_standard_targets.sh
Adds missing standard GNU targets to an existing Makefile. Script path: scripts/add_standard_targets.sh
bash scripts/add_standard_targets.sh [MAKEFILE] [TARGETS...]
bash scripts/add_standard_targets.sh [TARGETS...] # uses ./Makefile
Targets: all, install, uninstall, clean, distclean, test, check, help, distExample:
bash scripts/add_standard_targets.sh Makefile install uninstall help
# Adds install, uninstall, help targets if they don't exist
bash scripts/add_standard_targets.sh clean test
# Explicit-target mode: modifies ./Makefile
bash scripts/add_standard_targets.sh -n Makefile dist
# Dry-run mode: shows planned changes without editing filesNote: Manual generation following the Stage 3 patterns produces equivalent results but allows for more customization.
Helper Script Regression Smoke Tests
Run after modifying helper scripts or templates:
bash test/test_helper_scripts.shDone Criteria
Consider the task complete only when all checks below are satisfied:
- Trigger matched and missing requirements were clarified (or documented defaults were applied).
- Relevant internal docs were opened via explicit file paths before generation.
- Generated Makefile has complete
.PHONYcoverage for non-file targets. - Go templates include optional
go.sumhandling and configurableGO_MAINentrypoint. - Validation ran with
makefile-validator(preferred) or documented fallback checks. - Formatting was applied with
mbake, or skipped with an explicit tool-unavailable/compatibility reason. - Final response includes the required structured validation report.
Documentation
Detailed guides in docs/:
- 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
Resources
# Placeholder to preserve directory in git
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 target1. 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 .hSpecial 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 efficient3. 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 lintWhy .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.hProblems:
- 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.mkconfig.mk (variables):
CC := gcc
CFLAGS := -Wall -O2
PREFIX := /usr/localrules.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; doneProblems 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 printedMulti-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 allError Handling
# - prefix ignores errors
clean:
-$(RM) *.o # Continue even if rm fails
# Without -:
clean:
$(RM) *.o # Make stops if rm fails10. 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
1. Use .DELETE_ON_ERROR to prevent corrupted builds 2. Declare .PHONY for all non-file targets 3. Use ?= for user-overridable variables (CC, CFLAGS, PREFIX) 4. Use := for project variables (SOURCES, OBJECTS) 5. Use automatic variables ($@, $<, $^) for concise rules 6. Generate dependencies automatically (-MMD -MP) 7. Prefer non-recursive make over recursive make 8. Use pattern rules (%.o: %.c) over suffix rules 9. Create directories automatically (@mkdir -p $(@D)) 10. 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 -jMaking 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.logControlling 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 completesGNU 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 conceptuallyUse 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 targetsWhen 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
endifPractical 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)
endifComparison: .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 step3Optimal 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 doneDependency 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.oOptimizing 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.hBetter: Only include where needed
# Only main.c actually uses config.h
main.o: main.c config.h
utils.o: utils.c
helper.o: helper.cBest: Use automatic dependencies
%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
-include $(DEPENDS)
# Automatically tracks which headers each file usesIntermediate 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 deletionAvoiding 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 $@.tmpBuild 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
endifBenefits:
- 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 := 10Build 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 cleanPerformance 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 defined2. 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; doneProblems:
- 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 buildsReference: "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.456sDebug 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.logFinding 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 once2. Accurate Dependencies
# Use automatic dependency generation
CFLAGS += -MMD -MP
-include $(OBJECTS:.o=.d)
# Not manual maintenance3. 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 ?= -O25. 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; \
fiComplete 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) allBenchmarking 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 compilationProfiling 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 --debugReferences
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
recipeBasic 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.oWith 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 ruleCustom 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.hAutomatic 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/*')
# go.sum may not exist in modules with no external dependencies
GO_SUM := $(wildcard go.sum)
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 tidyPattern 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' -deletePattern 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 || trueAdvanced 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 lib2Best Practices
1. Use static pattern rules for known file lists (more efficient) 2. Generate dependencies automatically (-MMD -MP) 3. Create directories with order-only prerequisites 4. Disable built-in rules (.SUFFIXES:) for explicit Makefiles 5. Use pattern rules for generic transformations 6. Mirror source structure in build directory 7. Separate C and C++ compilation in mixed projects 8. Use -fPIC when building shared libraries 9. Include generated dependencies with -include 10. 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 := AKIAIOSFODNN7EXAMPLEUse 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.shUse .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.shHashiCorp Vault:
deploy:
@DB_PASSWORD=$$(vault kv get -field=password secret/database) && \
./deploy.shShell 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_-])
endifQuote 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 ;; \
esacSecure 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/secretsCI/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.comFail 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.shEnvironment 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 HOMEAudit 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.gzTLS/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.txtContainer 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 userScan 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"; \
fiDon'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 HISTFILERequire Explicit Targets
# Prevent running all targets by accident
.PHONY: all
all:
@echo "Please specify a target. Run 'make help' for options."
@exit 1Security 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 helloHow it works: 1. Check if hello exists and is newer than hello.c 2. If not, run the recipe 3. 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 myprogramWithout .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 docsMultiple .PHONY Declarations
# Option 1: All at once (recommended)
.PHONY: all clean install test help
# Option 2: Separate declarations
.PHONY: all
.PHONY: clean
.PHONY: installStandard 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).1clean
## Remove built files (keep configuration)
.PHONY: clean
clean:
$(RM) $(OBJECTS) $(TARGET)
$(RM) -r $(BUILDDIR)
$(RM) *.o *.a *.soRequirements:
- 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: testDistribution 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)/distcheckDocumentation 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.shThe ## 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 cPattern 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.cAdvantages:
- 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 += -lcheckAdvanced 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 independentlyUse 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: %.oAutomatic 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
1. Use .PHONY for non-file targets 2. Implement standard GNU targets (all, install, clean, test) 3. Make 'all' the default target (first target) 4. Use automatic variables ($@, $<, $^) for concise rules 5. Create directories automatically (@mkdir -p $(@D)) 6. Document targets with ## comments 7. Use static pattern rules for better performance 8. Include .DELETE_ON_ERROR to prevent corrupted builds 9. Use order-only prerequisites for directory creation 10. 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 targetCharacteristics:
- 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 targetCharacteristics:
- 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; makeCharacteristics:
- 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 -DDEBUGCharacteristics:
- 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-configCompiler 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.dVariable 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.oText 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_VARChecking 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)
endifTarget-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 += -lcheckBest 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) # Bad2. 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./include4. 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)References
#!/usr/bin/env bash
#
# add_standard_targets.sh
# Description: Add standard GNU targets to an existing Makefile
# Usage: bash add_standard_targets.sh [MAKEFILE] [TARGETS...]
#
# This script adds missing standard GNU Makefile targets to an existing Makefile.
# It will not overwrite existing targets.
#
set -euo pipefail
# Script metadata
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly -a ALL_TARGETS=(all install uninstall clean distclean test check help dist)
# Colors for output
if [[ -t 1 ]]; then
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
else
readonly RED=''
readonly GREEN=''
readonly YELLOW=''
readonly BLUE=''
readonly NC=''
fi
# Print functions
print_error() {
echo -e "${RED}ERROR:${NC} $*" >&2
}
print_success() {
echo -e "${GREEN}SUCCESS:${NC} $*"
}
print_info() {
echo -e "${YELLOW}INFO:${NC} $*"
}
print_added() {
echo -e "${GREEN}+${NC} Added target: ${BLUE}$1${NC}"
}
print_skipped() {
echo -e "${YELLOW}-${NC} Skipped (exists): ${BLUE}$1${NC}"
}
# Usage information
usage() {
cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS] [MAKEFILE] [TARGETS...]
${SCRIPT_NAME} [OPTIONS] [TARGETS...]
${SCRIPT_NAME} [OPTIONS] --file MAKEFILE [TARGETS...]
${SCRIPT_NAME} [OPTIONS] [MAKEFILE] --targets TARGET [TARGET...]
${SCRIPT_NAME} [OPTIONS] --file MAKEFILE --targets TARGET [TARGET...]
Add standard GNU targets to an existing Makefile.
Arguments:
MAKEFILE Path to Makefile (default: Makefile)
TARGETS Targets to add (default: all standard targets)
Available Targets:
all Build all targets (default target)
install Install built files to PREFIX
uninstall Remove installed files
clean Remove built files
distclean Remove all generated files
test Run tests
check Alias for test
help Show available targets
dist Create distribution tarball
Options:
-h, --help Show this help message
-l, --list List available targets
-n, --dry-run Show what would be added without modifying
--file PATH Explicit Makefile path
--targets ... Explicit target mode (consume target names)
Examples:
${SCRIPT_NAME} # Add all missing targets to ./Makefile
${SCRIPT_NAME} build.mk # Add all targets to build.mk
${SCRIPT_NAME} Makefile clean test # Add only clean and test targets
${SCRIPT_NAME} clean test # Add clean and test to ./Makefile
${SCRIPT_NAME} --file custom.mk test # Add test target to custom.mk
${SCRIPT_NAME} --targets clean test # Force target-mode on ./Makefile
${SCRIPT_NAME} -n Makefile install # Preview install target addition
EOF
}
# List available targets
list_targets() {
cat << EOF
Available standard GNU targets:
all - Build all targets (default target)
install - Install built files to PREFIX
uninstall - Remove installed files from PREFIX
clean - Remove built files (keep configuration)
distclean - Remove all generated files
test - Run tests
check - Alias for test (GNU convention)
help - Show available targets and usage
dist - Create distribution tarball
EOF
}
# Check if a target name is part of the standard set
is_known_target() {
local candidate="$1"
local target
for target in "${ALL_TARGETS[@]}"; do
if [[ "$target" == "$candidate" ]]; then
return 0
fi
done
return 1
}
# Check if target exists in Makefile
target_exists() {
local target="$1"
local makefile="$2"
# Match target at beginning of line, followed by : (not ::)
grep -qE "^${target}[[:space:]]*:" "$makefile" 2>/dev/null
}
# Generate target code
generate_target() {
local target="$1"
case "$target" in
all)
cat << 'EOF'
## Build all targets (default)
.PHONY: all
all: $(TARGET)
EOF
;;
install)
cat << 'EOF'
## Install built files to PREFIX
.PHONY: install
install: all
$(INSTALL) -d $(DESTDIR)$(PREFIX)/bin
$(INSTALL) -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/
EOF
;;
uninstall)
cat << 'EOF'
## Remove installed files
.PHONY: uninstall
uninstall:
$(RM) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
EOF
;;
clean)
cat << 'EOF'
## Remove built files
.PHONY: clean
clean:
$(RM) -r $(BUILDDIR)
$(RM) $(TARGET)
$(RM) *.o *.d
EOF
;;
distclean)
cat << 'EOF'
## Remove all generated files (including configuration)
.PHONY: distclean
distclean: clean
$(RM) config.h config.log config.status
$(RM) -r autom4te.cache/
EOF
;;
test)
cat << 'EOF'
## Run tests
.PHONY: test
test:
@echo "Running tests..."
# Add test commands here
# Examples:
# ./run_tests.sh
# python -m pytest
# go test ./...
EOF
;;
check)
cat << 'EOF'
## Alias for test (GNU convention)
.PHONY: check
check: test
EOF
;;
help)
cat << 'EOF'
## Show available targets
.PHONY: help
help:
@echo "$(PROJECT) - Build System"
@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 ""
@echo "Examples:"
@echo " make # Build the project"
@echo " make install # Install to PREFIX"
@echo " make clean # Remove built files"
EOF
;;
dist)
cat << 'EOF'
## 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='$(BUILDDIR)' \
.
EOF
;;
*)
print_error "Unknown target: $target"
return 1
;;
esac
}
# Add required variables if missing
add_missing_variables() {
local makefile="$1"
local added=0
local vars_to_add=""
# Check for essential variables
if ! grep -qE '^(PROJECT|TARGET)\s*[:?]?=' "$makefile"; then
vars_to_add+="
# Project configuration
PROJECT := myproject
TARGET := \$(PROJECT)
"
added=1
fi
if ! grep -qE '^PREFIX\s*\?=' "$makefile"; then
vars_to_add+="
# Installation prefix
PREFIX ?= /usr/local
"
added=1
fi
if ! grep -qE '^INSTALL\s*\?=' "$makefile"; then
vars_to_add+="
# Install command
INSTALL ?= install
"
added=1
fi
if ! grep -qE '^RM\s*\?=' "$makefile"; then
vars_to_add+="
# Remove command
RM ?= rm -f
"
added=1
fi
if ! grep -qE '^BUILDDIR\s*[:?]?=' "$makefile"; then
vars_to_add+="
# Build directory
BUILDDIR := build
"
added=1
fi
if ! grep -qE '^VERSION\s*[:?]?=' "$makefile"; then
vars_to_add+="
# Project version
VERSION := 1.0.0
"
added=1
fi
if [[ $added -eq 1 ]]; then
# Prepend variables to the file (after any header comments)
local header=""
local content=""
# Extract header comments (lines starting with #)
while IFS= read -r line; do
if [[ "$line" =~ ^# ]] || [[ -z "$line" ]]; then
header+="$line"$'\n'
else
break
fi
done < "$makefile"
# Get the rest of the file
content=$(tail -n +$(($(echo -n "$header" | grep -c $'\n') + 1)) "$makefile" 2>/dev/null || cat "$makefile")
# Reconstruct file
{
echo -n "$header"
echo "# ============================================"
echo "# Added by add_standard_targets.sh"
echo "# ============================================"
echo "$vars_to_add"
echo "$content"
} > "${makefile}.tmp"
mv "${makefile}.tmp" "$makefile"
print_info "Added missing variable definitions"
fi
}
# Main function
main() {
local dry_run=0
local makefile_path="Makefile"
local explicit_file=0
local explicit_targets=0
local -a targets=()
local -a positional=()
# Parse options and positional args.
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-l|--list)
list_targets
exit 0
;;
-n|--dry-run)
dry_run=1
shift
;;
--file)
if [[ $# -lt 2 ]]; then
print_error "--file requires a path argument"
exit 1
fi
makefile_path="$2"
explicit_file=1
shift 2
;;
--targets)
explicit_targets=1
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--|--file|--targets|-h|--help|-l|--list|-n|--dry-run)
break
;;
-*)
print_error "Unknown option: $1"
usage
exit 1
;;
*)
targets+=("$1")
shift
;;
esac
done
;;
--)
shift
while [[ $# -gt 0 ]]; do
positional+=("$1")
shift
done
;;
-*)
print_error "Unknown option: $1"
usage
exit 1
;;
*)
positional+=("$1")
shift
;;
esac
done
# Resolve positional args with backward compatibility for unambiguous usage.
if [[ ${#positional[@]} -gt 0 ]]; then
if [[ $explicit_targets -eq 1 ]]; then
if [[ $explicit_file -eq 1 ]]; then
print_error "Unexpected positional args with --file and --targets"
print_error "Use: ${SCRIPT_NAME} --file <path> --targets <target...>"
exit 1
fi
if [[ ${#positional[@]} -gt 1 ]]; then
print_error "Too many positional args with --targets"
print_error "Use: ${SCRIPT_NAME} [--file <path>] --targets <target...>"
exit 1
fi
makefile_path="${positional[0]}"
explicit_file=1
elif [[ $explicit_file -eq 1 ]]; then
targets+=("${positional[@]}")
else
if is_known_target "${positional[0]}"; then
if [[ -e "${positional[0]}" ]]; then
print_error "Ambiguous input '${positional[0]}': matches a target and existing path."
print_error "Use --file <path> or --targets <target...> explicitly."
exit 2
fi
targets=("${positional[@]}")
else
makefile_path="${positional[0]}"
if [[ ${#positional[@]} -gt 1 ]]; then
targets=("${positional[@]:1}")
fi
fi
fi
fi
if [[ $explicit_targets -eq 1 ]] && [[ ${#targets[@]} -eq 0 ]]; then
print_error "--targets requires at least one target name"
exit 1
fi
# Default target set when none explicitly requested
if [[ ${#targets[@]} -eq 0 ]]; then
targets=("${ALL_TARGETS[@]}")
fi
# Validate requested target names before modifying files
local target
for target in "${targets[@]}"; do
if ! is_known_target "$target"; then
print_error "Unknown target: $target"
echo ""
list_targets
exit 1
fi
done
# Check if Makefile exists
if [[ ! -f "$makefile_path" ]]; then
print_error "Makefile not found: $makefile_path"
exit 1
fi
print_info "Processing: $makefile_path"
echo ""
# Track what we'll add
local targets_added=0
local targets_skipped=0
local content_to_add=""
local -a added_targets=()
# Check each target
for target in "${targets[@]}"; do
if target_exists "$target" "$makefile_path"; then
print_skipped "$target"
targets_skipped=$((targets_skipped + 1))
continue
fi
if [[ $dry_run -eq 1 ]]; then
echo -e "${GREEN}+${NC} Would add: ${BLUE}$target${NC}"
else
content_to_add+="$(generate_target "$target")"
added_targets+=("$target")
fi
targets_added=$((targets_added + 1))
done
echo ""
# Apply changes if not dry run
if [[ $dry_run -eq 0 ]] && [[ $targets_added -gt 0 ]]; then
# Add missing variables first
add_missing_variables "$makefile_path"
# Append targets to Makefile
echo "$content_to_add" >> "$makefile_path"
# Report results
for target in "${added_targets[@]}"; do
print_added "$target"
done
print_success "Added $targets_added target(s) to $makefile_path"
elif [[ $dry_run -eq 1 ]]; then
print_info "Dry run - no changes made"
echo "Would add $targets_added target(s), skip $targets_skipped existing target(s)"
else
print_info "No targets to add (all requested targets already exist)"
fi
# Summary
echo ""
echo "Summary:"
echo " Targets added: $targets_added"
echo " Targets skipped: $targets_skipped"
if [[ $targets_added -gt 0 ]] && [[ $dry_run -eq 0 ]]; then
echo ""
print_info "Run 'make help' to see available targets"
print_info "Run 'make -n <target>' to preview what a target does"
fi
}
# Run main function
main "$@"
#!/usr/bin/env bash
#
# generate_makefile_template.sh
# Description: Generate Makefile templates for different project types
# Usage: bash generate_makefile_template.sh [OPTIONS] [PROJECT_TYPE] [PROJECT_NAME] [OUTPUT_FILE]
#
set -euo pipefail
# Script metadata
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
FORCE=0
PROJECT_TYPE=""
PROJECT_NAME="myproject"
OUTPUT_FILE="Makefile"
# Colors for output
if [[ -t 1 ]]; then
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
else
readonly RED=''
readonly GREEN=''
readonly YELLOW=''
readonly NC=''
fi
# Print functions
print_error() {
echo -e "${RED}ERROR:${NC} $*" >&2
}
print_success() {
echo -e "${GREEN}SUCCESS:${NC} $*"
}
print_info() {
echo -e "${YELLOW}INFO:${NC} $*"
}
# Usage information
usage() {
cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS] [PROJECT_TYPE] [PROJECT_NAME] [OUTPUT_FILE]
Generate Makefile templates for different project types.
Options:
-f, --force Overwrite existing files without prompting
-h, --help Show this help message
Arguments:
PROJECT_TYPE Type of project (required)
PROJECT_NAME Name of the project (default: myproject)
OUTPUT_FILE Output file path (default: Makefile)
Project Types:
c Simple C project
c-lib C library project
cpp C++ project
go Go project
python Python project
java Java project
generic Generic project
Examples:
${SCRIPT_NAME} c myapp
${SCRIPT_NAME} go server Makefile
${SCRIPT_NAME} python mypackage build.mk
${SCRIPT_NAME} -f c myapp Makefile # Force overwrite
EOF
}
# Parse command line options
parse_args() {
local -a positional=()
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--force)
FORCE=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
print_error "Unknown option: $1"
usage
exit 1
;;
*)
positional+=("$1")
shift
;;
esac
done
if [[ ${#positional[@]} -gt 3 ]]; then
print_error "Too many positional arguments. Expected: [PROJECT_TYPE] [PROJECT_NAME] [OUTPUT_FILE]"
usage
exit 1
fi
if [[ ${#positional[@]} -ge 1 ]]; then
PROJECT_TYPE="${positional[0]}"
fi
if [[ ${#positional[@]} -ge 2 ]]; then
PROJECT_NAME="${positional[1]}"
fi
if [[ ${#positional[@]} -ge 3 ]]; then
OUTPUT_FILE="${positional[2]}"
fi
}
# Escape replacement text for sed "s///" usage.
escape_sed_replacement() {
printf '%s' "$1" | sed -e 's/[\\/&]/\\&/g'
}
# Render a template from stdin and replace PROJECT_NAME safely.
render_template_with_project() {
local project="$1"
local escaped_project
escaped_project="$(escape_sed_replacement "$project")"
sed "s/PROJECT_NAME/${escaped_project}/g"
}
# Generate C project Makefile
generate_c_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Makefile for C project
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
.SUFFIXES:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
PROJECT := PROJECT_NAME
VERSION := 1.0.0
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 the application (default)
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)
## 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 help
help:
@echo "$(PROJECT) v$(VERSION)"
@echo ""
@sed -n 's/^## //p' $(MAKEFILE_LIST)
EOF
}
# Generate C++ project Makefile
generate_cpp_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Makefile for C++ project
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
.SUFFIXES:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
PROJECT := PROJECT_NAME
VERSION := 1.0.0
CXX ?= g++
CXXFLAGS ?= -Wall -Wextra -std=c++17 -O2
PREFIX ?= /usr/local
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj
SOURCES := $(wildcard $(SRCDIR)/*.cpp)
OBJECTS := $(SOURCES:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)
.PHONY: all clean install test help
## Build the application (default)
all: $(TARGET)
$(TARGET): $(OBJECTS)
@mkdir -p $(@D)
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
@mkdir -p $(@D)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -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 help
help:
@echo "$(PROJECT) v$(VERSION)"
@echo ""
@sed -n 's/^## //p' $(MAKEFILE_LIST)
EOF
}
# Generate C library Makefile
generate_c_lib_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Makefile for C library project
.DELETE_ON_ERROR:
PROJECT := PROJECT_NAME
VERSION := 1.0.0
CC ?= gcc
AR ?= ar
RANLIB ?= ranlib
CFLAGS ?= -Wall -Wextra -O2 -fPIC
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 help
## Build both static and shared libraries
all: static shared
## Build static library
static: $(STATIC_LIB)
## Build shared library
shared: $(SHARED_LIB)
$(STATIC_LIB): $(OBJECTS)
@mkdir -p $(@D)
$(AR) rcs $@ $^
$(RANLIB) $@
$(SHARED_LIB): $(OBJECTS)
@mkdir -p $(@D)
$(CC) -shared -Wl,-soname,lib$(PROJECT).so.1 $^ -o $@
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
## Install libraries
install: all
install -d $(DESTDIR)$(PREFIX)/lib
install -m 644 $(STATIC_LIB) $(DESTDIR)$(PREFIX)/lib/
install -m 755 $(SHARED_LIB) $(DESTDIR)$(PREFIX)/lib/
install -d $(DESTDIR)$(PREFIX)/include/$(PROJECT)
install -m 644 $(HEADERS) $(DESTDIR)$(PREFIX)/include/$(PROJECT)/
## Clean build artifacts
clean:
$(RM) -r $(BUILDDIR)
## Show help
help:
@sed -n 's/^## //p' $(MAKEFILE_LIST)
EOF
}
# Generate Go project Makefile
generate_go_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Makefile for Go project
.PHONY: all build install test clean fmt lint help
PROJECT := PROJECT_NAME
GO ?= go
PREFIX ?= /usr/local
GO_MAIN ?= ./cmd/$(PROJECT)
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
LDFLAGS := -ldflags "-X main.version=$(VERSION)"
SOURCES := $(shell find . -name '*.go' -not -path './vendor/*')
GO_SUM := $(wildcard go.sum)
TARGET := $(PROJECT)
## Build the application (default)
all: build
## Build binary
build: $(TARGET)
$(TARGET): $(SOURCES) go.mod $(GO_SUM)
$(GO) build $(LDFLAGS) -o $@ $(GO_MAIN)
## Install to PREFIX
install: $(TARGET)
install -d $(DESTDIR)$(PREFIX)/bin
install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/
## Run tests
test:
$(GO) test -v ./...
## Clean build artifacts
clean:
$(RM) $(TARGET)
$(GO) clean
## Format code
fmt:
$(GO) fmt ./...
## Run linter
lint:
golangci-lint run
## Show help
help:
@echo "$(PROJECT) - Go Project"
@echo "Version: $(VERSION)"
@echo ""
@sed -n 's/^## //p' $(MAKEFILE_LIST)
EOF
}
# Generate Python project Makefile
generate_python_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Makefile for Python project
.PHONY: all build install develop test format lint clean help
PROJECT := PROJECT_NAME
PYTHON ?= python3
PIP ?= $(PYTHON) -m pip
## Build the package (default)
all: build
## Build Python package
build:
$(PYTHON) -m build
## Install package
install:
$(PIP) install .
## Install in development mode
develop:
$(PIP) install -e .[dev]
## Run tests
test:
$(PYTHON) -m pytest tests/ -v
## Format code
format:
$(PYTHON) -m black src/ tests/
$(PYTHON) -m isort src/ tests/
## Run linters
lint:
$(PYTHON) -m flake8 src/ tests/
$(PYTHON) -m pylint src/
## Clean build artifacts
clean:
$(RM) -r build/ dist/ *.egg-info/
$(RM) -r .pytest_cache/ .coverage htmlcov/
find . -type d -name '__pycache__' -exec rm -r {} +
## Show help
help:
@echo "$(PROJECT) - Python Project"
@echo ""
@sed -n 's/^## //p' $(MAKEFILE_LIST)
EOF
}
# Generate Java project Makefile
generate_java_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Makefile for Java project
.DELETE_ON_ERROR:
PROJECT := PROJECT_NAME
VERSION := 1.0.0
MAIN_CLASS := Main
# Java tools
JAVAC ?= javac
JAVA ?= java
JAR ?= jar
# Compiler flags
JAVAC_FLAGS ?= -Xlint:all -encoding UTF-8
JAR_FLAGS := cvf
# Directories
SRCDIR := src
BUILDDIR := build
CLASSDIR := $(BUILDDIR)/classes
LIBDIR := lib
DOCDIR := $(BUILDDIR)/docs
DISTDIR := dist
# Source and class files
JAVA_SOURCES := $(shell find $(SRCDIR) -name '*.java')
JAVA_CLASSES := $(JAVA_SOURCES:$(SRCDIR)/%.java=$(CLASSDIR)/%.class)
# Classpath
CLASSPATH := $(CLASSDIR)
ifneq ($(wildcard $(LIBDIR)/*.jar),)
CLASSPATH := $(CLASSPATH):$(LIBDIR)/*
endif
# Output JAR
TARGET := $(DISTDIR)/$(PROJECT)-$(VERSION).jar
.PHONY: all build compile jar run run-jar test clean distclean docs help
## Build the project (default)
all: build
## Compile and create JAR
build: jar
## Compile Java sources
compile: $(CLASSDIR)/.compiled
$(CLASSDIR)/.compiled: $(JAVA_SOURCES)
@mkdir -p $(CLASSDIR)
$(JAVAC) $(JAVAC_FLAGS) -d $(CLASSDIR) -cp "$(CLASSPATH)" $(JAVA_SOURCES)
@touch $@
## Create JAR file
jar: compile
@mkdir -p $(DISTDIR)
@echo "Manifest-Version: 1.0" > $(BUILDDIR)/MANIFEST.MF
@echo "Main-Class: $(MAIN_CLASS)" >> $(BUILDDIR)/MANIFEST.MF
@echo "Created-By: Makefile" >> $(BUILDDIR)/MANIFEST.MF
$(JAR) $(JAR_FLAGS)m $(TARGET) $(BUILDDIR)/MANIFEST.MF -C $(CLASSDIR) .
@echo "Created: $(TARGET)"
## Run the application
run: compile
$(JAVA) -cp "$(CLASSPATH)" $(MAIN_CLASS)
## Run with JAR
run-jar: jar
$(JAVA) -jar $(TARGET)
## Run tests (requires JUnit in lib/)
test: compile
@echo "Running tests..."
@if [ -d "$(SRCDIR)/test" ]; then \
$(JAVA) -cp "$(CLASSPATH):$(LIBDIR)/*" org.junit.runner.JUnitCore; \
else \
echo "No test directory found"; \
fi
## Generate Javadoc
docs:
@mkdir -p $(DOCDIR)
javadoc -d $(DOCDIR) -sourcepath $(SRCDIR) -subpackages .
@echo "Documentation generated: $(DOCDIR)/index.html"
## Remove build artifacts
clean:
$(RM) -r $(BUILDDIR)
@echo "Clean complete"
## Remove all generated files
distclean: clean
$(RM) -r $(DISTDIR)
@echo "Distclean complete"
## Show available targets
help:
@echo "$(PROJECT) v$(VERSION) - Java Project"
@echo ""
@echo "Targets:"
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed 's/^/ /'
@echo ""
@echo "Variables:"
@echo " JAVAC=$(JAVAC)"
@echo " JAVAC_FLAGS=$(JAVAC_FLAGS)"
@echo " MAIN_CLASS=$(MAIN_CLASS)"
@echo ""
@echo "Directory Structure:"
@echo " $(SRCDIR)/ - Java source files"
@echo " $(LIBDIR)/ - External JAR dependencies"
@echo " $(BUILDDIR)/ - Build output"
@echo " $(DISTDIR)/ - Distribution JARs"
@echo ""
@echo "Examples:"
@echo " make # Build the project"
@echo " make run # Compile and run"
@echo " make jar # Create JAR file"
@echo " make MAIN_CLASS=MyApp # Set main class"
EOF
}
# Generate generic Makefile
generate_generic_makefile() {
local project="$1"
render_template_with_project "$project" << 'EOF'
# Generic Makefile
.PHONY: all build clean install test help
PROJECT := PROJECT_NAME
VERSION := 1.0.0
## Build the project (default)
all: build
## Build target
build:
@echo "Building $(PROJECT)..."
# Add build commands here
## Install target
install: build
@echo "Installing $(PROJECT)..."
# Add installation commands here
## Run tests
test:
@echo "Running tests..."
# Add test commands here
## Clean build artifacts
clean:
@echo "Cleaning..."
# Add cleanup commands here
## Show help
help:
@echo "$(PROJECT) v$(VERSION)"
@echo ""
@echo "Available targets:"
@sed -n 's/^## //p' $(MAKEFILE_LIST)
EOF
}
# Main generation logic
generate_makefile() {
local type="$1"
local project="$2"
local output="$3"
# Check if output file exists
if [[ -f "${output}" ]]; then
if [[ "${FORCE}" -eq 1 ]]; then
print_info "Overwriting existing file: ${output}"
elif [[ -t 0 ]]; then
# Interactive mode - prompt user
print_error "File '${output}' already exists"
read -p "Overwrite? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Aborted"
exit 0
fi
else
# Non-interactive mode - fail without --force
print_error "File '${output}' already exists. Use -f/--force to overwrite."
exit 1
fi
fi
print_info "Generating ${type} Makefile for project '${project}'..."
case "${type}" in
c)
generate_c_makefile "${project}" > "${output}"
;;
c-lib)
generate_c_lib_makefile "${project}" > "${output}"
;;
cpp)
generate_cpp_makefile "${project}" > "${output}"
;;
go)
generate_go_makefile "${project}" > "${output}"
;;
python)
generate_python_makefile "${project}" > "${output}"
;;
java)
generate_java_makefile "${project}" > "${output}"
;;
generic)
generate_generic_makefile "${project}" > "${output}"
;;
*)
print_error "Unknown project type: ${type}"
echo ""
usage
exit 1
;;
esac
print_success "Makefile generated: ${output}"
print_info "Edit the Makefile to customize for your project"
print_info "Run 'make help' to see available targets"
}
# Parse arguments
parse_args "$@"
# Check required argument
if [[ -z "${PROJECT_TYPE}" ]]; then
usage
exit 0
fi
# Main execution
generate_makefile "${PROJECT_TYPE}" "${PROJECT_NAME}" "${OUTPUT_FILE}"
Related skills
How it compares
Use makefile-generator to scaffold new Makefiles; use makefile-validator from the same plugin to audit an existing Makefile against conventions.
FAQ
Which languages does makefile-generator support?
makefile-generator supports C/C++, Python, Go, and Java projects with language-specific patterns documented in patterns-guide.md, including Go ldflags versioning and Docker integration via Pattern 8.
What standard Makefile targets does makefile-generator include?
makefile-generator typically scaffolds GNU-standard targets such as all, build, test, clean, install, dist, and help with .PHONY declarations, security hardening, and header comments listing consulted internal docs.