Basic CMake, part 2: libraries

My previous post about CMake provided a simple CMakeLists.txt for a small, self-contained, project. In practice, very few projects are fully self-contained, as they either depend on external libraries or are themselves libraries that other projects depend on. This post shows how to create and consume simple libraries using modern CMake.

Consuming libraries

Let's say we want to build a program using a SAT solver[1], specifically Minisat[2]. To check that using the library works, we will use this main.cpp to build a binary.

// main.cpp
#include <minisat/core/Solver.h>

#include <iostream>

int main() {
    Minisat::Solver solver;
    auto x = Minisat::mkLit(solver.newVar());

    solver.addClause( x);
    solver.addClause(~x);

    if (solver.solve()) {
        std::cout << "SAT\n";
    } else {
        std::cout << "UNSAT\n";
    }
}

It creates a CNF formula with 2 clauses, x and ~x. Obviously a variable cannot be set to both true and false at the same time, so the output should be "UNSAT".

So how does CMakeLists.txt for building this executable look like? To start with, we will assume that the Minisat library has proper CMake-based build and has been already built and installed in the system we are building on.

cmake_minimum_required(VERSION 3.5)
project(foo-sat LANGUAGES CXX)

add_executable(foo main.cpp)

find_package(MiniSat 2.2 REQUIRED)
target_link_libraries(foo MiniSat::libminisat)

And that is it.

find_package(MiniSat 2.2 REQUIRED) looks for MiniSat package, in version 2.2, in the local CMake package registry. It being REQUIRED means that if CMake cannot find it, it should abort the configuration step. If CMake finds the package, all exported MiniSat targets are imported -- this is where we get the MiniSat::libminisat library target.

Because MiniSat::libminisat properly exports its include paths and other compilation settings it needs, linking against it is enough to get proper compilation settings for the foo binary.

Building subproject dependencies

The above works well if the package is already installed on the system we are building on. But what if we expect that it isn't, and would rather not make the user build and install the library separately?

If the library accommodates this in its CMakeLists.txt, we can do almost the same thing, except use add_subdirectory instead of find_package:

cmake_minimum_required(VERSION 3.5)
project(foo-sat LANGUAGES CXX)

add_executable(foo main.cpp)

add_subdirectory(lib/minisat)
target_link_libraries(foo MiniSat::libminisat)

This assumes that our folder structure looks like this:

lib/
└── minisat/
    └── <stuff>
CMakeLists.txt
main.cpp

Easy.

What is harder is making this transparent: in both cases the executable links against a target with the same name, MiniSat::libminisat, but the way that this target gets into scope is different. The only solution I know of for this problem is not very satisfying[3] or elegant.

Using non-CMake libraries

Until now we assumed that the library we want to use has a high-quality CMake build. This opens up a question: what if the library is not built using CMake, or maybe it is built using CMake, but the maintainer did not take care to enable proper installation? As an example, Boost is a common library that is not built using CMake, so in theory, we cannot depend on there being targets provided for it. There are two ways around this:

  1. Wuss out and hardcode platform-specific flags
  2. Use a Find*.cmake to provide the targets instead

If you go with 2) and the library you want to use is common enough, there is a good chance that it will work out of the box, because CMake comes with some Find*.cmake scripts preinstalled, e.g. it provides FindBoost.cmake or FindThreads.cmake[4] for you out of the box. Alternatively, you can look for one online, or write your own[5].

Creating libraries

As we have seen, using libraries from CMake can be downright pleasant, as long as the library supports this usage properly. The question now becomes, how do we create such library? Let's go over writing CMakeLists.txt for the Minisat library we were using in the first part of this post[6].

The first step is to build the library and binaries themselves. Going by the previous post about CMake and skipping over the IDE related improvements, we will end up with something like this[7]:

cmake_minimum_required(VERSION 3.5)
project(MiniSat VERSION 2.2 LANGUAGES CXX)

add_library(libminisat STATIC
    minisat/core/Solver.cc
    minisat/utils/Options.cc
    minisat/utils/System.cc
    minisat/simp/SimpSolver.cc
)

target_compile_features(libminisat
    PUBLIC
      cxx_attributes
      cxx_defaulted_functions
      cxx_deleted_functions
      cxx_final
)

target_include_directories(libminisat PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

target_compile_definitions(libminisat PUBLIC __STDC_LIMIT_MACROS __STDC_FORMAT_MACROS)

# Also build the two MiniSat executables
add_executable(minisat minisat/core/Main.cc)
target_link_libraries(minisat libminisat)

add_executable(minisat-simp minisat/simp/Main.cc)
target_link_libraries(minisat-simp libminisat)

target_compile_features was not mentioned in the previous post, it allows us to set what C++ features are used by the target and CMake then tries to figure out what flags are needed by the compiler to enable them. In this case, our fork of Minisat uses some C++11 features (final, = delete, = default and [[]] attributes), so we enable those.

This CMakeLists.txt will build a static library and the two binaries that depend on it. However, if we build this project on Linux, the library will be named liblibminisat.a, because CMake knows that library files on Linux are prefixed with lib as a convention, and it tries to be helpful. However, we cannot name the target just minisat, because that is the name of a target for executable. Let's fix that by instead changing the OUTPUT_NAME property of our target to minisat, to make the output of libminisat target libminisat.a on Linux and minisat.lib on Windows:

set_target_properties(libminisat
    PROPERTIES
      OUTPUT_NAME "minisat"
)

Now we have a functional[8] CMakeLists.txt, but it does not know how to install the resulting binaries.

Installing targets

CMake supports installing build artifacts made as part of a target via the install command. We can have CMake install the resulting library and binaries with this snippet

install(
    TARGETS
      libminisat
      minisat
      minisat-simp
    LIBRARY DESTINATION /usr/local/lib
    ARCHIVE DESTINATION /usr/local/lib
    RUNTIME DESTINATION /usr/local/bin
)

This means install outputs of libminisat, minisat, minisat-simp to appropriate locations (LIBRARY is the destination for dynamic libraries, ARCHIVE is the destination for static libraries and RUNTIME is the destination for executables). This snippet has 3 problems

  1. The installation paths are hardcoded and obviously make no sense on Windows
  2. Only the build artifacts are installed, without any integration with CMake, so the libraries cannot be used the way shown at start of this post.
  3. There are no headers to be used with the library

We can fix the first one by relying on utility package GNUInstallDirs to provide reasonable default paths for Linux (Windows does not have a default path):

include(GNUInstallDirs)

install(
    TARGETS
      minisat
      minisat-simp
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

This will get the two binaries installed into a reasonable default paths, namely /usr/local/bin on Linux and `` (empty, meaning local) on Windows. The library target has been split off because it will need special treatment to fix the second problem of the original install command.

The second problem, integrating nicely with other CMake builds, takes a lot of boilerplate CMake:

set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/MiniSat)

install(
    TARGETS
      libminisat
    EXPORT
      MiniSatTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(EXPORT MiniSatTargets
    FILE MiniSatTargets.cmake
    NAMESPACE MiniSat::
    DESTINATION ${INSTALL_CONFIGDIR}
)

install(DIRECTORY minisat/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/minisat
    FILES_MATCHING PATTERN "*.h*"
)

The first install command marks the libminisat target for export[9] under the name MiniSatTargets (and obviously also installs the library). The second install command then saves the libminisat target into file MiniSatTargets.cmake, in namespace MiniSat:: in a subfolder of the library folder and the third install command copies all headers from the minisat subdirectory to the proper destination.

This is enough to use the MiniSat::libminisat target from external projects, but not enough to have it imported by the find_package command for us. For this to happen, we need 2 more files, MiniSatConfig.cmake and MiniSatConfigVersion.cmake[10], to be used by find_package:

#####################
# ConfigVersion file
##
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/MiniSatConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)

configure_package_config_file(
    ${CMAKE_CURRENT_LIST_DIR}/CMake/MiniSatConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MiniSatConfig.cmake
    INSTALL_DESTINATION ${INSTALL_CONFIGDIR}
)

## Install all the helper files
install(
    FILES
      ${CMAKE_CURRENT_BINARY_DIR}/MiniSatConfig.cmake
      ${CMAKE_CURRENT_BINARY_DIR}/MiniSatConfigVersion.cmake
    DESTINATION ${INSTALL_CONFIGDIR}
)

write_basic_package_version_file is a helper function that makes creating proper *ConfigVersion files easy, the only part that is not self-explanatory is COMPATIBILITY argument. AnyNewerVersion means that the MiniSatConfigVersion.cmake accepts requests for MiniSat versions 2.2 and lesser (2.1, 2.0, ...).

configure_package_config_file is a package-specific replacement for configure_file, that provides package-oriented helpers. This takes a file template CMake/MiniSatConfig.cmake.in and creates from it MiniSatConfig.cmake, that can then be imported via find_package to provide the targets. Because MiniSat does not have any dependencies, the config template is trivial, as it only needs to include MiniSatTargets.cmake:

@PACKAGE_INIT@

include(${CMAKE_CURRENT_LIST_DIR}/MiniSatTargets.cmake)

There is only one more thing to do, before our CMakeLists for MiniSat properly packages the library target for reuse, setting up proper include paths. Right now, libminisat target uses ${CMAKE_CURRENT_SOURCE_DIR} for its include paths. This means that if the library was cloned to /mnt/c/ubuntu/minisat, built and installed, then a project linking against MiniSat::libminisat would look for its includes in /mnt/c/ubuntu/minisat, rather than in, e.g. /usr/local/include. We cannot change the include paths blindly to the installed location either, as that would prevent the build from working. What we need to do is to have a different set of include paths when the target is built versus when the target is installed somewhere, which can be done using generator expressions:

target_include_directories(libminisat
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

Support for use as a subdirectory

After all this work, our CMakeLists for MiniSat supports installation and CMake package export, but cannot be properly used as a subdirectory, without installation. Luckily, supporting this is trivial, all we need to do is to create an alias for libminisat with namespaced[11] name:

add_library(MiniSat::libminisat ALIAS libminisat)

Now we are done. At least for simple libraries like Minisat, which have no dependencies of their own.

Packaging libraries with dependencies

So what can you do when your library has a dependency? Your package should check whether its dependency is present while configuring itself, which means that the checks go into FooConfig.cmake. There is even a helper macro for use within FooConfig.cmake, find_dependency.

As an example, if your library depends on Boost.Regex, your FooConfig.cmake.in will look something like this:

@PACKAGE_INIT@

find_dependency(Boost 1.60 REQUIRED COMPONENTS regex)
include("${CMAKE_CURRENT_LIST_DIR}/FooTargets.cmake")

Other things that go into FooConfig are various variables that you want your package to provide to consumers, platform-specific configuration and so on.


The actual CMakeLists from our Minisat fork can be found here. It should be functionally the same as the one explained in this post, but with some minor differences.


  1. Modern SAT solvers are unreasonably good at their job, in fact, one of the best ways to prototype a solver for a new problem quickly is to translate the problem into SAT. But that is a topic for another post. ↩︎

  2. Minisat used to be a state of the art solver back in 2005. Nowadays it no longer wins SAT solving competitions, but is still fast, easy to use and easy to read/modify. ↩︎

  3. As is often said, all problems can be solved with another level of indirection. This also holds for this one: you introduce a top-level project that somehow (you can, e.g. hardcode an authoritative list, or check the filesystem and see what is there) keeps track for which packages find_package should be skipped. You can then override the built-in find_package function with a macro of your own, and delegate to the built-in only if it should not be skipped for given function. ↩︎

  4. Doing find_package(Threads REQUIRED) and linking against Threads::Threads is a great way to enabled threading in a cross-platform manner. ↩︎

  5. In the interest of keeping this post at least a little bit focused and reasonably long, writing your own Find*.cmake is not covered. ↩︎

  6. The original Minisat uses an interesting Makefile to build itself, but we have our own fork that is built using modern CMake and contains various small cleanups for better compatibility with different platforms. ↩︎

  7. Because Minisat was originally built only on Linux, it does not support being used as a dynamic library on Windows, so libminisat is always set to be built as a static library. Dynamic linking on Windows requires the library's code to support it explicitly via __declspec(dllexport) and __declspec(dllimport) annotations in its interface. ↩︎

  8. It is missing warnings, which would be bad for further development, but missing warnings have no bearing on building and installing a library. ↩︎

  9. Not to be confused with the export command, which does something else and is not covered in this post, as it is only rarely useful. ↩︎

  10. The filenames of MiniSatConfig.cmake and MiniSatConfigVersion.cmake are important, as CMake looks for files named using one of 2 patterns, FooConfig.cmake and foo-config.cmake, FooConfigVersion.cmake and foo-config-version.cmake. ↩︎

  11. CMake does not really have a concept of namespaces. While it is a convention to use :: to separate target's namespace from target's name (just like C++ does), it is just a convention. This means that you could just as well have MiniSat (no colons) namespace, leading to MiniSatlibminisat exported target, or you could use MiniSat:::: for MiniSat::::libminisat if you like colons too much. ↩︎