BCM

Paul Fultz II

Motivation

This provides cmake modules that can be re-used by boost and other dependencies. It provides modules to reduce the boilerplate for installing, versioning, setting up package config, and creating tests.

Usage

The modules can be installed using standard cmake install:

mkdir build
cd build
cmake ..
cmake --build . --target install

Once installed, the modules can be used by using find_package and then including the appropriate module:

find_package(BCM)
include(BCMDeploy)

Quick Start

Building a boost library

The BCM modules provide some high-level cmake functions to take care of all the cmake boilerplate needed to build, install and configuration setup. To setup a simple boost library we can do:

cmake_minimum_required (VERSION 3.5)
project(boost_config)

find_package(BCM)
include(BCMDeploy)
include(BCMSetupVersion)

bcm_setup_version(VERSION 1.64.0)

add_library(boost_config INTERFACE)
add_library(boost::config ALIAS boost_config)
set_property(TARGET boost_config PROPERTY EXPORT_NAME config)

bcm_deploy(TARGETS config INCLUDE include)

This sets up the Boost.Config cmake with the version 1.64.0. More importantly the user can now install the library, like this:

mkdir build
cd build
cmake ..
cmake --build . --target install

And then the user can build with Boost.Config using cmake’s find_package:

project(foo)

find_package(boost_config)
add_executable(foo foo.cpp)
target_link_libraries(foo boost::config)

Or if the user isn’t using cmake, then pkg-config can be used instead:

g++ `pkg-config boost_config --cflags --libs` foo.cpp

Tests

The BCM modules provide functions for creating tests that integrate into cmake’s ctest infrastructure. All tests can be built and ran using make check. The bcm_test function can add a test to be ran:

bcm_test(NAME config_test_c SOURCES config_test_c.c)

This will compile the SOURCES and run them. The test also needs to link in boost_config. This can be done with target_link_libraries:

target_link_libraries(config_test_c boost::config)

Or all tests in the directory can be set using bcm_test_link_libraries:

bcm_test_link_libraries(boost::config)

And all tests in the directory will use boost::config.

Also, tests can be specified as compile-only or as expected to fail:

bcm_test(NAME test_thread_fail1 SOURCES threads/test_thread_fail1.cpp COMPILE_ONLY WILL_FAIL)

Building

There are two scenarios where the users will consume their dependencies in the build:

  • Prebuilt binaries using find_package
  • Integrated builds using add_subdirectory

When we build libraries using cmake, we want to be able to support both scenarios.

The first scenario the user would build and install each dependency. With this scenario, we need to generate usage requirements that can be consumed by the user, and ultimately this is done through cmake’s find_package mechanism.

In the integrated build scenario, the user adds the sources with add_subdirectory, and then all dependencies are built in the user’s build. There is no need to generate usage requirements as the cmake targets are directly available in the build.

Let’s first look at standalone build.

Building standalone with cmake

Let’s look at building a library like Boost.Filesystem using just cmake. When we start a cmake, we start with minimuim requirement and the project name:

cmake_minimum_required(VERSION 3.5)
project(boost_filesystem)

Then we can define the library and the sources it will build:

add_library(boost_filesystem
    src/operations.cpp
    src/portability.cpp
    src/codecvt_error_category.cpp
    src/utf8_codecvt_facet.cpp
    src/windows_file_codecvt.cpp
    src/unique_path.cpp
    src/path.cpp
    src/path_traits.cpp
)

So this will build the library named boost_filesystem, however, we need to supply the dependencies to boost_filesystem and add the include directories. To add the include directory we use target_include_directories. For this, we tell cmake to use local include directory, but since this is only valid during build and not after installation, we use the BUILD_INTERFACE generator expression so that cmake will only use it during build and not installation:

target_include_directories(boost_filesystem PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)

Using PUBLIC means this include directory will be used internally to build, and downstream users need this include as well. Next, we need to pull in the dependencies. To do this, we call find_package, and for the sake of the turtorial we assume that the upstream boost libraries have already set this up:

find_package(boost_core)
find_package(boost_static_assert)
find_package(boost_iterator)
find_package(boost_detail)
find_package(boost_system)
find_package(boost_functional)
find_package(boost_assert)
find_package(boost_range)
find_package(boost_type_traits)
find_package(boost_smart_ptr)
find_package(boost_io)
find_package(boost_config)

Calling find_package will find those libraries and provide a target we can use to link against. The next step is to link it using target_link_libraries:

target_link_libraries(boost_filesystem PUBLIC
    boost::core
    boost::static_assert
    boost::iterator
    boost::detail
    boost::system
    boost::functional
    boost::assert
    boost::range
    boost::type_traits
    boost::smart_ptr
    boost::io
    boost::config
)

Now, some of these libraries are header-only, but when we call target_link_libraries it will add all the flags necessary to use those libraries. Next step is installation, using the install command:

install(DIRECTORY include/ DESTINATION include)

install(TARGETS boost_filesystem EXPORT boost_filesystem-targets
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)

So this will install the include directories and install the library. The EXPORT command will have cmake generate an export file that will create the target’s usage requirements in cmake. This will enable the target to be used by downstream libraries, just like we used `boost::system. However, this will only tells cmake which targets are in the export file. To generate it we use install(EXPORT):

install(EXPORT boost_filesystem-targets
    FILE boost_filesystem-targets.cmake
    NAMESPACE boost::
    DESTINATION lib/cmake/boost_filesystem
)

This sets a namespace boost:: on the target, but our target is named boost_filesystem, and we want the exported target to be boost::filesystem not boost::boost_filesystem. We can do that by setting the export name:

set_property(TARGET boost_filesystem PROPERTY EXPORT_NAME filesystem)

We can also define a target alias to boost::filesystem, which helps integrated builds:

add_library(boost::filesystem ALIAS boost_filesystem)

So now have exported targets we want to generate a boost_filesystem-config.cmake file so it can be used with find_package(boost_filesystem). To do this we generate a file the includes the export file, but it also calls find_dependency on each dependency so that the user does not have to call it:

file(WRITE "${PROJECT_BINARY_DIR}/boost_filesystem-config.cmake" "
include(CMakeFindDependencyMacro)
find_dependency(boost_core)
find_dependency(boost_static_assert)
find_dependency(boost_iterator)
find_dependency(boost_detail)
find_dependency(boost_system)
find_dependency(boost_functional)
find_dependency(boost_assert)
find_dependency(boost_range)
find_dependency(boost_type_traits)
find_dependency(boost_smart_ptr)
find_dependency(boost_io)
find_dependency(boost_config)
include(\"\${CMAKE_CURRENT_LIST_DIR}/boost_filesystem-targets.cmake\")
")

Besides the boost_filesystem-config.cmake, we also need a version file to check compatibility. This can be done using cmake’s write_basic_package_version_file function:

write_basic_package_version_file("${PROJECT_BINARY_DIR}/boost_filesystem-config-version.cmake"
    VERSION 1.64
    COMPATIBILITY AnyNewerVersion
)

Then finally we install these files:

install(FILES
    "${PROJECT_BINARY_DIR}/boost_filesystem-config.cmake"
    "${PROJECT_BINARY_DIR}/boost_filesystem-config-version.cmake"
    DESTINATION lib/cmake/boost_filesystem
)

Putting it all together we have a cmake file that looks like this:

cmake_minimum_required(VERSION 3.5)
project(boost_filesystem)
include(CMakePackageConfigHelpers)

find_package(boost_core)
find_package(boost_static_assert)
find_package(boost_iterator)
find_package(boost_detail)
find_package(boost_system)
find_package(boost_functional)
find_package(boost_assert)
find_package(boost_range)
find_package(boost_type_traits)
find_package(boost_smart_ptr)
find_package(boost_io)
find_package(boost_config)

add_library(boost_filesystem
  src/operations.cpp
  src/portability.cpp
  src/codecvt_error_category.cpp
  src/utf8_codecvt_facet.cpp
  src/windows_file_codecvt.cpp
  src/unique_path.cpp
  src/path.cpp
  src/path_traits.cpp
)
add_library(boost::filesystem ALIAS boost_filesystem)
set_property(TARGET boost_filesystem PROPERTY EXPORT_NAME filesystem)

target_include_directories(boost_filesystem PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_link_libraries(boost_filesystem PUBLIC
    boost::core
    boost::static_assert
    boost::iterator
    boost::detail
    boost::system
    boost::functional
    boost::assert
    boost::range
    boost::type_traits
    boost::smart_ptr
    boost::io
    boost::config
)


install(DIRECTORY include/ DESTINATION include)

install(TARGETS boost_filesystem EXPORT boost_filesystem-targets
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)

install(EXPORT boost_filesystem-targets
    FILE boost_filesystem-targets.cmake
    NAMESPACE boost::
    DESTINATION lib/cmake/boost_filesystem
)

file(WRITE "${PROJECT_BINARY_DIR}/boost_filesystem-config.cmake" "
include(CMakeFindDependencyMacro)
find_dependency(boost_core)
find_dependency(boost_static_assert)
find_dependency(boost_iterator)
find_dependency(boost_detail)
find_dependency(boost_system)
find_dependency(boost_functional)
find_dependency(boost_assert)
find_dependency(boost_range)
find_dependency(boost_type_traits)
find_dependency(boost_smart_ptr)
find_dependency(boost_io)
find_dependency(boost_config)
include(\"\${CMAKE_CURRENT_LIST_DIR}/boost_filesystem-targets.cmake\")
")

write_basic_package_version_file("${PROJECT_BINARY_DIR}/boost_filesystem-config-version.cmake"
    VERSION 1.64
    COMPATIBILITY AnyNewerVersion
)

install(FILES
    "${PROJECT_BINARY_DIR}/boost_filesystem-config.cmake"
    "${PROJECT_BINARY_DIR}/boost_filesystem-config-version.cmake"
    DESTINATION lib/cmake/boost_filesystem
)

Building standalone with BCM

The boost cmake modules can help reduce the boilerplate needed in writing these libraries. To use these modules we just call find_package(BCM) first:

cmake_minimum_required(VERSION 3.5)
project(boost_filesystem)
find_package(BCM)

Next we can setup the version for the project using bcm_setup_version:

bcm_setup_version(VERSION 1.64)

Next, we add the library and link against the dependencies like always:

find_package(boost_core)
find_package(boost_static_assert)
find_package(boost_iterator)
find_package(boost_detail)
find_package(boost_system)
find_package(boost_functional)
find_package(boost_assert)
find_package(boost_range)
find_package(boost_type_traits)
find_package(boost_smart_ptr)
find_package(boost_io)
find_package(boost_config)

add_library(boost_filesystem
  src/operations.cpp
  src/portability.cpp
  src/codecvt_error_category.cpp
  src/utf8_codecvt_facet.cpp
  src/windows_file_codecvt.cpp
  src/unique_path.cpp
  src/path.cpp
  src/path_traits.cpp
)
add_library(boost::filesystem ALIAS boost_filesystem)
set_property(TARGET boost_filesystem PROPERTY EXPORT_NAME filesystem)

target_link_libraries(boost_filesystem PUBLIC
    boost::core
    boost::static_assert
    boost::iterator
    boost::detail
    boost::system
    boost::functional
    boost::assert
    boost::range
    boost::type_traits
    boost::smart_ptr
    boost::io
    boost::config
)

Then to install, and generate package configuration we just use bcm_deploy:

bcm_deploy(TARGETS boost_filesystem NAMESPACE boost::)

In addition to generating package configuration for cmake, this will also generate the package configuration for pkgconfig.

Integrated builds

As we were setting up cmake for standalone builds, we made sure we didn’t do anything to prevent an integrated build, and even provided an alias target to help ease the process. Finally, to integrate the sources into the build is just a matter of calling add_subdirectory on each project:

file(GLOB LIBS libs/*)
foreach(lib ${LIBS})
    add_subdirectory(${lib})
endforeach()

We could also use add_subdirectory(${lib} EXCLUDE_FROM_ALL) so it builds targets that are not necessary. Of course, every project is still calling find_package to find prebuilt binaries. Since we don’t need to search for those libraries because they are integrated into the build we can call bcm_ignore_package to ignore those dependencies:

file(GLOB LIBS libs/*)

foreach(lib ${LIBS})
    bcm_ignore_package(${lib})
endforeach()

foreach(lib ${LIBS})
    add_subdirectory(${lib})
endforeach()

Of course, this assumes we have conveniently named each directory the same as its package name.

Modules

BCMDeploy

bcm_deploy

This will install targets, as well as generate package configuration for both cmake and pkgconfig.

TARGETS <target-name>...

The name of the targets to deploy.

INCLUDE <directory>...

Include directories to be installed. It also makes the include directory available for targets to be installed.

NAMESPACE <namespace>

This is the namespace to add to the targets that are exported.

COMPATIBILITY <compatibility>

This uses the version compatibility specified by cmake version config.

BCMExport

bcm_auto_export

This generates a simple cmake config file that includes the exported targets.

EXPORT

This specifies an export file. By default, the export file will be named ${PROJECT_NAME}-targets.

NAMESPACE <namespace>

This is the namespace to add to the targets that are exported.

NAME <name>

This is the name to use for the package config file. By default, this uses the project name, but this parameter can override it.

TARGETS <target>...

These include the targets to be exported.

BCMIgnorePackage

bcm_ignore_package

This will ignore a package so that subsequent calls to find_package will be treated as found. This is useful in the superproject of integrated builds because it will ingore the find_package calls to a dependency becaue the targets are already provided by add_subdirectory.

NAME

The name of the package to ignore.

BCMInstallTargets

bcm_install_targets

This installs the targets specified. The directories will be installed according to GNUInstallDirs. It will also install a corresponding cmake package config(which can be found with find_package) to link against the library targets.

TARGETS <target-name>...

The name of the targets to install.

INCLUDE <directory>...

Include directories to be installed. It also makes the include directory available for targets to be installed.

EXPORT

This specifies an export file. By default, the export file will be named ${PROJECT_NAME}-targets.

BCMPkgConfig

bcm_generate_pkgconfig_file

This will generate a simple pkgconfig file.

NAME <name>

This is the name of the pkgconfig module.

LIB_DIR <directory>

This is the directory where the library is linked to. This defaults to ${CMAKE_INSTALL_LIBDIR}.

INCLUDE_DIR <directory>

This is the include directory where the header file are installed. This defaults to ${CMAKE_INSTALL_INCLUDEDIR}.

DESCRIPTION <text>

A description about the library.

TARGETS <targets>...

The library targets to link.

CFLAGS <flags>...

Additionaly, compiler flags.

LIBS <library flags>...

Additional libraries to be linked.

REQUIRES <packages>...

List of other pkgconfig packages that this module depends on.

bcm_auto_pkgconfig

This will auto generate pkgconfig from a given target. All the compiler and linker flags come from the target.

NAME <name>

This is the name of the pkgconfig module. By default, this will use the project name.

TARGET <TARGET>

This is the target which will be used to set the various pkgconfig fields.

BCMProperties

This module defines several properties that can be used to control language features in C++.

CXX_EXCEPTIONS

This property can be used to enable or disable C++ exceptions. This can be applied at global, directory or target scope. At global scope this defaults to On.

CXX_RTTI

This property can be used to enable or disable C++ runtime type information. This can be applied at global, directory or target scope. At global scope this defaults to On.

CXX_STATIC_RUNTIME

This property can be used to enable or disable linking against the static C++ runtime. This can be applied at global, directory or target scope. At global scope this defaults to Off.

CXX_WARNINGS

The CXX_WARNINGS property controls the warning level of compilers. It has the following values:

  • off - disables all warnings.
  • on - enables default warning level for the tool.
  • all - enables all warnings.

Default value is on.

CXX_WARNINGS_AS_ERRORS

The CXX_WARNINGS_AS_ERRORS property makes it possible to treat warnings as errors and abort compilation on a warning. The value on enables this behaviour. The default value is off.

INTERFACE_DESCRIPTION

Description of the target.

INTERFACE_URL

An URL where people can get more information about and download the package.

INTERFACE_PKG_CONFIG_REQUIRES

A list of packages required by this package for pkgconfig. The versions of these packages may be specified using the comparison operators =, <, >, <= or >=.

INTERFACE_PKG_CONFIG_NAME

The name of the pkgconfig package for this target.

BCMSetupVersion

bcm_setup_version

This sets up the project version by setting these version variables:

PROJECT_VERSION, ${PROJECT_NAME}_VERSION
PROJECT_VERSION_MAJOR, ${PROJECT_NAME}_VERSION_MAJOR
PROJECT_VERSION_MINOR, ${PROJECT_NAME}_VERSION_MINOR
PROJECT_VERSION_PATCH, ${PROJECT_NAME}_VERSION_PATCH

It also generates a cmake package config version file as well.

VERSION <major>.<minor>.<patch>

This is the version to be set.

GENERATE_HEADER <header-name>

This is a header which will be generated with defines for the version number.

PREFIX <identifier>

By default, the upper case of the project name is used as a prefix for the version macros that are defined in the generated header: ${PREFIX}_VERSION_MAJOR, ${PREFIX}_VERSION_MINOR, ${PREFIX}_VERSION_PATCH, and ${PREFIX}_VERSION. The PREFIX option allows overriding the prefix name used for the macros.

PARSE_HEADER <header-name>

Rather than set a version and generate a header, this will parse a header with macros that define the version, and then use those values to set the version for the project.

COMPATIBILITY <compatibility>

This uses the version compatibility specified by cmake version config.

NAME <name>

This is the name to use for the package config version file. By default, this uses the project name, but this parameter can override it.

BCMTest

bcm_mark_as_test

This marks the target as a test, so it will be built with the tests target. If BUILD_TESTING is set to off then the target will not be built as part of the all target.

bcm_test

This setups a test. By default, a test will be built and executed.

SOURCES <source-files>...

Source files to be compiled for the test.

CONTENT <content>

This a string that will be used to create a test to be compiled and/or ran.

NAME <name>

Name of the test.

ARGS <args>

This sets additional arguments to be passed to the test executable when it will be ran.

COMPILE_ONLY

This just compiles the test instead of running it. As such, a main function is not required.

WILL_FAIL

Specifies that the test will fail.

NO_TEST_LIBS

This won’t link in the libraries specified by bcm_test_link_libraries

bcm_test_header

This creates a test to test the include of a header.

NAME <name>

Name of the test.

HEADER <header-file>

The header to include.

STATIC

Rather than just test the include, using STATIC option will test the include across translation units. This helps check for incorrect include guards and duplicate symbols.

NO_TEST_LIBS

This won’t link in the libraries specified by bcm_test_link_libraries

bcm_add_test_subdirectory

This calls add_subdirectory if the ENABLE_TESTS property is true. The default value for the property is set by CMAKE_ENABLE_TESTS variable.