Mapping CMAKE_BUILD_TYPE and CMAKE_CONFIGURATION_TYPES between project subdirectories

Better to explain with a use case:
ProjectThird uses non-standard CMAKE_CONFIGURATION_TYPES, ie: checked, release
I want to include ProjectThird into my ProjectOwn, however ProjectOwn has the standard CMAKE_CONFIGURATION_TYPES: Release, RelWithDebInfo, Debug, …

How can I map the CMAKE_CONFIGURATION_TYPE from ProjectOwn, to CMAKE_CONFIGURATION_TYPES of ProjectThird?

  • with FetchContent:
  # Fetch ProjectThird (CMake phcerdan branch)
  include(FetchContent)
  FetchContent_Declare(
    ProjectThird
    GIT_REPOSITORY https://github.com/phcerdan/ProjectThird
    GIT_TAG cmake_for_easier_integration
  )
  FetchContent_GetProperties(ProjectThird)
  if(NOT ProjectThird_POPULATED)
    set(original_cmake_build_type ${CMAKE_BUILD_TYPE})
    # Try to always use release for ProjectThird, no matter what current config/build_type
    set(CMAKE_BUILD_TYPE "release" CACHE INTERNAL "")
    FetchContent_Populate(ProjectThird)
    add_subdirectory(${ProjectThird_SOURCE_DIR} ${ProjectThird_BINARY_DIR})
    # Restore CMAKE_BUILD_TYPE to the original of the current project.
    set(CMAKE_BUILD_TYPE "${original_cmake_build_type}" CACHE INTERNAL "")
     # Not Working. Not able to set CMAKE_BUILD_TYPE only for the subdirectory...
  endif()

I am starting to think that the only way to handle this third party is with ExternalProject if there is not a way to create “CACHE” variables with a scope limited to subdirectories.

Have you faced this problem before? What would you recommend?

I am guessing something like the following work (WIP) proposed by @craig.scott should work:
https://gitlab.kitware.com/cmake/cmake/issues/18831

# WIP from https://gitlab.kitware.com/cmake/cmake/issues/18831
FetchContent_GetProperties(boost)
if(NOT boost_POPULATED)
    FetchContent_Populate(boost)

    # The following needs to be run after initial checkout or
    # after the dependency details of boost are changed.
    # The following option is provided to prevent having to
    # keep reconfiguring and rerunning the boost build each
    # time CMake runs once it has been successfully performed
    # for the version specified.
    option(ENABLE_BOOST_BUILD "Enable reconfiguring and rerunning the boost build" ON)
    if(ENABLE_BOOST_BUILD)
        # This file comes from the following location:
        #   https://github.com/pfultz2/cget/blob/master/cget/cmake/boost.cmake
        configure_file(boost.cmake
                       ${boost_SOURCE_DIR}/CMakeLists.txt
                       COPYONLY
        )

        unset(generatorArgs)
        set(cacheArgs
            "-DCMAKE_INSTALL_PREFIX:STRING=${boost_BINARY_DIR}/install"
            "-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=YES"
            "-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}"
        )
        if(CMAKE_TOOLCHAIN_FILE)
            list(APPEND cacheArgs "-DCMAKE_TOOLCHAIN_FILE:FILEPATH=${CMAKE_TOOLCHAIN_FILE}")
        else()
            list(APPEND cacheArgs "-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}"
                                  "-DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}"
            )
        endif()

        get_property(isMulti GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
        if(NOT isMulti)
            list(APPEND cacheArgs "-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}")
        endif()

        if(CMAKE_GENERATOR_PLATFORM)
            list(APPEND generatorArgs
                 --build-generator-platform "${CMAKE_GENERATOR_PLATFORM}"
            )
        endif()
        if(CMAKE_GENERATOR_TOOLSET)
            list(APPEND generatorArgs
                 --build-generator-toolset  "${CMAKE_GENERATOR_TOOLSET}"
            )
        endif()

        # Assume parent dir has set BOOST_LIBS to a list of the boost modules
        # we want build and made available
        foreach(lib IN LISTS BOOST_LIBS)
            list(APPEND cacheArgs "-DBOOST_WITH_${lib}:STRING=--with-${lib}")
        endforeach()

        message(STATUS "Configuring and building boost immediately")
        execute_process(
            COMMAND ${CMAKE_CTEST_COMMAND}
                    --build-and-test  ${boost_SOURCE_DIR} ${boost_BINARY_DIR}
                    --build-generator ${CMAKE_GENERATOR} ${generatorArgs}
                    --build-target    install
                    --build-noclean
                    --build-options   ${cacheArgs}
            WORKING_DIRECTORY ${boost_SOURCE_DIR}
            OUTPUT_FILE       ${boost_BINARY_DIR}/build_output.log
            ERROR_FILE        ${boost_BINARY_DIR}/build_output.log
            RESULT_VARIABLE   result
        )
        message(STATUS "boost build complete")
        if(result)
            message(FATAL_ERROR "Failed boost build, see build log at:\n"
                "    ${boost_BINARY_DIR}/build_output.log")
        endif()

    endif()
    set(CMAKE_PREFIX_PATH ${boost_BINARY_DIR}/install)
endif()

# Confirm we can find Boost. If this is the first time we've
# tried to find it in this build dir, this call will force the
# location of each Boost library to be saved in the cache, so
# later calls elsewhere in the project will find the same ones.
find_package(Boost QUIET REQUIRED COMPONENTS ${BOOST_LIBS})

Instead of add_subdirectory this solution uses execute_process where we can pass and set CACHE variables explicitly.
@craig.scott, do you think this is a good solution for my case of different CMAKE_CONFIGURATION_TYPES between projects, or you think there a simpler solution? Thanks!

I will reply in parts to keep the discussion threads a bit more manageable. I’ll reply here first to address some points in the example, see the other reply coming later for more direct discussion of your query.

The first thing to clear up is that you should think of CMAKE_BUILD_TYPE and CMAKE_CONFIGURATION_TYPES as separate things and only one of the two is relevant for a given build. For single configuration generators (e.g. Unix Makefiles or Ninja), only CMAKE_BUILD_TYPE is relevant. CMAKE_CONFIGURATION_TYPES is ignored and indeed should be left unset. For multi-config generators (e.g. Xcode and Visual Studio), only CMAKE_CONFIGURATION_TYPES is relevant and CMAKE_BUILD_TYPE will be ignored.

In your situation, it sounds like you are only having difficulties with CMAKE_CONFIGURATION_TYPES, so you should be leaving CMAKE_BUILD_TYPE alone. Even if this were not the case, the code in your example would result in CMAKE_BUILD_TYPE getting hidden in the CMake cache because it changes the variable type to INTERNAL. This should never be done, it is meant to be under developer control. You should also not try to mix different values of CMAKE_BUILD_TYPE within one build, it will likely result in inconsistencies in the build, if not build failure.

CMAKE_CONFIGURATION_TYPES represents a set of defined build types that are global to the whole build. You can’t have different sets of types for different parts of your build. It maps to a single combo box in Visual Studio and to something similar in the schemes used in Xcode. The user selects one of these values for the entire build.

In your case, you can potentially discard the configuration types defined by the third party project, or you could merge them into those expected by the main project. It depends on what the third party project tries to do with this information. If you really do need to allow the third party project to have its own set of configuration types, then you have to isolate it from the main build. That means doing its build off to the side with something like ExternalProject_Add() instead of incorporating it directly into the main build with add_subdirectory().

The sample code of mine for boost that you referenced is somewhat of a special case. Boost is complex and it’s still a moving target in terms of what it defines, how libraries are named, how distributions package it up, etc. You want to leave as much handling those differences as you can to boost’s own build and perhaps CMake’s own FindBoost module. My sample code tries to do that but still is pretty complex and doesn’t necessarily work with all platforms/generators. If your third party project has more predictable build artefacts, then it may be simpler to use ExternalProject_Add() and manually create imported targets to represent the build artefacts you want to use from it. It’s not a path I normally recommend, but it can be made to work in some situations. A more traditional super build would probably be more advisable (where your main project would also become a sub-build brought in vial ExternalProject_Add()), but that comes with its own disadvantages. You will need to investigate and see which choices offer the set of compromises you are most happy with.

Thanks @craig.scott for your great responses.

The differences of CMAKE_BUILD_TYPE and CMAKE_CONFIGURATION_TYPES are now clear.

This makes impossible to use add_subdirectories (when using FetchContent or git submodules) with projects with different CMAKE_CONFIGURATION_TYPES. Especially when that project has more complicated logic depending on the configuration.
Can a map/dictionary be created in a project with non-default CONFIGURATION_TYPES to reconciliate it with the default types?

# Config dictionary at top of project with type1, type2 (non-default) CONFIGURATION_TYPES
# to map default configs to this project.
Release -> type1
Debug -> type2
RelWithDebInfo -> type3
MinSizeRel-> type1 + some extra flags.

I am not familiar with CMake codebase, but I guess CONFIGURATION related variable with per project scope, CONFIG_PROJECT, CMAKE_PROJECT_CONFIGURATION_TYPES can help to achieve this mapping.

In my case, I would need to go the route of ExternalProject, or fork and change third-party codebase to make it compatible with the CMake defaults. The problem is that the code was open-sourced recently and has configurations internal to that company (used in their mono repos or whatever), so it’s going to make future changes challenging if I change it.

If you bring in the third party project with ExternalProject_Add(), you can map its configurations to yours using the MAP_IMPORTED_CONFIG_<CONFIG> target property (you’d probably use the CMAKE_MAP_IMPORTED_CONFIG_<CONFIG> variable to do it globally for your build). It requires you to define imported targets for each of the targets you wanted to bring into your build from the third party project, but you would probably have to do that anyway.

Sorry if that’s all a bit brief, but hopefully it points you in the right direction for your own further investigations.

Thanks @craig.scott, it helps, it would be great to extend that configuration mapping on imported targets to allow add_subdirectories to work.

I am posting what I am using right now, and it seems working. Kind of an hybrid approach, an external project built at configuration time (without add_subdirectory) where I can set the CONFIGURATION_TYPE of this project with a CACHE option in my own project. In case it helps anybody.

option(FETCH_PROJECTTHIRD "Build project_thrid at configure time." ON)
if(FETCH_PROJECTTHIRD)
  # Fetch project_third
  include(FetchContent)
  FetchContent_Declare(
    project_third
    GIT_REPOSITORY https://github.com/phcerdan/project_third
    GIT_TAG cmake_for_easier_integration
  )
  FetchContent_GetProperties(project_third)
  if(NOT project_third_POPULATED)
    message(STATUS "Populating project_third...")
    FetchContent_Populate(project_third)
    message(STATUS "Configuring project_third...")
    execute_process(
      COMMAND ${CMAKE_COMMAND}
        -S ${project_third_SOURCE_DIR}
        -B ${project_third_BINARY_DIR}
        -DCMAKE_GENERATOR=${CMAKE_GENERATOR}
        -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
      WORKING_DIRECTORY ${project_third_BINARY_DIR}
      COMMAND_ECHO STDOUT
      # OUTPUT_FILE       ${project_third_BINARY_DIR}/configure_output.log
      # ERROR_FILE        ${project_third_BINARY_DIR}/configure_output.log
      RESULT_VARIABLE   result_config
      )
    if(result_config)
        message(FATAL_ERROR "Failed project_third configuration")
        # see configuration log at:\n    ${project_third_BINARY_DIR}/configure_output.log")
    endif()
    # Right now, project_third is always on release mode, but can be explicitly changed by user:
    set(project_third_CONFIG_TYPE "release" CACHE INTERNAL "Config/build type for project_third")
    message(STATUS "Building project_third... with CONFIG: ${project_third_CONFIG_TYPE}")
    execute_process(
      COMMAND ${CMAKE_COMMAND}
      --build ${project_third_BINARY_DIR}
      --config ${project_third_CONFIG_TYPE}
      WORKING_DIRECTORY ${project_third_BINARY_DIR}
      COMMAND_ECHO STDOUT
      # OUTPUT_FILE       ${project_third_BINARY_DIR}/build_output.log
      # ERROR_FILE        ${project_third_BINARY_DIR}/build_output.log
      RESULT_VARIABLE   result_build
      )
    message(STATUS "project_third build complete")
    if(result_build)
        message(FATAL_ERROR "Failed project_third build")
        # see build log at:\n    ${project_third_BINARY_DIR}/build_output.log")
    endif()
  endif()
  # create rule to install project_third when installing this project
  install (CODE "
  execute_process(
    COMMAND ${CMAKE_COMMAND}
    --build ${project_third_BINARY_DIR}
    --config ${project_third_CONFIG_TYPE}
    --target install
    WORKING_DIRECTORY ${project_third_BINARY_DIR}
    COMMAND_ECHO STDOUT
    )")
  # find_package works
  find_package(project_third REQUIRED
    PATHS ${project_third_BINARY_DIR}
    NO_DEFAULT_PATH
    )
endif()