Introduction to Makefiles
A Makefile defines a set of rules to specify how files should be compiled. The primary advantage of using Makefiles is "automated compilation". Once properly configured, a simple make command can compile an entire project automatically, significantly improving software development efficiency.
Make primarily addresses two key challenges:
- Maintaining relationships among numerous source files
- Minimizing redundant compilation time
Makefiles are used to manage projects. They can be named either "makefile" or "Makefile" and are executed using the make command.
Basic Makefile Rules
The fundamental structure of a Makefile rule consists of:
target: dependencies
command
Key principles:
- The target's timestamp must be newer than its dependencies; otherwise, the target needs updating.
- If a dependency doesn't exist, make searches for rules to generate it.
Example: Simple Compilation
Let's create a basic program with a Makefile. First, create a main.c file:
#include<stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Now create a Makefile:
program: main.o
gcc main.o -o program
main.o: main.c
gcc -c main.c -o main.o
Running make will compile the program, and executing ./program will output "Hello World!"
Multi-file Project Example
For more complex projects with multiple source files, Makefiles become even more valuable. Consider a program with arithmetic operations:
Create main.c:
#include<stdio.h>
int add(int, int);
int subtract(int, int);
int divide(int, int);
int main(int argc, char *argv[]) {
int a = 10;
int b = 5;
printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d - %d = %d\n", a, b, subtract(a, b));
printf("%d / %d = %d\n", a, b, divide(a, b));
return 0;
}
Create separate implementation files:
// add.c
int add(int x, int y) {
return x + y;
}
// subtract.c
int subtract(int x, int y) {
return x - y;
}
// divide.c
int divide(int x, int y) {
return x / y;
}
Initial approach (inefficient):
app: main.c add.c subtract.c divide.c
gcc main.c add.c subtract.c divide.c -o app
This works but recompiles everything even when only one file changes. A better approach:
app: main.o add.o subtract.o divide.o
gcc main.o add.o subtract.o divide.o -o app
main.o: main.c
gcc -c main.c -o main.o
add.o: add.c
gcc -c add.c -o add.o
subtract.o: subtract.c
gcc -c subtract.c -o subtract.o
divide.o: divide.c
gcc -c divide.c -o divide.o
Now when you modify just one file, only that file and the final executable will be recompiled.
Makefile Functions
Makefiles include powerful functions for file manipulation:
# Find all .c files in current directory
sources = $(wildcard *.c)
# Convert .c files to .o files
objects = $(patsubst %.c, %.o, $(sources))
The wildcard function matches all files matching a pattern, while patsubst performs pattern substitution.
Adding a Clean Target
It's good practice to include a clean target to remove generated files:
clean:
rm -rf $(objects) app
The - prefix before rm prevents make from stopping if a file doesn't exist:
clean:
-rm -rf $(objects) app
Automatic Variables
Makefiles provide automatic variables to simplify rules:
$@: The target of the rule$<: The first dependency of the rule$^: All dependencies of the rule (with duplicates removed)
Using these variables, our Makefile becomes:
sources = $(wildcard *.c)
objects = $(patsubst %.c, %.o, $(sources))
app: $(objects)
gcc $^ -o $@
%.o: %.c
gcc -c $< -o $@
clean:
-rm -rf $(objects) app
Pattern Rules
Pattern rules provide a more concise way to define similar rules. The previous example already uses a pattern rule:
%.o: %.c
gcc -c $< -o $@
This rule applies to any file that needs to be compiled from a .c file to a .o file.
Static Pattern Rules
For more control, you can specify which files a pattern rule applies to:
$(objects): %.o: %.c
gcc -c $< -o $@
Phony Targets
Targets that don't represent actual files should be declared as phony:
.PHONY: clean app
Advanced Makefile Example
For a more organized project structure with source, object, and include directories:
# Define directories
src_dir = ./src
obj_dir = ./obj
inc_dir = ./inc
# Find all .c files in source directory
sources = $(wildcard $(src_dir)/*.c)
# Convert .c paths to .o paths
objects = $(patsubst $(src_dir)/%.c, $(obj_dir)/%.o, $(sources))
# Compiler flags
CFLAGS = -Wall -g -I$(inc_dir)
# Default target
app: $(objects)
gcc $^ -o $@ $(CFLAGS)
# Pattern rule for compiling .c to .o
$(obj_dir)/%.o: $(src_dir)/%.c
gcc -c $< -o $@ $(CFLAGS)
# Clean target
clean:
rm -rf $(objects) app
# Declare phony targets
.PHONY: clean app
Compiling Multiple Independent Programs
To compile each .c file into a separate executable:
# Find all .c files
sources = $(wildcard *.c)
# Remove extension to get target names
targets = $(patsubst %.c, %, $(sources))
# Default target
all: $(targets)
# Pattern rule to compile each .c to its executable
%: %.c
gcc $< -o $@
# Clean target
clean:
rm -rf $(targets)
# Declare phony targets
.PHONY: clean all
This Makefile will create separate executables for each .c file in the directory.