Package-config generated by `install(EXPORT)` is inconsistent with enforcing dependency checking

Introduction:

When installing an export-set using install(EXPORT) a package-config script is generated which, when loaded via a find_package call, creates imported targets and also sets their INTERFACE_LINK_LIBRARIES property.

case 1:

If the INTERFACE_LINK_LIBRARIES property contains dependencies to targets that have been imported (e.g. by a call to find_package) during build-time, then that generated package-config script does not check if these dependency targets exist when it is loaded by find_package.

At the end of the generated package-config you can find the following comment:

# This file does not depend on other imported targets which have
# been exported from the same project but in a separate export set.

case 2:

However, if the INTERFACE_LINK_LIBRARIES contains dependencies to targets that themselves are also built and exported in another export-set, then the generated package-config script checks that these dependency targets already exist and fails the current find_package call if they are not.

At the end of the generated package-config you can find the following code instead of the comment from case 1:

# Make sure the targets which have been exported in some other
# export set exist.
unset(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets)
foreach(_target "dependency1" "dependency2" )
  if(NOT TARGET "${_target}" )
    set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets "${${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets} ${_target}")
  endif()
endforeach()

if(DEFINED ${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets)
  if(CMAKE_FIND_PACKAGE_NAME)
    set( ${CMAKE_FIND_PACKAGE_NAME}_FOUND FALSE)
    set( ${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE "The following imported targets are referenced, but are missing: ${${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets}")
  else()
    message(FATAL_ERROR "The following imported targets are referenced, but are missing: ${${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets}")
  endif()
endif()
unset(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE_targets)

Observation:

Note, the exact wording of the comment in case 1: it talks about targets which have been exported from the same project (but in some other export set).

I can understand this reasoning. Targets from the same project probably have something in common and therefore might be expected to be installed together, especially if they depend on each other.

However, the check in case 2 is always generated for all dependencies that were built together, regardless of the project they belong to.

Hypothesis:

Either the generated comment in case 1 is misleading or the check in case 2 should not be generated in all cases.

Suggestion:

Whether the check from case 2 should be generated for all dependencies (as currently) or only for the ones from the same project, I would prefer to have some mechanism to disable this check.
Maybe some variable that is checked and only if it evaluates to TRUE should the specific check be done.

That would allow to introduce the required dependencies after find_package (successfully) returns which is a valid use-case, especially if find_package is called recursively to find indirect dependencies / components.
Otherwise, the (indirect) dependencies and their order has to be known at the time find_package is called for one (direct) dependency.

Of course, that problem currently only manifests if the check is in the generated package-config, which is the case if all dependencies are built monolithically.

Example for reproduction:

Possibly, I want to use find_package to find some dependency and use the pre-built and already installed one and only fall back to building it myself via add_subdirectory if I am unable to find it.
That would generate different package-config scripts, whether I found a pre-built dependency or had to build it myself:

source1/source1.cpp:

int source1() { return 1; }

source1/source2/source2.cpp:

int source2() { return 2; }

source1/CMakeLists.txt:

cmake_minimum_required( VERSION 3.23 )
project( lib1 VERSION 1.0.0 )

# Prefer pre-built dependency and fall back to building it ourselves.
if (NOT TARGET common::lib2)
    find_package( lib2 )
    if (NOT lib2_FOUND)
        add_subdirectory( source2 )
    endif()
endif()

# Build lib1.
add_library( ${PROJECT_NAME} SHARED )
add_library( common::${PROJECT_NAME} ALIAS ${PROJECT_NAME} )
target_sources( ${PROJECT_NAME} PRIVATE source1.cpp )
target_link_libraries( ${PROJECT_NAME} PUBLIC common::lib2 )

# Install lib1 and its package-config script.
install( TARGETS ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}_exportset
    COMPONENT ${PROJECT_NAME}
)
install( EXPORT ${PROJECT_NAME}_exportset
    DESTINATION lib/cmake/${PROJECT_NAME}-${PROJECT_VERSION}
    NAMESPACE common::
    COMPONENT ${PROJECT_NAME}
    FILE ${PROJECT_NAME}-config.cmake
)

source1/source2/CMakeLists.txt:

cmake_minimum_required( VERSION 3.23 )
project( lib2 VERSION 2.0.0 )

# Build lib2.
add_library( ${PROJECT_NAME} SHARED )
add_library( common::${PROJECT_NAME} ALIAS ${PROJECT_NAME} )
target_sources( ${PROJECT_NAME} PRIVATE source2.cpp )

# Install lib2 and its package-config script.
install( TARGETS ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}_exportset
    COMPONENT ${PROJECT_NAME}
)
install( EXPORT ${PROJECT_NAME}_exportset
    DESTINATION lib/cmake/${PROJECT_NAME}-${PROJECT_VERSION}
    NAMESPACE common::
    COMPONENT ${PROJECT_NAME}
    FILE ${PROJECT_NAME}-config.cmake
)

Build like so:

# Build and install only lib2:
cmake -S source1/source2 -B build2-only && cmake --build build2-only && cmake --install build2-only --component lib2 --prefix installed2-only

# Build and install only lib1, using pre-built lib2:
lib2_DIR=installed2-only cmake -S source1 -B build1-only && cmake --build build1-only && cmake --install build1-only --component lib1 --prefix installed1-only

# Build and install lib1 and lib2:
cmake -S source1 -B build-both && cmake --build build-both && cmake --install build-both --component lib1 --prefix installed-both && cmake --install build-both --component lib1 --prefix installed-both

You will notice that the two package-config scripts for lib1,

  • installed1-only/lib/cmake/lib1-1.0.0/lib1-config.cmake and
  • installed-both/lib/cmake/lib1-1.0.0/lib1-config.cmake

are not identically but only differ in the (absence of) the dependency check.

Mmhhh…, seeing this post now, this probably better belongs into a Gitlab issue!?

The corresponding Gitlab issue is here: https://gitlab.kitware.com/cmake/cmake/-/issues/23507