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.