What is Makefile
A Makefile is a build automation tool that has been used for decades to compile and link source code, generate executables, and manage dependencies in software development projects. It provides a convenient way to define and execute tasks, also known as "rules," which describe how to build specific targets and their dependencies. Makefiles rely on the make utility, a command-line tool that reads the Makefile and automatically executes the appropriate tasks.
Getting Started with Makefile
Installing Make
Before you can start using Makefiles, you need to install the make utility on your system. Most Unix-based systems, such as Linux and macOS, come with make pre-installed. However, if it's not already installed, you can use the following commands:
For Linux (Debian/Ubuntu):
$ sudo apt-get update
$ sudo apt-get install build-essential
For macOS (using Homebrew):
$ brew install make
For Windows, you can install make using the following methods:
- Using WSL (Windows Subsystem for Linux): Install a Linux distribution from the Microsoft Store and follow the Linux installation instructions.
- Using MSYS2 or Cygwin: Download and install one of these Unix-like environments for Windows, and then follow the installation instructions for Linux.
Creating Makefile
To create a Makefile, simply create a new file named Makefile (with no file extension) in the root directory of your project. You can use any text editor to write and edit the file.
Here's a simple example of a Makefile that compiles a C program:
CC = gcc
CFLAGS = -Wall -Wextra
hello: hello.c
$(CC) $(CFLAGS) -o hello hello.c
clean:
rm -f hello
In this example, we define two variables (CC and CFLAGS) and use them in the rule for building the hello target. The clean rule is a phony target used to remove the generated hello executable.
Basic Makefile Syntax and Rules
A Makefile consists of rules, variable assignments, and comments. Here's a brief overview of the basic syntax:
Rules
A rule consists of a target, its dependencies, and a recipe. The format is as follows:
target: dependencies
recipe
The recipe is a series of shell commands indented with a tab character.
Variables
Variables in a Makefile are assigned using the = operator:
VAR_NAME = value
You can use a variable in a rule by enclosing its name in parentheses and prefixing it with a dollar sign, like this: $(VAR_NAME).
Comments
Comments in a Makefile start with a # character and continue until the end of the line:
# This is a comment
Phony
Phony targets are special targets that do not represent actual files but serve as a way to group related tasks. To define a phony target, add a .PHONY directive before the rule:
.PHONY: clean
clean:
rm -f hello
Executing Makefile
Once you have created a Makefile for your project, you can execute it using the make utility. By default, make looks for a file named Makefile in the current directory and executes the first rule found in the file. You can also specify a target to build by providing its name as an argument.
Run make to build the default target (usually the first target defined in the Makefile):
$ make
To build a specific target, provide its name as an argument to the make command:
$ make clean
In our example, this command will execute the clean rule, removing the hello executable.
You can specify multiple targets by providing their names separated by spaces:
$ make target1 target2 target3
If you want to execute a Makefile with a different name or located in a different directory, you can use the -f or --file option:
$ make -f path/to/MyMakefile
Advanced Makefile Techniques
Defining and Using Variables
Variables in Makefiles allow you to store and reuse values, making your Makefile more maintainable and easier to read. Variables can be assigned using one of the following operators:
=: Recursive variable assignment:=: Simple variable assignment (non-recursive)?=: Conditional variable assignment (only if the variable is not already set)+=: Appending to a variable
Here's an example illustrating the use of variables:
CC := gcc
CFLAGS := -Wall -Wextra
SOURCES := main.c util.c
OBJECTS := $(SOURCES:.c=.o)
main: $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
Conditional Statements and Loops
Makefiles support conditional statements and loops, which can be useful for customizing the behavior of your Makefile based on the environment or the target platform.
Conditional statements are written using ifeq, ifneq, ifdef, and ifndef directives:
ifeq ($(OS),Windows_NT)
EXE_EXT := .exe
else
EXE_EXT :=
endif
hello: hello.c
$(CC) $(CFLAGS) -o hello$(EXE_EXT) hello.c
Loops can be created using foreach and eval directives, as well as call and value functions:
TARGETS := target1 target2 target3
RULES := $(foreach target,$(TARGETS),$(eval $(call create_rule,$(target))))
define create_rule
$(1): $(1).c
$(CC) $(CFLAGS) -o $(1) $(1).c
endef
$(RULES)
Pattern Rules and Wildcards
Pattern rules allow you to define generic rules for multiple targets based on file extensions or patterns. Wildcards can be used to match a group of files, simplifying your Makefile.
Here's an example using pattern rules and wildcards:
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
all: main
main: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
Managing Dependencies
Makefiles can automatically manage dependencies between targets, ensuring that only the necessary tasks are executed when a file changes.
To manage dependencies, you can use the -include directive to include the dependency files generated by the compiler:
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
DEPS := $(SRCS:.c=.d)
all: main
main: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -MMD -c -o $@ $<
-include $(DEPS)
In this example, the -MMD flag tells the compiler to generate a dependency file for each source file, which is then included in the Makefile using the -include directive.