So you're probably thinking that, for a single file, a makefile is useless. Sure, believe that I guess. Let's move on to where makefiles really shine...
multi-file projects.
Just for perspective of how big this is...
Have you ever tried to compile Chromium (basically Google Chrome) or OpenOffice on a Raspberry Pi?
It takes like a day the first try.
Now let's say you changed a single line of code and had to
compile it again.
I doubt you'd want to type in a lot of commands either when you can just automate it and only compile stuff that changed.
Rambling aside, example time. Let's say you have the following directory structure:
UNIX Command
UNIX> ls -l
total 0
-rw-r--r-- 1 cnguyen cnguyen 3022 2019/10/26 ds_common.cpp
-rw-r--r-- 1 cnguyen cnguyen 578 2019/10/26 ds_common.hpp
-rw-r--r-- 1 cnguyen cnguyen 1128 2019/10/26 main.cpp
-rw-r--r-- 1 cnguyen cnguyen 40942 2019/10/26 map.cpp
-rw-r--r-- 1 cnguyen cnguyen 9841 2019/10/26 map.hpp
-rw-r--r-- 1 cnguyen cnguyen 17043 2019/10/26 vec.cpp
-rw-r--r-- 1 cnguyen cnguyen 9213 2019/10/26 vec.hpp
For those who took
COSC 140, most of these files should be obvious. If you don't understand, that's fine. Assume that the compilation process follows the following dependency tree (stuff at the top requires what's down below. Use the arrows as reference):
Reading this is fairly trivial.
main
requires
main.cpp
. That file includes
map.hpp
and
vec.hpp
. Both of those files use some helper functions that are stored in
ds_common.hpp
. Lastly, all of these files have their functions defined in their respective cpp files (e.g.,
vec.hpp
has functions defined in
vec.cpp
). Let's write a makefile for this. There's a few ways to approach it... a naïve way, an object file, and a library come to mind. Let's go over the first two methods.
Method 1: The naïve way
To compile this with
g++
directly, we can use:
UNIX Command
UNIX> g++ -o main main.cpp vec.cpp map.cpp ds_common.cpp
Now then... let's put that in a makefile:
File (makefile)
CC = g++
all: main
main: main.cpp vec.cpp map.cpp ds_common.cpp
$(CC) -o $@ $^
And if we run it, boom, it'll give us exactly what we want. Mission accomplished, right? Sure. But we have two problems here...
-
We are not including hpp files in the recipe.
When you compile with
g++
, you NEVER put the header files (.h/.hpp/whatever) in the command.
The compiler figures it out by what the cpp files include.
But if we put the hpp files into the recipe, it'll put that into the $^
automatic variable.
We don't want that.
-
If one of the cpp files changes, the entire thing has to be recompiled.
If
vec.cpp/hpp
, map.cpp/hpp
, and ds_common.cpp/hpp
are from a library (that you stole off the Internet probably) and you are never changing it, there's no need to compile it every time main.cpp
changes.
This is still considered a small project and probably compiles in milliseconds.
But it's good to get into the mentality of trying to prevent compiling code that hasn't changed.
Honestly, if you don't care and can somehow guarantee that the files all exist, this solution works.
But what if you want to solve this? Well, let's look at method 2...
Method 2: Object Files
That tree diagram above sounded kinda useless for method 1.
However, we can use it to eliminate the two issues listed for method 1.
Introducing
Object files.
These files are diagnosed with
.o
as the file extension and contain compiled machine code.
However, unlike an executable, these don't need an entry point (e.g.
int main()
).
It's just code.
A linker then uses one or more of these to create a single executable.
See where I'm getting at here?
Since we know
vec.cpp/hpp
,
map.cpp/hpp
, and
ds_common.cpp/hpp
won't change, we can take them and put them into object files (let's call them
vec.o
,
map.o
, and
ds_common.o
). Then we can simply include them when compiling
main
. Let's give it a shot!
UNIX Commands
UNIX> g++ -o vec.o -c vec.cpp
UNIX> g++ -o map.o -c map.cpp
UNIX> g++ -o ds_common.o -c ds_common.cpp
Then, we can put those files into our
g++
call instead of the .c files like this:
UNIX Command
UNIX> g++ -o main main.cpp vec.o map.o ds_common.o
Why would we want to do this? The object files already contain vec, map, and ds_common compiled. So when we run
g++
, it'll only compile main.cpp, and then link the other object files together. They were already compiled so it won't have to do it again and again.
Oh yeah, let's put this into a makefile. I'm going to introduce a two new things while I'm at it... Behold:
File (makefile)
CC = g++
all: main
# The main executable
main: main.cpp vec.o map.o ds_common.o
$(CC) -o $@ $^
# Object files
vec.o: vec.cpp vec.hpp
$(CC) -o $@ -c $<
map.o: map.cpp map.hpp
$(CC) -o $@ -c $<
ds_common.o: ds_common.cpp ds_common.hpp
$(CC) -o $@ -c $<
Notice something new?
This is our first makefile with
multiple rules. Wonder how it works? In
all
, Make will know to try to make
main
from
main.cpp
and the object files. However, what if the object files don't exist (e.g., the first time we compile this program)? It will go to those rules and run them first. Think of it as a recursive tree (hence the diagram above)... we start at
main
, see that
vec.o
is required, then make that first. Do the same with the others as well. Then go back and make the executable once everything is done.
Also, this introduces a new automatic variable to us...
$<
. Unlike
$^
, which is the entire recipe,
$<
will only be the
first file. This is useful here because we want
gcc -o vec.o -c vec.c
... not
gcc -o vec.o -c vec.c vec.h
Let's run it. Look what commands it ran (again, it prints out the commands it runs).
UNIX Command
UNIX> make
g++ -o vec.o -c vec.cpp
g++ -o map.o -c map.cpp
g++ -o ds_common.o -c ds_common.cpp
g++ -o main main.cpp vec.o map.o ds_common.o