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 bysomething.cpp
$@
will be replaced bysomething.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!