check_required_components logics

Hi,

I’m not sure to understand the logics of check_required_components macro generated by configure_package_config_file of CMakePackageConfigHelpers package. It reads as follows:

macro(check_required_components _NAME)
  foreach(comp ${${_NAME}_FIND_COMPONENTS})
    if(NOT ${_NAME}_${comp}_FOUND)
      if(${_NAME}_FIND_REQUIRED_${comp})
        set(${_NAME}_FOUND FALSE)
      endif()
    endif()
  endforeach()
endmacro()

This has the effect of setting <package>_FOUND to FALSE when one of its components is found! Shouldn’t line if(${_NAME}_FIND_REQUIRED_${comp}) rather reads as if(NOT ${_NAME}_FIND_REQUIRED_${comp}) so that <package>_FOUND is set to FALSE when one of its components is missing?

I’ve read the documentation Adding Components where I can see that generation of check_required_components macro is explicitely disabled by flag NO_CHECK_REQUIRED_COMPONENTS_MACRO in configure_package_config_file call, but I’m not getting the point of the original check_required_components logics, then :thinking:

The line before the one you quoted explicitly tests if the component is NOT found. What the combined logic is saying is that if a component is not found, and it is a required component, the package as a whole is considered not found.

1 Like

Indeed, thanks!

But playing with the examples/more/components example in this test repo that I’ve found reading some posts here, I’m thus not getting the point.

Author created a phrases package with greetings and farewells components. The library/phrases-config.cmake.in file doesn’t check the package’s components and simply builds out a list of the packages’s components:

foreach(component ${@PROJECT_NAME@_FIND_COMPONENTS})
    include(${CMAKE_CURRENT_LIST_DIR}/${component}-config.cmake)
endforeach()

In the consuming application, the application/CMakeLists.txt looks for the phrases package, requiring the greetings and farewells components:

find_package(phrases REQUIRED CONFIG COMPONENTS greetings farewells)

So far, so good.

Now, I want to go one step further and use CMake’s built-in features to generate and check the phrases package’s config file. To this end, in library/CMakeLists.txt, the call to configure_file is replaced by:

include(CMakePackageConfigHelpers)

configure_package_config_file(${PROJECT_NAME}-config.cmake.in ${PROJECT_NAME}-config.cmake
                              INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})

And library/phrases-config.cmake.in file is updated to add check of the package’s components:

@PACKAGE_INIT@

foreach(component ${@PROJECT_NAME@_FIND_COMPONENTS})
    include(${CMAKE_CURRENT_LIST_DIR}/${component}-config.cmake)
endforeach()

check_required_components(@PROJECT_NAME@)

With these changes, the consuming application now complains that the phrases package is considered to be NOT FOUND:

CMake Error at CMakeLists.txt:4 (find_package):
  Found package configuration file:

    E:/dev/cmake-examples/examples/more/components/library/install/lib/cmake/phrases/phrases-config.cmake

  but it set phrases_FOUND to FALSE so package "phrases" is considered to be
  NOT FOUND.

Is it expected behaviour? Hence my initial post…

@tomhh may be better placed to answer your questions about what was done in his repo and why.

Thanks for the tag @craig.scott and interesting problem @emaschino.

I’m afraid I created this example some time ago so I’m just trying to refresh my memory :sweat_smile: I’m away from my machine at the moment and will try and check later this weekend, but in the meantime maybe try printing out all the CMake variables which might tell you more about what’s going on. You can do this with something like…

function(list_cmake_variables)
  get_cmake_property(variable_names VARIABLES)
  foreach(variable_name ${variable_names})
    message(STATUS "${variable_name}=${${variable_name}}")
  endforeach()
endfunction()

My bet is the name used in check_required_components is maybe wrong, though I’m afraid I’m not sure. I’ll try and get back to this and have another look before the end of the weekend :+1:

I had a little look into this and I’m afraid I can’t really see how check_required_components is meant to work in this context either…

From what I can tell, the way check_required_components is generated won’t work because of how it looks for the component variables (${comp}_FOUND). These don’t seem to exist when looking at all variables (only phrases_FOUND=1 is set).

I found this reference which makes more sense and is essentially a manual version of writing the check_required_components macro.

For the phrases example, the phrases-config.cmake.in might look something like this:

@PACKAGE_INIT@

set(_phrases_supported_components greetings farewells)

foreach(component ${@PROJECT_NAME@_FIND_COMPONENTS})
  if (NOT ${component} IN_LIST _phrases_supported_components)
    set(phrases_FOUND False)
    set(phrases_NOT_FOUND_MESSAGE "Unsupported component: ${component}")
  else()
    include(${CMAKE_CURRENT_LIST_DIR}/${component}-config.cmake)
  endif()
endforeach()

Then from the application CMakeLists.txt, if file you call find_package with chitchat (non-existing component):

find_package(phrases REQUIRED CONFIG COMPONENTS greetings farewells chitchat)

You get the error:

CMake Error at CMakeLists.txt:4 (find_package):
  Found package configuration file:

    /path/to/cmake-example/examples/more/components/library/install/lib/cmake/phrases/phrases-config.cmake

  but it set phrases_FOUND to FALSE so package "phrases" is considered to be
  NOT FOUND.  Reason given by package:

  Unsupported component: chitchat

The thing is, if you just try and pass a component that doesn’t exist with the existing code you get:

CMake Error at path/to/cmake-example/examples/more/components/library/install/lib/cmake/phrases/phrases-config.cmake:2 (include):
  include could not find requested file:

    path/to/cmake-example/examples/more/components/library/install/lib/cmake/phrases/chitchat-config.cmake
Call Stack (most recent call first):
  CMakeLists.txt:4 (find_package)

Which maybe isn’t quite as informative, but does give a clue the problem is with chitchat.

I also hope @craig.scott doesn’t mind me referencing a section from his book, Professional CMake, but on page 527 of the 16th edition there’s this section (emphasis mine):

In the past when all details about a package were provided through variables, it was customary to check whether all required variables were set at the end of the config file before returning. A macro called check_required_components() was defined for this purpose, but projects that provide imported targets should perform these checks themselves. The imported targets should only be created if all required components will be found. Otherwise, a failed find_package() call will still leave behind targets, which would interfere with any later call to find_package() for the same package name but with different arguments (e.g. to search in different locations).) This makes the check_required_components() macro largely redundant.

I hope this helps, and please let me know if I missed something.

FYI I’ve also updated the repo with some improvements after this investigation, added here for visibility - Component example improvement by pr0g · Pull Request #12 · pr0g/cmake-examples · GitHub.

Any suggestions on fixes/improvements let me know, I’ll merge tomorrow most likely.

Also this whole experience reminds me of this xkcd :sweat_smile:

Thanks @craig.scott and @tomhh for taking the time to look at this. I ended up with a solution similar to your PR that doesn’t make use of check_required_components.

1 Like