Cristian Adam

Building multiple configurations with CMake in one go!

Coming from other build systems to CMake one will quickly learn that CMake can build only one configuration at a time. In practice you need to set up multiple build directories and configure/build with CMake for each and every one.

Autotools can do static and shared builds of libraries. For CMake most of the project would do a static build, then a shared build by setting the CMake variable BUILD_SHARED_LIBS to ON.

QMake can do debug and release builds at the same time, and as we can read at Qt for Android better than ever before, it can configure multiple Android architecture configurations at the same time.

What can we do to get the same level of convenience with CMake?

Shared and static

CMake needs to have unique target names, so if we would have to build a shared and static build we would need to have different target names.

Since we need to build the same library twice, but with only one cmake --build invocation, it would mean that CMake needs to call itself.

That’s it what I’m going to do. Build the same source directory in two different build directories. The add_subdirectory CMake command allows a second parameter for a build directory.

Here is what’s needed to have a library build itself shared and static:

cmake_minimum_required(VERSION 3.9)

project(lib LANGUAGES CXX)

if (NOT ${PROJECT_NAME}-MultiBuild)
  set(${PROJECT_NAME}-MultiBuild ON)

  macro (setup_library library_name build_type)
    set(LIBNAME ${library_name})
    set(LIBTYPE ${build_type})

    add_subdirectory(
      ${CMAKE_CURRENT_SOURCE_DIR}
      build-${build_type}
    )
  endmacro()

  setup_library(${PROJECT_NAME}_s STATIC)
  setup_library(${PROJECT_NAME} SHARED)

  return()
endif()

# The normal CMake library code goes here

add_library(${LIBNAME} ${LIBTYPE} lib.cpp)

Debug and release

If we apply the same idea to a debug and release build, we have:

cmake_minimum_required(VERSION 3.9)

project(lib LANGUAGES CXX)

if (NOT ${PROJECT_NAME}-MultiBuild)
  set(${PROJECT_NAME}-MultiBuild ON)

  macro (setup_library library_name build_type)
    set(LIBNAME ${library_name})
    set(CMAKE_BUILD_TYPE ${build_type})

    add_subdirectory(
      ${CMAKE_CURRENT_SOURCE_DIR}
      build-${build_type}
    )
  endmacro()

  setup_library(${PROJECT_NAME}_d Debug)
  setup_library(${PROJECT_NAME} Release)

  return()
endif()

# The normal CMake library code goes here

add_library(${LIBNAME} lib.cpp)

This will work with command line generators like Ninja or Makefiles, but it won’t work with multi-config generators like Visual Studio.

Debug and release for Visual Studio

In order to get Visual Studio to produce a debug and release mode, we need to be able to invoke CMake with separate --config <CONFIG> values for Debug and Release.

Even if we fiddle with CMAKE_CONFIGURATION_TYPES the above method is not enough. msbuild will fail to build.

We need to get independent CMake runs on the same source code. Luckily CMake provides us with ExternalProject module.

ExternalProject is meant for software downloaded from the internet, but it also works fine with existing source code smile

The code looks like this:

cmake_minimum_required(VERSION 3.9)

project(lib LANGUAGES CXX)

if (NOT ${PROJECT_NAME}-MultiBuild)
  include(ExternalProject)

  macro (setup_library library_name build_type)
    ExternalProject_Add(${library_name}-builder
      SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}"
      CMAKE_ARGS
        -DLIBNAME=${library_name}
        -DCMAKE_BUILD_TYPE=${build_type}
        -DCMAKE_CONFIGURATION_TYPES=${build_type}
        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}
        -D${PROJECT_NAME}-MultiBuild=ON
      BUILD_COMMAND
        ${CMAKE_COMMAND} --build . --config ${build_type}
      INSTALL_COMMAND
        ${CMAKE_COMMAND} -P cmake_install.cmake
    )
  endmacro()

  setup_library(${PROJECT_NAME}_d Debug)
  setup_library(${PROJECT_NAME} Release)

  return()
endif()

# The normal CMake library code goes here

add_library(${LIBNAME} lib.cpp)
install(TARGETS ${LIBNAME})

I needed to restrict the CMAKE_CONFIGURATION_TYPES only for the needed configuration, and to have a custom BUILD_COMMAND, INSTALL_COMMAND, and to install the library. At the end in the build directory I’ve got a lib directory containing the two libraries.

If you have multiple libraries depending on each other, you will have to have proper CMake packages for the libraries, and set the appropriate CMAKE_PREFIX_PATH values.

Android multi architecture

In order to test the same setup for Android, I am assuming you have the Android NDK somewhere in your system.

I configured and build the project from a Windows command prompt window like this:

$ cmake -GNinja -DCMAKE_TOOLCHAIN_FILE=c:\Tools\android-ndk-r20\build\cmake\android.toolchain.cmake ..
$ cmake --build .

The CMake code which builds for armeabi-v7a, arm64-v8a, x86, x86_64 is below:

cmake_minimum_required(VERSION 3.9)

project(lib)

if (NOT ${PROJECT_NAME}-MultiBuild)
  include(ExternalProject)

  file(TO_CMAKE_PATH "${CMAKE_TOOLCHAIN_FILE}" toolchain_file)

  macro (setup_library library_name android_abi)
    ExternalProject_Add(${library_name}-builder
      SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}"
      CMAKE_ARGS
        -DLIBNAME=${library_name}
        -DANDROID_ABI=${android_abi}
        -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}
        -DCMAKE_TOOLCHAIN_FILE=${toolchain_file}
        -D${PROJECT_NAME}-MultiBuild=ON
    )
  endmacro()

  setup_library(${PROJECT_NAME}-v7a armeabi-v7a)
  setup_library(${PROJECT_NAME}-v8a arm64-v8a)
  setup_library(${PROJECT_NAME}-x86 x86)
  setup_library(${PROJECT_NAME}-x86_64 x86_64)

  return()
endif()

# The normal CMake library code

add_library(${LIBNAME} lib.cpp)
install(TARGETS ${LIBNAME})

I only needed to pass the ANDROID_ABI, and CMAKE_TOOLCHAIN_FILE variables.

Conclusion

With the technique presented here CMake can easily do multiple configuration builds in one go! metal

Comments