Makefiles

Makefiles and the associated program make are the defacto standard for building software on Unix style machines. Makefiles can also be a little bit arcane in their structure owing to just how long they have been around. The fundamental purpose of makefiles is to define all of the compilation instructions for the source code in a project, and to recognize the dependencies therein.

There are more flexible and/or modern tools like autotools, cmake, meson, or others. Most of these ultimately generate Makefiles, but provide an extra layer on top of it that can be helpful for complicated projects with many (external) dependencies that also need to run on many different systems. These tools are pretty clunky on smaller code bases, so we’ll ignore them in this class.

Makefile Rules

A makefile takes a set of rules in the following format

target ... : prerequisites ...
 (tab character) recipe
 (tab character) ...
  • A target usually refers to a file name that is generated by compiling or linking your code.

  • A prerequisite is a file that is required as input in order to generate the target. These prerequisites may also be targets that have their own dependencies.

  • A recipe is an action that takes place when make builds a target. You must offset the recipe with a tab character. This will not work with spaces.

    • You can technically change the character used for offsetting the recipes, but we’re going to ignore that for now.

Makefile examples

Here we provide an example Fortran code fullcode.f90 that consists of a main program and two subroutines:

 1! /codes/multifile/fullcode.f90
 2
 3program demo
 4  print *, "In main program"
 5  call sub1()
 6  call sub2()
 7end program demo
 8
 9subroutine sub1()
10  print *, "In sub1"
11end subroutine sub1
12
13subroutine sub2()
14  print *, "In sub2"
15end subroutine sub2

To illustrate the construction of a Makefile, we’ll first break this up into three separate files:

1! /codes/multifile/demo.f90
2
3program demo
4  print *, "In main program"
5  call sub1()
6  call sub2()
7end program demo

Download this code

1! /codes/multifile/sub1.f90
2
3subroutine sub1()
4  print *, "In sub1"
5end subroutine sub1

Download this code

1! /codes/multifile/sub2.f90
2
3subroutine sub2()
4  print *, "In sub2"
5end subroutine sub2

Download this code

Below, we study several Makefiles that get successively more sophisticated to compile the code provided in this section.

In the first version we write out explicitly what to do for each file. In order to run this Makefile, just type make. This will work as long as your makefile has one of a few a default names like Makefile or makefile:

 1# /codes/multifile/Makefile
 2
 3output.txt: main.ex
 4	./main.ex > output.txt
 5
 6main.ex: main.o sub1.o sub2.o
 7	gfortran main.o sub1.o sub2.o -o main.ex
 8
 9main.o: main.f90
10	gfortran -c main.f90
11sub1.o: sub1.f90
12	gfortran -c sub1.f90
13sub2.o: sub2.f90
14	gfortran -c sub2.f90

Download this code

This is pretty tedious, and not much better than just writing all of the compiler calls on the command line.

In the second version there is a general rule for creating .o files from .f90 files, called inference rules or pattern rules (see more pattern rules). There are also an old style rules called suffix rules which we will not use in our course (see article-suffix).

The last line (i.e., recipe) has the special macro $< which corresponds to the name of the first prerequisite (i.e., *.f90 files). There are seven frequently used automatic variables. In order to run this non-default Makefile2, you need to type in make -f Makefile2:

 1# /codes/multifile/Makefile2
 2
 3output.txt: main.ex
 4	./main.ex > output.txt
 5
 6main.ex: main.o sub1.o sub2.o
 7	gfortran -o main.ex main.o sub1.o sub2.o
 8
 9%.o : %.f90
10	gfortran -c $< 

Download this code

In the third version we define the variable OBJECTS so that we only have to write out this list once, which minimizes the chance of introducing errors:

 1# /codes/multifile/Makefile3
 2
 3OBJECTS = main.o sub1.o sub2.o
 4
 5output.txt: main.ex
 6	./main.ex > output.txt
 7
 8main.ex: $(OBJECTS)
 9	gfortran -o main.ex $(OBJECTS)
10
11%.o : %.f90
12	gfortran -c $< 

Download this code

In the fourth version, we place the Fortran compiler into the variable FC. This allows a user to override the choice of compiler if they want to. Additionally, compiler flags for debugging and optimization (see this StackOverflow answer for more information about the BUILD variable), as well as a linker flag (blank in this example), have been added:

 1# /codes/multifile/Makefile4
 2
 3FC = gfortran
 4BUILD ?= debug
 5fflags.debug = -g
 6fflags.release = -O3
 7FFLAGS = ${fflags.${BUILD}}
 8LFLAGS =
 9
10OBJECTS = main.o sub1.o sub2.o
11
12output.txt: main.ex
13	./main.ex > output.txt
14
15main.ex: $(OBJECTS)
16	$(FC) $(LFLAGS) -o main.ex $(OBJECTS)
17
18%.o : %.f90
19	$(FC) -c $(FFLAGS) $<

Download this code

Next we add a phony target clean that removes the files created when compiling the code in order to facilitate cleanup. It is phony because it does not create a file named clean, rather clean is an action. To run clean, type make -f Makefile5 clean:

 1# /codes/multifile/Makefile5
 2
 3FC = gfortran
 4BUILD ?= debug
 5fflags.debug = -g
 6fflags.release = -O3
 7FFLAGS = ${fflags.${BUILD}}
 8LFLAGS =
 9
10OBJECTS = main.o sub1.o sub2.o
11.PHONY: clean
12
13output.txt: main.ex
14	./main.ex > output.txt
15
16main.ex: $(OBJECTS)
17	$(FC) $(LFLAGS) -o main.ex $(OBJECTS)
18
19%.o : %.f90
20	$(FC) -c $(DFLAGS) $<
21
22clean:
23	rm -f main.ex $(OBJECTS)

Download this code

Fancier things are also possible, for example automatically detecting all the .f90 files in the directory to construct the list of SOURCES and OBJECTS:

 1# /codes/multifile/Makefile7
 2
 3FC = gfortran
 4BUILD ?= debug
 5fflags.debug = -g
 6fflags.release = -O3
 7FFLAGS = ${fflags.${BUILD}}
 8LFLAGS =
 9
10SOURCES = $(wildcard *.f90)  
11OBJECTS = $(subst .f90,.o,$(SOURCES))
12
13.PHONY: clean
14
15output.txt: main.ex
16	@echo BUILD=${BUILD}
17	./main.ex > output.txt
18
19main.ex: $(OBJECTS)
20	$(FC) $(LFLAGS) -o main.ex $(OBJECTS)
21
22%.o : %.f90
23	$(FC) -c $(FFLAGS) $<
24
25clean:
26	rm -f main.ex $(OBJECTS)

Download this code

Further reading