Make is a program that makes other programs. This is especially useful when your programs become large, and recompiling after an edit requires multiple steps. Using a
makefile, we can configure a program to compile simply by typing the
make command into terminal. This lab will teach you how to write a basic makefile to be used in assignments from here on out.
1 - Using make
When you type
make into terminal, it will automatically look for a file named
makefile or 'Makefile' for instructions. Let's start with a makefile for a single cpp file.
In the part 1 folder, you will find a file called
charizard.cpp. You can compile this simply with the following instruction:
g++ -g -Wall charizard.cpp -o charizard
But let's use make.
1.1 - Syntax and Structure
A basic makefile's structure is the following:
target: dependencies system command
Each of these is called a rule. If you type
make <target>, the make tool searches for the appropriate target. The dependencies are the files that can affect the compilation result of your target. The system command is basically what you would type into terminal if you were to compile the file manually. Note that the system command is and must be precedeeded by a tab.
If you ever get an error message like:
makefile:2: *** missing separator. Stop.
It means on line 2, make is expecting a tab but didn't find it.
In this instance, we want to use this line as our system command:
g++ -g -Wall charizard.cpp -o charizard
1.2 - Default Target
But what happens when you just run
make? Good question! There's a very nice default target called 'all' made just for that. When you just run
make, the make program will look for all targets listed after
all, find the corresponding rule for each target, and compile them.
Let's have 'all' make the charizard file, which will become the name of our target. So our resulting
makefile is as below:
all: charizard charizard: charizard.cpp g++ -g -Wall charizard.cpp -o charizard
Save this file as
makefile in the part1 directory and type make. ~*~ MAGIC ~*~
1.3 - Cleaning Up
Now large projects will eventually generate a lot of binary files. We want to keep our workspace relatively clean by writing a rule to delete the generated files. This is also helpful when you're having mysterious problems as they sometimes go away after you delete and recompile your binaries.
Here's our clean rule:
clean: rm charizard
Very simple, since we only generate one file. You can add this to the end of your makefile. Now clean your directory using the
make clean rule.
2 - Compiling Multi-File Programs
2.1 - Object Files
Compiling a multi-file program requires two main steps: compiling each .cpp file separately, and putting them all together to form the executable.
Here we introduce a new binary file type: .o files. These are the intermediate files we make in preparation to compile the executable. Any file that doesn't contain the main function must be compiled into a .o file.
Let's look inside the part 2 folder.
$ cd ../part2 $ ls *
You will find three classes: AttackMove, Battle, and Pokemon. We want to compile each of these into their own .o file of the same name.
Since we're going to have a lot of binary files beyond this point, let's make a bin directory to hold them all for easy cleanup.
$ mkdir bin
To make an object file, we simply need to add the
-c flag in the compile command. Let's compile AttackMove first.
g++ -g -Wall -c src/attackMove.cpp -o bin/attackMove.o
Simple as that. Do the same for the other two classes, and we can then compile the main.
2.2 - Putting It All Together
To compile the main, we just have to include all the .o files that we've already made in the g++ command.
g++ -g -Wall bin/attackMove.o bin/battle.o bin/pokemon.o main.cpp -o bin/lab3
Note: A .o file is compiled code that doesn't get linked to other code even if it calls functions from other classes. We tell the compiler this using the
-c flag so that the compiler does not check whether the functions from other classes are implemented. When we want to compile the full executable, we do not want to have the
-c flag in that statement because we want the compiler to link all the code together in the final step. If you get a linker error, it's probably something to do with the
And you have your own pokemon battle simulator! Run it like normal using:
3 - Makefiles for Multi-File Programs
Well that's great and all, but how do we do that in a makefile?
3.1 - Dependencies
Let's go back to the basic make rule structure:
target: dependencies system command
We skipped dependencies before, but it's something we want to use now. If a target has dependencies, make first checks if those dependencies exist before executing the system command associated with that rule. If the dependencies don't exist, make will run the rule to make those dependencies if they exist.
Make will also check to see if the dependencies have been updated since the last make and will only recompile the dependencies that have changed. This can save you a lot of time if you make a change and don't want to recompile all the files in your project.
Remember that dependencies are the files that can affect the compilation result of your target. This includes all the non-standard-library header files that you
#include, a class's own header file and .cpp file, and, if you are compiling into an executable, all the .o files you need.
Fill out the new makefile below. Note: if you copy and paste the code from course website and paste it into your makefile, you will need to fix all the spaces and change them into tabs.
all: bin/lab3 bin/lab3: bin/attackMove.o bin/<???> bin/<???> <???> bin/attackMove.o: lib/attackMove.h src/attackMove.cpp g++ -g -Wall -c src/attackMove.cpp -o bin/attackMove.o bin/<???>: <???> <???> bin/<???>: <???> <???>
To test if your makefile works, run
make and try to run the program.
3.2 - Variables
Prof. Redekopp decides he doesn't like the students compiling to the
bin directory. He instead wants the directory to be named
exe. You could easily find and replace bin with exe, but that's messy and could generate errors. Why not take advantage of variables instead?
At the top of your makefile, let's define:
BIN_DIR = exe
Now let's replace every instance of
$(BIN_DIR), like so:
$(BIN_DIR)/attackMove.o: lib/attackMove.h src/attackMove.cpp g++ -g -Wall -c src/attackMove.cpp -o $(BIN_DIR)/attackMove.o
Now when the profesor changes his mind again and wants a different name for the directory, we can just change the variable at the top. (But in general, keep the
binaries in a directory named
Another use for this is to have a CPPFLAGS (compiler flags) variable that holds all the flags you frequently use to compile. We can have a CXX variable to specify which compiler to use. For example:
CXX = g++ CPPFLAGS = -Wall -g $(BIN_DIR)/attackMove.o: lib/attackMove.h src/attackMove.cpp $(CXX) $(CPPFLAGS) -c src/attackMove.cpp -o $(BIN_DIR)/attackMove.o
3.3 - DIRSTAMP
If you tried to make again with the above changes, you'll probably get an error that the exe directory doesn't exist. Well that's a pain.
Let's define a rule to make sure the exe directory exists.
$(BIN_DIR)/.dirstamp: mkdir -p $(BIN_DIR) touch $(BIN_DIR)/.dirstamp
The .dirstamp file is a hidden file we make to make sure a directory exists. Notice that this rule does not have any dependency. When a rule has no dependency, it will always be executed when we type in
make <target>. You can add this rule to the dependency list of your compile commands.
$(BIN_DIR)/attackMove.o: lib/attackMove.h src/attackMove.cpp $(BIN_DIR)/.dirstamp
3.4 - PHONY and Clean
Remember back when we wrote our clean rule for part 1? Well there's a small problem with the original rule. If a file named 'clean' exists in our directory, make won't run the clean rule because it sees that the file already exists!
To get around this, we declare the clean rule as PHONY to tell make that it's not associated with a file.
.PHONY: clean clean: rm -rf $(BIN_DIR)
Since we put all our executables in BIN_DIR, we can simply delete the whole folder. Simple as that!
Note: there's a danger when using rm -rf as it will irreversably delete whatever BIN_DIR is without prompting additional confirmation. Be good and don't delete your entire OS.
4 - Compile Flags
You might have noticed that we have been using some magic commands when we compile files. Namely, you should have seen
-c. Here is a brief explanation of what exactly these commands do. Note that when you append a flag to your compile command, you should also add a space between the flags (i.e.
First of all,
g++ is used to compile your programs using the GNU Compiler Collection (GCC). This command specifically is used for c++ programs. There are other compilers out there, and when you want to use other compilers, you replace
g++ with the command that's used by the other compiler. In this class, we ask that you always compile your program with gcc compiler using the
When you see a terminal command that has a
- followed by some text, this is usually a flag. A flag can be used to specify a setting or add additional information about the command. In the
g++ command, you will often see
-std= command. Here's a descriptino of what they do and how to use them:
-g: Provide debugging feature for your program. You will need this when you want to use gdb or valgrind. To use this flag, simply append the command to the end of your compile command.
-Wall: Turn on all warnings. This is helpful because, as you might have seen, not all errors cause your program to not compile. There are some problematic operations that can cause undefined or unexpected behaviors in edge cases. One example you have seen is the 'comparison between signed and unsigned values'. By turning on all warnings, you make sure that you eliminate these potentially dangerous operations. In the case of the signed/unsigned comparison, usually you do so by casting one type into another. To use this flag, simply append the command to the end of your compile command.
-o: This flag controls the output of your compilation. When you compile your program, it's important to note that the name of your executable or object files do not have to share the same name of your .cpp files, even though by convention they are usually the same. When you append
-o filenameto the end of your compile command, the compiled binary output will be the filename you specificed.
-c: Compile the files but do not link them. This is usually used to compile intermediate object files. To use this flag, simply append the command to the end of your compile command.
-std=: This flag is used to specify the c++ version that you would like to use. As you can expect, c++ is an evolving language, and it keeps adding new features to the language. Therefore, if you use a feature that's introduced in a newer version but try to compile the code with an older version, the compiler wouldn't know what the features are and the compilation will fail. You use this flag by appending this flag to the end of your compile comand and speficy the version number after the equal sign (i.e.
-std=c++11). In this class, most of the times you can get away with using the default version and don't need to include this command at all. There are times when you need to use the c++ 11 and you do it with
-std=c++11. We ask that you either use the default version or c++ 11 in this class.
Lastly, you might sometimes see people compile files with
g++ something.cpp main.cpp -o main -g -Wall, and you might wonder why they list the source files before the options. As it turns out, the order that you specify the options does not matter. If you really want to, you can even use
g++ something.cpp -g -Wall main.cpp -o main to compile your program. However, by convention, we usually group the list of source files together and the list of options together. The two most common command conventions you will see and should stick to will be:
g++ <sourcefile> -o <filename> -flags and
g++ -flags <sourcefile> -o <filename>.
5 - Assignment: Complete the Makefile
Your final makefile should look something like this:
CXX = g++ CPPFLAGS = -g -Wall BIN_DIR = bin all: $(BIN_DIR)/lab3 $(BIN_DIR)/lab3: <???> <???> <???>.o: <???> <???> <???>.o: <???> <???> <???>.o: <???> <???> .PHONY: clean clean: rm -rf $(BIN_DIR) $(BIN_DIR)/.dirstamp: mkdir -p $(BIN_DIR) touch $(BIN_DIR)/.dirstamp
If you really want, you can even have the makefile run the executable for you.
all: $(BIN_DIR)/lab3 ./$(BIN_DIR)/lab3
5.1 - Review Questions
What is the purpose of the
What is the advantage of compiling to .o files via makefile compared to typing it into terminal or compiling the executable together in one step?
Why do you not have to include attackMove.cpp when compiling pokemon.cpp into pokemon.o?
6 - More about Makefiles
If you would like to know more about Makefile, you can visit GNU Make Manual. It covers both the basic and more advanced topics of the Makefiles.
CAUTION Do not use these advanced make commands until you are very comfortable with Makefiles.
7 - More about GCC
We have listed some commonly used flags and options that you will see in this class. This is just a list of the common flags that you will use in this class. This is in no way comprehensive. If you see a flag that you do not understand, or if you are curious about other options, you can refer to this official document from GCC. Feel free to play around with the flags in your free time.
IMPORTANT We will be compiling your code with
-g -Wall -std=c++11, so you must use the same options to check that your code compiles and produces no warnings.