16

I have projectA, into which I'm importing a library with:

add_library(foo STATIC IMPORTED)
set_property(TARGET foo PROPERTY IMPORTED_LOCATION /path/to/foo.a)

I then use foo in several places within the project, and it all works fine.

A couple of directory levels down I want to export a library built within this project for use in yet another project with a completely disconnected CMake config. I've got:

...
target_link_libraries(thislib foo)
export(TARGETS thislib FILE /path/to/thislib.cmake)

The importing projectB also needs foo though (because the imported library needs it), and complains that it cannot find -lfoo. I tried adding it to the export command, but then I get:

CMake Error at thislib/CMakeLists.txt:37 (export):
  export given target "foo" which is not built by this project.

I just want to export the same configuration I use locally to the other (importing) project. I don't want to have to tell projectB about foo explicitly. Is there some way to accomplish this?

Gil Hamilton
  • 11,973
  • 28
  • 51

3 Answers3

4

I also can't find an ideal way to do this. But here is the workaround I'm using for now. It's extra work, and not DRY, but I think it achieves the right thing.

Imagine lib B depends on third party lib A. A either has a find module defined, or we can implement a custom find module for it. Both are doable. Assume we've already written FindA.cmake and stored in in ${CMAKE_SOURCE_DIR}/cmake. Also, let's assume that when you run cmake to generate B's build system, you provide A_ROOT to help cmake locate A.

Then in B's top-level CMakeLists.txt we need:

# Use FindA.cmake, defined in the cmake/ directory.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(A REQUIRED)

# ... (define the B target here)...

# Copy the config file to the build dir
configure_file(cmake/BConfig.cmake cmake/BConfig.cmake @ONLY)

# Copy A's find module (skip this step if FindA is not custom).
configure_file(cmake/FindA.cmake cmake/FindA.cmake COPYONLY)

# Create the target export.
export(EXPORT BTargets
    FILE ${CMAKE_BINARY_DIR}/cmake/BTargets.cmake
    NAMESPACE B::
    )

# Register B so cmake can find B's config file.
export(PACKAGE B)

Now in cmake/BConfig.cmake:

# Get the exported find module onto the module path.
# This step is unnecessary if the find module is part of cmake.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}")

# Store the root so that find_package will work.
# You may have to store other variables here, too, but
# the principle is just more of the same.
set(A_ROOT @A_ROOT@) # **Could optionally cache this.
find_package(A MODULE REQUIRED)

# The usual one-liner for a config file.
include("${CMAKE_CURRENT_LIST_DIR}/BTargets.cmake")

Just to drive the solution home, let's look at a second example, Rocket this time using Boost, which already has a find module defined.

CMakeLists.txt:

option(Boost_USE_MULTITHREADED ON)
option(Boost_USE_STATIC_LIBS OFF)
find_package(Boost REQUIRED COMPONENTS filesystem program_options)

add_library(Rocket rocket.cpp)
target_link_libraries(Rocket
    PUBLIC Boost::filesystem
    PRIVATE Boost::program_options
    )

configure_file(cmake/BConfig.cmake cmake/BConfig.cmake @ONLY)

export(EXPORT RocketTargets 
    FILE ${CMAKE_BINARY_DIR}/RocketTargets.cmake
    NAMESPACE Rocket::
    )
export(PACKAGE Rocket)

Then cmake/RocketConfig.cmake would have:

set(BOOST_ROOT @BOOST_ROOT@ CACHE PATH "In case boost was relocated.")
set(Boost_USE_MULTITHREADED @Boost_USE_MULTITHREADED@)
set(Boost_USE_STATIC LIBS @Boost_USE_STATIC_LIBS@)
find_package(Boost REQUIRED COMPONENTS filesystem program_options)

include("${CMAKE_CURRENT_LIST_DIR}/RocketTargets.cmake")

So yeah. It seems like too much typing. It seems like cmake should be able to generate all of that code from an export statement in CMakeLists.txt. But this strategy seems to achieve the goal.

Also, I haven't tested yet, but I suspect that, if your transitive dependency uses a CONFIG instead of a MODULE for find_package, it should be very easy just to add to CMAKE_PREFIX_PATH in your config file as well.

Edit (2021): Tsyvarev has pointed out the find_dependency() macro, which can simplify the above approach somewhat. Consider using that instead of find_package().

chadsgilbert
  • 390
  • 1
  • 13
  • 1
    Yes, adding search for `A` package inside the script `BConfig.cmake` is a correct approach. But instead of `find_package`, it is better to implement that search via [find_dependency](https://cmake.org/cmake/help/latest/module/CMakeFindDependencyMacro.html) macro. That macro automatically "derives" REQUIRED option from the call `find_package(B)`, and in case of failure to find `A` it prints the name of `B` package too. Before using `find_dependency` macro one should include CMake script which defines it: `include(CMakeFindDependencyMacro)`. – Tsyvarev Oct 24 '21 at 12:01
  • Thanks for pointing that out. I didn't know about that macro at the time, but the documentation makes it very clear. That's the problem with CMake - it's well documented in the details, but so complicated it can be hard to find out what features exist and how best to use them. – chadsgilbert Oct 26 '21 at 23:42
2

I did not find an actual solution for the problem as stated, but am posting my own workaround for future reference.

I realized that the foo dependency was being emitted in the export; it just didn't have a path with it. And since I still haven't figured out how to get cmake to export the path along with it, I reverted my export command to that shown in the question above (without foo).

Then I went back to the original place where foo was being imported and removed the add_library and set_property, replacing them with this:

set(foo /path/to/foo.a)

Then changed the target_link_libraries to:

target_link_libraries(thislib ${foo})

In other words, rather than making it a real "imported library", it's just a raw library path. This does get correctly written into the export file and allows projectB to link.

Gil Hamilton
  • 11,973
  • 28
  • 51
  • 2
    One problem with that is that it doesn't allow you to set target properties such as INTERFACE_INCLUDE_DIRECTORIES – RiaD Dec 30 '16 at 11:20
  • 1
    Another problem is that your path will be copied in export scripts, which makes them most probably useless on other machines. – null Jun 04 '21 at 08:06
0

You need to

  1. Export foo from where it is built (to, say, foolibs.cmake)
  2. Instead of using /path/to/thislib.cmake directly (the export file generated by export(TARGETS thislib... create another one, thislib-and-deps.cmake which includes both:

    include(<...>/foolibs.cmake)
    include(${CMAKE_CURRENT_LIST_DIR}/thislib.cmake)
    
tamas.kenez
  • 7,301
  • 4
  • 24
  • 34
  • Does an easy solution exist to implement this in a reusable manner without implementing lots of CMake code per project? This doesn't seem to be the *CMake-way-to-go*. Currently I repeat the `find_package( CONFIG)` call for each transitive dependency, which also sucks, since it breaks the concept of transitive dependencies. – Florian Wolters Jun 27 '17 at 15:04