Makefiles
The make
command allows programmers to easily manage programs with
large numbers of files. It aids in developing large programs by encoding
instructions on how to build the program, keeping track of which
portions of the entire program have been changed, and compiling only
those parts of the program which have changed since the last compile.
The make
program gets its set of compile rules from a text file
called Makefile
which resides in the same directory as the source
files. It contains information on how to compile the software, e.g. the
compiler to use, the optimization level, whether to include debugging
info in the executable, etc.. It also contains information on where to
install the finished compiled binaries (executables), manual pages, data
files, dependent library files, configuration files, etc.. For example,
when we built the units
program in the previous tutorial, the
configure
program automatically created a Makefile
for building
units
, so that we did not need to compile everything manually.
Retrieve the set of files for this tutorial either through clicking here or by copying the relevant files at the command line:
$ cp -R /hpc/examples/workshops/hpc/makefile_tutorial .
You should now see a new subdirectory entitled makefile_tutorial
in
your current directory. This is where we will work for the rest of this
section. Inside this directory you will see a number of files:
driver.cpp vector_difference.cpp vector_sum.cpp
one_norm.cpp vector_product.cpp
Here, the main program is held in the file driver.cpp
, and
supporting subroutines are held in the remaining files. To compile these
on ManeFrame, it takes a number of steps.
Let’s first compile and assemble the auxiliary subroutine
one_norm.cpp
:
$ g++ -c one_norm.cpp
This calls the GNU C++ compiler, g++
, to create an object file,
named one_norm.o
, that contains compiler-generated CPU instructions
on how to execute the function in the file one_norm.cpp
.
Use similar instructions to create the object files driver.o
,
vector_difference.o
, vector_product.o
and vector_sum.o
in a
similar fashion.
You should now have the files driver.o
, one_norm.o
,
vector_difference.o
, vector_product.o
and vector_sum.o
in
your directory. The final stage in creating the executable is to link
these files together. We may call g++
one more time to do this
(which itself calls the system-dependent linker), supplying all of the
object files as arguments so that g++
knows which files to link
together:
$ g++ driver.o one_norm.o vector_difference.o vector_product.o \
vector_sum.o -lm
This creates an executable file named a.out
, which is the default
(entirely non-descriptive) name given by most compilers to the resulting
executable. The additional argument -lm
is used to tell g++
to
link these functions against the built-in math library (so that we can
use the absolute value function, fabs()
, that is called inside the
one_norm.cpp
file.
You can instead give your executable a more descriptive name with the
-o
option:
$ g++ driver.o one_norm.o vector_difference.o vector_product.o \
vector_sum.o -lm -o driver.exe
This will create the same executable, but with the more descriptive name
driver.exe
.
How can a Makefile help?
While you may find it to be quite enjoyable to compile every source file
by hand, and then manually link them together into an executable, the
process can be completely automated by using a Makefile
.
A few rules about Makefiles
:
The
make
program will look for any of the files:GNUmakefile
,makefile
, andMakefile
(in that order) for build instructions. Most people consider the nameMakefile
as best practice, though any are acceptable.Inside the
Makefile
, lines beginning with the#
character are treated as comments, and are ignored.Blank lines are ignored.
You specify a target for
make
to build using the syntax,target : dependencies build command 1 build command 2 build command 3
where each of the lines following the
target :
line must begin with a[Tab]
character. Each of these lines are executed whenmake
is called. These lines are executed as if they were typed directly at the command line (as with a shell script).More than one target may be included in any
Makefile
.If you just type
make
at the command line, only the first target is run.
As an example, examine the Makefile from the previous tutorial. Here, all of the lines are either blank or are comment lines except for the four sets:
hello_cpp.exe : hello.cpp
g++ hello.cpp -o hello_cpp.exe
hello_c.exe : hello.c
gcc hello.c -o hello_c.exe
hello_f90.exe : hello.f90
gfortran hello.f90 -o hello_f90.exe
hello_f77.exe : hello.f
gfortran hello.f -o hello_f77.exe
Here, we have four build targets, hello_cpp.exe
, hello_c.exe
,
hello_f90.exe
and hello_f77.exe
(it is traditional to give the
target the same name as the output of the build commands).
Each of these targets depend a source code file listed to the right of
the colon; here these are hello.cpp
, hello.c
, hello.f90
and
hello.f
, respectively.
The indented lines (each require a single [Tab] character) under each
target contain the instructions on how to build that executable. For
example, make
will build hello_cpp.exe
by issuing the command
g++ hello.cpp -o hello_cpp.exe
, which does the compilation, assembly
and linking all in one step (since there is only one source code file).
Alternatively, this Makefile could have been written:
hello_cpp.exe : hello.cpp
g++ -c hello.cpp
g++ hello.o -o hello_cpp.exe
hello_c.exe : hello.c
gcc -c hello.c
gcc hello.o -o hello_c.exe
hello_f90.exe : hello.f90
gfortran -c hello.f90
gfortran hello.o -o hello_f90.exe
hello_f77.exe : hello.f
gfortran -c hello.f
gfortran hello.o -o hello_f77.exe
or even as
hello_cpp.exe :
g++ hello.cpp -o hello_cpp.exe
hello_c.exe :
gcc hello.c -o hello_c.exe
hello_f90.exe :
gfortran hello.f90 -o hello_f90.exe
hello_f77.exe :
gfortran hello.f -o hello_f77.exe
(which ignores the dependency on the source code files hello.cpp
,
hello.c
, hello.f90
and hello.f
, respectively).
Makefile Variables
As you likely noticed, many of the above commands seemed very repetitive
(e.g., continually calling gfortran
, or repeating the dependencies
and target name in the compile line).
As with anything in Linux, we’d prefer to do things as easily as
possible, which is where Makefile variables come into the picture. We
can define our own variable in a Makefile
by placing the variable to
the left of an equal sign, with the value to the right (as with Bash):
VAR = value
The main difference with Bash comes in how we use these variables.
Again, it requires a $
, but we also need to use parentheses or
braces, $(VAR)
or ${VAR}
. In addition, there are a few built-in
variables within Makefile
commands that can be quite handy:
$^
– in a compilation recipe, this references all of the dependencies for the target$<
– in a compilation recipe, this references the first dependency for the target$@
– in a compilation recipe, this references the target name
With these, we can streamline our previous Makefile
example
considerably:
CC=gcc
CXX=g++
FC=gfortran
hello_cpp.exe : hello.cpp
$(CXX) $^ -o $@
hello_c.exe : hello.c
$(CC) $^ -o $@
hello_f90.exe : hello.f90
$(FC) $^ -o $@
hello_f77.exe : hello.f
$(FC) $^ -o $@
Advanced Usage
If we have one main routine in the file driver.c
that uses functions
residing in multiple input files, e.g., func1.c
, func2.c
,
func3.c
and func4.c
, it is standard to compile each of the input
functions into .o
files separately, and then to link them together
with the driver at the last stage. This can be very helpful when
developing/debugging code, since if you only change one line in
file2.c
, you do not need to re-compile all of your input
functions, just the one that you changed. By setting up your
Makefile
so that the targets are the .o
files, and if the
Makefile knows how to build each .o
file so that it depends on the
respective .c
file, recompilation of your project can be very
efficient. For example,
CC=gcc
driver.exe : driver.o func1.o func2.o func3.o func4.o
$(CC) $^ -o $@
driver.o : driver.c
$(CC) -c $^ -o $@
func1.o : func1.c
$(CC) -c $^ -o $@
func2.o : func2.c
$(CC) -c $^ -o $@
func3.o : func3.c
$(CC) -c $^ -o $@
func4.o : func4.c
$(CC) -c $^ -o $@
However, if this actually depends on a large number of input
functions, the Makefile can become very long if you have to specify the
recipe for compiling each .c
file into a .o
file. To this end,
we can supply an explicit rule for how to perform this conversion,
e.g.,
CC=gcc
OBJS=driver.o func1.o func2.o func3.o func4.o func5.o \
func6.o func7.o func8.o func9.o func10.o func11.o \
func12.o func13.o func14.o func15.o
driver.exe : $(OBJS)
$(CC) $^ -o $@
%.o : %.c
$(CC) -c $^ -o $@
Here, the last block specifies the rule for how to convert any .c
file into a .o
file. Similarly, we have defined the OBJS
variable to list out all of the .o
files that we need to generate
our executable. Notice that the line continuation character is \
:
The
\
must be the last character on the line (no trailing spaces)Continued lines must use spaces to start the line (no “Tab”), though they aren’t required to line up as pretty as in this example.
As a final example, let’s now suppose that all of the files in our
project #include
the same header file, head.h
. Of course, if we
change even a single line in this header file, we’ll need to recompile
all of our .c
files, so we need to add head.h
as a dependency
for processing our .c
files into .o
files:
CC=gcc
OBJS=driver.o func1.o func2.o func3.o func4.o func5.o \
func6.o func7.o func8.o func9.o func10.o func11.o \
func12.o func13.o func14.o func15.o
driver.exe : $(OBJS)
$(CC) $^ -o $@
%.o : %.c head.h
$(CC) -c $< -o $@
Note that to the right of the colon in our explicit rule we have now
listed the header file, head.h
. Also notice that within the explicit
rule, we now use the $<
instead of the $^
, this is because we
want the compilation line to be, e.g.,
gcc -c func3.c -o func3.o
and not
gcc -c func3.c head.h -o func3.o
so we only wanted to automatically list the first dependency from the list, and not all dependencies.
Makefile Exercise
Create a Makefile
to compile the executable driver.exe
for this
workshop tutorial, out of the files driver.cpp
, one_norm.cpp
,
vector_difference.cpp
, vector_product.cpp
and
vector_sum.cpp
. This should encode all of the commands that we
earlier needed to do by hand. Start out with the command:
$ gedit Makefile &
to have gedit
create the file Makefile
in the background, so
that while you edit the Makefile
you can still use the terminal
window to try out make
as you add commands.
You can incorporate more than one target into your Makefile
. The
first target in the file will be executed by a make
command without
any arguments. All other targets may be executed through the command
make target
, where target
is the name you have specified for a
target in the Makefile
.
For example, a standard Makefile
target is to clean up the temporary
files created during compilation of the executable, typically entitled
clean
. In our compilation process, we created the temporary files
driver.o
, one_norm.o
, vector_product.o
, vector_sum.o
and
vector_difference.o
. These could be cleaned up with the single
command make clean
if we add the following lines to the
Makefile
, after your commands to create driver.exe
:
clean :
rm -f *.o
Now type make clean
in the terminal – all of the temporary build
files have been removed.
Makefiles
can be much more complicated than those outlined here, but
for our needs in this tutorial these commands should suffice. For
additional information on the make
system, see the PDF manual listed
below.
Make resources:
Support
Navigation
Parts
- About
- Accounts
- Access
- Portal
- Usage
- Applications
- Development Tools
- Leadership and Staff
- Faculty Advisory Council
- Fellows
- Programs and Policies
- Frequently Asked Questions
- Workshops
- Newsletters
- Work Storage Migration