CMake Overview

This article is part of a series.

Historically I’ve built C++ as tiny specific programs for tiny specific chips. This means not even using regular gcc, but avr-gcc or arm-gcc. Honestly I haven’t even used clang a ton until starting with Swift. I’ve written Makefiles, and run the occasional provided CMake file. Understanding CMake has hardly been necessary since CMake shines for large software projects that need to be built on several platforms and that isn’t what I’ve generally done.

Now however, OpenUSD has a pretty impressive build script and resulting CMake files that I’d like to understand better if I may ultimately have to do a specialized build of it. Also, if I’m going to mix C++ and Swift, understanding the build systems better will only help.

Simplest CMake

The simplest main CMake file (always called CMakeLists.txt) has 3 parts

#PART 1: Minimum Version of CMake file is valid for
cmake_minimum_required(VERSION 3.24)
#PART 2: Project Name (technically optional)
project(Hello)
#PART 3: Executable Target name and files list
add_executable(ClickHello HelloWorld.cpp)

Imagine a project with this layout:

└── Project
    └── source
    │   └── CMakeLists.txt
    |   └── HelloWorld.cpp
    └── build
        └── nothing yet...

To build the source files into the build folder there are a couple of ways to go about it:

# The classic Style
cd $BUILD_DIR
cmake $SOURCE_DIR # the location of the CMakeLists.txt
make # assumes CMake made Makefiles
./$EXECUTABLE_NAME #run the code

## Modern Style
cd $SOURCE_DIR
cmake -S . -B ../build 
cmake --build ../build #can be run no matter the build system
../build/$EXECUTABLE_NAME

The makefile generated by this simplest of processes already has huge amounts more thrown in than the ones I’ve written in the past.

Example CMake for Multi-File Project

To test CMake for a multi-file project, I made a simple prime number supplier project with the following file layout:

└── Project
    └── source
    │   └── CMakeLists.txt
    |   └── resources
    |   |   └── prime_numbers.txt
    |   └── Prime.cpp
    |   └── PrimeLib.cpp
    |   └── PrimeLib.hpp    
    └── build
        └── PrimeEmitter
        └── resources
            └── prime_numbers.txt

Note that the text file with the primes is copied into the build folder (at compile time), NOT mashed into the binary. That’s accomplished by the last part of the CMakeLists.txt:

# Set the minimum required version
cmake_minimum_required(VERSION 3.24)
# Project Information
project(Primes
        VERSION 0.0.1
        DESCRIPTION "Complete Miscellany"
        LANGUAGES CXX)  

#Name the library and the files it uses         
add_library(PrimeNumberStore PrimeLib.cpp PrimeLib.hpp)

# Name the Executable Target and say which files it uses
add_executable(PrimeEmitter Prime.cpp)
target_compile_features(PrimeEmitter PRIVATE cxx_std_20)

# Link the library
target_link_libraries(PrimeEmitter PRIVATE PrimeNumberStore)
 
# Example of add_custom_command(), Move a Resources Folder
# Uses cleaner $<TARGET_FILE_DIR:PrimeEmitter> instead of ${CMAKE_CURRENT_BINARY_DIR}
add_custom_command(
	TARGET PrimeEmitter POST_BUILD
	COMMAND ${CMAKE_COMMAND} -E copy_directory
		${CMAKE_SOURCE_DIR}/resources $<TARGET_FILE_DIR:PrimeEmitter>/resources
)

It all worked great using:

cd $PROJECT_FOLDER/source
cmake -S . -B ../build # must do prep
cmake --build ../build # before can build
../build/PrimeEmitter # gives random prime
../build/PrimeEmitter 3 998 9999 # gives 3rd, 998th and 9999th prime

References

File Contents for Multi-File project

Prime.cpp

#include "PrimeLib.hpp"
#include <iostream>

using namespace std;

int main(int argc, char *argv[]) {
    if (argc == 1) {
        cout << random_prime() << " ";
    } else {
        for (int i = 1; i <= argc - 1; i++) {
            //in a real program would want to handle error
            const long num = std::stoi(argv[i]);
            cout << prime_at(num) << " ";
        }
    }

    cout << endl;
    return 0;
}

PrimeLib.hpp

#ifndef PRIME_INCLUDED
#define PRIME_INCLUDED

//prime in first 10000 primes at index
int prime_at(int index);
//random prime within first 10000 primes
int random_prime();

#endif

PrimeLib.cpp

#include "PrimeLib.hpp"
#include <fstream>
#include <random>
#include <string>

// Max line is 12 characters long
const int max_line_length = 16;
// 10,000th prime is 104729
const int max_prime_length = 6;

int readNthLine(std::ifstream &in, int index) {
    for (int i = 0; i < index - 1; ++i) {
        // if don't know max line length
        //  in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        in.ignore(max_line_length, '\n');
    }
    // What about a char *p instead of string?
    // Why/Why not do that in "Modern C++"
    std::string s;
    s.reserve(max_prime_length);
    std::getline(in.ignore(max_line_length, ' '), s);
    return std::stoi(s);
};

// just because.
// https://stackoverflow.com/questions/5008804/generating-a-random-integer-from-a-range
template <typename T> T fancy_random(const T &min, const T &max) {
    std::random_device seed_maker;
    std::mt19937 generator(
        seed_maker()); // Random-number engine to use (Mersenne-Twister)
    std::uniform_int_distribution<T> distribution(min,
                                                  max); // Guaranteed unbiased
    return distribution(generator);
}

// random prime within first 10000 primes
int random_prime() { return prime_at(fancy_random(0, 10000)); }

// prime in first 10000 primes at index
int prime_at(int index) {
    std::ifstream primesFile("resources/b000040.txt");
    if (primesFile.is_open()) {
        return readNthLine(primesFile, index);
    } else {
        // handle error
        return -1;
    }
}

Prime Numbers text

The supplied prime numbers file has the prime’s index number followed by the prime itself. From the The On-Line Encyclopedia of Integer Sequences

249 1579
250 1583
251 1597
252 1601
253 1607
254 1609
255 1613
256 1619

This article is part of a series.