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_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:
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.
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.
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
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.