“Those who cannot remember the past are condemned to repeat it.”
(Hence) Some History Before We Begin
In the beginning, there was Make.
Any library or executable that was made compiling more than a couple of source files involved Make. Every self-respecting Unix system had a version of Make bundled with it. Hence it was easy for programmers to create a Makefile, define the standard targets and expect Make to take care of the rest. Given the source and the Makefile, programmer instinctively opened a terminal and wrote:
make && make install
Success meant the program or library was immediately available for use. Failure mostly meant fixing compilation errors which programmers were adept at anyway. The system worked flawlessly because it was built by programmers to be used by programmers.
As times went by systems started to get diverse. Programmers wanted to compile the same source across these diverse systems but the Makefile was a constraint. The Makefile had to be customized specific to the system. Programmers being programmers wanted these customizations to be done automatically. They applied the fundamental theorem of Software Engineering and introduced the extra level of indirection – Makefile.in
It is the responsibility of the configure script to take the Makfile.in and produce a customized Makefile specific to the system. Given the source and the Makefile.in, programmer instinctively opened a terminal and now wrote:
./configure && make && make install
This came to be popularly known as the build dance as it involved the configuring, building and installing steps.
Complexity: The Party Pooper
Creating and maintaining the configure script and Makefile.in were difficult due to their complexity. When the configure scripts stated to get bigger than the actual program, programmers knew they had to do something. They did what they best knew – introduce the extra level of indirection. But of course, all problems in computer science can be solved by another layer of indirection except for the problem of too many layers of indirection. 😊 When configure.ac and automake were introduced to compile a mere ten-line program the problem of too many (layers) was apparent. Setting up a build system the right way became extremely difficult with configure and Makefile.in. Efforts to further automate the process by introducing tools like automake, autoconf and m4 increased the complexity.
“Any sufficiently advanced technology is indistinguishable from magic.”
Arthur C. Clarke
Make vs Make: Cmake – the Cross-Platform Make
The Cmake Magic
From the previous exposition it is clear that setting up a cross platform build system is no child’s play. What if there was a tool that generates build systems from a single starting point – akin to a Makefile? Enter CMake. CMake is a generator of build systems that can produce Makefiles for Unix like systems, Visual Studio Solutions for Windows and XCode projects for Mac OS. All these from the same base – a single CMakeLists.txt file. But any magic – not just advanced technology – can be understood if we break it into stages.
The Cmake Build Process
A program (or a library) is built using CMake in two stages. In the first stage, standard build files are created from CMakeLists.txt. Then the platform’s native toolchain is used for the actual building. CMake effectively composes Make as a build system within itself. This is the best possible reuse without incurring the cost of unnecessary inheritance baggage from Make. Unlike Make, CMake’s two stage build process lets the programmer work with platform’s debugging tools if necessary. During the CMake build process it is customary to create a folder named `build` that keeps all the build artifacts. This prevents source code getting polluted with build related artifacts like object files. The easiest way to accomplish such an out of source build is:
cmake -S . -B build ß Assumes sources are in the current folder
cmake –build build ß Generate a binary with a build tool for the system
The CMakeLists.txt File
The CMakeLists.txt file contains a set of directives and instructions describing the source and targets. Unlike a complex Makefile that looks intimidating to a beginner, a CMakeLists.txt file is more structured and well written. This is owing to the well defined cmake-language with which the CMakeLists.txt are written. The cmake-language includes most modern programming language constructs like
- Variable definitions
- Strings and Text Globbing
- Conditional blocks
- Functions and Macros
Since the syntax of cmake-language is not too different from modern programming languages it becomes easier for programmers to create CMakeLists.txt file and maintain it. Here is an example CMakeLists.txt file that even non-programmers will agree to be easy to comprehend.
The CMakeCache.txt File
After the first configuration of a project, CMake persists variable information in a text file called CMakeCache.txt. Caches are used to improve. When CMake is re-run on a project the cache is read before starting so that some re-parsing time can be saved on CMakeLists.txt. Here is where passing parameters to CMake befuddles a beginner. If a variable is passed via the command line that variable is stored in the cache. Accessing that variable on future runs of CMake will always get the value stored inside the cache and new value passed through the command line are ignored. To make CMake take the new value passed through the command line the first value have to be explicitly undefined. Like so:
cmake -U <previously defined variable> -D <previously defined variable>[=new value]
(A better approach is to use CMake internal variables. For more information refer the CMake manual here.)
GUI Makes Things Easier
Make belongs to a generation when state of the art editors was Vi or Emacs. Dropping to a shell to get a build was no big deal. For the modern programmer spoiled by choice of integrated development environments, having a graphical user interface makes things easier. CMake comes bundled with a GUI for all platforms it supports. This makes a smoother learning curve for CMake that greatly influenced its quick adoption. Using GUI of CMake, the programmer can configure the build first and then generate the build. The well-designed GUI of CMake guides the programmer to get a build. Of course, in a continuous integration scenario where builds are executed through git workflows or Azure pipelines or Jenkins or [insert your favourite nightly build setup here], having a command line client or a plugin is mandatory. Since CMake is actively being developed it supports most platforms and use cases. This support – unlike Make – has greatly improved the adoption of CMake in the build world.
Modern Cmake: From Tried-and-Tested to Better
CMake was developed by Kitware and released in year 2000. It is still maintained and supported by Kitware but developed in collaboration with a vibrant open source community under a very permissive BSD license. Adopting CMake for a build system prevents any source of dreaded vendor lock-in. Better still, permissive license enables companies like Microsoft to bundle CMake along with its flagship Visual Studio IDE. It is worth noting that VS6.0 had support for Makefile based projects using nmake and VS2017/VS2019 natively supports CMake.
CMake has been under continuous development for the last 20 years. Starting with just C/C++ project support, modern CMake now supports languages like Fortran, C# and CUDA. Over the years a lot of open source projects migrated from Makefile based build system to CMake. Here is a partial list of famous open source projects using CMake as their build system in no particular order:
“The three virtues of a programmer are: Laziness, Impatience and Hubris”
CMake Vs Make (for the Impatient)
If you were not patient enough to go through the full blog post as programmers are wont to do, here is a quick comparison chart between CMake vs Make.
Through this blog post I wanted to bring forth the major differences between CMake vs Make. Make still has its dedicated fan base (note all major version control systems like git or subversion still uses make for its build) but the future belongs to CMake. Using modern CMake for generating builds are definitely recommended.