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:
- Wuss out and hardcode platform-specific flags
- 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, but 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.
Note that since CMake version 3.8, the use of coarse-grained features for target_compile_features
is discouraged. The reason is that as new standards add more and more features, trying to detect their support piecemeal is harder[8] and harder. Instead, cxx_std_XX
compile feature should be used to set the required C++ standard version to XX
. This means that if we targetted newer CMake versions, we would instead use target_compile_features(libminisat PUBLIC cxx_std_11)
.
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[9] 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
- The installation paths are hardcoded and obviously make no sense on Windows
- 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.
- 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[10] 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
[11], 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[12] 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.
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. ↩︎
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. ↩︎
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-infind_package
function with a macro of your own, and delegate to the built-in only if it should not be skipped for given function. ↩︎Doing
find_package(Threads REQUIRED)
and linking againstThreads::Threads
is a great way to enabled threading in a cross-platform manner. ↩︎In the interest of keeping this post at least a little bit focused and reasonably long, writing your own
Find*.cmake
is not covered. ↩︎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. ↩︎
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. ↩︎Not to mention handling compiler bugs around the complex interplay of different features. ↩︎
It is missing warnings, which would be bad for further development, but missing warnings have no bearing on building and installing a library. ↩︎
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. ↩︎The filenames of
MiniSatConfig.cmake
andMiniSatConfigVersion.cmake
are important, as CMake looks for files named using one of 2 patterns,FooConfig.cmake
andfoo-config.cmake
,FooConfigVersion.cmake
andfoo-config-version.cmake
. ↩︎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 haveMiniSat
(no colons) namespace, leading toMiniSatlibminisat
exported target, or you could useMiniSat::::
forMiniSat::::libminisat
if you like colons too much. ↩︎