CMake

CMake is commonly used in open source projects. Over the years, CMake has evolved and gained new features, but its behavior has also changed.

Projects that support CMake come with a file named CMakeLists.txt. The file contains instructions and configuration settings that CMake uses to determine how to build and compile a project. It specifies the version of CMake the file was written for, which is important when troubleshooting failing builds. The CMake documentation webpage allows you to select the version of CMake you're interested in, ensuring you get the relevant information for that version.

Building with CMake is a two-stage process. In the first stage, called configure, CMake reads CMakeLists.txt (and possibly other included files) into memory. In the second stage, called generate, CMake generates build files for a specific generator, such as Make or Ninja.

The following example cross compiles hello.cpp for QNX. First, create the following files in your project folder:

  • CMakeLists.txt
  • hello.cpp
  • qnx.toolchain.cmake

The CMakeLists.txt file creates the hello executable:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(hello LANGUAGES CXX)

add_executable(hello hello.cpp)

The qnx.toolchain.cmake file is a CMake toolchain file required for cross compiling (here is a minimalistic version for cross compiling C++ code for a QNX target with aarch64 architecture):

set(CMAKE_SYSTEM_NAME QNX)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CXX_COMPILER q++)
set(CMAKE_CXX_COMPILER_TARGET gcc_nto${CMAKE_SYSTEM_PROCESSOR}le)
set(CMAKE_SYSROOT $ENV{QNX_TARGET})

Cross compilation with CMake is very similar to native compilation. The only difference is setting the CMAKE_TOOLCHAIN_FILE variable:

mkdir build_qnx
cd build_qnx
cmake .. -DCMAKE_TOOLCHAIN_FILE=../qnx.toolchain.cmake
cmake --build .

In this simple example, no adaptations were made to the CMakeLists.txt file.

Unfortunately, complex projects have complex CMake files with conditional statements checking CMAKE_SYSTEM_NAME, etc. These CMake configuration files need to be extended for QNX.

Another example for QNX toolchain file might look like this:

set(QNX TRUE)
set(CMAKE_SYSTEM_NAME QNX)
set(QNX_HOST "$ENV{QNX_HOST}")
set(QNX_TARGET "$ENV{QNX_TARGET}")

# specify the cross compiler
# Notice that if Windows is the host platform, append ".exe" to the compiler paths
set(CMAKE_C_COMPILER ${QNX_HOST}/usr/bin/qcc)
set(CMAKE_CXX_COMPILER ${QNX_HOST}/usr/bin/q++)
set(CMAKE_ASM_COMPILER ${QNX_HOST}/usr/bin/qcc)
set(CMAKE_SYSROOT "${QNX_TARGET}/aarch64le") # or set(CMAKE_SYSROOT "${QNX_TARGET}/x86_64") depending on the target platform

# extra flags
set(QNX_FLAGS "-D_QNX_SOURCE")
set(CMAKE_C_FLAGS "${QNX_FLAGS} ${CMAKE_C_FLAGS} ${EXTRA_CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${QNX_FLAGS} ${CMAKE_CXX_FLAGS} ${EXTRA_CMAKE_CXX_FLAGS}")
set(CMAKE_ASM_FLAGS "${QNX_FLAGS} ${CMAKE_ASM_FLAGS} ${EXTRA_CMAKE_ASM_FLAGS}")

set(QNX_LINKER_FLAGS "-lm -lsocket -lgcc_s")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${QNX_LINKER_FLAGS} ${EXTRA_CMAKE_LINKER_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${QNX_LINKER_FLAGS} ${EXTRA_CMAKE_LINKER_FLAGS}")

There are multiple ways to include the toolchain files into the existing project's build structure and are commonly specified during the configuration stage of CMake:

# set CMake options
cmake_args="-DCMAKE_TOOLCHAIN_FILE=/path/to/qnx_toolchain.cmake \
            -DCMAKE_INSTALL_PREFIX=/prefix \
            -DCMAKE_STAGING_PREFIX=/prefix \
            -DEXTRA_CMAKE_C_FLAGS="-extra_flags" \
            -DEXTRA_CMAKE_LINKER_FLAGS="-extra_linker_flags" \
            -DBUILD_SHARED_LIBS=ON \ # set this to OFF to build static libraries
             "

# Configure using CMake, here Ninja is the build tool used
cmake ${cmake_args} -G Ninja -B ./build

# Build using Ninja
ninja -C ./build

If the project you're porting has dependencies specified using CMake's find_package() commands or pkg-config related commands, you need to set some extra CMake variables. Refer to CMake's "Using Dependencies Guide" for in depth instructions. Additionally, please refer to CMAKE_SYSTEM_PREFIX_PATH for more details on search paths that CMake uses.

Here is a brief example of some of the commonly used extra variables:

cmake_args="..."
cmake_extra_args="-DCMAKE_MODULE_PATH=/path/to/deps/install/dir/lib/cmake \
                  -DCMAKE_FIND_ROOT_PATH=/path/to/deps/install/dir \
                 "

# export pkg-config environment variables so CMake can find *.pc files
export PKG_CONFIG_LIBDIR=/path/to/deps/install/dir/lib/pkgconfig
export PKG_CONFIG_SYSROOT_DIR=/path/to/deps/install/dir

# Configure using CMake with extra arguments and build using Ninja
cmake ${cmake_args} ${cmake_extra_args} -G Ninja -B ./build
ninja -C ./build

If the software is successfully built using CMake, install it using Ninja:

DESTDIR="/path/to/install/dir" ninja -C ./build install

It's also possible to let CMake call the build tools instead, but they are separated here for the sake of clarity. Here Ninja is used as an example, but Make can be used interchangeably with Ninja for most of the time.

Page updated: