Last Updated: May 30, 2016
·
31.64K
· daniperez

Use CMake-enabled libraries in your CMake project


Summary

In this protip I explain how to re-use CMake-enabled libraries within another CMake-enabled project when the former aren't installed in your system but in Github or in a subdirectory.

Note: A pair of follow-ups to this solution exist: II and III.


The problem

Very often I make use of CMake-enabled libraries of mine in new projects. Very often as well, those libraries are not installed in my system but somewhere in Github or Bitbucket.

Even if the libraries are not installed, CMake makes it easy to use them: you copy the library into a folder in the project source tree (or use Git Submodules or whatever) and then use add_subdirectories to include the project. Consider for example this sample project A with libraries B and C, where C depends on B. The resulting source tree will be something like this:

A/
  CMakeLists.txt
  B/
    CMakeLists.txt
  C/
    CMakeLists.txt  // depends on B

Where A's CMakeLists.txt would have add_subdirectories(B) and add_subdirectories(C). Nevertheless this approach has a few drawbacks. Notably, C would probably try to find B by means of find_package(B) and this won't work out of the box for a couple of reasons:

  • find_package(B) in C's CMakeLists.txt won't find B unless you provide some hints about where to look up (e.g. pointing CMAKE_MODULE_PATH to B's configuration file). This implies pointing C to A somehow (ugly and in the worst case not possible if we don't own the module) or setting CMAKE_MODULE_PATH in A, which could be an acceptable solution.

  • If eventually we get C to find B, CMake will complain about a "duplicated target" coming from B. The problem arises because B will define targets that later on are "imported" again by C. This happens because A, B and C are sharing the same "namespace" with regards to variables and targets. This problem can be circumvented but it requires changing C which, as aforementioned, is not desirable.

The solution

After trying several solutions, the one that works for me is the following:

  • Step 1: Use ExternalProject_Add instead of add_subdirectories.
  • Step 2: Use CMake's install(EXPORT) feature to export B's configuration.
  • Step 3: Use CMake's export(PACKAGE) to add B's configuration to the "package registry" where C can later on find it.

This procedure is clean in the sense that we don't have to modify C in order to find B. The "package registry" is one of the places where CMake looks for package configuration files. Nevertheless, we still have to modify B or any project we depend upon in order to get its configuration file exported, which is, generally speaking, a good practice.

Step 1: Add external projects

In A's CMakeLists.txt we replace add_subdirectories calls by:

include(ExternalProject)

ExternalProject_Add (
       B
      GIT_REPOSITORY "<git_repo_url>"
      GIT_TAG "<git_tag_commit_or_branch>"
      UPDATE_COMMAND ""
      INSTALL_COMMAND "" )

ExternalProject_Add (
       C
       GIT_REPOSITORY "<git_repo_url>"
       GIT_TAG "<git_tag_commit_or_branch>"
       UPDATE_COMMAND ""
       INSTALL_COMMAND "" )

You can also use SVN or CVS, or include the sources right in your source tree and then point SOURCE_DIR parameter to them (have a look at ExternalProject_Add).

UPDATE_COMMAND and INSTALL_COMMAND lines will avoid using the default update and install procedures, which are usually not needed.

Step 2: Export a configuration

If we want C be able to see B by means of find_package, we must export B's configuration. To achieve that, we do the following in B's CMakeLists.txt:

  • Add targets to a export group:
install ( TARGETS <targets> ... EXPORT <export_name> )
  • Export the export group (sic). CMake will write a configuration file of the current build tree:
export ( TARGETS <targets> FILE <file> )

Use ${CMAKE_BINARY_DIR}/<package name>-config.cmake or ${CMAKE_BINARY_DIR}/<package name>Config.cmake, otherwise the configuration file won't be found.

Step 3: Export build tree to the package registry

Add the build tree to CMake's package registry found in .cmake/packages/<package>/ in unix systems by default:

export ( PACKAGE <package_name> )

Once all these steps are done, in C's CMakeLists.txt, you can do just as if B were a regular package:

find_package ( <package_name> )

The previous call implies find_package's "no module" or "config" signature. The find_package( <package_name> MODULE ) signature doesn't look up the package registry.

Please, don't hesitate to share with me if you found a better way of achieving the same goal!

References

3 Responses
Add your response

Hi, Nice post.
I use a different way to achieve this goal.
My tips is to write a single CMakeList.txt for my dependency. That CMakelist.txt only calls cmake / cmake --build on itself with a -DINSTALLPREFIX used later by findlibrary and a -DINSTALLING=TRUE, when INSTALLING is TRUE, it defines a single project with an ExternalProject_Add and a specified install prefix.

That CMakeList looks something like this :
```
if(NOT INSTALLING)
set(WORKING DIRECTORY "${CMAKECURRENTBINARYDIR}/Install")
file(MAKE
DIRECTORY "${DIRECTORY}"")
executeprocess(
COMMAND "${CMAKE
COMMAND}" -DINSTALLING=TRUE -DINSTALLPREFIX=${MYINSTALLPREFIX} "${CMAKECURRENTSOURCEDIR}"
WORKING_DIRECTORY "${DIRECTORY}"
)

executeprocess(
COMMAND "${CMAKE
COMMAND}" --build . --config Release
WORKING_DIRECTORY "${DIRECTORY}"

)
else()

project("<external_project>")

include(ExternalProject)

ExternalProjectAdd(
external
project
GITREPOSITORY "<gitrepourl>"
INSTALL
DIR "${INSTALLPREFIX}"
CMAKE
ARGS -DCMAKEINSTALLPREFIX:PATH=<INSTALL_DIR>
)
endif()
```

over 1 year ago ·

thanks for the tip @twxs!

Your solution looks really simple! I must admit that my solution (as in the 3d article of the series) is quite "orthodox" in the sense that it uses the means provided by CMake for this purpose, yet I find it quite convoluted. I hope they improve that in the future.

By the way if you use the backtics with your code, other people will be able to read it and make the most of it ;-)

over 1 year ago ·

Here is a somewhat cleaned up version of TWXS's code:
https://gist.github.com/DanTheColoradan/22bf1ca6307b8edcb53e

if (NOT INSTALLING)
  set (WORKING DIRECTORY "${CMAKECURRENTBINARYDIR}/Install")
  file (MAKEDIRECTORY "${DIRECTORY}")
  executeprocess (COMMAND "${CMAKECOMMAND}"
    -DINSTALLING=TRUE
    -DINSTALLPREFIX=${MYINSTALLPREFIX} "${CMAKECURRENTSOURCEDIR}"
    WORKING_DIRECTORY "${DIRECTORY}"
  )
  executeprocess (COMMAND "${CMAKECOMMAND}"
    --build .
    --config Release WORKING_DIRECTORY "${DIRECTORY}"
  )
else (NOT INSTALLING)
  project ("")
  include (ExternalProject)
  ExternalProjectAdd (externalproject GITREPOSITORY "<gitrepourl>"
    INSTALLDIR "${INSTALLPREFIX}"
    CMAKEARGS -DCMAKEINSTALLPREFIX:PATH=
  )
endif (NOT INSTALLING)
over 1 year ago ·