Building C and C++ Projects with CMake: A Practical Guide
Build Systems: Make versus CMake
While GCC serves as the standard compiler suite for C, C++, and other languages, manually invoking compilation commands becomes unwieldy for projects containing numerous source files. Build automation tools like GNU Make solve this by processing rule-based instruction files (Makefiles) to manage compilation. However, hand-writing Makefiles presents its own challenges for large projects, particularly when targeting multiple platforms. Each Make variant—whether GNU Make, Qt's qmake, or Microsoft's nmake—follows distinct specifications, forcing developers to maintain separate Makefiles for each environment.
CMake addresses this portability problem by introducing a higher-level abstraction. Developers write platform-agnostic CMakeLists.txt files, which CMake then translates into the native build files required by the target system (Makefiles, Visual Studio projects, etc.). This workflow separates build configuration from platform-specific implementation details.
Basic Project Setup
Installation on Ubuntu is straightforward:
sudo apt install cmake
For other platforms, downloads are available at the official CMake website.
Single Source File Example
Consider a simple program calculating exponential values:
#include <stdio.h>
#include <stdlib.h>
double calculate_power(double base_val, int exp) {
double accum = base_val;
if (exp == 0) return 1.0;
for (int idx = 1; idx < exp; idx++) {
accum *= base_val;
}
return accum;
}
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("Usage: %s <base> <exponent>\n", argv[0]);
return 1;
}
double b = atof(argv[1]);
int e = atoi(argv[2]);
double res = calculate_power(b, e);
printf("Result: %g ^ %d = %g\n", b, e, res);
return 0;
}
The corresponding CMakeLists.txt in the same directory:
cmake_minimum_required(VERSION 3.10)
project(PowerCalculator)
add_executable(calc_app main.c)
Build commands:
cmake .
make
Handling Multiple Source Files
Files in the Same Directory
Extracting the calculation logic into separate files:
./project_dir/
├── main.c
├── math_ops.c
└── math_ops.h
The CMakeLists.txt can explicitly list sources:
cmake_minimum_required(VERSION 3.10)
project(PowerCalculator)
add_executable(calc_app main.c math_ops.c)
Alternatively, use automatic source discovery:
cmake_minimum_required(VERSION 3.10)
project(PowerCalculator)
aux_source_directory(. SRC_LIST)
add_executable(calc_app ${SRC_LIST})
Files Across Multiple Directories
With a nested structure:
./project_root/
├── main.c
└── math_lib/
├── math_ops.c
└── math_ops.h
Root CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(PowerCalculator)
aux_source_directory(. TOP_SRCS)
add_subdirectory(math_lib)
add_executable(calc_app ${TOP_SRCS})
target_link_libraries(calc_app MathLib)
Subdirectory math_lib/CMakeLists.txt:
aux_source_directory(. LIB_SRCS)
add_library(MathLib STATIC ${LIB_SRCS})
Configurable Build Options
CMake supports conditional compilation through options. Define a toggle for using a custom math library:
cmake_minimum_required(VERSION 3.10)
project(PowerCalculator)
configure_file(
"${PROJECT_SOURCE_DIR}/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
option(USE_CUSTOM_MATH "Enable custom math implementation" ON)
if(USE_CUSTOM_MATH)
include_directories("${PROJECT_SOURCE_DIR}/math_lib")
add_subdirectory(math_lib)
set(EXTRA_LIBS ${EXTRA_LIBS} MathLib)
endif()
aux_source_directory(. MAIN_SRCS)
add_executable(calc_app ${MAIN_SRCS})
target_link_libraries(calc_app ${EXTRA_LIBS})
Template file config.h.in:
#cmakedefine USE_CUSTOM_MATH
Modified main.c using conditional includes:
#include <stdio.h>
#include <stdlib.h>
#include "config.h"
#ifdef USE_CUSTOM_MATH
#include "math_lib/math_ops.h"
#else
#include <math.h>
#endif
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("Usage: %s <base> <exponent>\n", argv[0]);
return 1;
}
double b = atof(argv[1]);
int e = atoi(argv[2]);
#ifdef USE_CUSTOM_MATH
printf("Using custom math library\n");
double result = calculate_power(b, e);
#else
printf("Using standard library\n");
double result = pow(b, e);
#endif
printf("%g ^ %d = %g\n", b, e, result);
return 0;
}
Use ccmake for interactive configuration, toggling the USE_CUSTOM_MATH option.
Installation Rules and Testing
Defining Install Targets
In math_lib/CMakeLists.txt, add:
install(TARGETS MathLib DESTINATION lib)
install(FILES math_ops.h DESTINATION include)
Root CMakeLists.txt additions:
install(TARGETS calc_app DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/config.h" DESTINATION include)
Execute with sudo make install. Files install to /usr/local by default, configurable via CMAKE_INSTALL_PREFIX.
Adding Tests with CTest
enable_testing()
add_test(NAME BasicRun COMMAND calc_app 5 2)
set_tests_properties(BasicRun PROPERTIES PASS_REGULAR_EXPRESSION "25")
add_test(NAME UsageTest COMMAND calc_app)
set_tests_properties(UsageTest PROPERTIES PASS_REGULAR_EXPRESSION "Usage:")
function(add_math_test base exp expected)
add_test(NAME "Test_${base}_${exp}" COMMAND calc_app ${base} ${exp})
set_tests_properties("Test_${base}_${exp}" PROPERTIES PASS_REGULAR_EXPRESSION ${expected})
endfunction()
add_math_test(5 2 "25")
add_math_test(10 5 "100000")
Run tests using make test or ctest.
Debug Build Configuration
Enable GDB debugging support:
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -g -ggdb")
System Capability Detection
Check for system functions before use:
include(CheckFunctionExists)
check_function_exists(pow SYSTEM_HAS_POW)
Update config.h.in:
#cmakedefine SYSTEM_HAS_POW
Code adaptation:
#ifdef SYSTEM_HAS_POW
double result = pow(base, exponent);
#else
double result = calculate_power(base, exponent);
#endif
Version Information Integration
Define version variables:
set(APP_VERSION_MAJOR 1)
set(APP_VERSION_MINOR 0)
Template updates in config.h.in:
#define APP_VERSION_MAJOR @APP_VERSION_MAJOR@
#define APP_VERSION_MINOR @APP_VERSION_MINOR@
Display in application code:
printf("Calculator v%d.%d\n", APP_VERSION_MAJOR, APP_VERSION_MINOR);
Package Generation with CPack
Generate distributable packages by adding to the root CMake file:
include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_VERSION_MAJOR ${APP_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${APP_VERSION_MINOR})
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
include(CPack)
Build packages:
cpack -C CPackConfig.cmake
This generates binary distribution files in formats like .tar.gz and .sh self-extracting archives.