[CMake] Shortcomings with exporting and importing targets

Benjamin Shadwick benshadwick at gmail.com
Mon Jul 15 13:15:33 EDT 2019


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:

   1. Extend my import target + package config functions to support writing
   either build tree or install flavored modules.
   2. Generate the install flavored modules with an "INSTALL-" prefix on
   their filenames, and have install() rename them on install.
   3. 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
import 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:

   1. install() does not support re-exporting imported library
   targets+properties.
   2. 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).
   3. 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.
   4. 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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://cmake.org/pipermail/cmake/attachments/20190715/5ca06a25/attachment.html>


More information about the CMake mailing list