I’d love to simply use GNU Make for my C/C++ projects because it’s so simple to get started with. Unfortunately it’s lacking a few essential qualify of life features:
- Out of tree builds
- Automatic header dependency detection
- Recompile on CFLAGS change
- First class build targets
Out of tree builds
If you want your build to happen in an isolated build directory (as opposed to creating object files in your source tree), you need to implement this yourself.
It involves a lot of juggling paths. Not fun!
Automatic header dependency detection
In C/C++, when a header file changes, you must recompile all translation units (i.e. object files, roughly) that depend on (i.e. include) that header. If you don’t, the object file will become stale and none of your changes to constants, defines, or struct definitions (for example) will be picked up.
In Make rules, you typically express dependencies between source files, and object files, e.g:
%.o: %.c
# run compiler command here
This will recompile the object file when the source file changes, but won’t recompile when any headers included by that source file change. So it’s not good enough out of the box.
To fix this, you need to manually implement this by:
- Passing the proper flags to the compiler to cause it to emit header dependency information. (Something like
-MMD
. I don’t know them exactly because that’s my whole point =) - Instructing the build to include that generate dependency info (Something like
)
-include $(OBJECTS:.o=.d)
The generated dependency info looks like this:
pmap.c.obj: \
kern/pmap.c \
inc/x86.h \
inc/types.h \
inc/mmu.h \
inc/error.h \
inc/string.h \
Recompile on CFLAGS change
In addition to recompiling if headers change, you also want to recompile if any of your compiler, linker, or other dev tool flags change.
Make doesn’t provide this out of the box, you’ll also have to implement this yourself.
This is somewhat nontrivial. For an example, check out how the JOS OS (from MIT 6.828 (2018)) does it: https://github.com/offlinemark/jos/blob/1d95b3e576dd5f84b739fa3df773ae569fa2f018/kern/Makefrag#L48
First class build targets
In general, it’s nice to have build targets as a first class concept. They express source files compiled by the module, and include paths to reach headers. Targets can depend on each other and seamlessly access the headers of another target (the build system makes sure all -I
flags are passed correctly).
This is also something you’d have to implement yourself, and there are probably limitations to how well your can do it in pure Make.
Make definitely has it’s place for certain tasks (easily execute commonly used command lines), but I find it hard to justify using it for anything non-trivially sized compared to more modern alternatives like CMake, Meson, Bazel, etc.
That said, large projects like Linux use it, so somehow they must make it work!