Advanced Build Systems / cmake#

Today’s assignment: https://classroom.github.com/a/2XbwIK5D

Introduction#

The make tool that you’ve got introduced to is most definitely a legacy tool. It primarily focuses on one single thing, that is, given proper dependency information, it will rebuild all the needed parts, to update the target(s) requested.

It handles some more basics of adapting/customizing the build process to fit a particular environment, but it’s quite lacking as a complete build system.

Historically, in the open source software world, the autotools were developed as a layer on top of make to fix many of its shortcomings. If you install software from source you may well come across it (on the user side) – here’s an example.

[kai@macbook class.wiki (master)]$ tar zxvf hello-0.02.tar.gz
[kai@macbook class.wiki (master)]$ cd hello-0.02
[kai@macbook hello-0.02 (master)]$ ./configure
[...]
[kai@macbook hello-0.02 (master)]$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-am
  CC       hello.o
  CC       greeting.o
  CC       factorial.o
  CCLD     hello

On the other hand, modern IDEs like Microsoft Visual Studio or Apple’s Xcode typically provide their own ways of building projects. These are much more advanced, but often proprietary and not portable.

cmake#

cmake is an alternative to the autotools, which has been gaining in popularity. Its goals are rather similar to the autotools, but the underlying technology is quite different.

The main goal is to enable the developer to provide code that will work on different platforms without having to customize the build and related tasks for each system. The autotools are built on top of traditional Makefiles, while cmake is more general and has a number of choices for its backend – one can just use regular Makefiles again, but one can also use IDEs like Microsoft Visual Studio or Apple’s XCode.

The autotools are built using existing languages (shell, m4, perl in the background), while cmake has its own DSL (domain specific language) to describe what to build and how.

Building an executable#

We will now build a simple project (the hello project yet again) using cmake. We’ll do the basics here, but you it’ll be helpful to look at the cmake tutorial for going into more depth.

First of all, before we start using a new way of building code, let’s clean house.

Your turn#

We’re still working on the same code example (last time – I promise :wink:). I copied my version of the code, built by a make, into this class’ assignment repo. Since the Makefile file is committed to git, it’ll be there in the history forever, if one ever wanted to revisit it. But to move on, let’s remove it, and also remove any object files, executables, etc., you may still have lying around.

Removing them can be done in your IDE / git GUI frontend, or if you like to do things on the command line:

vscode  /workspaces/class-5 (work) $ git rm Makefile 
rm 'Makefile'
vscode  /workspaces/class-5 (work) $ rm *.o hello
vscode  /workspaces/class-5 (work) $ ls
factorial.c  greeting.c  hello.c  hello.h  README.md
vscode  /workspaces/class-5 (work) $ git commit -m "Remove Makefile and built files"

Adding CMakeLists.txt#

Instead of a Makefile, cmake uses another file that provides info on what to build, it’s called CMakeLists.txt.

cmake_minimum_required(VERSION 3.16)
project(hello LANGUAGES C)

# Set C standard
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Create the executable
add_executable(hello
  hello.c
  greeting.c
  factorial.c
)

This is mostly boilerplate code. Usually, I just copy an existing CMakeLists.txt file from another project and modify it as needed. This time, I actually asked my editor’s AI to create one for me, and the above is what I got. It’s pretty much the same I had usually created myself, except that it added some lines to make sure we’re using C99 as the C standard, which is a good thing.

So let’s go through it line by line.

The first line states the minimum required cmake version. Here I version 3.16, so that if someone uses, e.g., an ancient cmake 2.8, they’ll get a useful error rather than all kinds of things going wrong.

The project is then given a name (which for now isn’t really being used).

As I said above, it added some lines to set the C standard to C99. This is generally a good idea, as different compilers have different default standards, and having different users compile the same code with different standards can lead to subtle inconsistencies.

Finally, the last lines do the real work. This command says that you want to add an executable to the project. That executable is named hello, and it’s to be built from the three .c sources stated.

Out-of-tree builds#

cmake supports “out-of-(source-)tree” builds. They’re optional, but you should definitely get in the habit of using them. That is, instead of putting all kinds of object files, executables, etc in the same directory as your sources, you can have all that stuff happen in a separate directory.

Your turn#

  • Create the CMakeLists.txt file with the contents shown above, and use it to build the code. It should go as shown below. Note that I’m first creating a separate build directory, and then call cmake and do the build from there, which is much preferred over building the code directly in the same directory as the sources. Once you got it working, commit your work. You do not want to commit the actual build files in the build/ directory, though, just the CMakeLists.txt (and other changes you made to the code, if any.)

vscode  /workspaces/class-5 (work) $ mkdir build
vscode  /workspaces/class-5 (work) $ cd build
vscode  /workspaces/class-5/build (work) $ cmake -S ..
-- The C compiler identification is GNU 13.3.0
-- The CXX compiler identification is GNU 13.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.5s)
-- Generating done (0.0s)
-- Build files have been written to: /workspaces/class-5/build
vscode  /workspaces/class-5/build (work) $ make
[ 25%] Building C object CMakeFiles/hello.dir/hello.c.o
[ 50%] Building C object CMakeFiles/hello.dir/greeting.c.o
[ 75%] Building C object CMakeFiles/hello.dir/factorial.c.o
[100%] Linking C executable hello
[100%] Built target hello
vscode  /workspaces/class-5/build (work) $ ./hello 
Hi there, class!
10 factorial is 3628800

[As may have happened before, as we’re using more tools, I’m kinda just hoping that they are present on your system. But actually, these days, even for Linux, most people using Linux aren’t programmers, so often those development tools aren’t installed by default. On Ubuntu you may see the helpful message To install cmake, use the command "sudo apt install cmake". On Mac OS, one can certainly find all these packages in many variants on the web, but I recommend using either https://macports.com or https://brew.sh as package managers – once those are set up, installing a package is typically as simple as sudo port install cmake.]

So what happens is that you create and change into a separate directory called build/ here. You then run cmake -S .. (or just cmake ..), which invokes cmake and tells it to find the sources into the parent directory ... What cmake does is, essentially, to create a custom Makefile, so you don’t have to write it yourself. Then you just call make and that’ll build your code. From this point on, as you develop / debug your code, you only need to call make again. As you can see, it figured out what compiler to use, and it also handles dependencies on header files automatically.

Another useful feature of cmake is that adds a clean rule automatically. (When using plain Makefiles, people often add one manually). It cleans up things – more specifically, it removes generated files, so one doesn’t have to do rm *.o hello or something like that by hand.

cmake by default doesn’t actually show the details of the commands that it runs, avoiding clutter. But sometimes, one might actually want to know what exactly is going on (in particular, when something is going wrong). In that case, make VERBOSE=1 is useful.

Actually, cmake supports many different build systems as backends. If you want to be generic, you might want to get in the habit of saying “cmake --build .” instead of just make. This will use whatever build system cmake generated files for. On Linux, that’s usually Makefiles, but on Windows, it might be Visual Studio project files, etc.

Building a library#

While not exactly called for here, in this simple example code, I’d like to introduce the concept of (software) libraries. In a nutshell, a library is a collection of various routines that are packaged together and where one can easily pull out those that one wants to use (and one doesn’t have to do it manually – the linker will do it). A very commonly used example is the C standard library, which contains functions like printf, fopen, and hundreds others. Whenever you use printf in your program, the compiler/linker will just pull out the actual printf code from the library and link it in.

There are numerous third-party libraries, and since it’s generally a good idea to avoid reinventing the wheel, we’ll use some as we go. Sometimes building libraries is also a good way of organizing your own code. Let’s pretend the factorial() and greeting() functions are actually useful and you want to package them in a library, called libstuff. This is how to change your CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(hello LANGUAGES C)

# Set C standard
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

# It's not a real meaningful library, so I'm not giving it a real
# meaningful name ("stuff") either...
add_library(stuff greeting.c factorial.c)

add_executable(hello hello.c)
target_link_libraries(hello PRIVATE stuff)

add_library is very similar to add_executable, it’s just that it creates library rather than an executable from the provided sources. In order to use a library (hello needs it), one uses target_link_libraries.

Your turn#

Make the changes above to build a library containing the greeting() and factorial() functions. Explain the commands that are run. Make sure everything still works.

IDE support for cmake#

Most modern IDEs have support for cmake projects. For example, in VSCode, if you have the CMake Tools extension installed, you can just open the folder containing the CMakeLists.txt file, and it will automatically detect it as a cmake project. You can then configure, build, and run your project directly from within VSCode, without having to use the command line.

Header files#

Unfortunately, given C/C++’s somewhat clunky handling of header files, cmake still needs help to properly associate the header files with a library it builds and provides to other targets. In the example above, the header file hello.h is just located in the current directory, so it’ll be found without further intervention (because of the #include "hello.h" syntax, rather than #include <hello.h>. But for the record, one should associate the library with the header files that it needs using

 It's not a real meaningful library, so I'm not giving it a real
# meaningful name ("stuff") either...
add_library(stuff greeting.c factorial.c)
# the header files need to be made available to users of the library
# In this simple layout, they are in the same directory as the sources
target_include_directories(stuff PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

Things get yet more complicated if one wants to build and install a library for other, separate, codes to use. But that’s beyond the scope of this class. See here for more, in particular the BUILD_INTERFACE vs INSTALL_INTERFACE.

Homework#

  • (by Thu) If you don’t already know them, figure out the scoring rules for the game of ten-pin bowling (spares, strikes, etc).

  • If you haven’t done so yet, update your wiki page from class 1 with some more formatting / picture or other advanced formatting.

  • I told you that cmake will automatically take care of header file dependencies, even if the header files (like hello.h) are never even mentioned in CMakeLists.txt. Come up with a plan to verify that this is in fact the case, and try it.

  • (A bit of a challenge) Figure out how to build a C++ program. You might want to use the C++ version of the hello program from class 3/4, or if you have anything more interesting, feel free to use that. Do this work in a new subdirectory of your assignment repository, e.g., cxx/. Create a proper CMakeLists.txt file to build it. You can look into cmake’s add_subdirectory() to set it up in a way where both the C and the C++ code get built as part of a single project.

  • To submit the homework, as before, push (sync) it back to github, and add comments to do Feedback PR (pull request).

  • For more info on pull requests, this is a nice write-up: https://medium.com/@urna.hybesis/pull-request-workflow-with-git-6-steps-guide-3858e30b5fa4