CMake Basics #1 - Executables & Libraries

Last Edited: 1/21/2025

This blog post introduces basics of CMake.

DevOps

In the last article on the DevOps series, Linux Basics #9 - Makefiles, we discussed how Make allows us to automatically build projects. However, as the codebase grows and demands cross-platform compatibility, Makefiles will also grow to the extent that they become intractable. CMake is a free and cross-platform build tool primarily for C++ that can automatically generate Makefiles and scripts for other build tools appropriate for any environment.

Setting Up CMake

To get started with CMake, you first need to download it using apt install cmake on Linux. For macOS and Windows, check out the official website and follow their instructions. CMake analyzes the CMakeLists.txt file placed at the top level of the directory we are in to automatically generate build scripts, as shown below.

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
# Project Name, Version, Language (default is C++), etc. 
project(MyProject C)
 
# Naming BINARY (We could have used PROJECT_NAME to set the output binary name to the project's name)
set(BINARY bin)
 
# Create the executables using the specified files
add_executable(${BINARY} main.c LinkedList.c)

Here, # is used for comments, and the capital letters indicate variables we define with set and predefined variables. We can run cmake -S <source_path> -B <build_path> to create the appropriate build script or Makefile in the specified <build_path> using CMakeLists.txt in the <source_path>. Conventionally, we create a /build directory under the project's root directory, where we generate the Makefile and build the executable with cmake -S . -B /build. Then, we can move to the /build directory and confirm that the Makefile has been successfully generated. We can run make to generate the executable and then run the executable with ./bin.

Building Libraries

From the above example, we can see that CMake allows us to simplify the build process compared to Makefiles. CMake also allows us to easily compile source code into a library, which we can link to the main source code. For example, we can set up a LinkedList directory, which contains LinkedList.h and LinkedList.c. Then, we can build a library as follows.

LinkedList/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(LinkedList C)
 
# Create a library (STATIC by default. We can specify it to be SHARED.)
add_library(LinkedList LinkedList.c)

The add_library function creates the libLinkedList.a static library, which can be linked to the main source code as follows.

cmake_minimum_required(VERSION 3.15)
 
project(MyProject C)
 
# Add library as subdirectory in `build_path` to run build on library and to reference it
add_subdirectory(LinkedList)
 
add_executable(${PROJECT_NAME} main.c)
 
# Tell where to look for header files not in standard location
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/LinkedList)
 
# Link Libraries
target_link_libraries(${PROJECT_NAME} LinkedList)

The add_subdirectory command makes CMake automatically build LinkedList using the CMakeLists.txt file under the subdirectory, which in this case creates a static library. We can specify the location of the header files with target_include_directories, so the main source code does not need to specify the full path to the header file. Then, we can link the library with target_link_libraries. For better organization, we often use the following file structure for setting up a library:

CMakeLists.txt
main.c
LibraryA/
├── CMakeLists.txt
├── src/
│   ├── module1.c
│   └── module2.c
├── include/
│   ├── module1.h
│   └── module2.h

For the above, we use the following LibraryA/CMakeLists.txt:

LibraryA/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(LibraryA C)
 
# Collect C files from /src
file(GLOB SRC_FILES ${CMAKE_SOURCE_DIR}/LibraryA/src/*.c)
 
add_library(LibraryA STATIC ${SRC_FILES})

Then, the CMakeLists.txt at the root level can specify the location of the header files and link the library as follows:

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(MyProject C)
 
add_subdirectory(LibraryA)
 
add_executable(${PROJECT_NAME} main.c)
 
target_link_libraries(${PROJECT_NAME} LibraryA)
 
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/LibraryA/include)

You can download someone else's library code or submodules and use the same instructions as above to build them into libraries and use them. This simplifies the process of integrating third-party code and custom libraries into larger projects.

Installing Executables and Libraries

Instead of using a library built in the directory specified by the -B flag, we can install it on our machine and use the installed library.

LibraryA/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(LibraryA C)
 
file(GLOB SRC_FILES ${CMAKE_SOURCE_DIR}/src/*.c)
 
add_library(LibraryA STATIC ${SRC_FILES})
 
# Collect header files from /include
file(GLOB HEADER_FILES ${CMAKE_SOURCE_DIR}/include/*.h)
 
# Set public header property
set_target_properties(LibraryA PROPERTIES PUBLIC_HEADER "${HEADER_FILES}")
 
# Install library to local environment "lib" and header files to "include" 
# (usually /usr/local/lib and /usr/local/include)
install(
     TARGETS LibraryA ARCHIVE DESTINATION lib
     PUBLIC_HEADER DESTINATION include
)

The ARCHIVE argument of install is for static libraries, and we should use LIBRARY for shared or dynamic libraries. With the above file, we can run sudo make install to install the library to the local environment, allowing all the executables in the same environment to use it without specifying the path. We can also install executables so they can be run from anywhere.

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
 
project(MyProject C)
 
add_subdirectory(LibraryA)
 
add_executable(${PROJECT_NAME} main.c)
 
target_link_libraries(${PROJECT_NAME} LibraryA)
 
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/LibraryA/include)
 
# Install executable to local environment "bin" (usually /usr/local/bin)
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)

By running sudo make install using the above configuration, we can install the executable, which can then be run by typing MyProject from anywhere.

Conclusion

In this article, we covered what CMake is, why to use CMake, how to get started with CMake, and the very basics of CMake. The CMake series will contain only a few articles (at least according to my plan), covering almost everything you need to know about CMake, so stay tuned.

Resources