UVa Physics Computer Facilities
|
Make, Makefiles and a Little about CMake
##########################################
Some notes about make, Makefiles and cmake.
##########################################
* "make" was invented as a tool for building a software project, but it
can be used to manage many kinds of projects. It uses a set of rules
(usually stored in a file called "Makefile") to compile, link, or otherwise
process the files comprising the project. "make" is useful because it
can determine which files need re-building, and rebuild only those.
* Two important things to remember:
1. Makefiles aren't programs, they're configuration files for the "make" program.
2. Make reads each Makefile twice (we'll come back to that later).
* Makefiles consist of rules made up of target, dependencies and commands.
These appear in sections that look like this:
myprogram: myprogram.c myprogram.h
gcc -o myprogram mprogram.c
echo Done.
Here, "myprogram:" specifies a "target", the list "mprogram.c
myprogram.h" specifies "dependencies" (also known as
"prerequisites") and the two lines below are a set of commands that
tell "make" how to create the target, given these dependencies.
* Makefiles can contain any number of such sections, for different
targets.
* Commands can be any list of shell commands, as many as you need.
* In the end, even the most complicated Makefile boils down to a list
of sections like this, for one or more targets. (As we'll see
later, "make" gives us a lot of tools to help write complicated
Makefiles.)
* How do we run make? Typing "make myprogram" will cause "make" to
look through the Makefile to find a target called "myprogram".
"make" will then look to see if a file called "myprogram" already
exists.
If it doesn't exist, then "make" will execute the commands specified
for this target in the Makefile in order to create "myprogam".
If the file "myprogram" already exists, make will look at each of
the dependencies to see if any of these files are newer than
"myprogam". If any of them are newer, "make" will execute the
specified commands in order to create a new version of "myprogram".
What if one of the dependencies of "myprogram" doesn't exist? In
that case, "make" will look through the Makefile and try to find
instructiions for creating each of the missing dependencies.
* If we don't specify a target on the command line (if we just type
"make"), then "make" will use the first target in the Makefile.
* "make" can be used with any programming language, or even for non-
programming tasks. Make's purpose is to keep targets up-to-date
with the things upon which they depend, using commands you specify to
do so. For example, "make" is often used with large LaTeX projects.
If you have a large LaTeX document that's created from several *.tex
(or other) files, you can use "make" to do what's necessary to
create a new version of your document whenever you edit one of the
files. (E.g., re-creating bibliographies, etc.)
* The targets in a Makefile don't even need to correspond to real
files. For example, consider the following section of a Makefile:
help:
echo "Here is how you use this Makefile:"
echo " - Thing 1..."
echo " - Thing 2..."
echo " - Thing 3..."
If you typed "make help", "make" would look for a file called
"help", which it presumably wouldn't find. Then these commands
would be executed and perhaps they'd give you some useful
information.
* But what if you, at some later time, accidentally created a file
called "help"? This would cause "make help" to do nothing, since
"make" would find that the target already exists. We can avoid this
by explicitly telling "make" that a target doesn't correspond to an
actual file. We do this by adding a line like the following to the
Makefile:
.PHONY: help
If "make" sees that a target is marked as "phony", it won't bother to
check to see if a file by that name exists.
In addition to "make help", some other common phony targets are
"make all" and "make install". The first of these might correspond to
lines like the following in a Makefile:
.PHONY: all
all: myprogram libmylib.a
* Why would you use "make"? Obviously, for the trivial "myprogram"
example above, you probably wouldn't use "make" at all. You'd just
type the "gcc" command directly. "make" is useful when you have a
software project that comprises lots of files. When you make a
change to one of the files, "make" rebuilds anything that depends on
the modified file. By using "make" you no longer have to keep track
of which files need to be rebuilt when you make a change. "make"
also only rebuilds things that need to be rebuilt, allowing you to
update your project's files more quickly when you make a change.
* Here's a more complicated Makefile:
all: energy range
energy: energy.c libparticle.a
gcc -o energy energy.c -L. -lparticle
range: range.c libparticle.a
gcc -o range range.c -L. -lparticle
libparticle.a: electron.o muon.o tau.o
ar -rcs libparticle.a electron.o muon.o tau.o
electron.o: electron.c
gcc -c electron.c
muon.o: muon.c
gcc -c muon.c
tau.o: tau.c
gcc -c tau.c
* This file builds two programs, called "range" and "energy". It also
builds a library called "libparticle.a" that's necessary to make the
two programs. The library contains functions that live in several
different source files (electron.c, etc.)
* As you can see, this could get very long, very quickly if we keep
adding files for other particles.
* "make" gives you several ways to shorten Makefiles. One of them
is wild-card expressions for targets and dependencies. For example,
we could replace all of the sections for electron.o, muon.o and
tau.o with a single section like this:
%.o: %.c
gcc -c $<
The expressions %.c and %.o are wildcard expressions that will match
any file ending in ".c" and ".o", respectively. In the commands for
the rule, we can use one of "make's" "automatic variables", "$<".
The expression $< means "the first dependency in the preceding
list". This section of the Makefile tells "make" how to make an
object file from a C file. It says "use the command gcc -c on the C
file".
* The $< expression is just one of many automatic variables
that are available for use inside Makefiles. Here's a short
list:
$< The first dependency of the current section.
$@ The target of the current section.
$^ The whole list of dependencies for the current section.
You can find an extensive table of automatic variables here:
https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html
* We can shorten Makefiles even more by taking advantage of "make's"
built-in rules. In the example Makefile above, we don't really need
a rule that tells make how to create a .o file from a .c file,
because "make" already has a predefined rule to do that. "make"
knows a little about C and Fortran and some other languages. If it
sees that it needs to create a .o file, and there's a .c file with a
corresponding name, make knows that it can generate the .o file
using "gcc -c".
You can see "make's" built-in rules by typing "make -p". You'll
notice a lot of odd notation in these rules, but don't despair.
Much of this will become clear when we talk about Makefile
variables.
Note that you can always override the built-in rules by specifying
rules of your own in the Makefile.
* In addition to "make's" automatic variables, you can define
your own variables inside Makefiles. Here are some examples:
MYLIB := libparticle.a
OBJS := electron.o muon.o tau.o
EXTRA := particle.o material.o geometry.o
ALLOBJS := $(OBJS) $(EXTRA)
The := assignment operator is one way to assign a value to a variable.
(We'll talk about other ways soon.) These examples should be
self-explanatory. Once defined, variables can be used as in the
right-hand side of the last example, above. The value of the
variable created this way can be as long as you want it to be, but
it can only be one line.
Makefile variables are very much like C pre-processor macros. In
particular, these variables can only contain character strings. For
example, what does this do?:
A := 1
B := 2
C := $(A) + $(B)
The variable C will contain the string "1 + 2". No arithmetic will
be done. Everything is just strings.
* Now we should come back to the fact (mentioned at the beginning)
that "make" reads a Makefile twice. The first time it reads the
Makefile, all of the variables are replaced with their values. This
gives a "pure" version of the Makefile that contains only targets,
dependencies and commands. (This "pure" version isn't written into
a file, it only exists in "make's" memory.)
Then, during the second pass, make uses those rules to decide what
it needs to do. By the time "make" starts using the Makefile to
compile/link, all that's left is a list of rules
(target/dependencies/commands).
This is very much like what happens when you use C pre-processor
macros in a C program. In that case, your C program is first sent
through the pre-processor and all macros are replaced with their
values, and then the pre-processed file is compiled.
* Here's a second way to define variables. We can define variables
with multi-line values using "define":
define LONGVAR
echo this is a test
gcc -o junk junk.c
echo I'm done!
endef
* The variables we've looked at so far are what "make" calls
"immediate" variables. They are "immediate" because "make" sets the
variable's value to a fixed string as soon as it sees the :=
operator.
"make" also offers a second kind of variable, called "deferred"
variables. These may seem stranger to you.
If we set an immediate variable, C, like this:
A := 1
B := 2
C := $(A) + $(B)
then C will contain "1 + 2" as soon as we define it, and forever
after unless we later redefine it with another "C := something"
statement.
But what if we'd made one small change?:
A := 1
B := 2
C = $(A) + $(B)
By omitting the colon before the equals sign when defining C, we've
made C into a "deferred" variable. Deferred variables act, in some
ways, like simple-minded functions. When "make" sees the "C = $(A)
+ $(B)" statement, it doesn't evaluate the variables A and B and
then construct the value of C. Instead, it stores the formula "$(A)
+ $(B)" in the variable C. Thereafter, whenever you type $(C),
"make" will look up the CURRENT values of A and B and construct a
CURRENT value for C. This means that, whenever we change the values
of A or B, the value of C will automatically change, too.
* To continue confusing you, there are still other ways to assign
values to variables. For example, there's "conditional assignment":
CC ?= gcc
This says, set the value of C to "gcc", but only if we haven't
already defined C somewhere above.
This can be useful because "make" allows us to include the contents
of other Makefiles, which may define variables. We can also set
Makefile variables from the command line, or from the shell
environment. We'll talk about all of these later.
Note that variables defined through ?= are "deferred".
* By default, "make" imports your shell's environment variables and
sets corresponding Makefile variables. For example, you can use the
variable $(HOME) in your Makefiles, and it will be replaced by the
value of $HOME in your shell environment.
You can turn off this behavior by using command-line
switches when invoking "make" (see "man make").
* You can use the "export" command inside a Makefile to export Makefile
variables to the shell environment used by your commands. For
example, you could say:
ROOTSYS := /common/lib/root
export ROOTSYS
blarg: stuff.c stuff.h
root blah blah blah
The value of the Makefile variable ROOTSYS would be exported
to the shell environment variable $ROOTSYS, where it could
be used by the "root" command.
If you just want to export all Makefile variables, write
"export" with no variable names following.
* Here's yet another weird way of setting variables: Pattern substitution:
OBJS := electron.o muon.o tau.o
LIST := $(OBJS:.o=.c)
The variable LIST would now have the value "electron.c muon.c tau.c",
having substituted ".c" for each ".o".
* As we mentioned above, you can "include" one Makefile in another.
The syntax for this is just:
include generic.make
There's also a "-include", which only tries to include the
file if it exists.
* As with the C pre-processor, Makefiles can have "ifdef" statements,
which conditionally set values if some variable is defined.
For example:
DEBUG := true
CFLAGS := -Wall
ifdef DEBUG
CFLAGS += -g
else
CFLAGS += -O3
endif
* Note that we've also introduced another way of changing the
value of a variable. The "+=" operator appends text onto
the end of a variable.
* Alternatively, we could leave off the "DEBUG :=" line and set the
variable when we invoke make from the command line, like this:
make myprogram DEBUG=true
That would let us turn DEBUG on or off, as needed.
* There's also conditional statement that checks the value of
a variable:
ifeq ($(CC),gcc)
LIBS = -L. -lmylib
else
LIBS = -lotherlib
endif
* "make" also provides many built-in functions. Here are
a few examples:
The "info" function just prints out some text:
$(info This is an informational message)
$(info CC is $(CC))
The "words" function counts the number of words:
NFILES := $(words $(OBJS))
The "word" function returns one word from a list
(here we get word number 2, the second one in the list):
FILE2 := $(word 2, $(OBJS))
We can also select certain words from a list based on pattern
matching. The following would return any files in the list with
names beginning with "electron":
EFILE := $(filter electron.%, $(OBJS))
Finally, we can execute any shell command like this:
OUTPUT := $(shell cat myfile.txt | grep something)
You'll find much more information about "make's"
built-in functions here:
http://www.gnu.org/software/make/manual/html_node/Functions.html
* Finally, "make" provides some command-line arguments that can
help you find problems with your Makefiles. You can use "make -n"
to cause "make" to print out a list of things it WOULD do, without
actually doing them. You can use "make -pn" to print out the values
of all of the macros. You can use "make -d" to cause "make" to print
out detailed debugging information while it works.
* Here are a couple of useful books about "make":
http://my.safaribooksonline.com/book/software-engineering-and-development/0596006101
http://my.safaribooksonline.com/book/software-engineering-and-development/9780132171953
(The second one covers "make" and several other similar tools.)
* Now for a few words about "cmake". Cmake is a tool for creating
Makefiles and other similar things, on a variety of different
platforms (Windows, OS X and Linux). It has the ability to
figure out some dependencies on its own, by reading files.
* Cmake is mainly useful when you have a large project that needs
to be built for several different operating systems.
* Here's the simplest example showing how cmake can be used:
mkdir testing
cd testing
Put some source files, say "junk.c" and "junk.h" into this directory.
edit CMakeLists.txt
CMakeLists.txt is cmake's equivalent of a Makefile. It
contains configuration information that tells cmake
how to do its job. Inside this simplest CMakeLists.txt
file, put the following line:
add_executable(junk junk.c junk.h)
Now type the following command (note the dot):
cmake .
This will cause cmake to create a Makefile for you. You
can then build your program by typing:
make
* For a good tutorial on Cmake, see:
http://mathnathan.com/2010/07/getting-started-with-cmake/
|