I'd like to discuss some shortcomings of CMake with regards to sharing targets among projects. Other people have brought this up on this mailing list in the past, but there has never been any useful feedback on it.
I'm going to refer to two projects:
- "Project A" produces some libraries
- "Project B" depends on libraries and headers from Project A
First I'll discuss the easier scenario: Project A is built and installed ahead of time, and is therefore already present as a complete installation before Project B is configured.
In the case that some of Project A's libraries are produced via ExternalProject_Add() (e.g. due to the lack of a CMake build system, or because they are dependencies of Project A's core code), CMake's install(TARGETS ...) does not support re-exporting shared imported library targets+properties.
I've worked around this by writing my own CMake functions that invoke file() to generate CMake modules containing target import and property set commands. I also use file() to generate a CMake package config that loads the target import module. I was careful to base everything on CMAKE_CURRENT_LIST_DIR, so that the installation is portable. This solution works great: Project B is able to do a find_package(ProjectA CONFIG) to get all the targets+properties for the libraries and headers in the Project A install, reducing Project B's build system dependence on Project A to a few target_link_libraries() commands referencing the imported targets. I don't even need to explicitly mention the Project A header directories anywhere in Project B, because CMake takes care of it via target properties.
Of course, this is still non-optimal because I now have a wacky target re-export function to maintain in Project A. It would be better if CMake would just handle things for me via install(TARGETS ...) or similar.
Now comes the more difficult case: I want to create a super-build Project X that builds both Project A and Project B.
My first instinct is to use ExternalProject_Add() to build Project A, because developers of Project X and/or B don't care about its source code - they only care about the headers and libraries. Unfortunately this is a complete non-starter, because ExternalProject_Add() doesn't invoke any part of Project A's build system until the *build* step of Project X, which means that we'll never get the package config stuff in time for Project B's configure step.
OK, so let's resign ourselves to the fact that we have to use add_subdirectory() to build Project A, polluting Project X both with Project A's source and its CMake cache variables. This is *still* not going to work (yet), because CMake bafflingly *only* flows first-class (add_library()/add_executable()) targets upwards from Project A to Project X & B and does *not* flow import targets upwards! I'm not sure this is documented anywhere, but I have convinced myself that it is absolutely the case in CMake 3.12.2.
So we can't use the Project A's native import targets even though we're doing add_subdirectory() in Project X. What can we do? My approach was to modify Project A as follows:
Extend my import target + package config functions to support writing either build tree or install flavored modules.
Generate the install flavored modules with an "INSTALL-" prefix on their filenames, and have install() rename them on install.
Generate build flavored modules in the build tree during the CMake configure step.
I then modified Project X to add the build directory to CMAKE_PREFIX_PATH, and Project B was able to load Package A's modules via find_package(ProjectA CONFIG).
This almost got everything working, but there was one final bump: Parallel building fails with an error that Project B cannot find Project A's libraries! This is because Project B is now using a separate set of import targets defined by modules produced by Project A, which do not tie back to the *actual* targets being used to build Project A! Oof.
Fortunately, it turns out that ExternalProject_Add() targets *do* flow upwards to Project X and back down to Project B, even though the imported library targets do not. This means I was able to modify Project A's target
module generator function to include add_dependencies() commands - in the build-flavored module only! - that tie the targets imported by Project B to the ExternalProject_Add() targets in Project A.
This also means that Project B can use a pre-build+installed Project A without any changes to the former's build system.
Now take a step back and look at how much text I just wrote, all because CMake has the following limitations that I had to overcome:
install() does not support re-exporting imported library targets+properties.
ExternalProject_Add() doesn't do anything until the build step (NOTE: I understand why this is the case, and also that there are ugly workarounds).
Sub-projects pulled in via add_subdirectory() do not flow imported library targets upwards, even though add_library(), add_executable(), and ExternalProject_Add() targets *do* flow upwards.
When a project creates imported library targets based on libraries produced by another project in the hierarchy, CMake is not smart enough to detect this and adjust the target dependency tree.
It would be really nice if CMake could do something to address items 1 and 3. For item 2, it might be nice to get an option in ExternalProject_Add() that allows running its configure step during the configure step of the parent project. Item 4 could be rendered OBE by addressing the other items.