The Problem Make Solves
Introduction
When working on small C++ projects with just one or two files, compiling manually using g++ or clang++ is simple. However, as projects grow and include multiple source files, dependencies, and libraries, managing the build process manually becomes tedious and error-prone. Let's look an example:
Manual Compilation
The following examples are for illustration purposes only. You don't have to create any files or run any of the commands if you don't want to. Our goal is to skim through one way of doing things, recognize its limitations, and appreciate why it's perhaps wiser to start using a more complete tool like CMake. Starting from the next chapter, we’ll take a more hands-on approach.
Consider a simple project structure with the following files:
Each module depends on its submodules. The main program P.cxx depends on A.cxx and B.cxx, which in turn depend on their respective submodules.
A possible manual compilation process might look like this:
We don't like all that typing. If we were compiling the entire source tree, we could use wildcards to simplify the compilation and also run it in parallel.
Much better. But as we'll see soon, this is only pragmatic for small codebases.
Problems with Manual Compilation
Say A1.cxx changes. We have a choice to recompile the entire codebase. As we've opined earlier, if we had a small codebase, this would be a reasonably pragmatic approach. On a large codebase, this will slow things down significantly. So we decide to track the dependencies ourselves. We recompile only A1.cxx and then link. This works too. As we begin to make changes across multiple files though, tracking dependencies manually starts to become cumbersome and error-prone rather quickly.
Why Make?
- Selective Compilation –
makeuses timestamps to determine which files have changed, and only recompiles them and related dependecies as we illustrate next. - Dependency Management – say you've declared a dependency of
A1.oonA1.cxxandA1.h. If eitherA1.cxxorA1.hchanges,makecan detect this and recompileA1.o. - Modular Rules –
makeallows defining rules for building individual components, making the build system more scalable. - Parallel Builds –
make -jenables parallel compilation, speeding up the process.
The Makefile
Make uses a Makefile to define rules for compiling and linking source files. A Makefile for our project might look like this:
Running Make
With this Makefile, instead of typing out long compilation commands, we can simply run:
This will only recompile the necessary files when they change, saving time.
To clean up generated files, use:
Limitations of Make
- Non-Portable: Makefiles are often platform-dependent.
- Complexity: Large projects require complex Makefiles.
- Lack of Built-in Dependency Management: External dependencies must be manually handled.
Transition to CMake
CMake is a more advanced build system generator that overcomes these limitations by generating platform-independent build scripts (including Makefiles). In the next chapter, we will introduce CMake and demonstrate how it simplifies build management.