A friend of mine and I, we were recently discussing about the layout of a GNU makefile that allows you to add source files to your source tree in an arbitrary directory hierarchy without having to modify the makefile. As other requirements, we only wanted to create a single top-level makefile in the build directory, though no cascading sub-directory makefiles, and the makefile should be able to handle multiple source files with the same name.
Beside the usual make logic, GNU make provides the makefile writer with a good set of helper functions, e.g. for text replacement or directory/file operations. For a full reference of GNU make functions, please check out the official documentation.
As mentioned in the title, this makefile is very simplistic. It makes the following assumption about the software project. It is for now basically oriented towards software development in C, but it should be easy possible to enhance it for other languages.
- all the source files are located in a single directory, e.g. src
- there is a separate build directory containing the created object files, e.g. build (this is not a hard requirement, but I like to separate the sources from the object and executable files
- all source files having the same file type will be built with the same compiler options
So, here it is:
source_files = $(shell find ../src -type f -iname '*.c' | sed 's/^\.\.\/src\///')
obj_files = $(subst .c,.o,$(source_files))
CFLAGS = -g
vpath %.c ../src/
gcc $(LDFLAGS) $^ -o $@
-mkdir -p $(dir $@)
gcc $(CFLAGS) $< -c -o $@
.PHONY: all clean
rm -rf $(obj_files) $(dir $(obj_files)) bin
So, what is this makefile doing?
The first line collects all the source files, in this case all C source files, from the source directory. The $(shell …) command will execute any shell command from within the makefile. These specific shell commands will only record the file path relative to the source directory. I will explain below why this is useful. The second line is a text replacement to determine the object files corresponding the source files.
The next interesting line is the vpath command. The vpath command allows the makefile writer to specify directories where make should look for dependencies in addition to the local directory. It furthermore allows you to specify alternative directories based on the file extension. Here, we say that make should also look for .c files in the source directory.
Finally, the most important part of the makefile is the generic rule to translate .c files into .o files (%.o: %.c). This rule is used for each object file requested as dependency for the final executable (bin). For example, assume that the final executable only depends on the object file generic/a/bin.o, then GNU make will internally handle the generic rule as:
-mkdir -p $(dir $@)
gcc $< -c -o $@
The next important aspect here is that we use the special make variables $< (refering to the first dependency) and $@ (refering to the target). This rule is also where the vpath instruction from the beginning of the makefile comes into play. When searching for the file generic/a/bin.c as dependency, GNU make will first try to find this file relative to the current directory, but since it does not exist relative to the local directory, it will then search relative to the vpath directory, though the source directory. There, GNU make will find the file and use it. As a short side note about this rule: the $(dir …) make function extracts the directory part of a file name and the dash (-) in front of the rule tells make to ignore the return value of the mkdir command. This is a somewhat nasty hack, but simplifies the makefile by avoiding checks for directory existence.
As said, this makefile is very simplistic and might not match all requirements. However, it at least shows some interesting aspects and functions of GNU make.