Writing Makefile for C++ Projects

Setting variables

Let’s start by setting some basic variables:

# The program name:
NAME = program
 
# The compiler
CXX = c++

Considerations

Note: The variable CXX is used to specify the C++ compiler. The command c++ is analogous to using cc for compiling C programs. This is similar to how g++/gcc are used for GNU Compilers, and clang/clang++ for Clang compilers.

Flags

Here, we are setting the compilation flags used in École 42’s C++ modules.

More specifically:

  • -Wall: Enable all compiler’s warning messages
  • -Wextra: Enable extra warning messages that are not turned on by -Wall
  • -Werror: Turn all warning messages into errors
  • -std=c++98: Specify the C++ version to conform
CXXFLAGS = -Wall -Wextra -Werror -std=c++98

Specifying the source files

In the SRCS variable we can specify all the .cpp files used in the project. These are the files that are going to be compiled into object files (.o) and then combined into an executable file.

SRCS = main.cpp
OBJS = $(SRCS:.cpp=.o)
INCLUDES = main.hpp

Notes on OBJS

The OBJS variable is a special computed variable that takes all the values from the SRCS variable that has the .cpp extension and turns into a .o file.

Defining some basic rules

When writing a Makefile we usually define 4 basic rules. Those are:

  • all: Compile the program
  • clean: Clean all object files
  • fclean: Clean all object files + the executable file
  • re: Clean everything that was generated and generate everything again (fclean + all)
all: $(NAME)
 
$(NAME): $(OBJS)
	$(CXX) $(CXXFLAGS) $(OBJS) -o $(NAME)
 
clean:
	$(RM) $(OBJS)
 
fclean: clean
	$(RM) $(NAME)
 
 
re: fclean all

Let’s analyze each line here.

all

To compile the program we just have to run make all but as all is the first rule that is getting specified, simply running make will execute make all.

all: $(NAME)

As we can see here, all depends on the creation of $(NAME) which is the program name, in other words the executable file. At this point, we don’t have the executable file yet, so make searches for a rule that creates $(NAME):

$(NAME): $(OBJS)
	$(CXX) $(CXXFLAGS) $(OBJS) -o $(NAME)

The creation of $(NAME) depends on the creation of all object files on this project so they are created. After the creation of $(OBJS) we finally can run $(CXX) (the compiler) specifying $(CXXFLAGS) (our compilation flags) passing all the object files to compile and naming the final product to $(NAME).

To better illustrate this imagine you have 2 C++ files:

main.cpp
other.cpp

We want to name our executable file as program:

c++ -Wall -Wextra -Werror -std=c++98 main.o other.o -o program

This is the actual command that gets executed.

Creating object files

This is the rule used to create object files. It basically says: For any C++ file, generate an object file. It also depends on the INCLUDES so if any header file changes, all object files are going to get regenerated. Then it compiles each .cpp file (represented by the automatic variable $< here) to an object file (with the -c flag) and outputs a file with the same name but with the .o extension. Here $@ represents each .o file:

%.o: %.cpp $(INCLUDES)
	$(CXX) $(CXXFLAGS) -c $< -o $@

Implementing clean

To implement clean, fclean and re is really simple.

clean just removes all object files so:

clean:
	$(RM) $(OBJS)

fclean calls clean to remove all object files and in addition, also removes the executable file:

fclean: clean
	$(RM) $(NAME)

re runs fclean and all:

re: fclean all

.PHONY?

.PHONY is used to indicate that a target is not a real file but a command or routine to be executed. For example, running make clean executes the clean routine, even though there is no file named clean.

.PHONY: all clean fclean re

This sums up the creation of a basic Makefile for some basic C++ projects.