There’s quite a few questions in this thread, but hopefully my response here will at least provide some clarity around the advice my book offers.
The book recommends pulling in dependencies from their own directory scope. The pattern it gives is a simple add_subdirectory(dependencies)
from the top level CMakeLists.txt
file. One of the advantages the book mentions is that this ensures no non-cache variables used to set up the dependencies can bleed out to other parts of the build accidentally. If you are pulling in your dependencies with FetchContent
, you can set variables just before you call FetchContent_MakeAvailable()
to influence the dependencies, since they will see these variables. But you might not want the rest of your project to see those variables, they should be considered an implementation detail of the dependencies, if possible. Furthermore, some other project could be consuming yours and it might be pulling the dependency in earlier with a different set of variables, so you can’t rely on a dependency being pulled in exactly how your project sets it up. This might sound like a problem, but if this is what is happening, also remember that the consuming project takes on the responsibility for ensuring its child dependencies still build correctly if they override how that dependency is brought into the build. To maximise the chances of a consuming project being able to do that, the rest of your project should try to avoid relying on these variables and use only the targets that are provided by the dependency. If your project avoids referencing any variables used to configure the dependency or that the dependency provides and only uses its targets, that will give you the greatest robustness and flexibility for consumers.
Another reason for the dedicated dependencies
directory is to ensure that all your FetchContent_Declare()
calls are made before any calls to FetchContent_MakeAvailable()
or FetchContent_Populate()
. This is a critical part of what makes FetchContent
effective. Always declare all the dependency details before starting to pull in any of them. The first place that calls FetchContent_Declare()
for a particular dependency wins. If you delay calling FetchContent_Declare()
until after some dependencies have been pulled in, then those dependencies may declare details for further dependencies before you do and then you are not in control of those further dependencies. For cases where you only conditionally need some dependencies, you can put the logic that makes that decision in the top level of the project (either an option()
or some non-cache variable that you compute based on whatever conditions you have). You query that in the dependencies
folder and you also query that in other parts of the project where needed. For example, the top level CMakeLists.txt
file could define a variable MYPROJ_ENABLE_TESTS
and in your dependencies
folder, you only pull in Catch2 if that variable is set to true. Elsewhere in your project, you only add the tests or build targets related to those tests if MYPROJ_ENABLE_TESTS
is true.
As noted earlier in the discussion thread, find_package()
creates imported targets that only have local scope. If you call find_package()
from within the dependencies
directory scope, the rest of the project won’t see them by default. If your project does need them, then you have a few options:
-
Call find_package()
in your dependencies
folder with all components defined that any part of your project may need. Then in those relevant parts of the project, call find_package()
there as well, potentially with a reduced set of components if relevant. The first time find_package()
is called in the dependencies
folder, it ensures the dependency can be found and saves its location in the CMake cache. Subsequent calls to find_package()
for the same package in other parts of the project will re-use the same location. The package’s imported targets still remain local, but this approach is robust and should still be pretty efficient. Where dependencies are noisy and output a lot of detail to the log (which they shouldn’t, but many do), it can be annoying seeing the same info in the log more than once, but that’s a relatively minor annoyance in most cases.
-
Call find_package()
in your dependencies
folder with all components defined that any part of your project may need. For each of the package’s imported targets that you want to make visible to the rest of your project, promote those targets to global visibility by setting their IMPORTED_GLOBAL
target property to true. Personally, I’d only use this in very specific cases where you are in full control of the dependency package and that package has no further dependencies of its own. I’d generally avoid this method if you can.
-
Call find_package()
at the top level scope of your project instead of within the dependencies
directory scope. This would be a pragmatic choice if you decide you don’t want to call find_package()
multiple times for the same dependency for some reason. It could be appropriate, for example, if the call to find_package()
is complicated with many options (which should hopefully be rare).
The discussion thread in this post also asks how to influence dependencies that use cache variables instead of non-cache variables for configuring themselves. As @Leon0402 mentioned, the behavior of option()
and set()
can be different for a boolean cache variable, which is unfortunate (I didn’t push hard enough for set()
to be updated when the behavior of the option()
command was modified in CMake 3.13 unfortunately). This means that for any variable you want to set before pulling in a dependency, you need to understand how the dependency defines or uses that variable. If it defines it with a set(someVar someValue CACHE type ....)
form, then your hands are tied. To ensure your preference is honored, you have to use set()
with either INTERNAL or FORCE. I usually go for INTERNAL because I’m forcing an option and therefore the user no longer has a say, so it should no longer show up in the CMake GUI as a user-configurable option. I might do this when my project cannot work unless that option has a certain value, or in commercial software where all uses of projects can be well-defined and enforcing a consistent way of doing something makes sense. If the dependency project uses option()
instead and the dependency project requires CMake 3.13 or later (more accurately, if policy CMP0077 will be NEW at the point the dependency calls option()
), you can set just a regular non-cache variable in your project and not force any cache value. Otherwise, you have to use set()
with INTERNAL or FORCE as for the set()
command. If you can’t be sure how the dependency project defines the option, use set()
with INTERNAL or FORCE.
Hope that clarifies some of your questions.