Using the Make Utility and Makefiles in Linux [Guide]
This is a complete beginner's guide to using the make command in Linux.
You'll learn:
- The purpose of the make command
- Installation of the make command
- Creating and using the makefile for a sample C project
What is the make utility?
The make utility is one of the handiest utilities for a programmer. Its primary purpose is to compile a medium-to-large software project. The make utility is so helpful and versatile that even the Linux kernel uses it!
To understand the usefulness of the make utility, one must first understand why it was needed in the first place.
As your software gets more extensive, you start relying more and more on external dependencies (i.e., libraries). Your code starts splitting into multiple files with God knows what is in each file. Compiling each file and linking them together sanely to produce necessary binaries becomes complicated.
"But I can create a Bash script for that!"
Why yes, you can! More power to you! But as your project grows, you must deal with incremental rebuilds. How will you handle it generically, such that the logic stays true even when your number of files increases?
This is all handled by the make utility. So let us not reinvent the wheel and see how to install and make good use of the make utility.
Installing the make utility
The make utility is already available in the first-party repositories of almost all Linux distributions.
To install make on Debian, Ubuntu, and their derivatives, use the apt
package manager like so:
sudo apt install make
To install make on Fedora and RHEL-based Linux distributions, use the dnf
package manger like so:
sudo dnf install make
To install make on Arch Linux and its derivatives, use the pacman
package manager like so:
sudo pacman -Sy make
Now that the make utility is installed, you can proceed to understand it with examples.
Creating a basic makefile
The make utility compiles your code based on the instructions specified in the makefile in the top level directory of your project's code repository.
Below is the directory structure of my project:
$ tree make-tutorial
make-tutorial
└── src
├── calculator.c
├── greeter.c
├── main.c
└── userheader.h
1 directory, 4 files
Below are the contents of the main.c
source file:
#include <stdio.h>
#include "userheader.h"
int main()
{
greeter_func();
printf("\nAdding 5 and 10 together gives us '%d'.\n", add(5, 10));
printf("Subtracting 10 from 32 results in '%d'.\n", sub(10, 32));
printf("If 43 is multiplied with 2, we get '%d'.\n", mul(43, 2));
printf("The result of dividing any even number like 78 with 2 is a whole number like '%f'.\n", div(78, 2));
return 0;
}
Next are the contents of the greeter.c
source file:
#include <stdio.h>
#include "userheader.h"
void greeter_func()
{
printf("Hello, user! I hope you are ready for today's basic Mathematics class!\n");
}
Below are the contents of the calculator.c
source file:
#include <stdio.h>
#include "userheader.h"
int add(int a, int b)
{
return (a + b);
}
int sub(int a, int b)
{
if (a > b)
return (a - b);
else if (a < b)
return (b - a);
else return 0;
}
int mul(int a, int b)
{
return (a * b);
}
double div(int a, int b)
{
if (a > b)
return ((double)a / (double)b);
else if (a < b)
return ((double)b / (double)a);
else
return 0;
}
Finally, below are the contents of the userheader.h
header file:
#ifndef USERHEADER_DOT_H
#define USERHEADER_DOT_H
void greeter_func();
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
double div(int a, int b);
#endif /* USERHEADER_DOT_H */
Basics of a makefile
Before we create a bare-bones makefile, let us take a look at the syntax of a makefile. The basic building block of a Makefile consists of one or many "rules" and "variables".
Rules in a makefile
Let us first take a look at rules in the makefile. A rule of makefile has the following syntax:
target : prerequisites
recipe
...
- A
target
is the name of file that will be generated by make. These are usually object files that are later on used for linking everything together. - A
prerequisite
is a file that is necessary for the target to be generated. This is where you usually specify your.c
,.o
and.h
files. - Finally, a
recipe
is one or many steps needed to generate thetarget
.
Macros/Variables in makefile
In C and C++, a basic language feature is variables. They allow us to store values that we might want to use in a lot of places. This helps us use the same variable name where needed. An added benefit is we only need to make one change if we need to change the value.
Similarly, a makefile can contain variables. They are sometimes referred to as macros. The syntax to declare a variable in a Makefile is as follows:
variable = value
A variable and the value(s) it holds are separated by an equals (=
) sign. Multiple values are separated by spaces between each other.
In general, variables are used to store various items necessary for compilation. Let's say that you want to enable run-time buffer overflow detection and enable full ASLR for the executable; this can be achieved by storing all the compiler flags in one variable, like CFLAGS
.
Below is a demonstration doing this:
CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
We created a variable called CFLAGS
(compiler flags) and added all of our compiler flags here.
To use our variable, we can enclose it in parentheses beginning with a dollar sign, like so:
gcc $(CFLAGS) -c main.c
The above line in our makefile will add all of our specified compiler flags and compile the main.c
file as we require.
Automatic variables
The make utility has a few automatic variables to help ease repetition even further. These variables are commonly used in a rule's recipe.
Some of the automatic variables are as follows:
Automatic variables | Meaning |
---|---|
$@ | Name of the rule of target. Usually used to specify the output filename. |
$< | Name of the first pre-requisite. |
$? | Names of all pre-requisites that are newer than the target. i.e. files that have been modified after the most recent code compilation |
$^ | Names of all pre-requisites with spaces between them. |
You can find the full list of the automatic variables on GNU Make's official documentation.
Implicit Variables
Like the automatic variables covered above, make also has some variables that have a set use. As I previously used the CFLAGS
macro/variable to store compiler flags, there are other variables that have an assumed use.
This can be thought not of as "reserved keywords" but more like the "general consensus" of naming variables.
These conventional variables are as follows:
Implicit variables | Description |
---|---|
VPATH | Make utility's equivalent of Bash's PATH variable. Paths are separated by the colon sign (: ). This is empty by default. |
AS | This is the assembler. The default is the as assembler. |
CC | The program for compiling C files. The default is cc . (Usually, cc points to gcc .) |
CXX | The program for compiling C++ files. The default is the g++ compiler. |
CPP | The program that runs the C pre-processor. The default is set to $(CC) -E . |
LEX | The program that turns Lexical grammars into source code. The default is lex . (You should change this to flex .) |
LINT | The program that lints your source code. The default is lint . |
RM | The command to remove a file. The default is rm -f . (Please pay strong attention to this!) |
ASFLAGS | This contains all the flags for the assembler. |
CFLAGS | This contains all the flags for the C compiler (cc ). |
CXXFLAGS | This contains all the flags for the C++ compiler (g++ ). |
CPPFLAGS | This contains all the flags for the C pre-processor. |
.PHONY | Specify targets that do not resembe name of a file. An example is the "make clean" target; where clean is a value of .PHONY |
Comments in a makefile
Comments in a makefile are like those in a shell script. They start with the pound/hash symbol (#
) and the contents of said line (after the pound/hash symbol) are considered as a comment by the make utility and is ignored.
Below is an example demonstrating this:
CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
# The '-D_FORTIFY_SOURCE=2' flag enables run-time buffer overflow detection
# The flags '-fpie -Wl,-pie' are for enabling complete address space layout randomization
Initial draft of a makefile
Now that I have described the basic syntax of a makefile's elements and also the dependency tree of my simple project, let us now write a very bare-bones Makefile to compile our code and link everything together.
Let us start with setting up the CFLAGS
, CC
and the VPATH
variables that are necessary for our compilation. (This is not the complete makefile. We will be building this progressively.)
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src
With that done, let us define our rules for building. I will create 3 rules, for each .c
file. My executable binary will be called make_tutorial
but yours can be whatever you want!
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src
make_tutorial : main.o calculator.o greeter.o
$(CC) $(CFLAGS) $? -o $@
main.o : main.c
$(CC) $(CFLAGS) -c $? -o $@
calculator.o : calculator.c
$(CC) $(CFLAGS) -c $? -o $@
greeter.o : greeter.c
$(CC) $(CFLAGS) -c $? -o $@
As you can see, I am compiling all the .c
files into object files (.o
) and linking them together at the end.
When we run the make
command, it will start with the first rule (make_tutorial
). This rule is to create a final executable binary of the same name. It has 3 prerequisite object files for each .c
files.
Each consecutive rule after the make_tutorial
rule is creating an object file from the source file of same the name. I can understand how complex this feels. So let us break down each of these automatic and implicit variables and understand what they mean.
$(CC)
: Calls the GNU C Compiler (gcc
).$(CFLAGS)
: An implicit variable to pass in our compiler flags like-Wall
, etc.$?
: Names of all prerequisite files that are newer than the target. In the rule formain.o
,$?
will expand tomain.c
IFmain.c
has been modified aftermain.o
had been generated.$@
: This is the target name. I am using this to omit typing the rule name twice. In rule formain.o
,$@
expands tomain.o
.
Finally, the options -c
and -o
are gcc
's options for compiling/assembling source files without linking and specifying an output file name respectively. You can check this by running the man 1 gcc
command in your terminal.
Now let's try and run this makefile and hope it works on first try!
$ make
gcc -Wall -Wextra -c src/main.c -o main.o
gcc -Wall -Wextra -c src/calculator.c -o calculator.o
gcc -Wall -Wextra -c src/greeter.c -o greeter.o
gcc -Wall -Wextra main.o calculator.o greeter.o -o make_tutorial
If you look closely, each step of compilation contains all the flags we specified in the CFLAGS
implicit variable. We can also see that the source files were automatically sourced from the "src" directory. This occurred automatically because we specified "src" in the VPATH
implicit variable.
Let's try and run the make_tutorial
binary and verify if everything works as intended.
$ ./make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!
Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.
Improving the makefile
"What is there to improve?"
Let us run the ls
command you can see that for yourself ;)
$ ls --group-directories-first -1
src
calculator.o
greeter.o
main.o
Makefile
make_tutorial
Do you see the build artifacts (object files)? Yeah, they can clutter things up for the worse. Let's use our build directory and reduce this clutter.
Below is the modified makefile:
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build
make_tutorial : main.o calculator.o greeter.o
$(CC) $(CFLAGS) $? -o $@
build/main.o : main.c
mkdir build
$(CC) $(CFLAGS) -c $? -o $@
build/calculator.o : calculator.c
$(CC) $(CFLAGS) -c $? -o $@
build/greeter.o : greeter.c
$(CC) $(CFLAGS) -c $? -o $@
Here, I have made one simple change: I added the build/
string before each rule that generates an object file. This will put each object file inside the "build" directory. I also added "build" to the VPATH
variable.
If you look closely, our first compilation target is make_tutorial
. But it will not be the target that is pedantically the first. The first target whose recipe runs is main.o
(or rather build/main.o
). Therefore, I added the "mkdir build" command as a recipe in the main.o
target.
If I were to not create the "build" directory, I would get the following error:
$ make
gcc -Wall -Wextra -c src/main.c -o build/main.o
Assembler messages:
Fatal error: can't create build/main.o: No such file or directory
make: *** [Makefile:12: build/main.o] Error 1
Now that we have modified our makefile, let us remove the current build artifacts along with the compiled binary and rerun the make utility.
$ rm -v *.o make_tutorial
removed 'calculator.o'
removed 'greeter.o'
removed 'main.o'
removed 'make_tutorial'
$ make
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o make_tutorial
This compiled perfectly! If you look closely, we had already specified the "build" directory in the VPATH
variable, making it possible for the make utility to search for our object files inside the "build" directory.
Our source and header files were automatically found from the "src" directory and the build artifacts (object files) were kept inside and linked from the "build" directory, just as we intended.
Adding .PHONY targets
We can take this improvement one step further. Let's add the "make clean" and "make run" targets.
Below is our final makefile:
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build
build/bin/make_tutorial : main.o calculator.o greeter.o
mkdir build/bin
$(CC) $(CFLAGS) $? -o $@
build/main.o : main.c
mkdir build
$(CC) $(CFLAGS) -c $? -o $@
build/calculator.o : calculator.c
$(CC) $(CFLAGS) -c $? -o $@
build/greeter.o : greeter.c
$(CC) $(CFLAGS) -c $? -o $@
.PHONY = clean
clean :
rm -rvf build
.PHONY = run
run: make_tutorial
./build/bin/make_tutorial
Everything about the build targets is the same, except for a change where I specify that I want the make_tutorial
binary executable file placed inside the build/bin/
directory.
Then, I set .PHONY
variable to clean
, to specify that clean
is not a file that the make utility needs to worry about. It is... phony. Under the clean
target, I specify what must be removed to "clean everything".
I do the same for the run
target. If you are a Rust developer you will like this pattern. Like the cargo run
command, I use the make run
command to run the compiled binary.
For us to run the make_tutorial
binary, it must exist. So I added it to the prerequisite for the run
target.
Let's run make clean
first and then run make run
directly!
$ make clean
rm -rvf build
removed 'build/greeter.o'
removed 'build/main.o'
removed 'build/calculator.o'
removed 'build/bin/make_tutorial'
removed directory 'build/bin'
removed directory 'build'
$ make run
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
mkdir build/bin
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o build/bin/make_tutorial
./build/bin/make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!
Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.
As you see here, we did not run the make
command to compile our project first. Upon running the make run
, compilation was taken care of. Let's understand how it happened.
Upon running the make run
command, the make utility first looks at the run
target. A prerequisite for the run
target is our binary file that we compile. So our make_tutorial
binary file gets compiled first.
The make_tutorial
has its own prerequisites which are placed inside the build/
directory. Once those object files are compiled, our make_tutorial
binary is compiled; finally, the Make utility returns back to the run
target and the binary file ./build/bin/make_tutorial
is executed.
such elegance much wow
Conclusion
This article covers the basics of a makefile, a file that the make utility depends on, to simplify compilation of your software repository. This is done by starting from a basic Makefile and building it as our needs grow.