Make is a tool for building both simple and complicated projects. It can be used with pretty much any development tool that can be run from the command line. It’s most common use is building compiled programs from source code. But it would be equally effective applying visual filters to raw camera images.
Setup
For this walkthrough we’ll be using the gcc
C compiler and the Gnu make
utility. You will also need a text editor. By now you probably have a development setup with these tools or you’re working on a lab machine.
To be sure your gcc
and make
are installed use the following command:
gcc --version
make --version
If you find they aren’t installed you can install them with the following commands:
sudo apt install gcc
sudo apt install make
Let’s start with a simple C program consisting of three source files. Create a folder (directory) for your project. In that folder create two .c and one .h source code files with the following contents:
pasta.c
#include <stdio.h>
#include "servings.h"
int main()
{
int servings = 5;
int ounces = servings2ounces(servings);
printf("You need %d ounces of dry pasta to make %d servings.\n", ounces, servings);
}
servings.h
int servings2ounces(int servings);
servings.c
#include "servings.h"
int servings2ounces(int servings)
{
return servings * 3;
}
You can build this program with the following command:
gcc pasta.c servings.c -o pasta
Then you can run it with this command:
./pasta
Compiling and Linking
The previous command actually performed three operations:
- Compile pasta.c to machine language
- Compile math.c to machine language
- Link the machine language produced in steps 1 and 2 with the runtime libraries and put the result into the
pasta
executable file.
You can do these steps separately as follows:
gcc -c pasta.c
gcc -c servings.c
gcc pasta.o servings.o -o pasta
Each of the compile steps produced a corresponding object file with a .o
extension. The object files contain the machine code of the functions from the source files plus information needed to link them together into a executable program. The third, linking, step combines these files plus runtime libraries that into the executable file - with no extension.
But why do this in separate steps when the simple one-step build works?
Suppose you have a large application with dozens of source code files. Then you would only want to compile the files that have changed before linking everything together. That saves build time. Make orchestrates complicated build processes and does it in a simple way.
Makefiles
A make file is composed of rules. Each rule has three parts; plus it may have a comment:
- The target is the thing that the rule is intended to create. If you were making a cake, it would be the cake itself.
- The prerequisites are the inputs for creating the target. Sort of like the ingredients to the cake.
- The recipe is the set of commands required to produce the target from the prerequisites.
A Makefile is a text file containing a set of rules. By default, the file is named just that, “Makefile” without any extension. Let’s create one for our project.
Makefile
pasta: pasta.c servings.c servings.h
gcc pasta.c servings.c -o pasta
IMPORTANT: You must use a tab to indent the lines of the recipe. Spaces won’t work. So, in the second line of this Makefile, be sure to use a tab.
This is a very simple makefile with only one rule. It says that pasta
(the executable file) depends on pasta.c
, servings.c
, and servings.h
. If any of those files changes, then the recipe commands are executed to re-create it.
Let’s run the make file. At the command-line just type the following:
make
Make automatically looks for a Makefile
in the current directory and executes the rules in that file. When necessary, you can use the -f
option to specify a different filename for the Makefile but that’s uncommon.
If you already built pasta
using the gcc
command then make simply responds with “‘pasta’ is up to date.” and does nothing. How does it know that that pasta
is up to date? By checking the file modification dates. If the date modified on pasta
is more recent than all of its prerequisites then the recipe doesn’t need to be executed.
Let’s test this. Make a change to pasta.c
. You could change the message, add a comment, anything. In this example I changed “You” to “We” in the message:
pasta.c
#include <stdio.h>
#include "servings.h"
int main()
{
int servings = 5;
int ounces = servings2ounces(servings);
printf("We need %d ounces of dry pasta to make %d servings.\n", ounces, servings);
}
Now run make:
make
This time it executes the command to build pasta
. Run make
again and it will report that pasta
is up to date.
Of course, this simple make file just builds the whole thing whenever something changes. A better Makefile
would only build what’s necessary.
Multiple Rules
Update your Makefile
as follows:
pasta: pasta.o servings.o
gcc pasta.o servings.o -o pasta
pasta.o: pasta.c servings.h
gcc -c pasta.c
servings.o: servings.c servings.h
gcc -c servings.c
Again, make sure those are tabs on the indents and not spaces. Conveniently, most editors such as VS code recognize Makefile
as a particular format and insert literal tabs when you press the Tab
key.
Run this version of the Makefile
make
This version compiles each .c file separately and then links them together. Of course, the rules are in the opposite order with the linking coming first and then the compiling of each source file. This is because the first target in a Makefile
is the default - the final result that the Makefile is supposed to build.
Run the make
again and it will report that everything is up to date.
Touch
The touch
command is handy when experimenting with make
. touch
simply updates the modified date of a file. So, make
will treat that file as if it has been modified.
Run the following commands:
touch servings.c
make
It compiles servings.c
into servings.o
and then links everything together. It didn’t need to recompile pasta.c
because it hadn’t changed.
Try this:
touch servings.h
make
This time it compiles both source files. That’s because they both depend on servings.h
.
Other Targets
Add a clean:
target to your Makefile
so it looks like this:
Makefile
pasta: pasta.o servings.o
gcc pasta.o servings.o -o pasta
clean:
rm *.o
rm pasta
pasta.o: pasta.c servings.h
gcc -c pasta.c
servings.o: servings.c servings.h
gcc -c servings.c
Notice that the clean:
rule comes after the pasta:
rule. That’s because we want to keep pasta:
as the default rule.
Type the following command:
make clean
It removes all .o
object files and the final pasta
executable file leaving the directory with just the source files.
Now type:
make
It rebuilds the whole project from scratch.
More Make
Make has a lot of options for more sophisticated Makefiles than these. It has variables, pattern rules, string substitution and more. The Makefile Tutorial is an excellent resource for learning the details. But for our purposes, the simple rules in this walkthrough are sufficient.