Makefiles

Introduction

For this course we will be using Makefiles for the purpose of compiling programs will multiple files and class dependencies. You can simply break down this process into two parts, compiling your .cpp files into object files and then linking your object files into an executable which runs your program, let's see how we get there.

A Makefile is typically made of multiple entries which can be essentially broken up into three separate sections: Targets, Dependencies and Commands. Before we get into the syntax and structure of makefiles lets first clearly understand targets and dependencies. The target is going to be the file you aim to create and the dependencies are the other files that the target is dependent on to run. Our targets are going to be one of the two types of files we are aiming to create, Executable and Object files. Object files are made from compiling .cpp files and Executable files are made from linking Object files.

Make Basics

The general structure of a make file is a series of chunks that follow this pattern:

{Target}:  {Dependencies} 
{Tab} {Command} 

{target} is the name of the target you are trying to build. You can name it whatever you like, but generally you'll name most targets after the file that target will build.

The target must be followed by a colon(:) character.

{Dependencies} is a list of files or targets that must be in place before this target can be built. Normally the dependencies will either be files or other targets defined somewhere else in the make file.

The line immediately underneath the target must begin with a tab (Spaces will not work, and this has caused a surprising amount of angst over the years.)

Following the tab, you can write ANY legitimate commands for your operating system. Typically, though, this is the command you need to create the target. You can have multiple command lines for a target, but each needs to begin with a tab.

So imagine the simplest case where you have a main.cpp with no other dependencies. You can write a make file for this program that looks like this:

myProg: main.cpp
	g++ main.cpp -o myProg

Here, myProg is the target name. It requires main.cpp, and when it is activated, it runs the g++ compiler and renames the output myProg. If you call this file "makefile" and put it in the same directory as main.cpp, you can type "make" and your program will automatically compile. If you simply type make, the make utility will try to create the topmost target (which in this case is the only one we have.)

Make with multi-file projects

Makefiles are great with simple programs, but they really come into their own when you want to build a more complex program that involves a lot of different files.

So imagine you're building a program that simulates a horse race. You might have a horse class, which contains a cpp file and an h file. You might also have a race class. The race class has its own cpp and h files, but the race needs to instantiate the horse class. (The race isn't much fun if the horses don't show up.) So the race class is dependant on the horse class. If the horse class gets changed, the race class will need to be recompiled to handle any differences. Likewise, it doesn't make sense to compile the race if the horse hasn't been compiled yet. Let's add a main.cpp to tie the whole thing together. Here's all the files we're working with so far:

main.cpp
race.cpp
race.h
horse.cpp
horse.h

I put those files in a rough order. You need horse.h to compile horse.cpp, and you need at least a definition of the horse before you can compile the race.cpp. Of course race.cpp also needs race.h, and main.cpp won't compile until everything else is working.

Make files are there to help you tame this complexity. They allow you to build your program step by step, creating {targets} along the way. Your make file tells the system how to build each target, and eventually how to build the entire application from the various targets.

It might be easiest to think of this from the top down. Recall that when you have a multi-file project, you can compile intermediate object code (that is code without a main function) and then you can link these object files together at the end to build a final project. So if we begin at the main target, this is what we'll probably need:

horseRace: main.o race.o horse.o
	g++ main.o race.o horse.o -o horseRace

What I'm saying here is "I want to build the horseRace target. In order to do this, I need working object files for main.o, race.o, and horse.o If those are in place use g++ to link these files together and call the output horseRace."

Of course, this implies that we have the ability to create all these object files. The complete first pass of the makefile might look like this:

horseRace: main.o race.o horse.o
	g++ main.o race.o horse.o -o horseRace

main.o: main.cpp race.h horse.h
	g++ -c main.cpp

race.o: race.cpp race.h horse.h
	g++ -c race.cpp

horse.o: horse.cpp horse.h
	g++ -c horse.cpp

The targets all make sense when you think about them. To build main.o, you need main.cpp and the header files for the race and the horse (you can compile an object with only the headers, so you can specify .h rather than .o here.)

Remember that the -c compiler directive creates an object file with the same name as the original cpp file. That's why each of the .o targets uses the -c directive. Note that apart from the first target (which should usually be the final build) the order of targets is not critical. Still, it's helpful to go from more general to more specific.

If you save this file as "makefile" and run make, the make utility will try to build the first target (horseRace.) If this is the first pass, none of the object files will exist, so make will automatically go to any missing targets, check their dependencies, and run them. In this way, make will compile only those elements that have changed since the last successful compile, but will ultimately ensure your entire project is compiled.

Adding Utility targets

It's common to also add utility targets to the bottom of your make file. These targets allow you to run various os commands through the make utility. Here is the completed horseRace with two new utility targets added:

horseRace: main.o race.o horse.o
	g++ main.o race.o horse.o -o horseRace

main.o: main.cpp race.h horse.h
	g++ -c main.cpp

race.o: race.cpp race.h horse.h
	g++ -c race.cpp

horse.o: horse.cpp horse.h
	g++ -c horse.cpp

clean: 
	rm *.o
	rm horseRace

run:	horseRace
	./horseRace

The clean target is special. It isn't really about building anything. Instead, it's a utility for cleaning things up. Imagine you've written a program and it compiles, but then you find a mistake. When you fix the mistake, it's possible that the old incorrect executable file is sitting on your directory, and you could think you're looking a a new version when you're actually looking at the old version. It would be nice to have the ability to quickly clean out the older versions of things to guarantee you have a fresh compile. That's what the clean target does. It removes all the .o files as well as the final executable. To run it, just type "make clean" from the command line.

It's interesting that clean doesn't have any dependencies. That's because you can run it any time. It doesn't really matter what you've built before, because you want to delete all of the compiled code. Be careful not to delete all files with a clean command, because that would destroy your source code, too!

The run target is optional, but very nice. It allows a user to quickly run a program with "make run" It's very handy when debugging your code, because you can use the same tool (make) to both compile and run your code. In our class, we'll expect you to include a run target in your make files so the grader can easily run your code without having to figure out which file is executable.

The run target has horseRace (the final version of the program) set as its dependency. That way, if the user tries to run the program and it isn't up to date, the make utility will recompile any necessary components.

Sometimes you'll also see an install target. This normally compiles the final version of the code while also doing other housekeeping like checking for external dependancies, creating a file structure, and maybe even moving files around so they are available to the entire operating system.

You may have been impressed by unix users telling you they compile all their applications by hand, but in reality this often simply means they run

sudo make install

Sudo gives a temporary form of root-like access to subsequent commands.

Fun with macros

You can also use macros in your makefile. The most common macros (by tradition) are

CC=g++

and

CFLAGS=-g -Wall

(-g means include debugging information and -Wall means include all warnings)

You can then use the $() syntax to incorporate the compiler and flags into your operations, so

g++ -c foo.cpp

becomes

$(CC) $(CFLAGS) -c foo.cpp

The nice thing about this is it's very easy to change the way the compiler behaves. The "-g -Wall" stuff is great for debugging, but when you're ready to finalize your program, just change the CFLAGS to nothing, and you'll get a more streamlined compilation.

A complete version of the horseRace makefile might look like this:


#makefile for horserace

#standard g++ compiler
CC = g++

#for now, always compile with debugging information
#set CFLAGS to empty for final compilation

CFLAGS = -g -Wall

horseRace: main.o race.o horse.o
	$(CC) $(CFLAGS) main.o race.o horse.o -o horseRace

main.o: main.cpp race.h horse.h
	$(CC) $(CFLAGS) -c main.cpp

race.o: race.cpp race.h horse.h
	$(CC) $(CFLAGS) -c race.cpp

horse.o: horse.cpp horse.h
	$(CC) $(CFLAGS) -c horse.cpp

clean: 
	rm *.o
	rm horseRace

run:	horseRace
	./horseRace

Working samples

You can see a sample makefile at http://cs.iupui.edu/~aharris/240/classes/critter/makefile

and a variation with macros here: http://cs.iupui.edu/~aharris/240/classes/critter/makefileVars

There's MUCH more you can do with makefiles, but this is good enough for the first pass.

Notable Makefile Facts:

When using the command make, if you just type make it will start executing the first target specified in the Makefile. If you type make followed by a target name it will begin at that target. "make HorseRace" will take you to the executable target.

If you do not specify the dependencies the makefile will try to guess which files it needs by looking in the directory for files that match the targets name. So in the example, "Race.o: Race.cpp" and I do not put Race.h as a dependency it will look in the directory and include it for you. As a rule of thumb, do we ever want to rely on computers for anything? Especially if we have no idea how it works? Please write all of the dependencies and do not rely on the makefile to do it for you.

Using the # symbol allows you to write comments inside your makefile.

Makefiles do not have a file extension, they are just a file. If you create the Makefile on Pegasus/Tesla it will know what you mean and not add that extension. If you try to create it outside you may have to remove it manually. I suggest turning off hidden file extensions, this way simply deleting it from the end of your filename will change it.

While all these examples use C++ and many C++ programs use make, you can really use make for any kind of compilation. We'll even use make files for Java, as the problems that make solves are every bit as applicable in Java.