How to handle encapsulated/embedded packages?

This a “How do others handle this?” sort of question. The description is a bit long, but not sure how to say it that much shorter. Please bear with me.

We have a reasonably mature codebase that is built using CMake and, it being now several years old, are looking to modernise the CMake usage. Our codebase is built from several modules linked together, and one of the key things we wish to do is to encapsulate usage - so if I add or remove (say) a shared library within an embedded module, the top layer should just work and I don’t have to modify its CMakeLists.txt.

A highly simplified but not atypical hierarchy might be:

  • exe1 - added via add_executable()
    • sharedlib2
      • staticlib3
        • sharedlib4 - added via find_package()

Each module is added to the “parent” using target_link_libraries() - whether it is PRIVATE or PUBLIC will vary but does not make much difference to the question. We use the hunter package manager, again not sure if that is vitally important here. Key thing is that sharedlib4 is always pulled in as a binary - perhaps because it is something delivered to us that way or (like tensorflow) it is easier to use a pre-built binary. sharedlib2 and staticlib3, though, are our own and (to add an extra complexity) we have two slightly different build environments we wish to support:

  1. (standard build environment) where each module is built as a separate TGZ cmake package - a build of sharedlib2 uses staticlib3 via find_package. We add a find_package(sharedlib4) into the generated config file for staticlib3, and a find_package(staticlib3) into the generated config for sharedlib2 - so sharedlib4 is pulled into the exe1 environment effectively automatically. [This build environment means on changes to sharedlib2, we only build that and use a pre-built binary for staticlib3]
  2. (special debug environment) our own modules are built under the same cmake project. sharedlib2 is added under exe1 via add_subdirectory, and staticlib3 is added under sharedlib2 via add_subdirectory. [This build environment enables developers to open all the modules in the same IDE session, making debugging and some modifications easier]

Notice that in Method2 the find_package is buried within two layers of add_subdirectory. Because imported packages are not normally global, this will be invisible to exe1. However, and not particularly consistently, sharedlib2 and staticlib3 (created via standard add_library commands) are naturally global. [Remember Method1 has no such issue as find_package pulls in its dependencies at the lower-level]

I have a number of scenarios where I need to know about all dependencies of the top-level exe1 (not just its primary/first-order dependencies). These can be summed up in examples like:

  1. List all shared library dependencies of exe1 - generally, I actually want the .so/.dll files, not just the targets.
  2. List all static library dependencies of exe1 - again I really want the .a files.
  3. “Is sharedlib4 an ultimate dependency of exe1?”

As said above, it will vary whether (say) sharedlib4 is a PRIVATE or PUBLIC dependency of staticlib3. [Similarly staticlib3 and sharedlib2.] One way of the other it will be added to the INTERFACE_LINK_LIBRARIES of staticlib3, either straight or as $<LINK_ONLY:> I know that the INTERFACE_LINK_LIBRARIES of static3 will be propagated to LINK_LIBRARIES on sharedlib2 etc but my understanding is that this only happens at the end of the scripting phase, so for scripting I have a function which effectively traverses the graph and returns a list of dependent targets. I can then use things like $<TARGET_FILE:> $<TARGET_NAME:> to refer to the actual libraries.

Now the problem of this. In BuildMethod2, exe1 knows nothing about sharedlib4 - even though it can find out that staticlib3 depends on sharedlib4, it cannot do get_target_properties on sharedlib4 or use generator expressions.

I’ve so far derived or identified two workarounds to this (I hesitate to say solutions):

  1. Following its find_package, we turn sharedlib4 global by setting IMPORTED_GLOBAL True. This means exe1’s CMakeList.txt can interrogate the target. However, properties like sharedlib4_FOUND (or even things like OpenCV_LIBS or PROTOBUF_ROOT) are not propagated. I have a solution based on using global properties that seems to work but is relatively complex. If there were a an equivalent of set(… PARENT_SCOPE), such as set(… GLOBAL_SCOPE) that would be good but does not seem to exist. Just using PARENT_SCOPE alone means having to modify sharedlib2’s CMakeList.txt, which is hardly modularising.
  2. We modify things so that all packages are pulled in at the top level, at least on Method2 builds. This might be easier said than done as (I have not shown this) but developers might build the sharedlib2 or even staticlib3 environment - they don’t build from the top. I guess we could add some include that is always pulled in at the top level and not further down.

I can’t but help thinking I’ve missed a trick and am jumping through hoops I should not need to use. I am thus wondering what others do?

You should look at the file(GET_RUNTIME_DEPENDENCIES) command for this.

Interesting, I will check it out, although I note in the doc:

Please note that this sub-command is not intended to be used in project mode. Instead, use it in an install(CODE) or install(SCRIPT) block

Are you saying you reckon that this caveat can be ignored?

No. In project mode, your binary is not built yet, so you can’t really use it at that point.

OK. I suspect that is too specialised for most of those use cases then, but perhaps it is usable for part of it. Thanks for the info though, again.