SETTINGS
Appearance
Language
About

Settings

Select a category to the left.

Appearance

Theme

Light or dark? Choose how the site looks to you by clicking an image below.

Light Dark

Language

Preferred Language

All content on utk.claranguyen.me is originally in UK English. However, if content exists in your preferred language, it will display as that instead. Feel free to choose that below. This will require a page refresh to take effect.

About

"utk.claranguyen.me" details

Domain Name: claranguyen.me
Site Version: 3.0.1
Last Updated: 2019/08/18
Synopsis
By now, you've probably used the GCC suite of compiler tools to compile your code and run it. Has it been a pain to type it every time? Maybe you accidentally misplaced the -o and deleted your code? It happens. There are tools out there to help with compilation, one of them being make. How would you like it if, every time you had to compile your code, you just had to type "make"? It'd get it right every time, and only compiles what needs to be compiled. Curious? Well let's get started.
Make: Compiling with "Recipes"
Makefiles are like recipe books. You are given a program, and how to create it. We'll call this a "recipe" (for obvious reasons). Here's a very simple one. Let's say that you have the following directory structure:
UNIX Command
UNIX> ls -l
total 0
-rw-r--r--  1 cnguyen cnguyen 420 2019/10/26 main.cpp

You've done this before (if not...). You take main.cpp, chuck it at g++ and get an executable out.
UNIX Command
UNIX> g++ -o main main.cpp

When being productive on the Terminal, I certainly don't want to have to keep typing this over and over (or spamming "up" on previous commands until I find it). I just want something to automate it. Ah, what about make? Let's make a makefile for this. Open up your favourite text editor (vim...) and let's make makefile.
File (makefile)
CC = g++

all: main

main: main.cpp
    $(CC) -o $@ $^
Wait, stop. Don't hit the "back" button on that browser. Let me explain...

Defined Variables

Notice CC = ... at the top. These are defined variables. This is just like in C++. The variable name is to the left of the equal sign and the value is to the right. In this case, we are setting CC to g++. That's simple right? You can access these variables via $(VARIABLE_NAME). In this case, we can get that variable with $(CC).

Targets, Recipes (Prerequisites), and Automatic Variables

Let's take a look at that all: ... line. What is it doing? For simplicity, let's just say that it's defining the programs to be compiled.

Now then, we know that main is a program that needs to be made. So let's check out that section real quick:
File (makefile) line 5-6
main: main.cpp
    $(CC) -o $@ $^

We call this a rule. How do we read a rule? Well the first line follows the target: recipe pattern (the formal terminology is target: prerequisites). In layman's terms, we are using the recipe (main.cpp) to make the target (make). Then, every line following will have a series of commands used to create that target using the files in the recipe.

That second line is probably what's scaring you a bit. Let's make it less scary for a second. We know $(CC) resolves to g++ so:
File (makefile) line 6
    g++ -o $@ $^

So what's up with $@ and $^? Well these are known as Automatic Variables. They are generated based on each rule. In this case:
  • $@ - This is the target. In this rule, it resolves to "main".
  • $^ - This is the recipe. In this rule, it resolves to "main.cpp".

Thus, the command becomes this:
File (makefile) line 6
    g++ -o main main.cpp

Cool huh? Now we can type in make as a command and it'll do the compilation for us. It'll also print out the commands used.
UNIX Command
UNIX> make
g++ -o main main.cpp

Make also will only run a rule if the target hasn't been created... or if one of the recipe files is more recent than the target itself. This means that your program will always be up-to-date, and won't recompile if it doesn't have to. For larger, industry-level applications that can take minutes or hours to compile, this is very important.
A more applicable example: Multi-file Projects
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...
  1. 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.

  2. 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
Other notes

"How about the make clean command?"

You are allowed to make custom rules that aren't linked anywhere (like in all). Then you can invoke them via:
UNIX Command
UNIX> make CUSTOM_RULE

The property I just mentioned is what make clean uses. So, let's make a custom rule at the bottom of the makefile from Method 2 above:
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 $<

# Custom rule
clean:
	$(RM) vec.o map.o ds_common.o main

As you can see, all doesn't call clean. Neither does main's rule or any of the object file rules. The only way to call it is if you manually call it with this:
UNIX Command
UNIX> make clean

What does it do? Well it deletes all object files and the executable. You can use this to "clean" up your project directory (hence the name) and do a fresh compilation if you want.

"Why $(RM)? Isn't rm good enough?"

Nope. rm fails if it tries to remove a file in the list that doesn't exist. The only way to force it is with rm -f. Guess what $(RM) resolves to? Yup, on most machines, it's rm -f. Use it. In the event that $(RM) is different on your machine, make will decide it for you.

"What else can I do?"

Pretty much anything. You can shove bash commands in there (hence g++ and rm discussed earlier). You can make a custom rule to auto-TAR your assignment via make package if you want.
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 $<

# Custom rule
clean:
	$(RM) vec.o map.o ds_common.o main

# Prepare for submission
package: main.cpp vec.cpp vec.hpp map.cpp map.hpp ds_common.cpp ds_common.hpp
	tar -cf submission.tar $^

You can also put the source code files into variables to make your makefile just a bit cleaner...
File (makefile)
CC = g++

# Files
CPP_FILES = vec.cpp map.cpp ds_common.cpp
HPP_FILES = vec.hpp map.hpp ds_common.hpp
OBJ_FILES = vec.o   map.o   ds_common.o

all: main

# The main executable
main: main.cpp $(OBJ_FILES)
    $(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 $<

# Custom rule
clean:
	$(RM) main $(OBJ_FILES)

# Prepare for submission
package: main.cpp $(CPP_FILES) $(HPP_FILES)
	tar -cf submission.tar $^

Even more of an extreme example, a friend of mine made Makefiles that did tar'ing and called a Python script to auto-submit to Instructure Canvas (it requires some specific stuff, so don't just go downloading it hoping it'd solve all of your problems). The possibilities are endless.

Have at making these! Once you get the hang of it, you have a pretty powerful tool to help make life easier when developing. For COSC 302 students, there's an assignment where you are forced to create and submit a makefile. I hope it helps there especially.
Guide Information
Basic Information
Name: Introduction to Makefiles
Description: Because automation is nice
ID: Makefiles
File Information
File Size: 17.33 KB (17744 bytes)
Last Modified: 2019/10/28 06:07:53
Version: 1.0.0
Translators
en-gb Clara Nguyen (iDestyKK)