Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Building C and C++ Projects with CMake: A Practical Guide

Notes May 12 3

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.

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.