How to install multiple FILE_SET for a library in a generic way?

With this CMake code snippet from boost/tools/cmake I can install i.e Boost::any lib with HEADERS and CXX_MODULES.

But it fails, if there is more than one FILE_SET used in a library target!

  if(NOT CMAKE_VERSION VERSION_LESS 3.28)
    get_target_property(INTERFACE_CXX_MODULE_SETS ${LIB} INTERFACE_CXX_MODULE_SETS)
    if(INTERFACE_CXX_MODULE_SETS)
      boost_message(DEBUG "boost_install_target: '${__TARGET}' has INTERFACE_CXX_MODULE_SETS=${INTERFACE_CXX_MODULE_SETS}")
      set(__INSTALL_CXX_MODULES FILE_SET ${INTERFACE_CXX_MODULE_SETS} DESTINATION ${CMAKE_INSTALL_DATADIR})
    endif()
  endif()

  if(NOT CMAKE_VERSION VERSION_LESS 3.23)
    get_target_property(INTERFACE_HEADER_SETS ${LIB} INTERFACE_HEADER_SETS)
    if(INTERFACE_HEADER_SETS)
      boost_message(DEBUG "boost_install_target: '${__TARGET}' has INTERFACE_HEADER_SETS=${INTERFACE_HEADER_SETS}")
      set(__INSTALL_HEADER_SETS FILE_SET ${INTERFACE_HEADER_SETS})
    endif()
  endif()

  install(TARGETS ${LIB} EXPORT ${LIB}-targets
    # explicit destination specification required for 3.13, 3.14 no longer needs it
    RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
    LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
    ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
    PRIVATE_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
    PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
    # explicit needed if used starting with cmake v3.28
    # XXX FILE_SET CXX_MODULES DESTINATION "${CMAKE_INSTALL_DATADIR}"
    ${__INSTALL_CXX_MODULES}
    # Any module files from C++ modules from PUBLIC sources in a file set of type CXX_MODULES will be installed to the given DESTINATION.
    # explicit needed if used starting with cmake v3.23
    # XXX FILE_SET HEADERS
    ${__INSTALL_HEADER_SETS}
  )

cmake --preset release --log-level=TRACE # XXX --fresh
Preset CMake variables:

  CMAKE_BUILD_TYPE="RelWithDebInfo"
  CMAKE_CXX_EXTENSIONS:BOOL="TRUE"
  CMAKE_CXX_SCAN_FOR_MODULES:BOOL="TRUE"
  CMAKE_CXX_STANDARD="23"
  CMAKE_CXX_STANDARD_REQUIRED:BOOL="TRUE"
  CMAKE_EXPORT_COMPILE_COMMANDS:BOOL="TRUE"
  CMAKE_INSTALL_MESSAGE="LAZY"
  CMAKE_INSTALL_PREFIX:PATH="/Users/clausklein/Workspace/cpp/beman-project/execution26/stagedir"
  CMAKE_PREFIX_PATH:STRING="/Users/clausklein/Workspace/cpp/beman-project/execution26/stagedir"
  CMAKE_SKIP_TEST_ALL_DEPENDENCY:BOOL="FALSE"

-- use ccache
-- The CXX compiler identification is GNU 15.2.0
-- Checking whether CXX compiler has -isysroot
-- Checking whether CXX compiler has -isysroot - yes
-- Checking whether CXX compiler supports OSX deployment target flag
-- Checking whether CXX compiler supports OSX deployment target flag - yes
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/local/bin/g++ - skipped
-- Detecting CXX compile features
CMake Warning (dev) at /Users/clausklein/.direnv/python-3.14/lib/python3.14/site-packages/cmake/data/share/cmake-4.2/Modules/Compiler/CMakeCommonCompilerMacros.cmake:248 (cmake_language):
  CMake's support for `import std;` in C++23 and newer is experimental.  It
  is meant only for experimentation and feedback to CMake developers.
Call Stack (most recent call first):
  /Users/clausklein/.direnv/python-3.14/lib/python3.14/site-packages/cmake/data/share/cmake-4.2/Modules/CMakeDetermineCompilerSupport.cmake:113 (cmake_create_cxx_import_std)
  /Users/clausklein/.direnv/python-3.14/lib/python3.14/site-packages/cmake/data/share/cmake-4.2/Modules/CMakeTestCXXCompiler.cmake:83 (CMAKE_DETERMINE_COMPILER_SUPPORT)
  CMakeLists.txt:11 (project)
This warning is for project developers.  Use -Wno-dev to suppress it.

-- Detecting CXX compile features - done
-- CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES=/usr/local/Cellar/gcc/15.2.0/include/c++/15;/usr/local/Cellar/gcc/15.2.0/include/c++/15/x86_64-apple-darwin23;/usr/local/Cellar/gcc/15.2.0/include/c++/15/backward;/usr/local/Cellar/gcc/15.2.0/lib/gcc/current/gcc/x86_64-apple-darwin23/15/include;/usr/local/Cellar/gcc/15.2.0/lib/gcc/current/gcc/x86_64-apple-darwin23/15/include-fixed;/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/usr/include;/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/System/Library/Frameworks
-- BEMAN_USE_STD_MODULE=ON
-- CMAKE_CXX_COMPILER_IMPORT_STD=23;26
-- CMAKE_CXX_MODULE_STD=ON
-- BEMAN_HAS_IMPORT_STD=ON
-- BEMAN_USE_MODULES=ON
-- CMAKE_CXX_SCAN_FOR_MODULES=ON
-- Performing Test COMPILER_HAS_HIDDEN_VISIBILITY
-- Performing Test COMPILER_HAS_HIDDEN_VISIBILITY - Success
-- Performing Test COMPILER_HAS_HIDDEN_INLINE_VISIBILITY
-- Performing Test COMPILER_HAS_HIDDEN_INLINE_VISIBILITY - Success
-- Performing Test COMPILER_HAS_DEPRECATED_ATTR
-- Performing Test COMPILER_HAS_DEPRECATED_ATTR - Success
-- beman-install-library: COMPONENT execution for TARGET 'beman.execution'
-- beman-install-library: 'beman.execution' has INTERFACE_CXX_MODULE_SETS=CXX_MODULES
-- beman-install-library: COMPONENT execution for TARGET 'beman.execution_headers'
-- beman-install-library: 'beman.execution_headers' has INTERFACE_HEADER_SETS=public_headers;detail_headers
CMake Error at cmake/beman-install-library-config.cmake:142 (install):
  install TARGETS given unknown argument "detail_headers".
Call Stack (most recent call first):
  CMakeLists.txt:77 (beman_install_library)


-- Configuring incomplete, errors occurred!

The only working generic solution (today)

You must emit one install(TARGETS) call per file set.
That feels wrong, but it is the only valid approach.

Minimal correct pattern:

get_target_property(header_sets ${target} HEADER_SETS)

foreach(set IN LISTS header_sets)
  install(TARGETS ${target}
    FILE_SET ${set}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
  )
endforeach()

Yes:

Multiple install(TARGETS) calls for the same target are allowed
Each call installs only the referenced file set
CMake merges them internally
This is undocumented, but confirmed by Kitware developers in practice.

But:

It does not work!

CMake Error at cmake/beman-install-library-config.cmake:134 (install):
  install TARGETS target beman.execution_headers is exported but not all of
  its interface file sets are installed
Call Stack (most recent call first):
  CMakeLists.txt:78 (beman_install_library)


CMake Error at cmake/beman-install-library-config.cmake:134 (install):
  install TARGETS target beman.execution_headers is exported but not all of
  its interface file sets are installed
Call Stack (most recent call first):
  CMakeLists.txt:78 (beman_install_library)


CMake Error at cmake/beman-install-library-config.cmake:152 (install):
  install TARGETS target beman.execution_headers is exported but not all of
  its interface file sets are installed
Call Stack (most recent call first):
  CMakeLists.txt:78 (beman_install_library)


-- Configuring incomplete, errors occurred!

It’s not confirmed by me. I don’t know who told you this would work.

Just list all the interface file sets in a single call to install, there is no “generic”.

I know, it was from AI → chatgpt.com :scream:

The Boost project and Beman project need this!

Would it be possible, that instead of sending an error message, CMake install() could do it implicit?

Can you better explain the use case? I don’t understand what’s wrong with listing the relevant file sets.

Both projects use a central infrastructure repo with CMake modules and one to install each library.

I’m aware, that doesn’t explain the use case here. There’s no reason the central infrastructure used by Beman and boost preclude projects from expressing the file sets associated with a given target

My question is how write the needed code?

The error is saying you didn’t install a file set associated with the target.

install(
  TARGETS myTarget
  FILE_SET CXX_MODULES
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/myTarget/cxx-modules
  FILE_SET customFileSet
    DESTINATION ${CMAKE_INSTALL_DATADIR}/myTarget/custom-thing
)

Etc, for each file set you have associated with the target

This is not an option for a central cmake module.

Some targets do not have FILE_SET, other more than one

If a target does not have a file set, the project owning that target will not list any file sets in the project’s call to install(). If a target has more than one file set, the project owning that target will list all the INTERFACE file sets in that project’s call to install().

The person writing the install() command for a given target needs to know what the target is, what artifacts are associated with the target, and where they wish those artifacts to be installed. There is no way around this. That’s why we leave it to the project to call install().

What to install and the destination to install is known.

How would you write the central generic cmake module?

I wouldn’t. In the same way any wrapper of the install() command needs to accept the list of targets from the project calling the wrapper, it also needs to accept the list of file sets.

my_custom_install(
  TARGETS
    myTarget
    myOtherTarget
  FILE_SETS
    CXX_MODULES
    HEADERS
    myCustomFileSet
)

If the file sets which need to be installed are known, list them in the install() command and the error will go away.

I don’t understand how to write the central code?

Then don’t, and have projects use install() directly. Or only wrap the parts you care about and forward the rest of ARGN directly to install() so projects can handle their file sets on their own.