Some notes on how I write makefiles [WIP]
When it comes to building software, I personally like to keep things simple. I worked with CMake for around 5 years. I also tried Meson which I really liked. But in the end, I always come back to make.
Make
was created by Stuart feldman in 1977 at Bell Labs. The tool is built to build software in Unix-based systems. Its primary goal is to rebuild only the parts of a project that have changed, making it especially useful for large projects. For smaller or medium-sized projects, the efficiency gains may not be as noticeable, given the speed of modern computers and compilers. Beyond its role in building software, I also see Make as a way to define cross-platform build configurations due to its widespread support across different systems. It allows you to compile files in various configurations, such as debug or release. Additionally, Make isn't limited to building — it can execute any command, including running tests.
A makefile has the following structure
target: dependencies
commands (like a recipe for producing target from dependencies)
When invoking the `make` command, the software will look for a file called `Makefile` in the current folder. It will read it, starting from the first target. The target can be a token (any word) or a file (in the currrent folder). Any way, it will first check if the target dependencies have been updated. Dependencies can be files or other targets. It will then recursively call the commands for each outdated target recursively, starting from the most independant items.
A sample build process in fortran can go like this: we have the executable which solely depends on the single source file `main.f90`. Be carefule that Makefiles require tabulations.
main.exe: main.f90
gfortran main.f90 -o main.exe
If you get an error like,
Makefile:2: *** missing separator. Stop.
check your tabulations!
If we want separate compilation and link steps, we can do this:
main.exe: main.o
gfortran main.o -o main.exe
main.o: main.f90
gfortran -c main.f90 -o main.o
If we use for example two files: `main.f90` which depends on `linspace.f90`. As you see, we depend on the mod file for compiling `main.f90` and on the object file for the link.
main.exe: main.o linspace.o
gfortran main.o linspace.o -o main.exe
main.o: main.f90 linspace.mod
gfortran -c main.f90 -o main.o
linspace.mod: linspace.f90
gfortran -c linspace.f90 -o linspace.o
With the above rules, we can buld anything. We will now see how to make this makefile modifiable easily. For example, we can modify the previous sample by make the compiler and executable name a parameter. We also add FFLAGS, the list of compilation flags we want to set.
FC=gfortran
EXECUTABLE=main.exe
FFLAGS=-fcheck=all -fmax-errors=2 -Wall -O2
$(EXECUTABLE): main.o linspace.o
$(FC) main.o linspace.o -o $(EXECUTABLE)
main.o: main.f90 linspace.mod
$(FC) -c main.f90 -o main.o $(FFLAGS)
linspace.mod: linspace.f90
$(FC) -c linspace.f90 -o linspace.o $(FFLAGS)
Note that any of these variables can be overriden when calling make this way:
make EXECUTABLE=plop FC=ifort
You can avoid typing again and again the same things by using automatic variables:
Whith those, we can rewrite our sample as
FC=gfortran
EXECUTABLE=main.exe
FFLAGS=-fcheck=all -fmax-errors=2 -Wall -O2
$(EXECUTABLE): main.o linspace.o
$(FC) $^ -o $@
main.o: main.f90 linspace.mod
$(FC) -c $< -o $@ $(FFLAGS)
linspace.mod: linspace.f90
$(FC) -c $< -o $(@:.mod=.o) $(FFLAGS)
Here note that $(@:.mod=.o) is there to change the extension of the target from .mod to .o. It is a substitution operation.
What if we just want to compile a bunch of fortran files, all in the same way ? We can stop writing all the targets and let make work using, for example wildcard and generic rules.
SRC=$(wildcard *.f90)
Now SRC contains all the files matching the regular expression: f90 files. What do we do with this? We can get the corresponding list of object files:
OBJ = $(SRC:.f90=.o)
This leads to the following Makefile which, sadly, will not work properly in fortran due to mod files requiring things to be built in a specific order.
EXECUTABLE=main.out
SRC=$(wildcard *.f90)
OBJ = $(SRC:.f90=.o)
$(EXECUTABLE): $(OBJ)
gfortran $^ -o $@
%.o: %.f90
gfortran -c $< -o $@
clean:
rm -f *.o *.mod $(EXECUTABLE)
We can fix this by explicitely listing all files in order of compilation and getting rid of wildcard.
EXECUTABLE=main.out
SRC=linspace.f90 main.f90
OBJ = $(SRC:.f90=.o)
$(EXECUTABLE): $(OBJ)
gfortran $^ -o $@
%.o: %.f90
gfortran -c $< -o $@
clean:
rm -f *.o *.mod $(EXECUTABLE)
home