Best practices to build vendored code with CMake

631 Views Asked by At

I'm trying to understand what some of the best practices are when using modern CMake (3.13+) with respect to building and including vendored or submoduled code.

Say I'm building a library MyLib. My file structure is something like this

MyLib
|-CMakeLists.txt
|-src
|-include
|-submodules
 |-libgeos

In this example, I've included libgeos as a git submodule, because it's really convenient to be able to clone the project and immediately build and run tests because that dependency is present. This could also be solved by using FetchContent or something, and my question still stands; the important thing is that I do not want to rely on libgeos being installed in build environment.

Note I picked libgeos arbitrarily; I have no idea if libgeos is set up as a cmake project appropriately for this example, but this is all theoretical and I just needed some concrete library name. Please do not use the specific details of how libgeos is configured to answer this, unless libgeos is a good example of conventional cmake.

But now, there's some other project that wants to use my project, and it needs libgeos and doesn't want to depend on my project providing it.

OtherProject
|-CMakeLists.txt
|-src
|-include
|-submodules
 |-libgeos
 |-MyLib
  |submodules
  |-libgeos

When you clone OtherProject, you get two versions of libgeos, and maybe that's not great; but it's not a huge issue either. And maybe they're not the same version; say MyLib requires libgeos >= 2.0, so 2.0 is what MyLib includes, and OtherProject requires libgeos>=2.1 so OtherProject includes libgeos >= 2.1.

Now we potentially end up with some build issues. If we have the following line in OtherProject/CMakeLists.txt

add_subdirectory(submodules/libgeos)

and then again, that same line within MyLib/CMakeLists.txt, we end up with cmake errors because libgeos as a target is defined twice in the build. This can be solved a couple of ways.

Check if geos exists before adding it

if(NOT TARGET geos)
  add_subdirectory(submodules/libgeos)
endif()

But this case has some issues; if that blob is in OtherProject at the top, it's fine and both projects use libgeos 2.1. But if it's in OtherProject after add_subdirectory(submodules/MyLib), then the geos 2.0 version gets added to the build, which may or may not fail loudly (Hopefully it would).

This could also be solved with find_package. Both projects include cmake/FindGeos.cmake which use that blurb above (if(NOT TARGET...)) to add geos the build and then the top project cmake files can do this

list(APPEND CMAKE_MODULE_PATH cmake)
find_package(geos 2) # (or 2.1)

then it doesn't matter what order they try to include geos, because they will both defer to FindGeos.cmake in OtherProject because it's first in the module path.

But now there's a new issue, some ThirdProject wants to use MyLib also, but ThirdProject wants to depend on libgeos which is in the system environment. It uses find_package(geos 2.1 CONFIG) to use the installed GeosConfig.cmake file, which adds geos::geos to the build and sets geos_FOUND. Suddenly, MyLib fails to build, because geos_FOUND was set, but I'm doing target_link_library(mylib PUBLIC geos).

So this could be solved by adding add_library(geos::geos ALIAS geos) in both custom FindGeos.cmake files, then it doesn't matter if geos was built from source or using the installed version, the target names are the same either way.

Now we get to my actual questions: Lets start with

  1. Am I crazy, no one does this, and my team is trying to use cmake all wrong?
  2. Is there some feature of cmake that I've just completely missed that solves all these problems?
  3. I suspect there's a good few books or presentations that cover this topic, but I just don't know where to look because there's so many; what should I be looking at? I've seen the CMake Packages page, which looks like it solves the problem when you're using all projects which are configured according to that page; but it doesn't really answer how to bridge the gap between older and newer projects.

If I'm not crazy and there's no straightforward answer or presentation that I can look at, then

  1. What should the cmake configuration for both MyLib and libgeos look like so that these cases work?
  • MyLib is built alone
  • MyLib is built as part of a larger project which provides a different version of geos
  • MyLib is built as part of a larger project which depends on a different version of geos in the environment

I understand that cmake provides helpers that could be used to produce MyLibConfig.cmake if I wanted to install it in the environment. I also see that the export() function exists, which could be used to save those files in the build tree somewhere and then find them with find_package in config mode. But this feels a bit odd to me to do because it's not a multi-stage build, it's just one invocation of cmake then make.

But lets say that's the right answer and the CMake for libgeos doesn't follow it. Would it be appropriate to have FindGeos.cmake do something like this?

if(NOT geos_FOUND)
    add_subdirectory(submodules/libgeos)
    export(geos NAMESPACE geos)
    find_package(geos CONFIG)
endif()
0

There are 0 best solutions below