How is ExternalProject_Add's BINARY_DIR meant to be set with CMAKE_CROSS_CONFIGS + CMAKE_DEFAULT_CONFIGS == all

Consider the following short project:

cmake_minimum_required(VERSION 3.16)
set(CMAKE_CONFIGURATION_TYPES "RelWithDebInfo;Debug")
set(CMAKE_DEFAULT_BUILD_TYPE "RelWithDebInfo")
set(CMAKE_DEFAULT_CONFIGS "all")
set(CMAKE_CROSS_CONFIGS "all")

project(ExternalProjectExample NONE)
include(ExternalProject)

# Assignment of config specific binary dir that causes build failure
set(ep_binary_dir    "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/mysub")

ExternalProject_Add(
  mysub
  #BINARY_DIR       "${ep_binary_dir}"
  SOURCE_DIR       ${CMAKE_CURRENT_SOURCE_DIR}/mysub
  INSTALL_COMMAND  ""
)
ExternalProject_Get_Property(mysub BINARY_DIR)
message(">> BINARY_DIR ${BINARY_DIR}")

# Workaround
#set_property(
    #TARGET "mysub"
    #PROPERTY EXCLUDE_FROM_ALL "$<NOT:$<CONFIG:RelWithDebInfo>>")

So it’s a multi-config cross-config project building one EP. Running ninja will configure and build the EP in both configurations, RelWithDebInfo and Debug.

The external project it configures

# $ cat mysub/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(mysub LANGUAGES CXX)
add_executable(app main.cpp)
// $ cat main.cpp
int main() {return 0;}

If you run ninja after configuring with cmake .. -G"Ninja Multi-Config"
then CMake will configure the external project twice in the exact same build directory, overriding CMakeCache.txt and other files.

...
cd /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/build/special/RelWithDebInfo && /usr/local/Cellar/cmake/3.26.4/bin/cmake "-GNinja Multi-Config" -S /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/mysub -B /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/build/special/RelWithDebInfo && /usr/local/Cellar/cmake/3.26.4/bin/cmake -E touch /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/build/mysub-prefix/src/mysub-stamp/RelWithDebInfo/mysub-configure
...
cd /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/build/special/Debug && /usr/local/Cellar/cmake/3.26.4/bin/cmake "-GNinja Multi-Config" -S /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/mysub -B /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/build/special/Debug && /usr/local/Cellar/cmake/3.26.4/bin/cmake -E touch /Volumes/T3/Dev/projects/cmake/general/external_project_multi_config/build/mysub-prefix/src/mysub-stamp/Debug/mysub-configure

This is because BINARY_DIR does not contain any config-specific subdirectory, despite other metadata files (timestamp files) being config-specific.
This happens starting with CMake 3.24.0 up to 3.27.1.

If I change the BINARY_DIR to contain a $<CONFIG> subdirectory, then at build time ninja fails with

/bin/sh: line 0: cd: external_project_multi_config/build/RelWithDebInfo/mysub: No such file or directory

I can work around the issue of the overridden CMakeCache.txt by explicitly excluding the Debug configuration from ALL, but that is not entirely safe, because one could still run ninja mysub:Debug explicitly and override the files.

I can also manually pre-create the subdirectories with something like:

set(ep_binary_dir    "${CMAKE_CURRENT_BINARY_DIR}/mysub/$<CONFIG>")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/mysub/RelWithDebInfo")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/mysub/Debug")

And the build appears to work, but i also end up with an actuall directory called \$\<CONFIG\> on the file system.

So what’s the intended way to assign unique BINARY_DIRs for each config in such a case? Is it just not possible with the current release CMake versions?

For multi-config generators, I’ve always just assumed there is still only a single configure step, and that there is a single <BINARY_DIR>. The build step is the part that would pass through the chosen build configuration, and the config-specific build artifacts would be in some config-specific location under the common <BINARY_DIR> (or have different config-specific names). I don’t think there’s separate time stamps for each config for the configure step, but I’m going from memory and haven’t checked what the code actually does. I don’t know if there’s config-specific time stamps for the build step, but it’s hard to imagine how it could work if it didn’t.

Your observation that CMake configures the external project twice is surprising (to me, at least). There may be something about the way the Ninja Multi-Config generator handles things that I’m not considering here. @kyle.edwards might have some thoughts on that.

My thinking is that this is not a supported use case. But again, I’m just going from memory here.

1 Like

Filed https://gitlab.kitware.com/cmake/cmake/-/issues/25160