From 4ee44fa981dec07de88ae22e9e0ceccc221a1e3b Mon Sep 17 00:00:00 2001 From: antoinemeyer5 Date: Tue, 29 Sep 2026 18:48:00 +0200 Subject: [PATCH 1/4] #2528: CMake to disable fmt for each library when necessary to avoid duplicate symbols --- cmake/link_vt.cmake | 12 +- cmake/load_bundled_libraries.cmake | 22 +++- cmake/setup_fmt.cmake | 169 +++++++++++++++++++++++++++++ cmake/trace_only_functions.cmake | 4 +- cmake/vtConfig.cmake.in | 2 +- src/CMakeLists.txt | 4 +- 6 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 cmake/setup_fmt.cmake diff --git a/cmake/link_vt.cmake b/cmake/link_vt.cmake index 1fb71103be..16e3ebd636 100644 --- a/cmake/link_vt.cmake +++ b/cmake/link_vt.cmake @@ -183,15 +183,9 @@ function(link_target_with_vt) message(STATUS "link_target_with_vt: fmt=${ARG_LINK_FMT}") endif() - if(${vt_external_fmt}) - target_link_libraries( - ${ARG_TARGET} PUBLIC ${ARG_BUILD_TYPE} fmt::fmt - ) - else() - target_link_libraries( - ${ARG_TARGET} PUBLIC ${ARG_BUILD_TYPE} ${FMT_LIBRARY} - ) - endif() + target_link_libraries( + ${ARG_TARGET} PUBLIC ${ARG_BUILD_TYPE} ${FMT_LINK_TARGET} + ) endif() diff --git a/cmake/load_bundled_libraries.cmake b/cmake/load_bundled_libraries.cmake index 26658dff73..c555e63150 100644 --- a/cmake/load_bundled_libraries.cmake +++ b/cmake/load_bundled_libraries.cmake @@ -20,7 +20,7 @@ endif() # CLI11 always included in the build add_subdirectory(${PROJECT_LIB_DIR}/CLI) -# use included fmt or external one +# Use an external fmt package or configure VT's shared DARMA fmt provider. if(${vt_external_fmt}) # user should provide 'fmt_DIR' or 'fmt_ROOT' to CMake (unless fmt is installed in system libs) if(fmt_ROOT) @@ -31,11 +31,23 @@ if(${vt_external_fmt}) message(STATUS "vt_external_fmt = ON but neither fmt_DIR nor fmt_ROOT is provided!") endif() find_package(fmt 10.2.1 REQUIRED) - + set(FMT_LIBRARY "") + set(FMT_LINK_TARGET fmt::fmt) + set(vt_uses_parent_fmt 0) else() - set(FMT_LIBRARY fmt) - add_subdirectory(${PROJECT_LIB_DIR}/fmt) - set_darma_compiler_flags(${FMT_LIBRARY}) + include(setup_fmt) + darma_setup_fmt( + SOURCE_DIR "${PROJECT_LIB_DIR}/fmt" + VENDORED_TARGET fmt + OUT_LIBRARY FMT_LIBRARY + OUT_LINK_TARGET FMT_LINK_TARGET + POST_ADD_COMMAND set_darma_compiler_flags + ) + if(FMT_LIBRARY) + set(vt_uses_parent_fmt 0) + else() + set(vt_uses_parent_fmt 1) + endif() endif() # yaml-cpp always included in the build diff --git a/cmake/setup_fmt.cmake b/cmake/setup_fmt.cmake new file mode 100644 index 0000000000..b16d7ac8bf --- /dev/null +++ b/cmake/setup_fmt.cmake @@ -0,0 +1,169 @@ +include_guard(GLOBAL) + +# Configure one compiled fmt provider across a hierarchy of DARMA projects. +# +# A top-level project builds its vendored fmt. An embedded project reuses the +# parent's fmt::fmt target and only contributes its local include layout (for +# example fmt-vt, fmt, or fmt-lb). Exact FMT_VERSION equality is required so +# local headers never call into an incompatible compiled fmt implementation. +# +# Copy this file into each repository and replace add_subdirectory(fmt) with: +# +# include(cmake/setup_fmt.cmake) +# darma_setup_fmt( +# SOURCE_DIR lib/fmt +# VENDORED_TARGET fmt +# OUT_LIBRARY FMT_LIBRARY +# OUT_LINK_TARGET FMT_LINK_TARGET +# ) +# +# VT, comm, and loc use lib/fmt + fmt; LB uses tpl/fmt + fmt-lb. +# Optional POST_ADD_COMMAND names a project function which is called only when +# the current project owns and compiles the vendored target. +function(darma_setup_fmt) + cmake_parse_arguments( + PARSE_ARGV 0 + DARMA_FMT + "" + "SOURCE_DIR;VENDORED_TARGET;OUT_LIBRARY;OUT_LINK_TARGET;POST_ADD_COMMAND" + "" + ) + + foreach(required_arg + SOURCE_DIR VENDORED_TARGET OUT_LIBRARY OUT_LINK_TARGET) + if(NOT DARMA_FMT_${required_arg}) + message(FATAL_ERROR "darma_setup_fmt requires ${required_arg}") + endif() + endforeach() + + if(IS_ABSOLUTE "${DARMA_FMT_SOURCE_DIR}") + set(fmt_source_dir "${DARMA_FMT_SOURCE_DIR}") + else() + get_filename_component( + fmt_source_dir + "${DARMA_FMT_SOURCE_DIR}" + ABSOLUTE + BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" + ) + endif() + + file(GLOB fmt_base_headers "${fmt_source_dir}/include/*/base.h") + list(LENGTH fmt_base_headers fmt_base_header_count) + if(NOT fmt_base_header_count EQUAL 1) + message( + FATAL_ERROR + "Expected one fmt base.h below ${fmt_source_dir}/include, found " + "${fmt_base_header_count}" + ) + endif() + + list(GET fmt_base_headers 0 fmt_base_header) + file(STRINGS + "${fmt_base_header}" + fmt_version_line + REGEX "^# *define FMT_VERSION [0-9]+" + LIMIT_COUNT 1 + ) + if(NOT fmt_version_line MATCHES "FMT_VERSION +([0-9]+)") + message(FATAL_ERROR "Could not read FMT_VERSION from ${fmt_base_header}") + endif() + set(local_fmt_version "${CMAKE_MATCH_1}") + + # Use the source-directory comparison rather than PROJECT_IS_TOP_LEVEL so + # the same module also works with LB's current CMake 3.20 minimum. + if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + if(TARGET fmt::fmt) + message( + FATAL_ERROR + "${PROJECT_NAME} is top-level, but fmt::fmt already exists. The " + "top-level DARMA project must be the sole compiled fmt provider." + ) + endif() + + add_subdirectory("${fmt_source_dir}") + + if(NOT TARGET ${DARMA_FMT_VENDORED_TARGET}) + message( + FATAL_ERROR + "${fmt_source_dir} did not create target " + "'${DARMA_FMT_VENDORED_TARGET}'" + ) + endif() + # VT and comm already expose fmt::fmt. LB's renamed vendored target is + # fmt-lb, so supply the common provider alias here when it is absent. + if(NOT TARGET fmt::fmt) + add_library(fmt::fmt ALIAS ${DARMA_FMT_VENDORED_TARGET}) + endif() + + if(DARMA_FMT_POST_ADD_COMMAND) + if(NOT COMMAND ${DARMA_FMT_POST_ADD_COMMAND}) + message( + FATAL_ERROR + "Unknown POST_ADD_COMMAND: ${DARMA_FMT_POST_ADD_COMMAND}" + ) + endif() + cmake_language( + CALL ${DARMA_FMT_POST_ADD_COMMAND} ${DARMA_FMT_VENDORED_TARGET} + ) + endif() + + set_property( + TARGET ${DARMA_FMT_VENDORED_TARGET} + PROPERTY INTERFACE_DARMA_FMT_VERSION "${local_fmt_version}" + ) + set(fmt_owned_library "${DARMA_FMT_VENDORED_TARGET}") + message( + STATUS + "${PROJECT_NAME}: building vendored fmt ${local_fmt_version}" + ) + else() + if(NOT TARGET fmt::fmt) + message( + FATAL_ERROR + "${PROJECT_NAME} is embedded, but its parent did not provide " + "fmt::fmt. Configure fmt before add_subdirectory(${PROJECT_NAME})." + ) + endif() + + get_target_property(fmt_provider_target fmt::fmt ALIASED_TARGET) + if(NOT fmt_provider_target) + set(fmt_provider_target fmt::fmt) + endif() + get_target_property( + parent_fmt_version + ${fmt_provider_target} + INTERFACE_DARMA_FMT_VERSION + ) + if(NOT parent_fmt_version) + message( + FATAL_ERROR + "The parent fmt::fmt target was not configured by setup_fmt.cmake; " + "its version cannot be verified." + ) + endif() + if(NOT local_fmt_version STREQUAL parent_fmt_version) + message( + FATAL_ERROR + "fmt version mismatch: ${PROJECT_NAME} has ${local_fmt_version}, " + "but the parent provider has ${parent_fmt_version}." + ) + endif() + + # The projects intentionally use different include directory names while + # sharing fmt's ABI. Make this project's headers visible through the one + # parent provider without installing or compiling another fmt library. + target_include_directories( + ${fmt_provider_target} INTERFACE + $ + ) + + set(fmt_owned_library "") + message( + STATUS + "${PROJECT_NAME}: reusing parent fmt ${parent_fmt_version}" + ) + endif() + + set(${DARMA_FMT_OUT_LIBRARY} "${fmt_owned_library}" PARENT_SCOPE) + set(${DARMA_FMT_OUT_LINK_TARGET} fmt::fmt PARENT_SCOPE) +endfunction() diff --git a/cmake/trace_only_functions.cmake b/cmake/trace_only_functions.cmake index 8584aa652f..4cabf84356 100644 --- a/cmake/trace_only_functions.cmake +++ b/cmake/trace_only_functions.cmake @@ -162,7 +162,9 @@ function(create_trace_only_target) CONFIGURATIONS ${build_type_list} ) - install(TARGETS ${FMT_LIBRARY} EXPORT ${VT_TRACE_LIB}) + if(FMT_LIBRARY) + install(TARGETS ${FMT_LIBRARY} EXPORT ${VT_TRACE_LIB}) + endif() export( TARGETS ${VT_TRACE_LIB} diff --git a/cmake/vtConfig.cmake.in b/cmake/vtConfig.cmake.in index bb68c85a20..cc1c6c52e7 100644 --- a/cmake/vtConfig.cmake.in +++ b/cmake/vtConfig.cmake.in @@ -27,7 +27,7 @@ endif() set (VT_INCLUDE_DIRS "@CMAKE_INSTALL_PREFIX@/include") set (VT_CXX_COMPILER "@CMAKE_CXX_COMPILER@") -if (@vt_external_fmt@) +if (@vt_external_fmt@ OR @vt_uses_parent_fmt@) set (fmt_DIR @fmt_DIR@) set (fmt_ROOT @fmt_ROOT@) find_dependency(fmt REQUIRED HINTS @fmt_DIR@ @fmt_ROOT@) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6572399cae..6f81975a8d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -300,7 +300,9 @@ install( install(TARGETS ${BROTLI_LIBRARY} EXPORT ${VIRTUAL_TRANSPORT_LIBRARY}) install(TARGETS ${ENG_FORMAT_LIBRARY} EXPORT ${VIRTUAL_TRANSPORT_LIBRARY}) -install(TARGETS ${FMT_LIBRARY} EXPORT ${VIRTUAL_TRANSPORT_LIBRARY}) +if(FMT_LIBRARY) + install(TARGETS ${FMT_LIBRARY} EXPORT ${VIRTUAL_TRANSPORT_LIBRARY}) +endif() install(TARGETS ${YAMLCPP_LIBRARY} EXPORT ${VIRTUAL_TRANSPORT_LIBRARY}) install(TARGETS ${JSON_LIBRARY} EXPORT ${VIRTUAL_TRANSPORT_LIBRARY}) From 845c2cb2e13867b8444116da6b70191d78c67fba Mon Sep 17 00:00:00 2001 From: antoinemeyer5 Date: Wed, 30 Sep 2026 10:01:30 +0200 Subject: [PATCH 2/4] #2528: fix common file --- cmake/setup_fmt.cmake | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/cmake/setup_fmt.cmake b/cmake/setup_fmt.cmake index b16d7ac8bf..9aa49c318e 100644 --- a/cmake/setup_fmt.cmake +++ b/cmake/setup_fmt.cmake @@ -2,10 +2,10 @@ include_guard(GLOBAL) # Configure one compiled fmt provider across a hierarchy of DARMA projects. # -# A top-level project builds its vendored fmt. An embedded project reuses the -# parent's fmt::fmt target and only contributes its local include layout (for -# example fmt-vt, fmt, or fmt-lb). Exact FMT_VERSION equality is required so -# local headers never call into an incompatible compiled fmt implementation. +# The first project in a hierarchy builds its vendored fmt. Later projects +# reuse that fmt::fmt target and only contribute their local include layout +# (for example fmt-vt, fmt, or fmt-lb). Exact FMT_VERSION equality is required +# so local headers never call into an incompatible compiled fmt implementation. # # Copy this file into each repository and replace add_subdirectory(fmt) with: # @@ -69,17 +69,10 @@ function(darma_setup_fmt) endif() set(local_fmt_version "${CMAKE_MATCH_1}") - # Use the source-directory comparison rather than PROJECT_IS_TOP_LEVEL so - # the same module also works with LB's current CMake 3.20 minimum. - if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) - if(TARGET fmt::fmt) - message( - FATAL_ERROR - "${PROJECT_NAME} is top-level, but fmt::fmt already exists. The " - "top-level DARMA project must be the sole compiled fmt provider." - ) - endif() - + # The first DARMA project in the CMake hierarchy owns fmt. It need not be + # the top-level CMake project: consumers commonly add VT as a subdirectory + # without configuring fmt themselves (for example vt-sample-project). + if(NOT TARGET fmt::fmt) add_subdirectory("${fmt_source_dir}") if(NOT TARGET ${DARMA_FMT_VENDORED_TARGET}) @@ -117,14 +110,6 @@ function(darma_setup_fmt) "${PROJECT_NAME}: building vendored fmt ${local_fmt_version}" ) else() - if(NOT TARGET fmt::fmt) - message( - FATAL_ERROR - "${PROJECT_NAME} is embedded, but its parent did not provide " - "fmt::fmt. Configure fmt before add_subdirectory(${PROJECT_NAME})." - ) - endif() - get_target_property(fmt_provider_target fmt::fmt ALIASED_TARGET) if(NOT fmt_provider_target) set(fmt_provider_target fmt::fmt) From ab96f020ca0d057cdd80bf282f3a75f08e413b60 Mon Sep 17 00:00:00 2001 From: antoinemeyer5 Date: Wed, 30 Sep 2026 10:19:59 +0200 Subject: [PATCH 3/4] #2528: fix common file --- cmake/setup_fmt.cmake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmake/setup_fmt.cmake b/cmake/setup_fmt.cmake index 9aa49c318e..d1a0670b7a 100644 --- a/cmake/setup_fmt.cmake +++ b/cmake/setup_fmt.cmake @@ -73,6 +73,10 @@ function(darma_setup_fmt) # the top-level CMake project: consumers commonly add VT as a subdirectory # without configuring fmt themselves (for example vt-sample-project). if(NOT TARGET fmt::fmt) + # fmt is compiled as ordinary C++20 and its module target is disabled by + # the caller. Prevent CMP0155 from requiring a compiler dependency scanner + # (for example clang-scan-deps) for these non-module sources. + set(CMAKE_CXX_SCAN_FOR_MODULES OFF) add_subdirectory("${fmt_source_dir}") if(NOT TARGET ${DARMA_FMT_VENDORED_TARGET}) From 4ee7f6f4b58a2005b0435cbf3a46f92fe7d8f345 Mon Sep 17 00:00:00 2001 From: antoinemeyer5 Date: Wed, 14 Oct 2026 10:36:42 +0200 Subject: [PATCH 4/4] #2528: Install local fmt headers when reusing a parent provider --- cmake/setup_fmt.cmake | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cmake/setup_fmt.cmake b/cmake/setup_fmt.cmake index d1a0670b7a..fdc8429144 100644 --- a/cmake/setup_fmt.cmake +++ b/cmake/setup_fmt.cmake @@ -146,6 +146,22 @@ function(darma_setup_fmt) $ ) + # The provider may install a different header layout (for example fmt-lb). + # Consumers of this project's installed headers still need its local fmt + # include names, even though they link to the parent's compiled library. + if(FMT_INC_DIR) + set(fmt_install_include_dir "${FMT_INC_DIR}") + else() + include(GNUInstallDirs) + set(fmt_install_include_dir "${CMAKE_INSTALL_INCLUDEDIR}") + endif() + install( + DIRECTORY "${fmt_source_dir}/include/" + DESTINATION "${fmt_install_include_dir}" + COMPONENT fmt-core + FILES_MATCHING PATTERN "*.h" + ) + set(fmt_owned_library "") message( STATUS