Make - the programmer’s best friend

Read time: 42 minutes (10513 words)

In working at the command line, you end up typing in a lot of commands. Unfortunately, you type in the same commands over and over, and that breaks a cardinal rule of programming: DRY!

Don’t Repeat Yourself!

Back in 1978, Marty Feldmann got sick of repeatedly typing in the commands to build his program and decided to do something about that silliness. He constructed a tool that used a simple text file to describe the commands he needed to type, and taught his tool to do the typing for him. The tool is called Make, and it has been part of the programming landscape ever since.

Hello, Make

First, let’s check that we have Make installed:

$ make --version
GNU Make 4.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Looks like it is installed (it was part of the build-essential package we installed).

Navigate into your Hello project

$ cd ~/cosc2325/homework-rblack42/HW2

Let’s teach Make how to manage this project.

What is Make?

Simply stated, make is a program used to “make” programs. It is a tool that issues a set of commands for you when you build a program involving multiple steps, so you do not have to type those commands in manually.

If that was all make did, it might be handy, but hardly “powerful”. What makes make unique is that you describe how programs are constructed out of the component parts, then make figures out what to do automatically. How it does this is interesting.

Dependencies

Make is driven by something called dependencies. If a file is needed to construct anything, that file is a dependency of that anything.

For example, you need the source file to build the object file, and ou need object files to build executables.

Make manages the actual steps needed to construct things from their dependencies.

Take a simple C++ program like “Hello, World”. That program is probably present in a single file named something like hello.cpp. To build the executable file from this source file, we have a few steps to work through.

Here is our test file:

#include <iostream>

int main(int argc, char ** argv) {
    std::cout << "Hello, World" << std::endl;
}

We know it is possible to build an executable directly from this source file, but doing so would not really set things up so make can work its magic. Let’s build the object file first, then link that file into an executable program.

The first thing we need to do is process the source file into an object file named something like hello.o. The command to do this step looks like this:

g++ -c -o hello.o hello.c

Here we invoke the C++ compiler to process the hello.cpp file and build the hello.o file from it. The -c argument tells the compiler to just build the object file and not try to link it into an executable. This only works if the source file has no errors.

In this step, we say the hello.o is dependent on hello.cpp. With no hello.cpp around, we cannot build hello.o.

In the next step, we link the object file to build the final executable. Here is the command:

g++ -o hello hello.o

Note

This is a Linux example, the final executable has no extension

Here, the executable hello is dependent on hello.o. If hello.o is not around, we cannot build the final executable file.

Makefiles

To use Make to manage all this, we need to create a simple text file containing information needed to describe this process we just went through manually. Here is a start:

# Makefile for hello.cpp

FILES = hello.cpp

Pretty simple so far, huh?

We name this file Makefile (no extension) and place it in the directory with the other project files (hello.cpp in this case).

Comments can be included, beginning with the sharp character.

The next line looks like an assignment statement, but it simply sets up a name for a bunch of text after the equal (and a bit of white space). The text assigned to that name is everything after the whitespace up to the end of the line.

Warning

When you type lines like this, be careful not to put extra spaces after the last character on the line. That can cause problems.

The next line we will add looks a bit strange:

OBJS = $(FILES:.cpp=.o)

In a Makefile, you cause the Make program to use a named chunk of text by surrounding the name in parentheses and putting a leading “$” in front. So, $(FILES) would place hello.cpp at this spot in this file. However, Make can do some neat tricks. Here the stuff after the colon is a substitution mechanism. In this example, and text that ends with “.cpp” will become “.o” when the name FILES is expanded by make. We save the result in a new name called OBJS. The net effect of all this is to create another line that could have been written as follows:

OBJS = hello.o

While this looks silly here, in a more complex program is will save a bunch of typing!

Targets

Make calls something it can build a target. We can ask Make to do a bunch of things, but for now, we just want to build our program. The rules we will define next set up that process. Be careful in this section, there is one funny quirk of Make we must deal with.

Targets begin with the name of something, followed by a colon. Targets are usually some real file name we want to build, but that something can just be a name of some operation we might want to do. Start this section with a target named all like this:

.PHONY: all
all:    hello

That .PHONY marker tells Make not to look for a file named all in the current directory.

In this example, the name of the target is all, and it is the first such line in the file. Make will automatically try to build the first target it finds in the Makefile if we launch the program using just make with no command-line arguments. In this example, we are saying that this name depends on hello. If make is managing this project and hello is already in the current directory, it is possible we have no work to do, so make might do nothing. More on that later.

Suppose hello does not exist. Make knows it is supposed to build it, and we need to describe the process.

Here is a new rule that will build the program:

hello:  $(OBJS)
        g++ -o hello $(OBJS)

Warning

See that indented line where the real build command is shown? The first character on that line must be a real tab character, or make will complain. This is the quirk! If you are editing in the virtual machine with vim, you type ctrl-v followed by the tab to enter the character assuming you are expanding tabs as I recommended.

In this rule, there is only one command needed, but Make will let you run a series of commands, as long as all are indented properly (with that silly tab!)

This line tells make how to manufacture hello out of the dependencies listed on the line after the colon. In this file, the $(OBJS) expands to just hello.o. If hello.o is around, all we need to do is to run the command shown, substituting for the OBJS string. That is exactly what we typed manually to do the last step. As you can see, this is just the link step, and g++ will run the linker to build the executable file.

Great, but how do we get hello.o constructed?

Implicit rules

One of the most powerful things that make can do is generate its own rules if you provide a pattern. Here is an example. It does look a bit weird.

%.o:    %.cpp
    g++ -c $< -o $@

In this funny rule, any file Make needs to build that is named something.o can be constructed out of a dependent file named something.cpp. If something.cpp is around, the command we execute is shown with two place holders for the expanded names. In this example

  • $< will be replaced by something.cpp

  • $@ will be replaced by something.o

The cool thing about rules like this is that any time you need a file like xxx.o, the same rule will build it out of xxx.cpp. All you need to do is add xxx.o as a dependency and make will take care of things.

Make is smart!

Once the required rules are in place to build a program, make does more than just issue the commands to rebuild it. It looks at the time-stamps on all the dependencies of every program component it knows how to build and determines if it really needs to reprocess anything. Think about it. If you did not change the source file, the object built from it will be newer than the source file. The executable built from the object file will be newer than the object file as well. Make can see that if you delete the executable, all it needs to do is re-link the current object file to rebuild the missing executable. There is no need to recompile the source, since it is older than the current object file. This kind of power speeds up building a complex project involving hundreds of files by only processing the bare minimum needed to get the program constructed!

Running Make

You launch Make by typing make on the command line.

Note

Make will look in the current directory (where you were working when you typed make), looking for a Makefile. Actually, the name can have either an upper case “m”, or a lower-case “m”. I stick with upper-case)

Make will read the complete Makefile and build some internal data structures so it can study the build process. It will check the time stamps on all files in the folder, then decide what needs to happen. Make starts this process on the first target if no target is named on the command line, or it starts with the named target rule if you provide that target name as a parameter.

Normally, developers set up their Makefile so the first target builds the program. (That is the all target in our example). If you want to run another target, you can provide the target name after make. For example make hello_main.o would ask Make to just build the object file from hello_main.cpp.

Note

Normally, I set up four primary targets in a C++ Makefile:

  • “make [all]” which builds the program (The “all” is optional).

  • “make clean” which does housekeeping

  • “make test” which runs unit tests (we will see that soon)

  • “make run” which launches the program

See the next example.

A more complex example

Let’s break up our simple “Hello, World” program into two parts:

Here is hello_main.cpp

void say_hello();

int main(int argc, char **argv) {
    say_hello();
}

And, here is hello_out.cpp:

#include <iostream>

void say_hello() {
    std::cout << "Hello, World" << std::endl;
}

Here is the Makefile that builds the program.

# Makefile for hello2 (two part example)

TARGET = demo
FILES = hello_main.cpp hello_out.cpp

OBJS = $(FILES:.cpp=.o)

all:    $(TARGET)

$(TARGET): $(OBJS)
	g++ -o $@ $^

%.o:    %.cpp
	g++ -c $< -o $@

clean:
	rm -f $(TARGET) $(OBJS)

run:
	./$(TARGET)

Notice something in this Makefile. I added a line that names the executable file I want to build. (demo in this case) The executable name really does not matter when you are building an application, it might when you release your program to users!) I use that name several places in this file, and naming it makes it easy to change the name later.

Look at the line that links the object files into the executable. The pattern here is $^ which expands to the complete list if names on the dependency line. $< expands to the first name on the dependency line (and we only have one name on the rule that builds an object file.

Finally, I added a build target for something called clean. This is very common in Makefiles and it deletes anything you do not really need to keep around. In this case, it will delete the executable, and all object files. They can always be rebuilt using Make. You should run “make clean” before telling Git to add files to your repository.

This example does not do any testing. We will learn about that in our next lectures.

Testing the Makefile

Before we leave this step, let’s try out our fancy new Makefile:

$ make clean
rm -f demo hello_main.o hello_out.o
$ make
g++ -c hello_main.cpp -o hello_main.o
g++ -c hello_out.cpp -o hello_out.o
g++ -o demo hello_main.o hello_out.o
$ ./demo
Hello, World

Looks like our fingers are going to be happy!