What is “Makefile” and when do we use it?

mehmet ali ipsuz
5 min readJan 19, 2021

If you have been developing small scale projects or using an IDE that comes with integrated compiler along with bunch of handy tools, you probably never needed makefile to compile your project.

When does a programmer need makefile?

Assuming that we are developing a project that consists of three source files named “main.cpp”, “sum.cpp”, “subtract.cpp”. This project can be easily compiled with following terminal command using g++.

$ g++ -o main main.cpp sum.cpp subtract.cpp//---main.cpp---#include <iostream>int sum(int, int);    //Function prototype that lives on sum.cpp
int subtract(int, int); //Function prototype that lives on subtract.cpp
int main(){
int x = 5, y = 3;
std::cout << sum(x, y) << std::endl;
std::cout << subtract(x, y) << std::endl;
return 0;
}
//---sum.cpp---
int sum(int x, int y){
return x+y;
}

//---subtract.cpp---
int subtract(){
return x-y;
}

As we have couple of files, typing their names to command line is not hustle for us. It doesn’t even take too much time to compile entire project.

Now let’s switch to a bigger project such as Linux kernel or an embedded application written using RTOS etc.. These big scale projects consist of numerous source files and it can be intimidating to type all source filenames to command line one by one for each time. There are couple of other downsides of using terminal window for larger projects. 1) This is not efficient. You may think that recompiling can be easy with up arrow key when you update your code. That is true as long as you don’t close terminal window. As soon as terminal is closed, all command history goes away. 2) It is hard to debug command on tiny terminal window in case you make mistake. 3) When project is attempted to compile on terminal window, all source files are compiled regardless of whether they have been modified since the last build.

This is where “makefile” comes in handy. Keep in mind that we are going to scratch from surface to show what is possible using “make” in this article.

Simplest makefile

Let’s return our simple project that has “main.cpp”, “subtract.cpp” and “sum.cpp” source files. First thing we need to do is to create “makefile” in our directory and write following command into it. You should be aware of that makefiles do not have file extension.

main: main.cpp subtract.cpp sum.cpp
g++ -o main main.cpp subtract.cpp sum.cpp

Open terminal in project directory and type “make” then hit enter. If everything is correct, you should have executable file in project directory waiting for you to run it.

Makefile structure is pretty straightforward. main statement before “:” is called as target. This target depends on files written after “:”. If there is change on any dependency files (also called prerequisite files) after last build, following command line (called recipe) is executed by make. Recipe command line starts with tab character. These target: prerequisites and recipe are called as rule. A basic rule with its components are shown below.

target: prerequisites
recipe

The target of very first rule in makefile is executed when make is invoked. In our example target is “main” and depends on “main.cpp”, “subtract.cpp”, “sum.cpp”. Then make executes recipe command line which is “g++ -o main main.cpp subtract.cpp sum.cpp”

You were told that “make” promises us to increase efficiency and reduce code redundancy. However, makefile we have just created does not have enough efficiency yet. Let’s modify a little bit more.

First we will add “CC” variable that holds compiler name. If we decide to change our compiler in the future, we can simply chance it here using this variable instead of changing every single recipes throughout makefile. Typing compiler name to each recipe is also code redundancy that we do not like. You probably noticed that we had to type source filenames in prerequisites and in recipe. This is another code redundancy that needs to be addressed. Our makefile should look like below with aforementioned changes.

CC = g++
src = *.cpp
main: $(src)
$(CC) -o $@ $^

I will go through each line just to make sure everything is understood. As we discussed earlier, CC is a variable that holds our compiler name. src is an another variable that holds list of “.cpp” files in our project directory. Definition and initialization of variable are straightforward. When we need to use this variable in any of rules components, we have to wrap it up with parentheses and it should start with “$” sign. No matter it is special or ordinary variable. Otherwise it is not interpreted as variable but something else. “@” is a special variable of particular rule that you are in. It holds target of the rule that is being processed. In our example “$@” is expanded with “main”. Have you noticed that we use “@” variable with “$” sign? Another special variable is “^” that holds prerequisites. In our example “$^” is expanded with “$(src)” that expands with “main.cpp subtract.cpp sum.cpp”.

Pattern Rules

Make code above is enough for almost all small-mid size projects but there are a few more features I would like to introduce here for your convenience.

First I will modify makefile as follow to create a new problem then I will explain pattern rules to show how pattern rules can address this issue.

CC = g++main: main.o subtract.o sum.o
$(CC) -o $@ $^
main.o: main.cpp
$(CC) -c $^
subtract.o: subtract.cpp
$(CC) -c $^
sum.o: sum.cpp
$(CC) -c $^

When the above makefile is run in terminal, make will try to locate the first rule of entire makefile. Whenever it finds “main” as a target of first rule, it will look dependencies (or prerequisites) which are “main.o”, “subtract.o” and “sum.o”. Now these dependencies needs to be resolved by make. Fortunately we have rules for these dependencies. Each of targets in the main’s dependency are executed and create object file in project directory. As you noticed object targets (main.o, subtract.o and sum.o) depend on source files (main.cpp, subtract.cpp, sum.cpp) and share same filenames with its target. For instance main.o depends on main.cpp so on so forth. Can we come up with a rule that automates this repetitive? Yes, we can! Pattern rules can solve this problem with an intuitive way.

CC = g++main: main.o subtract.o sum.o
$(CC) -o $@ $^
%.o: %.cpp
$(CC) -c $^

What did we do here? We simplified makefile with the help of pattern rule.

Pattern rules contain “%” sign on target and this “%” sign matches with any string. Here in our makefile, we said make that “any string”.o depends on “any string”.cpp and whenever you are tasked for this duty, execute $(CC) -c $^ command.

Let’s also automate main.o, subtract.o and sum.o with another feature of make function called “patsubst”. This function takes three parameters and generate a new string.

$(patsubst pattern,replacement,source)

We would like to generate a new string using name of “.cpp” files in our directory. For that we should get all filenames that end with “.cpp”. Then we will change “.cpp”s with “.o”. Check out final makefile below.

CC = g++
src = *.cpp
obj = $(patsubst %.cpp, %.o, $(src))
main: $(obj)
$(CC) -o $@ $^
%.o: %.cpp
$(CC) -c $^

As I wrote before, this is only a scratch from surface to get you familiar with make concept. You’ll learn more and more when you have bigger problems. There are tons of good articles out there waiting for you to solve your problems as well as make your life easier.

--

--