4. Makefiles#

4.1. What is a Makefile#

A makefile allows you to atomatize the compilation process by following given rules, and for large projects this could actually speed up significantly the compilation process. You can also use a Makefile for latex projects and other related task where you want to reload and create automatically an updated version of a document when you have updated a small part. A rule in a makefile is written as

target : dependencies separated by space
    rule to build target
#sumupdown.x : sumupdown.cpp
#   g++ sumupdown.cpp -o sumupdown.x

It is important to note that the beginning of the rule is indented with real tab, not with spaces.

Some modern tools/alternatives

Note: For more information, please see:

4.2. Application example: Final figure creation#

Imagine that you want to automatize the creation of the figure of the sumup and sumdown exercise from the numerical errors section. In this case you should think about the dependencies: the latest figure, sumupdown.pdf will depend on the a given gnuplot (or python) script (plot.gp) that read some data from a given file (data.txt), so any change on those files should trigger and update on the figure. Furhtermore, the data file will actually depend on the source (sumupdown.cpp) and its related executable (sumupdown.x), since any change there may imply new data. And actually the executable depends directly on the source code. We could express this dependencies as

Therefore we have:

  • The executable depends on the source code, if it changes, then it must be recompiled

      sumupdown.x: sumupdown.cpp
          g++ sumupdown.cpp -o sumupdown.x
    

    This is the typical usage, to update executables.

  • The data depends on the executable: if it changes then the data must be generated again

      data.txt: sumupdown.x
          ./sumupdown.x > data.txt
    

    Changes in the executable (or changes in the source code) will trigger this rule.

  • The final figure depends on updates on the plotting script and the data

    fig.pdf: script.gp data.txt
        gnuplot script.gp
    

    By the dependency tree formed, changes in the source, executable, or data, will trigger this.

Our final simple makefile will look like

fig.pdf: script.gp data.txt
    gnuplot script.gp

data.txt: sumupdown.x
    ./sumupdown.x > data.txt

sumupdown.x: sumupdown.cpp
    g++ sumupdown.cpp -o sumupdown.x

The actual order is not important. Every time you run make it will check if any target is outdated and run the command needed. The follwing code shows a simple solution for the sum up and down. You need to actually implement the functions, parametrize the data type, and also read the limit from the command line.

#include <iostream>
#include <cmath>

float sumup(int nterms);
float sumdown(int nterms);

int main(int argc, char **argv)
{
  std::cout.precision(6); std::cout.setf(std::ios::scientific);
  for (int ii = 1; ii <= 10000; ++ii) {
    double sum1 = sumup(ii);
    double sum2 = sumdown(ii);
    std::cout << ii << "\t" << sum1 << "\t" << sum2
              << "\t" << std::fabs(sum1-sum2)/sum2 << "\n";
  }

  return 0;
}

And this is a simple gnuplot script example (you can also use matplotlib or any tool that can be called from the command line)

set term pdf
set out "sumupdown.pdf"
# set xlabel "Nterms"
# set ylabel "Percentual difference"
plot 'data.txt' u 1:4 w lp pt 4

Now just run make and see what happens. Then uncomment, for example, the commented lines inside the gnuplot script and run make again. Check that make just reruns the needed rule, it does not recompile the whole program.

Actually, the makefile can be simplified and generalized using variables

fig.pdf: script.gp data.txt
    gnuplot $^

data.txt: sumupdown.x
    ./$^ > $@

# Generic rule to compile any cpp into its corresponding .x 
%.x: %.cpp
    g++ $^ -o $@

Explanation:

  • $^ : This is the first dependency

  • $@ : Thisis the target

4.3. Application example: many files to compile#

Now, let’s try to test our functions. To do so, we need another main function to test them, to separate our actual use from the tests. Therefore, it is better to put the functions declarations and implementations into their own files. You will now have three files:

  • functions.h: It will have the functions declarations. Add an inclusion guard with pragma or define.

  • functions.cpp: It will have the functions implementations (and will include functions.h).

  • main.cpp: This has the main function. Includes the headers functions.h. You can create as many main files as you want, one for using the function another for testing etc.

Your first task is to move the declarations into functions.h, and the implementations into functions.cpp. Remember to include functions.h both into the implementations and then into the main file.

The second task is to update the makefile to compile the whole thing. The manual command is

# this creates the object functions.o
g++ -c functions.cpp
# this creates the object main.o
g++ -c main.cpp
# this links with main and creates executable main.x
g++ main.o functions.o -o main.x

What could be the corresponding make rule to build the objects and the executable? here is an example for the later

main.x : main.o functions.o
    g++ -o main.x main.o functions.o

Test this with the old main function. Where are the rules for creating the object files? The objects are created automatically. These is done through something called automatic rules.

To test the usefulness of this:

  • Change a bit the functions.cpp file and recompile with make. Are all objects updated?

  • Create another main file called test.cpp which test something about your functions (like calling them with negative limits). Add the rule to the makefile, so you can just write make test to test the functions.

After seeing this advantages, let’s try to generalize and use more useful syntax for make. Fir instance, you can specify variables, which can be overridden on the command line. Something like

CXX=g++
CXXFLAGS=-I.

main.x: foo.o bar.o
  $(CXX) $(CXXFLAGS) -o main.x main.cpp foo.o bar.o

Compile again and test. Everything should work.

Now, we will specify a generic rule to create the .o objects, and also the name of the objects in a variable. Furthermore, we use $@ to specify the name of the target, and $^ which symbolizes all the items in the dependency list

CXX = g++
CXXFLAGS = -I.
OBJ = foo.o bar.o

main.x: $(OBJ)
  $(CXX) $(CXXFLAGS) -o $@ $^ main.cpp

Save and run it, you will still get the same but the makefile is becoming more generic, and therefore more useful.

Now let’s specify the generic rule to create the .o files, and also specify that if we change the .h files then a recompilation is needed (for now our makefile only detect changes in .cpp files):

CXX = g++
CXXFLAGS = -I.
OBJ = foo.o bar.o
DEPS = foo.h bar.h

%.o: %.cpp $(DEPS)
  $(CXX) -c -o $@ $< $(CXXFLAGS)

main.x: $(OBJ)
  $(CXX) $(CXXFLAGS) -o $@ $^ main.cpp

Where we have specify the dependency on DEPS, and also we are using a generic rule to tell how to create any .o file (exemplified with the generic %.o symbol), from any corresponding %.c file, and $< means the first item on the deps list. Again, save it and run it.

You can rules to clean the directory (to erase object files, for example), to add libs, to put the headers on another directory, etc. For example, the following adds a phony rule (which does not create any target) to clean the directory

CXX = g++
CXXFLAGS = -I.
OBJ = foo.o bar.o
DEPS = foo.h bar.h

%.o: %.cpp $(DEPS)
  $(CXX) -c -o $@ $< $(CXXFLAGS)

main.x: $(OBJ)
  $(CXX) $(CXXFLAGS) -o $@ $^ main.cpp

.PHONY: clean
clean:
  rm -f *.o *~ *.x

In order to run this rule, you need to write

make -f makefile-v5 clean

And, finally, a more complete example with comments and new syntax, which you can adapt for your own needs

CXX = g++
CXXFLAGS = -I.
LDFLAGS =
SOURCES = main.cpp foo.cpp bar.cpp
OBJ = $(SOURCES:.cpp=.o) # extracts automatically the objects names
DEPS = foo.h bar.h

all : main.x $(SOURCES) $(DEPS) # this is the default target

main.x: $(OBJ)
  @echo "Creating main executable ..."
  $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)

.cpp.o:
  $(CXX) -c -o $@ $< $(CXXFLAGS)

.PHONY: clean
clean:
  rm -f *.o *~ *.x

4.4. Exercises#

  1. Take the codes for overflow and underflow and automatize the detection of overflow with the standard function isinf.

  2. When they are detecting overflow automatically, stopping and printing where the overflow occurs, put them in separated files (.h and .cpp).

  3. Then create a main file which calls those functions.

  4. Now write a makefile to automatize the compilation. Use the latest version of the example Makefile.

  5. When everything is working send it to assigned repository.

  6. Do the same now including the code which computes the machine eps, using its own .h and .cpp .

  7. If you use latex, how will you write a Makefile for that? Well, at the end you can use latexmk, of course. Try to think on other uses for make (like automatic zipping and mailing of some important file, )