CMake Development

CMake is the industry standard build tool for multi-platform C++ codebases used by the oneAPI Construction Kit. This document covers best practices for CMake development and conventions within the project, but is not intended as a CMake tutorial.

oneAPI Construction Kit uses modern CMake, regarded as version 3.0 and later, with the minimum required version enforced in the root CMakeLists.txt by cmake_minimum_required.

See also

For general tips on writing CMake see the internal Codeplay knowledge sharing talk by Morris Hafner “Improve Your CMake With These 17 Weird Tricks”.

Toolchain Files

For non-native builds CMake supports a toolchain file mechanism which defines the path to the cross-compilation toolchain and location of non-native system libraries. Without using a toolchain file setting flags directly in CMake modules can be error prone. For example, setting the -m32 flag modifies CPU architecture after CMake has detected a 64-bit system, leading to inconsistencies such as CMAKE_SIZE_OF_VOID being 8 rather than 4, and CMake looking for 64-bit libraries in the native path. Using a toolchain file will also implicitly set the CMAKE_CROSSCOMPILING flag if the module sets CMAKE_SYSTEM_NAME as ours do, pruning the need for an extra user passed commandline option.

The oneAPI Construction Kit stores toolchain files in the root platform directory for all of the cross-compilation platforms the project supports. Our Arm Linux platform makes use of the CMAKE_CROSSCOMPILING_EMULATOR CMake feature with QEMU to emulate 64-bit and 32-bit Arm architectures as part of platform/arm-linux/aarch64-toolchain.cmake and platform/arm-linux/arm-toolchain.cmake. Utilizing an emulator allows us to run our check targets natively to verify cross-compiled builds, which although slower and more memory constrained than native, is valuable option when hardware is unavailable.

Shared Library Naming and Versioning

The OpenCL API is designed to work through an ICD loader, a shared library named OpenCL in the form lib<name>.so or <name>.dll, depending on if the platform is Linux or Windows. The ICD loader gets linked to application code so that at runtime the ICD loader will select the implementation of the standard to run and load the chosen shared library, allowing multiple platforms to coexist on the same system.

To avoid naming collisions with the ICD our OpenCL builds by default are named CL, however this can be overridden via the CA_CL_LIBRARY_NAME option. This provides customer teams with the flexibility to deliver on embedded systems without an ICD loader, where only our platform will exist and can be linked to directly by the application.

To act as the ICD loader, as well mimicking the library name we also need to replicate the library version. Shared libraries in CMake have two versioning properties, a VERSION property for the build version and a SOVERSION property for the API version. We set VERSION in the form major.minor, where the major component is incremented on API breaking changes and minor for non-breaking API changes, e.g bug fixes. The SOVERSION property can then be set as the major component of VERSION. We default VERSION for both OpenCL to our oneAPI Construction Kit PROJECT_VERSION, but provide the CA_CL_LIBRARY_VERSION option to override that behavior for OpenCL.

On Linux an OpenCL build with our default options will result in the following symbolic links being created to the fully qualified shared library.

$ ls -l build/lib/libCL.so*
libCL.so -> libCL.so.major
libCL.major -> libCL.major.minor
libCL.major.minor

Generator Expression Usage

The deferred evaluation of generator expressions from configuration time until the point of build system generation provides benefits in terms of expressibility. For example, the multi-configuration nature of MSVC generators we support means that we don’t know what the build type is at configure time, unlike single-configuration generators which can rely on CMAKE_BUILD_TYPE. By using generator expressions we can check the CONFIG variable query on MSVC to discover the build type and change our settings accordingly.

The variable query expressions also provides a concise syntax for conditionally including items in a list, particularly compared to appending inside nested if()/else() control flow. We often use this paradigm for setting compiler flags, see AddCA Module, using conditional expressions to set the appropriate flags for the various combinations of build configurations.

However, the exception to this is using generator expressions with a list of source files. This is not supported by multi-configuration MSVC generators where files must be known by CMake at configure time, and can’t be deferred for later optional inclusion. A possible workaround for this is defining a separate library which is only linked into the target when the condition expression evaluates to True.

Warning

Using generator expressions for source files will result in the MSVC error message “Target <target name> has source files which vary by configuration.