Proposal: New DEPENDENCIES block scope
Problem Summary
Dependencies providers currently only get to see requests for one dependency at a time. Each find_package()
call has to be handled before they get to see the next potential call to find_package()
in the project. This has a number of consequences, but two of the most important ones are:
- Providers often have to invoke an external tool for each request. The tool may have to repeat a full dependency analysis each time, which can be very inefficient.
- Providers don't get any sort of overall view of the full set of dependencies. Instead, they have to incrementally build up a view of the dependencies, and they might end up in a situation where they make a decision about what version of an earlier dependency to provide, only to find out later that it clashes with a later dependency request.
Proposal
Projects are often able to collect together their find_package()
and FetchContent_MakeAvailable()
calls in one place. I propose we add a new DEPENDENCIES
scope to the block()
command which allows the project to communicate that they are providing all the direct dependencies the project needs within that block. The following requirements would apply to that block:
- All dependencies that the project uses directly must be specified in a call to
find_package()
orFetchContent_MakeAvailable()
. Indirect dependencies do not need to be specified, a dependency provider (if used) is responsible for working those out itself. - The project is not allowed to check or use any result or thing defined by any call to
find_package()
orFetchContent_MakeAvailable()
within the block. It may only use those results after control leaves the block scope.
With the above constraints, a dependency provider would be allowed to receive each find_package()
and FetchContent_MakeAvailable()
call and record it internally somehow, then only have to process them when the endblock()
is reached or the block is exited. At that point, it would have a full view of the whole set of dependencies and would only need to resolve dependency relationships once. If a dependency provider that doesn't support or use this feature, or if there is no dependency provider, behavior would be exactly the same as now as though the block()
command had not been given.
If a dependency provider is registered, it would be notified at the start of a block(SCOPE_FOR DEPENDENCIES)
call and notified again when the block is complete (either reaching the endblock()
or exiting through an early return()
). I propose we extend the cmake_language(SET_DEPENDENCY_PROVIDER)
call with new keywords for specifying the function(s) to call for these notifications.
A top level project should put the block(SCOPE_FOR DEPENDENCIES)
call in its top level CMakeLists.txt
file, or a file pulled in by include()
, but not add_subdirectory()
. The dependencies should be added in the top level scope, which would make them visible to the whole project. If one or more of those dependencies is brought in via FetchContent, or a dependency provider embeds the source and brings it into the main build with add_subdirectory()
, we could encounter a situation where the nested project also calls block(SCOPE_FOR DEPENDENCIES)
. We should only issue notifications to the dependency provider for one block, and only from the top level scope. Any other DEPENDENCIES
blocks in other scopes would be treated as doing nothing special, and the dependencies within such blocks would be seen exactly how they would be today (i.e. each find_package()
and FetchContent_MakeAvailable()
call would be processed immediately).
Additional Notes
Projects can still use arbitrary logic within such blocks, they just have to avoid using results too early. See examples below.
This feature would disallow projects from using the pattern of trying to see if a particular dependency is available, then making decisions about other dependencies based on that. Such a pattern is increasingly being viewed as something projects shouldn't do anyway, they should instead say what they need (using CMake cache variables to enable/disable optional features) and error out if a dependency is missing.
Here's an example of a well-formed block:
# Details can be declared outside blocks
FetchContent_Declare(MyCoMagic ...)
block(SCOPE_FOR DEPENDENCIES)
find_package(Things REQUIRED)
# Defining and querying a CMake cache variable is fine
option(ENABLE_SOME_FEATURE "...")
if(ENABLE_SOME_FEATURE)
# Only pull in a dependency if option is set.
# This doesn't depend on the result of any other dependency,
# so it is legal.
find_package(SomethingElse REQUIRED)
endif()
# Content population must be inside the block
FetchContent_MakeAvailable(MyCoMagic)
endblock()
The following examples show some things you are not allowed to do:
block(SCOPE_FOR DEPENDENCIES)
find_package(Things)
# WRONG: Makes use of result of the above call within the block
if(Things_FOUND)
find_package(SomethingElse REQUIRED)
endif()
endblock()
# WRONG: Direct dependency outside the block
find_package(Thing REQUIRED)
block(SCOPE_FOR DEPENDENCIES)
find_package(SomethingElse)
endblock()