Cmake install prefix windows

cmake_minimum_required() —

Alternative Methods to cmake_minimum_required in CMake

It ensures that the build system has the features and compatibility required by the project’s build instructions.This command specifies the minimum required version of CMake that is necessary to build the project

file() —

Alternatives to ‘file()’ Command for File Manipulation in CMake

file(TIMESTAMP filename variable [format] [UTC]): This retrieves the timestamp (modification time) of the file and stores it in a variable

add_definitions() —

CMake add_definitions(): A Deep Dive into Preprocessor Macro Management

Influence Compilation Behavior These macros can significantly affect how your code is compiled, enabling features like: Conditional Compilation Control which parts of your code are included or excluded based on the defined macros

find_path() —

Beyond find_path(): Alternative Methods in CMake

Locates executable files find_path() is primarily used to find the full path to an executable file within the system. This is crucial when your CMake project needs to:

add_custom_command() —

Beyond add_custom_command(): Exploring CMake Alternatives

It’s highly versatile and lets you execute arbitrary commands during the build process.add_custom_command() is a crucial CMake command that allows you to define custom build steps that don’t fit the standard target-based approach

FetchContent —

Improving CMake Builds with FetchContent: Best Practices and Tips

Key Benefits Simplified Dependency Management No need to manually download, extract, and configure external libraries. FetchContent handles the entire process

cmake_path() —

Alternatives to cmake_path() in CMake Projects

It deals with paths in the format of the build system (the host platform where you’re running CMake), not the target system (where your compiled code will run)

CMAKE_INSTALL_PREFIX variable can be used to control destination directory
of install procedure:

cmake_minimum_required(VERSION 2.8)
project(foo)

add_library(foo foo.cpp)

install(TARGETS foo DESTINATION lib)
[install-examples]> rm -rf _builds
[install-examples]> cmake -Hsimple -B_builds -DCMAKE_INSTALL_PREFIX=_install/config-A
[install-examples]> cmake --build _builds --target install
Scanning dependencies of target foo
[ 50%] Building CXX object CMakeFiles/foo.dir/foo.cpp.o
[100%] Linking CXX static library libfoo.a
[100%] Built target foo
Install the project...
-- Install configuration: ""
-- Installing: /.../install-examples/_install/config-A/lib/libfoo.a

[install-examples]> cmake -Hsimple -B_builds -DCMAKE_INSTALL_PREFIX=_install/config-B
[install-examples]> cmake --build _builds --target install
[100%] Built target foo
Install the project...
-- Install configuration: ""
-- Installing: /.../install-examples/_install/config-B/lib/libfoo.a

3.17.9.1. Modify¶

This variable is designed to be modified on user side. Do not force it in
code!

cmake_minimum_required(VERSION 2.8)
project(foo)

set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/3rdParty/root") # BAD CODE!

add_library(foo foo.cpp)

install(TARGETS foo DESTINATION lib)
[install-examples]> rm -rf _builds
[install-examples]> cmake -Hmodify-bad -B_builds -DCMAKE_INSTALL_PREFIX="`pwd`/_install"
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /.../install-examples/_builds

Library unexpectedly installed to 3rdparty/root instead of _install:

[install-examples]> cmake --build _builds --target install
Scanning dependencies of target foo
[ 50%] Building CXX object CMakeFiles/foo.dir/foo.cpp.o
[100%] Linking CXX static library libfoo.a
[100%] Built target foo
Install the project...
-- Install configuration: ""
-- Installing: /.../install-examples/_builds/3rdParty/root/lib/libfoo.a

Note

Use CACHE in such case

3.17.9.2. On the fly¶

Make do support changing of install directory on the fly by DESTDIR:

[install-examples]> rm -rf _builds
[install-examples]> cmake -Hsimple -B_builds -DCMAKE_INSTALL_PREFIX=""
[install-examples]> make -C _builds DESTDIR="`pwd`/_install/config-A" install
...
Install the project...
-- Install configuration: ""
-- Installing: /.../install-examples/_install/config-A/lib/libfoo.a
make: Leaving directory '/.../install-examples/_builds'

[install-examples]> make -C _builds DESTDIR="`pwd`/_install/config-B" install
...
Install the project...
-- Install configuration: ""
-- Installing: /.../install-examples/_install/config-B/lib/libfoo.a
make: Leaving directory '/.../install-examples/_builds'

3.17.9.3. Read¶

Because of the DESTDIR feature, CPack functionality, different nature of
build and install stages often usage of CMAKE_INSTALL_PREFIX variable
on configure step is an indicator of wrongly written code:

cmake_minimum_required(VERSION 2.8)
project(foo)

add_library(foo foo.cpp)

install(TARGETS foo DESTINATION lib)

# BAD CODE!
file(
    COPY
    "${CMAKE_CURRENT_LIST_DIR}/README"
    DESTINATION
    "${CMAKE_INSTALL_PREFIX}/share/foo"
)

include(CPack)

User may not want to install such project at all, so copying of file to root
is something unintended and quite surprising. If you’re lucky you will get
problems with permissions on configure step instead of a silent copy:

[install-examples]> rm -rf _builds
[install-examples]> cmake -Hwrong-usage -B_builds
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Error at CMakeLists.txt:9 (file):
  file COPY cannot copy file
  "/.../install-examples/wrong-usage/README"
  to "/usr/local/share/foo/README".

-- Configuring incomplete, errors occurred!
See also "/.../install-examples/_builds/CMakeFiles/CMakeOutput.log".

CPack will use separate directory for install so README will not be included
in archive:

[install-examples]> rm -rf _builds _install
[install-examples]> cmake -Hwrong-usage -B_builds -DCMAKE_INSTALL_PREFIX="`pwd`/_install"
[install-examples]> (cd _builds && cpack -G TGZ)
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: foo
CPack: - Install project: foo
CPack: Create package
CPack: - package: /.../install-examples/_builds/foo-0.1.1-Linux.tar.gz generated.
[install-examples]> tar xf _builds/foo-0.1.1-Linux.tar.gz
[install-examples]> find foo-0.1.1-Linux -type f
foo-0.1.1-Linux/lib/libfoo.a

3.17.9.4. Implicit read¶

All work should be delegated to install command instead, in such case
CMAKE_INSTALL_PREFIX will be read implicitly:

cmake_minimum_required(VERSION 2.8)
project(foo)

add_library(foo foo.cpp)

install(TARGETS foo DESTINATION lib)
install(FILES README DESTINATION share/foo)

include(CPack)
[install-examples]> rm -rf _builds _install
[install-examples]> cmake -Hright-usage -B_builds -DCMAKE_INSTALL_PREFIX="`pwd`/_install"
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /.../install-examples/_builds

Correct install directory:

[install-examples]> cmake --build _builds --target install
Scanning dependencies of target foo
[ 50%] Building CXX object CMakeFiles/foo.dir/foo.cpp.o
[100%] Linking CXX static library libfoo.a
[100%] Built target foo
Install the project...
-- Install configuration: ""
-- Installing: /.../install-examples/_install/lib/libfoo.a
-- Installing: /.../install-examples/_install/share/foo/README

Correct packing:

[install-examples]> (cd _builds && cpack -G TGZ)
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: foo
CPack: - Install project: foo
CPack: Create package
CPack: - package: /.../install-examples/_builds/foo-0.1.1-Linux.tar.gz generated.
[install-examples]> tar xf _builds/foo-0.1.1-Linux.tar.gz
[install-examples]> find foo-0.1.1-Linux -type f
foo-0.1.1-Linux/share/foo/README
foo-0.1.1-Linux/lib/libfoo.a

3.17.9.5. Install script¶

Same logic can be applied if CMAKE_INSTALL_PREFIX used in script created
by configure_file command:

# Top-level CMakeLists.txt

cmake_minimum_required(VERSION 2.8)
project(foo)

set(script "${CMAKE_CURRENT_BINARY_DIR}/script.cmake")
configure_file(script.cmake.in "${script}" @ONLY)

install(SCRIPT "${script}")

include(CPack)
# script.cmake.in

cmake_minimum_required(VERSION 2.8)

set(correct "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}")

message("Incorrect value: '@CMAKE_INSTALL_PREFIX@'")
message("Correct value: '${correct}'")

file(WRITE "${correct}/share/foo/info" "Some info")

Configure for DESTDIR usage:

[install-examples]> rm -rf _builds _install foo-0.1.1-Linux
[install-examples]> cmake -Hconfigure -B_builds -DCMAKE_INSTALL_PREFIX=""
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /.../install-examples/_builds

DESTDIR read correctly:

[install-examples]> make DESTDIR="`pwd`/_install/config-A" -C _builds install
make: Entering directory '/.../install-examples/_builds'
Install the project...
-- Install configuration: ""
Incorrect value: ''
Correct value: '/.../install-examples/_install/config-A'
make: Leaving directory '/.../install-examples/_builds'
[install-examples]> find _install/config-A -type f
_install/config-A/share/foo/info

Changing directory on the fly:

[install-examples]> make DESTDIR="`pwd`/_install/config-B" -C _builds install
make: Entering directory '/.../install-examples/_builds'
Install the project...
-- Install configuration: ""
Incorrect value: ''
Correct value: '/.../install-examples/_install/config-B'
make: Leaving directory '/.../install-examples/_builds'
[install-examples]> find _install/config-B -type f
_install/config-B/share/foo/info

Regular install:

[install-examples]> rm -rf _builds _install
[install-examples]> cmake -Hconfigure -B_builds -DCMAKE_INSTALL_PREFIX="`pwd`/_install"
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /.../install-examples/_builds
[install-examples]> cmake --build _builds --target install
Install the project...
-- Install configuration: ""
Incorrect value: '/.../install-examples/_install'
Correct value: '/.../install-examples/_install'
[install-examples]> find _install -type f
_install/share/foo/info

Packing:

[install-examples]> (cd _builds && cpack -G TGZ)
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: foo
CPack: - Install project: foo
Incorrect value: '/.../install-examples/_install'
Correct value: '/.../install-examples/_builds/_CPack_Packages/Linux/TGZ/foo-0.1.1-Linux'
CPack: Create package
CPack: - package: /.../install-examples/_builds/foo-0.1.1-Linux.tar.gz generated.
[install-examples]> tar xf _builds/foo-0.1.1-Linux.tar.gz
[install-examples]> find foo-0.1.1-Linux -type f
foo-0.1.1-Linux/share/foo/info

3.17.9.6. Summary¶

  • Do not force value of CMAKE_INSTALL_PREFIX

  • Use of CMAKE_INSTALL_PREFIX on configure, generate, build steps is an
    indicator of badly designed code

  • Use install instead of CMAKE_INSTALL_PREFIX

  • Respect DESTDIR

Вступление

Популярность CMake растёт. Многие крупные проекты переходят с собственных инструментов для сборки на CMake. Проект Conan предлагает интеграцию с CMake для управления зависимостями.

Разработчики CMake активно развивают инструмент и добавляют новые функции, решающие общие проблемы, возникающие при сборке проектов. Переход с CMake 2 на CMake 3 был достаточно болезнен. Документация не покрывает все аспекты использования. Функционал крайне обширен, а возникающие трудности различаются от проекта к проекту. В статье я расскажу о инструментах, которые предлагает CMake 3.10 и выше. В каждой новой версии появляются новые детали или улучшаются старые. Об актуальном состоянии лучше проанализировать Changelog, так как многие улучшения последних версий весьма специфичны для отдельных проектов, как например улучшение поддержки Cuda компиляторов. Я же сфокусируюсь на общих понятиях, которые помогут организовать проект на С++ оптимальным образом с точки зрения использования CMake как основной системы сборки.

CMake предлагает широкий набор инструментов. Чтобы не потеряться в определениях, в статье сперва будут определены сущности, через которые будут объяснены конкретные примеры. Названия сущностей я продублирую на английском. Некоторые термины не имеют однозначного перевода на русский язык.

Обзор демонстрационного проекта

Примеры проблем при сборке проекта и рекомендуемые CMake пути решения взяты из моего проекта библиотеки, использующей нейронные сети для определения синхронизации между речью и изображением.

Проект написан на С++, предполагает кросс-платформенность. Содержит переиспользуемые компоненты и зависит от внешних библиотек. Предполагаемая область использования — интеграция в конечное приложение, выполняющее анализ видеопотока, поэтому проект также содержит демонстрационное приложение с примером использования API. Демо-приложение поставляется в исходных кодах вместе с двоичной библиотекой.

Структура файлов и папок проекта с именем ‘neuon’ следующая:

neuon
|-sources
  |-res
  |-src
  |-CMakeLists.txt
|-headers
  |-include
  |-CMakeLists.txt
|-examples
  |-res
  |-src
  |-CMakeLists.txt
|-cmake
  |-FindTensorflow.cmake
  |-FindJsonCpp.cmake
|-res
	|-neoun-config.cmake.in
|-CMakeLists.txt

Используемые сущности и понятия

  • Приложения в комплекте поставки (CMake, CPack, CTest, CDash)

  • Предопределенные и ожидаемые файлы и их назначение

  • Модули, конфигурации

  • Объектная модель скриптов — цели, свойства

  • Свойства, переменные, аргументы и параметры.

Приложения в комплекте поставки (CMake, CPack, CTest, CDash)

CMake поставляет в комплекте с несколькими приложениями, предназначенными для дополнения процесса сборки проекта. За саму сборку отвечает cmake, который работает с файлом, описывающим проект. Использование cmake состоит из нескольких шагов: шаг генерации(generation), шаг сборки(build) и шаг установки(install).

Первым этапом при сборке проекта необходимо сгенерировать скрипты сборки, используя описание проекта(CMakeLists.txt) на языке CMake. Результатом генерации является скрипт или набор файлов, необходимый для запуска нижележащей системы сборки(например Makefile или VIsual Studio Solution). CMake не выполняет запуск компилятора сам, хотя может быть использован как прокси для обобщения вызовов нижележащих инструментов.

Следующим шагом происходит непосредственно сборка проекта из исходного кода. Результатом являются бинарные файлы и другие артефакты, являющиеся частью конечного продукта для потребителя. Все артефакты, несмотря на свою доступность, находятся в специальном состоянии, контролируемом CMake и как правило непригодны к перемещению или распространению, однако, могут быть использованы для отладки локально.

Финальным шагом использования СMake является шаг установки. Установка подразумевает пересборку либо по необходимости перелинковку бинарных артефактов с целью сделать их пригодными к использованию на целевой системе. Как правило, при установке также происходит перемещение артефактов в желаемые расположения и создании желаемой раскладки по директориям. Установка на этом этапе не имеет ничего общего с установкой дистрибутива или распаковкой архива, но при некорректной конфигурации сборки может установить свежесобранные артефакты в локальную систему. При использовании шаг установки не выполняется вручную.

Базовый функционал CMake не заканчивается на консольном приложении. CMake также предоставляет графический интерфейс для управления этапами генерации и сборки — cmake-gui. А также предлагает широкий набор модулей(CMake modules) для использования в файлах описания проекта. Посторонние проекты могут предлагать свои конфигурации(Library Configs) для упрощения использования в связке с CMake.

CMake крайне гибкий и расширяемый инструмент. И цена за это — его перегруженность. Если проекту нужен какой-то функционал при сборке — имеет смысл изучить, что предлагает СMake. Может быть такая же проблема уже была решена и решение прошло проверку сообществом.

Приложение CTest расширяет возможности по сборке путем предоставления единого интерфейса по взаимодействию с тестами. Если проект содержит тесты, ctest играет роль единого запускающего механизма, формирующего отчет по запуску. Для использования CTest как универсального исполнителя каждый тест должен быть зарегистрирован. Согласно его регистрации, в финальный отчет попадет имя и результат выполнения теста. CTest также обеспечивает интеграцию с CDash — панелью общего доступа к результатам запусков тестов, с управлением запусками, группировкой тестов и прочим функционалом, использующимся в автоматизированный конвейерах сборки. (CI/CD Dashboard).

CPack — это инструмент упаковки скомпилированного проекта в платформо-зависимые пакеты и установщики. CPack c одной стороны универсален для создания установщиков целевого формата, с другой стороны зависит от системы, где запускается, так как полагается на системные инструменты для создания установщика. Единый формат командной строки для генерации NSIS установщика для Windows, DEB пакета для Ubuntu и RPM пакета для Centos не подразумевает генерацию RPM при запуске на Ubuntu или Windows. Основным преимуществом CPack перед отдельными упаковщиками является то, что вся конфигурация установщика находится рядом с проектом, и использует те же механизмы, что и сам CMake. Для добавления нового формата в проект достаточно доопределить формато-специфичные переменные и CPack сделает остальную работу.

Предопределенные и ожидаемые файлы и их назначение.

CMake широко использует файловую систему и управляет многими папками и файлами. CMake поддерживает сборку вне расположения проекта. Местоположение проекта называется исходной папкой( Source Directory). При генерации CMake сохраняет файлы в папку сборки (Build Directory). В некоторых контекстах папка сборки также именуется папкой бинарных артефактов(Binary Directory), как противопоставление исходной папке. При выполнении шага установки начинает фигурировать директория установки(Install directory). Эти местоположения могут указывать в одно место, или в разные — это контролируется независимо и доступно при использовании.

Помимо непосредственно файловой системы, CMake использует механизм префиксов для гибкой настройки относительных путей. Префикс установки((CMAKE_INSTALL_PREFIX)) определяет префикс, который будет использоваться для всех относительных путей, используемых проектом после установки. . Префикс установки и директория установки могут быть одинаковы. Но могут и различаться, что используется для кросс-компиляции или создания переносимых артефактов, не привязанных к конкретному местоположению. Префикс-путь (CMAKE_PREFIX_PATH) имеет особенный смысл для шага генерации. Как гласит документация, это список путей, которые СМаке использует в дополнение к своему местоположению как корневую папку для поиска модулей и расширений. При необходимости указать дополнительные места, используется префикс-путь. Он не влияет на результат сборки, однако крайне важен для поиска зависимостей, интеграции со сторонними библиотеками и использования модулей, не входящих в комплект поставки CMake либо являющихся частью проекта.

CMake использует некоторые предопределённые имена или части имени файла, наделяя эти файлы особенным смыслом.

Поддержка CMake в проектах обеспечивается CMakeLists.txt файлом, обычно расположенным в корне проекта. Этих файлов может быть несколько и они могут быть включены друг в друга различными способами. Основная идея этого файла — он предоставляет главную точку входа для CMake и описание проекта начинается с него.

В папке сборки после генерации можно найти файл CMakeCache.txt. Этот файл содержит полное представление всего проекта, которое CMake смог разобрать и сгенерировать. Этот файл может быть отредактирован вручную, для изменения некоторых параметров и переменных. Однако, при следующей генерации изменения могут быть утеряны, если параметры и аргументы запуска CMake утилиты изменились.

Файлы, имена которых регистро-зависимо оканчиваются на Config.cmake или -config.cmake, являются файлами CMake совместимой конфигурации библиотек и зависимостей. (CMake config files). Эти файлы как правило распространяются вместе с библиотеками и обеспечивают интеграции библиотек в проекты использующие CMake для сборки.

Файлы, имена который выглядят как Find*.cmake , содержат CMake модули(CMake modules). Модули расширяют функционал CMake, и используются в CMakeLists.txt для выполнения рутинных задач. Модули следует использовать как фреймворк или библиотеку функций при написании своего CMakeLists.txt

Другие файлы с расширением .cmake предполагают произвольное содержимое написанное с использованием CMake скриптового языка. Проекты включают такие файлы в целях переиспользования частей скриптов.

Иногда в CMake проектах могут встретиться файлы с расширением .in или .in.in Таким образом могут именоваться шаблоны файлов, которые инстанцируются CMake при генерации. Инстанцированные файлы, затем используются проектом как текстовые артефакты, либо как файлы, актуальные в момент генерации и сборки. Например они могут содержать в себе версии и дату сборки бинарных артефактов, или шаблон CMake конфигурации, которая будет распространятся с артефактами позже.

Модули, конфигурации

CMake Модули и CMake конфигурации содержат в себе код на CMake скрипте. Несмотря на схожесть, это разные по назначению сущности. Модули, обычно расширяют поведение CMake, предоставляя функции, макросы и алгоритмы для использования в CMakeLists.txt, поставляются в комплекте с CMake, именуются как Find*.cmake и располагаются в CMAKE_PREFIX_PATH. Модули не привязаны к конкретному проекту и должны быть проектно-независимы. Проект может дополнять список модулей по необходимости при генерации или при использовании, но рекомендуется публиковать модули с целью включить их в состав дистрибутива CMake. Модули играют роль «Стандартной библиотеки CMake». Модули поддерживаются и разрабатываются мейнтейнерами CMake.

Конфигурации же напротив, неотъемлемая часть проекта, поставляются вместе с заголовочными файлами и библиотеками импорта, и обеспечивают интеграцию библиотек в другие проекты. Конфигурация проекта не используется при сборке проекта. Конфигурация является результатом сборки проекта и входит в установщик, поддерживаются и разрабатываются авторами проекта.

Конфигурации не должны располагаться в CMAKE_PREFIX_PATH и должны следовать соглашению о местоположении и именовании, описанном в официальной документации. (https://cmake.org/cmake/help/latest/command/find_package.html#full-signature-and-config-mode)

Конфигурации описывают зависимости проекта, с целью облегчить его использование. Конфигурации крайне чувствительны к своей реализации. Следование практикам и рекомендациям поможет делать конфигурации, которые облегчают интеграции проекта заказчикам и пользователям.

Объектная модель скриптов — цели, свойства

Начиная с версии 3 CMake сместил парадигму своих скриптов с процедурно-ориентированной на объекто-ориентированную. В версии 2, описание проекта содержало набор вызовов функций, которые выполняли настройку свойств, необходимых для сборки и использования. В версии 3, на смену переменным и свойствам пришли цели. Цель(target) в контексте CMake, это некоторая сущность, над которой можно выполнять операции, изменять ее свойства, обеспечивать ее доступность и готовность. Целями могут являются, но не ограничиваются, бинарные артефакты и исполняемые файлы проекта, заголовочные файлы проекта, дополнительные файлы, создаваемые при генерации, зависимости целей, внешние библиотеки или файлы и т.д.

У целей есть свойства(properties), которые могут быть доступны как для чтения(все из них), так и для записи(многие), с целью тонкой настройки и обеспечения желаемого результата. Свойства — это именованные поля целей. Управляются CMake на основе дефолтных значений параметров и аргументов и\или окружения сборки, а также управляются сами проектом, в зависимости от назначения и желаемого результата.

Свойства, переменные, аргументы и параметры.

CMake обеспечивает гибкость настройки и использования предоставляя разные способы контроля и управления своим поведением.

Цели можно контролировать их свойствами. Командная строка при запуске CMake утилит может содержать аргументы командной строки(arguments), передаваемые напрямую в утилиту общепринятым способом --argument или -f lag . В командной строке также могут встречаться параметры(parameters). Параметры, передаваемые в командной строке через -DNAME=VALUE или -DNAME:TYPE=VALUE преобразуются в переменные(variables) с тем же именем в теле скрипта. Также параметрами могут являться переменные окружения или некоторые вхождения CMake переменных в файле CMakeCache.txt. Параметры от переменных в CMakeCache.txt практически ничем не отличаются.

CMake переменные — это переменные в общепринятом смысле слова при написании скриптов. Переменные могут быть объявлены или не быть объявлены. Могут иметь значение или не иметь значение, или иметь пустое значение. Помимо переменных унаследованных от параметров(или значений по умолчанию), в скриптах можно объявлять и использовать свои собственные переменные.

Таким образом, управление CMake утилитами выполняется через аргументы командной строки, параметры запуска, переменными в скриптах, и свойствами целей. Аргументы не оказывают влияние на состояние скриптов, параметры превращаются в переменные с тем же именем, свойства целей имеют предустановленное документированное имя и могут быть установлены в произвольные значения через вызов соответствующих функций.

Примеры задач и использование доступных инструментов

У меня в руках проект, который использует CMake. Что с ним делать?

Сгенерировать сборочные файлы в папке сборки, запустить сборку, опционально запустить установку.

cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release -Sneuon -Bcmake-release-build
cmake --build cmake-release-build -- -j8
cmake --build cmake-release-build --target install

В этом примере мы собираем релизную сборку проекта в отдельной директории. Релиз в данной ситуации подразумевает включение оптимизаций и удаления отладочной информации из бинарного артефакта. CMake имеет 5 известных значений CMAKE_BUILD_TYPE параметра: Debug, Release, RelWithDebInfo, MinSizeRel и None — если не установить конкретное значение. Также мы явно указываем CMAKE_INSTALL_PREFIX /usr/local является значением по умолчанию для Unix систем, а так как запись в эту директорию требует прав суперпользователя, то последняя команда установки вернет ошибку так как не сможет записать файлы по назначению. Следует либо запустить её с правами суперпользователя(что крайне не рекомендуется, если целевая платформа имеет пакетные менеджеры), либо сменить префикс установки в место, не требующее прав суперпользователя, либо не устанавливать проект в систему, либо установить его с использованием переназначения назначения. Для make как нижележащей системы можно использовать DESTDIR: cmake --build cmake-release-build --target install -- DESTDIR=$HOME/neuon

Переназначение назначения зависит от собственно системы сборки и не каждая из них умеет такое делать.

Пример сборки под Linux из реального проекта:

# Локальная сборка зависимости проекта и установка в отдельную директорию, 
# которая будет использоваться при сборке основного проекта.CMake автоматически собирает проект
# при установке по необходимости.
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=On -DCMAKE_INSTALL_PREFIX=/opt/neuon -DCMAKE_BUILD_TYPE=Release -s googletest -Bgoogletest-build
cmake --build googletest-build --target install -- DESTDIR=$PWD/deps -j8

# Сборка и упаковка в TGZ основного проекта в 8 потоков make.
cmake -DCMAKE_PREFIX_PATH=$PWD/deps -DCMAKE_INSTALL_PREFIX=/opt/neuon -DCPACK_SET_DEST_DIR=On -DCMAKE_BUILD_TYPE=Release -Dversion=0.0.0 -S neuon -B neuon-build
cmake --build neuon-build -- -j8
cd neuon-build && cpack -G "TGZ" 
# CPack на конец 2020 не поддерживает -B аргумент. Необходимо запускать в папке сборки

Для генерации под Windows, используя Visual Studio:

cmake -G "Visual Studio 16 2019" -A x64 -T host=x64 -DBUILD_SHARED_LIBS=On -DCMAKE_PREFIX_PATH=d:\msvc.env\ -DCMAKE_INSTALL_PREFIX=/neuon -DFFMPEG_ROOT=d:\msvc.env\ffmpeg -S neuon -B neuon-build

Как запустить тесты с CTest?

После генерации запустить CTest в папке сборки.

ctest .

По умолчанию, CTest не выводит ход выполнения тестов. Аргументы вызова помогут достигнуть желаемого поведения.

Как пользоваться целями и свойствами?

add_library(neuon
    ${CMAKE_CURRENT_BINARY_DIR}/generated/version.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/generated/birthday.cpp
    src/license.cpp
    src/model.cpp
    src/tensorflow_api.cpp
    src/tensorflow_dynamic.cpp
    src/tensorflow_static.cpp
    src/neuon_c.cpp
    src/neuon_cxx.cpp
    src/demo.cpp
    src/configuration.cpp
    src/speech_detection.cpp
    src/face_detection.cpp
    src/log.cpp
    )
add_library(neuon::neuon ALIAS neuon)
target_link_libraries(neuon PRIVATE Threads::Threads JsonCpp::JsonCpp Boost::headers Aquila::Aquila dlib::dlib Tensorflow::Tensorflow Boost::filesystem spdlog::spdlog PUBLIC neuon::headers )
target_compile_features(neuon PRIVATE cxx_std_17)
set_target_properties(neuon PROPERTIES CXX_EXTENSIONS OFF)
set_target_properties(neuon PROPERTIES INSTALL_RPATH "$ORIGIN")
set_target_properties(neuon PROPERTIES C_VISIBILITY_PRESET hidden)
set_target_properties(neuon PROPERTIES CXX_VISIBILITY_PRESET hidden)
set_target_properties(neuon PROPERTIES VISIBILITY_INLINES_HIDDEN On)
target_compile_definitions(neuon PRIVATE BOOST_UBLAS_INLINE= NEUON_ORIGINAL_BUILD )
target_include_directories(neuon
    PRIVATE src/
    ../depends/sdk/src
    ${CMAKE_CURRENT_BINARY_DIR}/generated
    )

install(TARGETS neuon EXPORT neuon-library
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT devel
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT devel NAMELINK_COMPONENT devel
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
    OBJECTS DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT devel
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    )
install(EXPORT neuon-library NAMESPACE neuon:: DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/neuon/cmake FILE neuon-targets.cmake COMPONENT devel)
install(FILES res/model.pb res/normale.json res/shape_predictor_68_face_landmarks.dat DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/neuon/res COMPONENT runtime)

Добавляем цель сборки — библиотеку. Не указываем тип библиотеки — разделяемая или статическая, оставляя на усмотрение собирающего. Наш конвейер сборки может собирать два варианта двумя разными вызовами генерации. Затем добавляем цель-псевдоним, это позволяет использовать пространство имен в CMake конфигурации проекта для более выразительного именования зависимостей. Цель-псевдоним используется тут же в проекте демо-приложением, которое при этом умеет использовать и файл конфигурации. Использование пространства имен в файле конфигурации поместит все цели в него. Без псевдонима, демо-приложение должно будет явно отличать связываться с целью родительского проекта, или с целью из конфигурации.

Указываем список библиотек и других зависимостей, с которыми необходимо линковать нашу библиотеку, и выставляем различные свойства, влияющие на параметры компиляции. В последней части, указываем как именно мы хотим устанавливать нашу библиотеку — установка цели описывается через введение экспортируемой сущности, для которой прописываются свойства, такие как папка назначения в зависимости от конкретного типа файла или типа цели или компонент для разделения установщиков на отдельные пакеты или наборы устанавливаемых файлов. Затем указывается установка экспортируемой сущности с указанием пространства имен для используемых целей.

И напоследок несколько файлов устанавливаются как есть. Это ресурсы, которые распространяются как часть проекта.

Как использовать файлы конфигурации?

Начиная с CMake 3.0 рекомендуется в файлах конфигурации объявлять цели, которые могут быть использованы потребителем. До CMake 3.0 файлы конфигурации объявляли несколько переменных, именованных по определенным правилам и эти переменные становились доступны потребителю. На настоящий момент, некоторые проект до сих пор используют только старый подход, некоторые проекты используют старый подход как дополнение к новому. Иногда появляются инициативы завернуть старые конфигурации в новые или в CMake модули. В этом примере мы уделяем внимание новому подходу, как рекомендуемому.

find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale swresample)

if(NOT TARGET neuon::neuon)
    find_package(Neuon REQUIRED COMPONENTS headers neuon)
endif()

add_executable(neuon-sample
    ${CMAKE_CURRENT_BINARY_DIR}/generated/version.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/generated/birthday.cpp
    src/neuon_sample.cpp
    depends/sdk/src/extraction.cpp
    depends/sdk/src/options.cpp
    depends/sdk/src/source.cpp
    depends/sdk/src/demuxer.cpp
    depends/sdk/src/decoder.cpp
    depends/sdk/src/interruption.cpp
    depends/sdk/src/track_adapter.cpp
    depends/sdk/src/access_unit_adapter.cpp
    depends/sdk/src/resample.cpp
    )
target_link_libraries(neuon-sample PRIVATE spdlog::spdlog Threads::Threads Boost::program_options FFMPEG::avcodec FFMPEG::avformat FFMPEG::swscale FFMPEG::swresample neuon::neuon)
target_compile_features(neuon-sample PRIVATE cxx_std_11)
set_target_properties(neuon-sample PROPERTIES CXX_EXTENSIONS OFF)
target_include_directories(neuon-sample
    PRIVATE src/
    depends/sdk/src
    ${CMAKE_CURRENT_BINARY_DIR}/generated
    )

find_package ищет файлы конфигурации, руководствуясь предопределенными правилами разрешения путей. Если этот CMakeLists.txt не включен в основной проект, то neuon::neuon цель будет недоступна и нам требуется явно подключить библиотеку штатным способом. В обратном случае — цель-псевдоним обеспечит нам идентичный функционал и наше приложение будет слинковано с библиотекой из той же директории сборки.

Мы заранее ввели цель-псевдоним в основном проекте библиотеки для универсальности нашего CMakeLists.txt в проекте демо-приложения. Теперь при сборке нашего демо-приложения как части сборки проекта — будет использоваться цель-псевдоним, а при сборке пользователем наша библиотека будет доступна через имя определенное в конфигурации, дополненное пространством имён.

Как добавить свои тесты в запуск CTest?

enable_testing()

find_package(GTest 1.8 CONFIG REQUIRED COMPONENTS gtest gmock gmock_main )
include(GoogleTest)

add_executable(ut_curl test/ut_curl.cpp src/curl.cpp)
target_link_libraries(ut_curl PRIVATE GTest::gmock GTest::gmock_main CURL::libcurl)
target_include_directories(ut_curl PRIVATE src/)

add_test(test_of_curl_wrapper ut_curl)

enable_testing() указывает CMake, что планируется использование CTest. add_test() регистрирует исполняемый файл, собираемый в проекте как один из тестов для запуска. Тестом может быть любая исполняемая сущность — приложение, скрипт, сторонний инструмент. CTest опирается на код возврата для определения пройден тест или нет и формирует соответственный отчет.

enable_testing()
find_package(GTest 1.8 CONFIG REQUIRED COMPONENTS gtest gmock gmock_main )
include(GoogleTest)

add_executable(ut_curl test/ut_curl.cpp src/curl.cpp)
target_link_libraries(ut_curl PRIVATE GTest::gmock GTest::gmock_main CURL::libcurl)
target_include_directories(ut_curl PRIVATE src/)
gtest_discover_tests(ut_curl)

CMake предлагает готовый модуль для работы с Google Test Framework. Если ваши тесты используют Googletest, то в исполняемом файле обычно множество юнит-тестов. Регистрация штатным способом не даст полной картины запуска юнит-тестов, так как с точки зрения CMake — одно приложение, один тест. include(GoogleTest) подключает стандартный модуль, который содержит функцию gtest_discover_tests, которая регистрирует все тесты из собранного тестового приложения как отдельные тесты в CTest. Отчет становится гораздо более информативным.

Как конфигурировать CPack?

include(CPackComponent)

cpack_add_component(runtime)
cpack_add_component(devel DEPENDS runtime)
cpack_add_component(sample DEPENDS runtime devel)

set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY 0)
set(CPACK_PACKAGE_CONTACT "info@example.com")

set(CPACK_ARCHIVE_COMPONENT_INSTALL On)

set(CPACK_RPM_COMPONENT_INSTALL On)
set(CPACK_RPM_PACKAGE_AUTOREQ On)
set(CPACK_RPM_PACKAGE_AUTOREQPROV Off)
set(CPACK_RPM_DEVEL_PACKAGE_REQUIRES "blas, lapack, atlas, jsoncpp-devel")
set(CPACK_RPM_SAMPLE_PACKAGE_REQUIRES "ffmpeg-devel, jsoncpp-devel")

set(CPACK_DEB_COMPONENT_INSTALL On)
set(CPACK_DEBIAN_DEVEL_PACKAGE_DEPENDS "libopenblas-base, libblas3, libjsoncpp-dev, libjsoncpp1, libopenblas-dev")
set(CPACK_DEBIAN_SAMPLE_PACKAGE_DEPENDS "libavformat-dev, libavcodec-dev, libswscale-dev, libswresample-dev, libavutil-dev, libopenblas-dev")

include(CPack)

Подключив требуемые модули расширяющие CPack, необходимо выставить значения задокументированных переменных для каждого конкретного формата установщика или пакета. Некоторые значения берутся из переменных и параметров CMake, некоторые могут быть заполнены автоматически при выполнение упаковки. После выставления переменных следует подключить собственно модуль CPack через include(CPack).

Как написать конфигурацию для своего проекта?

По созданию своего файла конфигурации есть статья здесь.

В общих чертах, файл конфигурации описывает цели, которые пользователь может использовать в своем проекте. Список целей генерируется CMake при создании экспортируемой сущности, привязанной к целям. Версионирование и проверки целостности конфигурации общего назначения реализованы в модуле CMakePackageConfigHelpers.

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
configure_package_config_file(res/neuon-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/neuon-config.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR})
write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/neuon-config-version.cmake VERSION ${version} COMPATIBILITY SameMajorVersion)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/neuon-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/neuon-config-version.cmake DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/neuon/cmake COMPONENT devel)

Шаблон конфигурации может быть похож на:

cmake_policy(PUSH)
cmake_policy(VERSION 3.10)

@PACKAGE_INIT@

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules/")

include(CMakeFindDependencyMacro)
find_dependency(Tensorflow)

include ( "${CMAKE_CURRENT_LIST_DIR}/neuon-headers-targets.cmake" )
include ( "${CMAKE_CURRENT_LIST_DIR}/neuon-targets.cmake" )

check_required_components(headers)
check_required_components(neuon)

list(REMOVE_AT CMAKE_MODULE_PATH -1)

cmake_policy(POP)

Как использовать конфигурации других проектов?

Корректно подготовленная конфигурация не требует никаких дополнительных действия для использования.

find_package(Neuon REQUIRED COMPONENTS headers neuon)
target_link_libraries(neuon-sample PRIVATE neuon::neuon)

Конфигурация декларирует какие библиотеки доступны для использования, какие дополнительные зависимости необходимо слинковать с конечным результатом, какие флаги компилятора должны быть выставлены при использовании. Где находятся заголовочные файлы, и библиотеки импорта тоже указывается посредством файла конфигурации и избавляет пользователя от ручного труда.

Как использовать стандартные CMake модули?

Несмотря на регламентированное именование модулей использование их в своих проектах двоякое. Многие стандартные модули CMake подключаются через функцию include(), однако многие модули, выполняющие поиск библиотек, не поддерживающих CMake самостоятельно через конфигурацию полагаются на find_package() в режиме модулей.

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CPackComponents)
include(CPack)
include(GoogleTest)

find_package(CURL)
find_package(Boost 1.70 MODULE REQUIRED COMPONENTS program_options log)

Библиотека Boost начиная с версии 1.70 обеспечивает поддержку CMake через конфигурации. СMake модуль обладает обратной совместимостью и умеет разрешать местонахождение Boost любой версии, используя конфигурации при наличии и создавая цели-псевдонимы в противном случае.

Как использовать модули из посторонних или личных источников?

Нередки случаи, когда стандартного модуля нет в наличии, или его состояние не удовлетворяет нуждам проекта. В таком случае проект может предоставить свои собственные CMake модули, для использования в своих конфигурациях. Ключевым моментом для использования собственных модулей является параметр CMAKE_MODULE_PATH, перечисляющий пути поиска модулей. Проект или конфигурация проекта могут самостоятельно изменять переменную, выведенную из этого параметра. Например, можно добавить в свой проект модуль, выполняющий заворачивание всех нееобходимых флагов компиляции для использования FFMpeg в цели CMake и связывать свое приложение с этими целями, не беспокоясь о флагах линковки, флагах компилятора и всех зависимостях.

set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/depends/sdk/cmake")

project(neuon-sample VERSION ${version})

find_package(Threads REQUIRED)
find_package(spdlog REQUIRED)
find_package(Boost 1.70 REQUIRED COMPONENTS program_options)
find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale swresample)

add_executable(neuon-sample src/neuon_sample.cpp)
target_link_libraries(neuon-sample PRIVATE spdlog::spdlog Threads::Threads Boost::program_options FFMPEG::avcodec FFMPEG::avformat FFMPEG::swscale

При изменении CMAKE_MODULE_PATH в своих конфигурациях, чтобы избежать конфликтов модулей с проектом пользователя, можно использовать функции по работе со списками. После добавления своих путей в переменную и использования, можно удалить эти вхождения из списка.

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules/")

include(CMakeFindDependencyMacro)
find_dependency(Tensorflow)

list(REMOVE_AT CMAKE_MODULE_PATH -1)

Как подключать и переиспользовать другие куски CMake скриптов?

CMake позволяет включать части скриптов друг в друга как есть, не выполняя дополнительной работы. Это может быть полезно для переиспользования функционала, когда использование модуля не обосновано, а дублирования хочется избежать. include() включит содержимое указанной файла в использующий файл. Включаемые файлы должны быть реентерабельны, и не изменять глобальное состояние.

include(depends/sdk/cmake/version.cmake)
configure_version(neuon)
configure_birthday(neuon)

Заключение

CMake 3.10+ однозначно полагается на объектную модель использования. Все есть цель. У цели есть свойства. Проект не должен изменять глобальное состояние. Проект описывает свои цели, и использует свойства целей для модификации своего поведения. Модули — расширяют функционал CMake. Конфигурации обеспечивают интерфейс интеграции проектов и зависимостей. Переиспользование функционала доступно и рекомендуется.

Глобальные свойства, параметры и переменные управляются CMake. Проект не должен их менять по своем усмотрению. Проект должен подстраиваться под глобальное состояние.

Сборка настраивается и модифицируется не внутри скриптов, а используя параметры и аргументы при запуске CMake. Позвольте будущему себе как автору проекта решать, разделяемая библиотека или статическая, установка в системные папки или в директории пользователя с ограниченными правами, включить максимальную оптимизацию или обеспечить отладочную информацию.

CMake предлагает очень широкий выбор доступных параметров и переменных, не стоит дублировать их в своем проекте.

Использование относительных путей предпочтительнее абсолютных.

CMake проекты могут быть включены друг в друга в произвольном порядке. Один проект не должен разрушать состояние, созданное другим.

CMake распространяется как самодостаточный архив под все популярные платформы. Файл конфигурации ограничен версией CMake потребителя, но для сборки и упаковки проекта можно притащить последнюю версию, развернуть локально как часть окружения сборки и пользоваться наиболее свежим функционалом, которые сделан, чтобы делать жизнь авторов проектов проще.

Полезные ссылки

Официальная документация CMake

Поддерживаемая сообществом при участии авторов CMake вики

Официальная страница распространения CMAKE

В чем разница между разными способами включения подпроектов?

CMake предлагает разные способы подключения проектов друг в друга. Если допустить, что проект описывается как минимум одним CMakeLists.txt, то проекты между собой могут быть связаны через add_subdirectory(), include(), ExternalProject_Add().

Основная идея в том, что CMakeLists.txt описывает проект как нечто именованное и версионированное. Директива project() устанавливает некоторые переменные, используемые CMake при генерации и сборке. Как минимум версию проекта.

include() включает содержимое как есть, а в одном CMakeLists.txt не может оказаться две активные директивы project().

ExternalProject_Add() предлагает способ сборки подпроекта из исходного кода, но не делает подпроект частью сборки основного проекта. Через ExternalProject_Add обычно подключаются сторонние зависимости, которые не могут быть удовлетворены окружением сборки или целевой системой.

add_subdirectory() единственный корректный способ подключить проект в проект, создав единое дерево зависимостей и связей между целями, сохранив при этом проектную идентичность и самодостаточность. Помимо непосредственно подключения проекта, CMake выполняет дополнительные операции по изоляции подпроектов и областей видимости.

При этом, можно встретить примеры добавлять CMakeLists.txt на каждый уровень вложенности папок — корневая папка, подпапка с файлами исходного кода, в подпапки еще глубже — и каждый из них включать друг в друга. На практике, никакой пользы от этого подхода не наблюдается. Логическое выделение подпроектов с собственным CMakeLists.txt, и корневой CMakeLists.txt включающий подпроекты и при этом выполняющий общепроектные операции по упаковке приводят к элегантно разграниченным проектам в многопроектных системах сборки как Visual Studio. Иметь одно MSVS решение(solution, ugh….) c проектами(projects) по сборке заголовочных файлов, библиотеки и демо-приложения гораздо приятнее, чем иметь с десяток искусственных проектов без четко обозначенного назначения.

In the previous part of the series, we looked at how to add tests to a project. The next step is providing support for deploying, or installing, the built binaries to our users. No matter if you’re working on a library or an executable application, users need to be able to deploy the binaries to a location of their choosing in a reliable way. In this part of the series, we take a closer look at installing libraries.

Defining installation requirements

What does it mean to “install” a library? Let’s first consider how a library is used. A library will generally be a dependency of another project – either a larger library or an application. It could be a development (build) dependency, or it could be runtime dependency. In the former case, the library could be either static or shared. In the latter case, it will always be a shared library.

A practical definition for “installing a library” could then be something along the lines of

“Deployment of the binaries, be it static or shared, and possibly header files – for development purposes, to a directory of user’s choosing, in a way that exposes the binaries, headers and potential information about transitive dependencies, for convenient consumption by depending projects.”

This may be somewhat vague, but the key point, I believe, is the convenience. In the context of CMake this would mean supporting the standard ways of incorporating dependencies into a project. In part 6 I introduced the FetchContent module. It’s a great solution for including dependencies that need to be built alongside the project. However, it’s not quite sufficient for binaries – it could be used to download the package, but what then? We still need the information about include and link paths, possibly defines, flags and transitive dependencies. In CMake all this is handled by Find modules, or the more recent Config files. Both of these are a standard way of exposing targets that encapsulate all the necessary information to the user. Find modules and Config files can be consumed with the help of the find_package command – it will find the Find module or Config file associated with the given package and process them, thus making targets they define available. But first things first. Let’s forget about Config files and find_package for a moment and see how to add the most basic installation support.

One more thing before I get into it. I first need to mention that deploying libraries, especially if multiplatform support is it be considered, is a huge topic that could be approached in many different ways. The number of variables to consider is simply too large, especially for an introductory post. Instead of discussing all the possibilities I will present and describe an approach that supports a decent number of use cases on Linux as well as on Windows. I have no idea about Apple platforms, sorry not sorry. Unfortunately for the same reasons I also do not consider how to handle shared libraries. I wouldn’t be able to cover this topic well here, so I’d rather postpone it to a future, dedicated post.

The approach I’m about to describe provides support for relocateable installs, that are find_package‘able via generated Config files. It’s a basic, but more or less standard setup that could be extended to accomodate specific project’s needs.

Install directory layout

One of the first things to consider when preparing for project installation is the install directory layout. Where the static and shared libraries, executables, and headers go. This is the bare minimum, one might also want to consider any possibly generated documentation, scripts, etc. Unless a project has some specific requirements regarding this layout, there is a standard approach that can be used. Anyone vaguely familiar with Linux would expect the following:

.
├── bin/        # executable binaries
├── include/    # header files
└── lib/        # static or shared libraries

And this is exactly the layout I’m going for. The specified paths are relative to a root install directory, which makes the installation relocateable – the user isn’t forced to install the package to /usr/, /usr/local, /opt/, or any other path one might think to hardcode. Instead only the relative layout below the root install directory is preserved – this is sufficient to relyably use the package.

This standard layout is provided by the GNUInstallDirs module. Despite the name, the module handles the layout in a cross-platform way, e.g. on Windows the DLLs will go into bin/ rather than lib/, as expected. Once included this module exposes the following variables (plus a few more):

CMAKE_INSTALL_BINDIR        # bin/
CMAKE_INSTALL_LIBDIR        # lib/
CMAKE_INSTALL_INCLUDEDIR    # include/

These variables can be used when specifying install paths, instead of hardcoding them. This handles the considerations of the target platform for us.

Installing targets

Now that we know what we’d like the install directory layout to look like we can proceed to actually installing the targets defined in our project and the resulting binaries. This is actually relatively simple and is handled by the aptly named install command. All that’s required is addition of the following lines to the top-level CMakeLists.txt:

include(GNUInstallDirs)

# (...)

install(TARGETS Add Subtract Maths MathsDemo
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

Seems too easy, right? Because that’s not the whole story of course. First, since in part 4 of the series I introduced the concept of modularization and insisted on applying it across the project it would now also make sense to handle installation on a per-module basis – each module should be responsible for correctly installing itself. So instead of calling install once on all targets it might be better to call it separately for each module. It’s a little more work, but it leads to more maintainable project setup. Specifying the DESTINATION for each artifact type seems like some boilerplate that could be done away with by wrapping the call in a function. That might be a reasonable thing to do, but since CMake 3.14 it’s unnecessary – the values given above are the default, if the GNUInstallDirs module is included. Let’s rely on that, and install each module separately:

CMakeLists.txt:

include(GNUInstallDirs)

# (...)

install(TARGETS MathsDemo)

add/CMakeLists.txt:

install(TARGETS Add)

subtract/CMakeLists.txt:

install(TARGETS Subtract)

Maths/CMakeLists.txt:

install(TARGETS Maths)

As a whole this is exquivalent to the previous single install command call. This approach however, makes each module self-contained. Anyway, let’s configure, build and install the project, to see what happens. The first thing to do when testing project installation is to set the CMAKE_INSTALL_PREFIX variable to a local install directory. Either specify it on the command line or hardcode it temporarily in the CMakeLists.txt. I’ve chosen the first approach:

$ cmake -DCMAKE_INSTALL_PREFIX:PATH=./install -S . -B build
$ cmake --build build -j7
$ cmake --build build --target install

-- Install configuration: "Debug"
-- Installing: /home/user/cmake_fundamentals/install/include
-- Installing: /home/user/cmake_fundamentals/install/include/gmock
-- Installing: /home/user/cmake_fundamentals/install/include/gmock/gmock-cardinalities.h
-- Installing: /home/user/cmake_fundamentals/install/include/gmock/gmock-generated-function-mockers.h.pump
(...)
-- Installing: /home/user/cmake_fundamentals/install/lib/libAdd.a
-- Installing: /home/user/cmake_fundamentals/install/lib/libSubtract.a
-- Installing: /home/user/cmake_fundamentals/install/lib/libMaths.a
-- Installing: /home/user/cmake_fundamentals/install/bin/MathsDemo

Wait, what? Why are gmock and gtest being installed? Since we’ve chosen to use FetchContent to embed googletest within our project, and gtest calls install for its own targets, it is being installed alongside our project. This can be easily dealt with by setting INSTALL_GTEST to OFF just before FetchContent_MakeAvailable(googletest). The relevant lines in tests/CMakeLists.txt now look as follows:

set(INSTALL_GTEST OFF)
FetchContent_MakeAvailable(googletest)

That was easy only because the googletest guys were nice enough to think about supporting this use case and made the installation conditional. The same should be done for our project if we wanted to support the use case of embeding our project via FetchContent as well. Let’s not get too ahead of ourselves though. Back to basic target installation.

If the project is intalled now, we get the following output:

$ cmake --build build --target install
Install the project...
-- Install configuration: "Debug"
-- Installing: /home/user/cmake_fundamentals/install/lib/libAdd.a
-- Installing: /home/user/cmake_fundamentals/install/lib/libSubtract.a
-- Installing: /home/user/cmake_fundamentals/install/lib/libMaths.a
-- Installing: /home/user/cmake_fundamentals/install/bin/MathsDemo

As you can see the libraries are now installed into the expected directories. You may have noticed that the header files are not installed though. This is because headers need to be handled explictly, to accomodate the potential layout differences – it’s unlikely that one would want all headers of a library to be dumped directly into the include directory. It’s much more likely to have some layout that logically groups the header files. We’ll deal with that next. Oh, by the way. I have chosen to rename the Main target to an equaly unimaginative MathsDemo.

Since the project is already modularized this will be straightfoward. Each module will of course be responsible for installing its headers. The layout should correspond to the module layout in the source tree. To achieve this we’ll need to use a different form of the install command – install(DIRECTORY …) as one might expect it install an entire directory. We might also choose to install on a per-file basis, but let’s go with the DIRECTORY approach, since it’s more succinct.

add/CMakeLists.txt:

install(DIRECTORY add
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

subtract/CMakeLists.txt:

install(DIRECTORY subtract
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

maths/CMakeLists.txt:

install(DIRECTORY maths
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

Again, nothing too difficult to understand. This could be wrapped along with the previous install call into a single function or macro to make the whole process even more painless. Re-installing the project now results in the following output:

-- Installing: /home/user/cmake_fundamentals/install/lib/libAdd.a
-- Installing: /home/user/cmake_fundamentals/install/include/add
-- Installing: /home/user/cmake_fundamentals/install/include/add/calc_status.h
-- Installing: /home/user/cmake_fundamentals/install/include/add/add.h
-- Installing: /home/user/cmake_fundamentals/install/lib/libSubtract.a
-- Installing: /home/user/cmake_fundamentals/install/include/subtract
-- Installing: /home/user/cmake_fundamentals/install/include/subtract/subtract.h
-- Installing: /home/user/cmake_fundamentals/install/lib/libMaths.a
-- Installing: /home/user/cmake_fundamentals/install/include/maths
-- Installing: /home/user/cmake_fundamentals/install/include/maths/maths.h
-- Installing: /home/user/cmake_fundamentals/install/bin/MathsDemo

Right, we now have the install layout we have defined in the first paragraph. So far so good. The next step is to make this installation consumable by other projects, that is, exposing all the necessary information – include directories, paths to the actual libraries, etc. in the form of targets. This is done using export targets.

Export target installation

Every install(TARGETS …) command may optionally specify an EXPORT set to which the target belongs to, thus adding the target to that EXPORT set. An EXPORT set (target) carries full information about properties of each target included into the EXPORT, and all the inter-target dependencies. We will have a single EXPORT set for the entire project (library), let’s name it Maths, since that’s the library we’d like to expose to the end user. Each install(TARGETS …) call needs to be extended as follows:

CMakeLists.txt:

include(GNUInstallDirs)

# (...)

install(TARGETS MathsDemo
    EXPORT Maths
)

add/CMakeLists.txt:

install(TARGETS Add
    EXPORT Maths
)

subtract/CMakeLists.txt:

install(TARGETS Subtract
    EXPORT Maths
)

Maths/CMakeLists.txt:

install(TARGETS Maths
    EXPORT Maths
)

Doing so loads the Maths export target with all the necessary information, it doesn’t expose it to the user however. That’s done by installing the export target itself, like so:

install(EXPORT Maths
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Maths
    NAMESPACE Maths::
)

Let’s unpack what’s going on here. We’re saying that the EXPORT target Maths should be installed to a directory lib/cmake/Maths – this is a CMake convention – I explain the why later. The NAMESPACE argument means that all targets exposed by the installed EXPORT target will have the given prefix – here Maths::. This means that the end user of our library will need to include the Add library by linking Maths::Add, rather than just Add. The namespace is introduced in order to avoid name clashes with other packages. Things are starting to get a little complicated. Fret not, we’re nearing the end. Though trying to install the project now would result in a number of error messages:

CMake Error in add/CMakeLists.txt:
  Target "Add" INTERFACE_INCLUDE_DIRECTORIES property contains path:

    "/home/user/cmake_fundamentals/add"

  which is prefixed in the source directory.


CMake Error: install(EXPORT "Maths" ...) includes target "Add" which requires target "ProjectConfiguration" that is not in any export set.

(...)

It all boils down to two issues:

  1. The targets in the export set contain include directories pointing to an absolute path in the source tree – there is zero chance that these paths will be correct on the end-users machine. We need a way to swap these paths out for correct paths, relative to the install directory.
  2. The EXPORT target needs to be able to provide full inter-target dependency context, but the ProjectConfiguration target is not installed. We need to install it, even though it’s only an interface target, and doing so will have no effect other than adding information about its dependencies to other targets.

The second issue is simpler, so let’s fix it first. It’s enough to add ProjectConfiguration to the install(TARGETS …) call in the top-level CMakeLists.txt:

CMakeLists.txt:

include(GNUInstallDirs)

# (...)

install(TARGETS MathsDemo ProjectConfiguration
    EXPORT Maths
)

The first issue requires more explenation. Each target defined in the project also adds include directories to its INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES properties. For example, the Add target:

target_include_directories(Add
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
)

The above call appends an absolute path expansion of CMAKE_CURRENT_SOURCE_DIR to both properties. This path is absolute and valid only when building the project. Since we want installed packages to be relocateable this path needs to be replaced with a path relative to the install root directory upon installation. This is done using a set of boilerplate generator expressions, the above target_include_directories command needs to be replaced with the following:

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

Without going into too much detail about how generator expressions work (see my post about generator expressions if you’d like to know more) this call will do exactly what we want – it will expand to the path in the source tree when building the project and to a path relative to CMAKE_INSTALL_INCLUDEDIR when the target is installed. After applying the same modification to all targets, the project can finally be installed.

$ cmake --build build -t install
(...)
-- Installing: /home/user/cmake_fundamentals/install/lib/cmake/Maths/Maths.cmake
-- Installing: /home/user/cmake_fundamentals/install/lib/cmake/Maths/Maths-debug.cmake

Two new files are installed with this call – Maths.cmake and Maths-debug.cmake. These are generated by installing the EXPORT target and contain all the information necessary to consume our library. Inspecting them is an excercise for the interested reader ( 😉 ). These files are the basis for adding the find_package support. The rest will be handled by yet another CMake module – CMakePackageConfigHelpers.

Package Config

The CMakePackageConfigHelpers module exposes a set of functions for generating package Config file – one of the ways to support the find_package command. The other is writing a Find module – this is the old way of doing things and should generally be avoided for new projects. Generating the Config file is relatively straightforward. It involves three steps:

  1. Write a <project-name>Config.cmake.in template file, which will be used to generate the Config.
  2. Call the configure_package_config_file function to actually generate the Config file.
  3. Install the generated Config file, the same way you would any other file.

The template file needs to be provided, because the decision of what it needs to contain is up to the us. In the most basic cases however, the same, very simple configuration file can be used:

cmake/MathsConfig.cmake.in:

@PACKAGE_INIT@

include(${CMAKE_CURRENT_LIST_DIR}/Maths.cmake)
set_and_check(Maths_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@")
set(Maths_LIBRARIES Maths::Maths)
check_required_components(Maths)

This correctly supports relocateable packages on all platforms. Notice that the Maths.cmake generated by installing the EXPORT target is being included here – that’s where all of the real work is done. Next, variables exposing the include path and the libraries are set. This is not strictly required, but can be expected by some users. Notice that a new set_and_check command is used here. This is a function provided by the

CMakePackageConfigHelpers

module, all it does is sets the variable and ensures the pats are valid. The

check_required_components

function should be self explenatory. All that’s left to do is configuring and installing the file:

include(CMakePackageConfigHelpers)

configure_package_config_file(
    cmake/MathsConfig.cmake.in                      # the template file
    ${CMAKE_CURRENT_BINARY_DIR}/MathsConfig.cmake   # destination
    PATH_VARS CMAKE_INSTALL_INCLUDEDIR
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Maths
)

install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MathsConfig.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Maths
)

What happens here is the Config template is used to generate the MathsConfig.cmake file, we also need to provide the information about where this Config file will be installed, so that the @PACKAGE_INFO@ tag is expanded correctly. We then actually install the generated Config into the same directory into which the Maths.cmake and Maths-debug.cmake were installed. Also note the PATH_VARS argument – variables passed in here, need to be prefixed with PACKAGE_ in the template. This ensures that the path is correctly expanded to a path prefixed in the package installation directory.

Place all of the above commands at the bottom of the top-level CMakeLists.txt for now. Aside for the template file – it should be located in the cmake directory. Let’s run the install target and inspect the result.

$ cmake --build build --target install
Install the project...
(...)
-- Installing: /home/user/cmake_fundamentals/install/lib/cmake/Maths/MathsConfig.cmake

As you can see the MathsConfig.cmake file has been installed alongside the library. And that’s it really. The package installation is now well behaved, relocateable and supports the find_package command. Before I prove to you that all this works, there’s a few more improvements that we could apply.

Supporting multiple configurations

If you look closely at the installed scripts, one of them is named Maths-debug.cmake – it’s configuration-specific. This implies that there might also need to exist Maths-release.cmake, and possibly other. The current setup could be considered fine if we required that each configuration is installed into a different install-prefix directory. This requirement is limiting. A common convention is to ensure that both debug and release build of the library can coexist by adding a suffix to the debug configuration. This is done by specifying an appropriate property on the target:

set_target_properties(Add
    PROPERTIES
        DEBUG_POSTFIX _d
)

This will cause _d to be appended to the binary resulting from debug builds of the Add target. Of course the same needs to be done for all targets in the project.

If you install the project now you’ll see that all the libraries have the newly specified suffix.

$ cmake --build build --target install
Install the project...
-- Install configuration: "Debug"
-- Installing: /home/user/cmake_fundamentals/install/lib/libAdd_d.a
-- Installing: /home/user/cmake_fundamentals/install/lib/libSubtract_d.a
-- Installing: /home/user/cmake_fundamentals/install/lib/libMaths_d.a
(...)

Some further considerations would be to add versioning and support for shared libraries – as is, if the project was built as a shared library it could be installed just fine, maybe even linked to, but you wouldn’t have much luck running the final binary. But like I already mentioned at the start I’d rather cover this topic in a dedicated post. For now, let’s briefly address how to add versioning to packages.

Versioning

Setting a project version number is rather simple – all it takes to do is is to pass an additional argument to the very first project command call:

project(Fundamentals VERSION 0.1.0)

This will have the result of the following cache variables being defined:

Variable Value
PROJECT_VERSION 0.1.0
PROJECT_VERSION_MAJOR 0
PROJECT_VERSION_MINOR 1
PROJECT_VERSION_PATCH 0

For completeness sake, I’ll just mention that a PROJECT_VERSION_TWEAK variable is defined as well, but this isn’t compliant with semver, so I’m choosing to ignore it (it’s debatable if semver makes sense in the context of C++, but that’s a different post). In addition to the above, there’s also a project-call-specific variable defined, corresponding to each of the above. This follows the pattern of <project-name>_VERSION, etc. This might be relevant in a larger project where you might want to package and version certain components of the project separately.

So the project has a version, great. How do we actually propagate that to the installed package? CMakePackageConfigHelpers has you covered. In addition to the configure_package_config_file function it also exposes write_basic_package_version_file, which does exactly that. It generates a version file, which then also needs to be installed, same as the Config file. An exaple follows.

write_basic_package_version_file(
  ${CMAKE_CURRENT_BINARY_DIR}/MathsConfigVersion.cmake
  VERSION ${PROJECT_VERSION}
  COMPATIBILITY SameMajorVersion
)

install(FILES
    # ...
    ${CMAKE_CURRENT_BINARY_DIR}/MathsConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Maths
)

Note the specified COMPATIBILITY – this let’s find_package determine if the package found is compatible with the one requested by the consuming project. SameMajorVersion is compatible with the semver concept. The following alternatives are available: AnyNewerVersion, SameMinorVersion, ExactVersion. You must decide for yourself which one suits your project at its current stage the best, taking into account API and ABI stability.

Installing the project now will result in the MathsConfigVersion.cmake file being installed, as expected:

Install the project...
(...)
-- Installing: /home/user/cmake_fundamentals/install/lib/cmake/Maths/MathsConfigVersion.cmake

And that’s it. Shared library support aside, this is a complete basic library packaging setup. The only thing to do is testing if it all works as expected.

Finding packages

The most direct way to test this solution would be to define a minimal project which uses it. Instead of defining something completely new I’ll just move the code associated to the MathsDemo executable to a separate project. I’ll spare you the details of what needs to be removed from the Fundamentals project, but here’s the complete setup of the test project:

directory tree:
.
├── CMakeLists.txt
└── main.cpp

CMakeLists.txt:

cmake_minimum_required(VERSION 3.15)
project(HelloInstalling)

find_package(Maths 0.1.0)

add_executable(MathsDemo)
target_sources(MathsDemo
    main.cpp
)

target_link_libraries(MathsDemo
    PRIVATE
        Maths::Maths
)

main.cpp (Same as before, moved from the Fundamentals project):
#include <iostream>
#include <limits>

#include <maths/maths.h>

void use_add(int a, int b)
{
    Maths::calc_status cs{};
    auto const result = Maths::add(a, b, cs);
    if (cs == Maths::calc_status::success)
    {
        std::cout << a << " + " << b << " = " << result << "\n";
    }
    else
    {
        std::cout << "Error calculating " << a << " + " << b << "\n";
    }
}

int main()
{
    const int a{42};
    const int b{102};

    use_add(a, b);
    use_add(a, std::numeric_limits<int>::max());
}

The interesting parts are in the CMakeLists.txt in bold. The find_package command call searches for the Maths package of the specified version (enforcing the compatibility specified in the package). Once the package is found, all of the targets defined in the MathsConfig.cmake file are available. In this case we’re linking the Maths::Maths target to the MathsDemo executable.

How does CMake know where to search for the package? By default it searches some platform-specific default directories, on a Unix-y system this is something like:

/usr/lib/cmake/<name>
/usr/lib/share/cmake/<name>
/usr/lib/<name>/cmake
(...)

This is just an example, see the documentation for the full story. What does this mean in practice? System-wide installed packages will be found without any additional actions needing to be done. What if we don’t want to install a package system-wide just for testing? Before the system paths are searched, find_package first searches all paths listed in the CMAKE_PREFIX_PATH variable. This is the best way of ensuring that a package is found without hard-coding any paths.

Let’s see an exapmle. In my case the cmake_fundamentals and the test_project are sybling directories:

.
├── cmake_fundamentals
│   └── install
└── test_project

The CMAKE_PREFIX_PATH needs to be set to the relative or absolute path of the cmake_fundamentals/install directory.

$ cmake -DCMAKE_PREFIX_PATH:PATH=cmake_fundamentals/install -B test_project/build -S test_project
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/test_project/build

$ cmake --build test_project/build/
Scanning dependencies of target MathsDemo
[ 50%] Building CXX object CMakeFiles/MathsDemo.dir/main.cpp.o
[100%] Linking CXX executable MathsDemo
[100%] Built target MathsDemo

$ test_project/build/MathsDemo
42 + 102 = 144
Error calculating 42 + 2147483647

Summary

In this post I demonstrated how to set up simple packaging for a library. This is a basic but solid skeleton, suitable for extension to accomodate project needs. A first good step would be to encapsulate all of the installation logic scattered across the project into a module. In the next post I’ll follow up on the packaging and demonstrate how to set up projects to support the FetchContent use case – thus making it possible to embed a dependency the same way we did googletest.

One more thing. No more posts this long. I promise.

References

  • Git gist
  • GNUInstallDirs
  • CMakePackageConfigHelpers
  • find_package
  • FetchContent

In this article, we will look at how we can use CMake to compile and install a library we have developed, be it a static, dynamic, or header-only library. We look at all the steps we need to take to make the installation successful, which will contain steps to automatically generate files that other projects need to discover and find in our library. We will end this article by testing the library installation by creating a simple project that consumes our developed library.

By the end of this article, you have all the tools at your disposal to develop a library that can be easily used by other projects, regardless of the operating system. So, let’s get straight into it and see how you can become a cross-platform library developer kingpin.

Download Resources

All developed code and resources in this article are available for download. If you are encountering issues running any of the scripts, please refer to the instructions for running scripts downloaded from this website.

  • Download: Build-Systems-Part-4

In this series

In this article

  • Introduction
  • Compiling and installing static, dynamic, and header-only libraries with CMake
    • Static and dynamic libraries
      • Project structure
      • Source files
      • CMake build scripts
      • Command line instructions
    • Header-only libraries
      • Project structure
      • Source files
      • CMake build scripts
      • Command line instructions
  • Using the installed libraries in other projects
    • Project structure
    • Source file of a simple test program for complex numbers
    • CMake build scripts
    • Command line instructions to build and execute the test program
  • Summary

Introduction

In this article, we look at the more complex aspect of CMake: dependency management. Working with dependencies/libraries in a compiled language is hard, and we have explored this topic numerous times on this website. First, we need to ensure that we have a matching interface, that is, class definitions that expose the functionality of our library to the end-user. Once we make a change, even just changing the name of one function, compilation errors are produced. In other words, breaking the application programming interface (API) will break the compilation.

But even if we keep the API consistent and are able to compile both the library and the code that depends on it, we may still get errors at runtime due to incompatible application binary interfaces (ABI). The ABI sets out how the compiled binaries (executable, library, operating system, etc.) communicate with each other. Different platforms have different communication (calling) patterns, and so we need to compile our executable or library for each operating system, build mode (debug/release), and architecture (32-bit/64-bit).

You may not be familiar with the term ABI but I’m sure you have come across software download pages where you had to specify your operating system, or architecture (32-bit/64-bit), and this is to ensure that the software you are downloading can correctly communicate with its dependencies, in this case, the operating system.

If we pick an interpreted language, for example, we have far fewer restrictions, as we only need to ensure a consistent API. We check at runtime if if the required functions are available, and then we simply execute them. Since all of these libraries use the same runtime (e.g. a Python/Matlab/Javascript/etc. interpreter), they automatically satisfy the ABI requirements, as it is the job of the interpreter to communicate with the operating system (and other potential dependencies). Of course, this means that we have to compile the interpreter for different platforms.

With CMake, working with dependencies can be rather simple and straightforward, but there are a few things we need to do as library developers to make it painless and simple. If we only provide instructions for CMake to build our project, as we did when we first looked at CMake, then the end-user will have a difficult time working with our library, and in this article, I want to look at the additional steps we need to take to make working with libraries a joyful experience.

We will return to our complex number library, as it is so simple that we don’t have to concentrate on the source code but rather look at the build script and how to provide a clean installation. In order to do that, we will need some of the advanced CMake features we looked at in the previous article. We will explore how to compile and install static, dynamic, and header-only libraries, as well as how to use these libraries in an external project, so as to show how we can consume the library as an end-user.

Once you have gone through this article, you will add to your CMake expertise and be able to work with additional dependencies in your project. This requires, of course, that these dependencies are already available. You can, of course, download, compile, and install your external dependencies manually, such as the CGNS library or GTest, but since these projects (and pretty much any other C++ library) already provide a CMake file for their build stage, there are better ways to integrate them directly with our own projects.

Furthermore, we can also use external package managers, such as Conan (yes, please) and vckpg (no, thank you), to manage our dependencies. They will download, compile (if necessary) and install all our dependencies and expose them to CMake automatically. All we have to do is include some files generated by Conan (I’m not even considering vckpg anymore) within our CMake file. We magically have access to all required dependencies. Life couldn’t be easier. In the next article, we will look at both Conan and CMake’s internal options for working with libraries.

Compiling and installing static, dynamic, and header-only libraries with CMake

Let us turn our attention to the complex number library again that we saw in the opening article on CMake and see how we can compile, install, and use it using either a static, dynamic, or header-only library. For the header-only library, we will need to make some changes to the source files, so we will treat it separately (as its own project). We will see later, that it doesn’t matter how our library is provided to the end user, they will include our library in the same way regardless of the way it was provided (i.e. as either a static, dynamic, or header-only library).

Static and dynamic libraries

Let us quickly refresh our memory on how the static and dynamic library was structured. We only looked at how to compile a static library with CMake when we first looked at it in conjunction with the complex number example, but getting a dynamic library to work is just as simple and requires only changing one keyword. If you sat through the pain of getting a dynamic library to work on Windows, you will feel relieved and realise that things can be simple, and CMake is working hard for us in the background so that we don’t have to.

Project structure

The project structure has slightly changed now, where we retain our build/ folder for all compiled source files, but we have gained a new folder called cmake/. It is a convention to use a folder called cmake/ is you want to include additional CMake files in your project (e.g. functions or configuration files). In our case, we use a file called <library-name>Config.cmake.in, where <library-name> is the name of our library (here complexNumber).

We need to provide this file, which will be used by CMake to generate a <library-name>Config.cmake file. This file is used by other projects that want to use our library. If these projects also use CMake, then this file will provide instructions to CMake for these other projects on where to find our library exactly. You may have also noticed the file ending *.in, which indicates that this is a configuration file and will be populated with information later by CMake. If you are new to configuration files, I have written about configuration files in the previous article.

root
├── build/
├── cmake/
│   └── complexNumbersConfig.cmake.in
├── complexNumberLib/
│   ├── CMakeLists.txt
│   ├── complexNumbers.hpp
│   └── complexNumbers.cpp
├── CMakeLists.txt
└── CMakeOptions.txt

Then, we have replaced the name of the src/ folder to complexNumberLib. This is, perhaps, a somewhat Pythonic way of doing things, where the source folder is typically named after the library itself (single name principle), which offers here the advantage that when we later install our header files, which are located in complexNumberLib/*.hpp, we will retain this structure in our installation directory, i.e. header files will be installed to <cmake-install-prefix>/include/complexNumberLib/*.hpp.

Sure, we could leave the folder name to src/ in our project, which would then result in an installation path of <cmake-install-prefix>/include/src/*.hpp, which isn’t ideal. We could then do some CMake magic to replace the folder name src/ but only for the installation path. But why force something in CMake if we can just provide a clean folder structure? Just because src/ is conventional, it doesn’t always mean it is the best choice. Let’s be brave and break conventions when they suck!

Finally, we have our CMakeLists.txt and CMakeOptions.txt files, which will be used to steer the compilation and install the library on our system. First, I want to have a quick look at the source file and changes that we have to do here compared to previous articles, and then we will go through the specific CMake files in the subsequent section.

Source files

Within the complexNumberLib/ folder, we have both a header and a source file. These are similar to what we have looked at before, but I want to reproduce here the header file to show what difference we have to make, relating to dealing with dynamic libraries on Windows. If you are a long-time reader of this website, you’ll know what I am about to show you, yes, it is time for preprocessor-hell!

If you have no idea what I am talking about, I already vented my frustration with Windows in a previous article, looking at this issue in great depth, only to realise later that this is a volatile (but required) hack. You won’t need these articles to understand the rest of the discussion but feel free to go through them if cross-compilation of dynamic libraries is important to you (I’d argue it should be, but I seem to be one of the only people in the CFD community who sees it that way …). Don’t worry; you will still be able to follow the rest of this article, even if you haven’t read the linked articles.

The header file in all its glory is reproduced below:

#pragma once

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
  #if defined(COMPILEDLL)
    #define EXPORT __declspec(dllexport)
  #elif defined(COMPILELIB)
    #define EXPORT
  #else
    #define EXPORT __declspec(dllimport)
  #endif
#else
  #define EXPORT
#endif

#include <iostream>
#include <cmath>
#include <stdexcept>
#include <limits>

class ComplexNumber {
public:
	EXPORT ComplexNumber(double real, double imaginary);
	EXPORT ~ComplexNumber();

	EXPORT double Re() const;
	EXPORT double Im() const;
  EXPORT void setRe(double real);
  EXPORT void setIm(double imaginary);

  EXPORT void conjugate();
  EXPORT double magnitude() const;
	
	EXPORT ComplexNumber operator+(const ComplexNumber &other);
  EXPORT ComplexNumber operator-(const ComplexNumber &other);
  EXPORT ComplexNumber operator*(const ComplexNumber &other);
  EXPORT ComplexNumber operator/(const ComplexNumber &other);

  EXPORT friend std::ostream &operator<<(std::ostream &os, const ComplexNumber &c);

private:
  void isNan(double real, double imaginary) const;
	
private:
	double _real;
	double _imaginary;
};

Now look at the beauty that is lines 3-13. Truly magnificent, windows-only, gobbledygook. In a nutshell, if this is new to you, what we have to do is add __declspec(dllexport) in-front of function calls to mark a function for export (this will then go into your dynamic library, i.e. into your *.dll (dynamic linked library, dll). If you forget to do this and you are trying to compile a dynamic library on Windows, you won’t get the *.dll file, which is a good indicator that you forgot to export function calls.

Later, when you want to use your *.dll library, then you have to use the lines __declspec(dllimport) instead. Since we now have a situation of having two competing function signatures, we could either write two header files where we only change the __declspec definition (but this includes copy and pasting, and we want to follow the DRY principle, so this isn’t a great idea), or hack our logic into the header file using pre-processor directives and then let the compiler insert the correct __declspec definition. We have opted for the latter, and that is common practice (unfortunately).

So, when we want to build a dynamic library on Windows, we see that we need to define the pre-processor variable COMPILEDLL. This requires us to also define the pre-processor directive COMPILELIB (which doesn’t set anything for the EXPORT variable) if we just want to use a static library. Otherwise, the default is to use the __declspec(dllimport), which is what we want when using this library later.

Incidentally, if you are on UNIX and enjoy the magic of Linux or macOS for developing code, you don’t have to worry about exporting function names. It is done automatically for you. We are in the 21st century, and Microsoft still hasn’t figured out how to do this automatically. But I remain hopeful that this is on the agenda for the 22nd century!

CMake build scripts

The CMakeLists.txt file in the root folder is given below. Look through it and then come back for a discussion underneath the file.

# Select the minimum cmake version required. If in doubt, use the same version you are using
# To find out your cmake version, run 'cmake --version' in the console
cmake_minimum_required(VERSION 3.23)

# Set the project name/description and project settings like the C++ standard required
project(
  complexNumbers
  LANGUAGES CXX
  VERSION 1.0.0
  DESCRIPTION "A simple C++ library for complex numbers"
)

# include custom options
include(CMakeOptions.txt)

# Add a postfix for debug library builds to avoid name clashes with release builds
if(WIN32)
  set(CMAKE_DEBUG_POSTFIX d)
endif()

if(${ENABLE_SHARED})
  # Create a shared (dynamic) library for the complex number class
  add_library(${CMAKE_PROJECT_NAME} SHARED)
  
  # add correct compiler flags so that we can correctly import/export symbols
  if (MSVC)
    target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL)
  endif()
else()
  # Create a static library for the complex number class
  add_library(${CMAKE_PROJECT_NAME} STATIC)

  # add correct compiler flags so that we can correctly import/export symbols
  if (MSVC)
    target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILELIB)
  endif()
endif()

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

# go through the complexNumberLib/ subdirectory and add any files to the library
add_subdirectory(complexNumberLib)

### INSTALLING ###

# install the headers that we specified with FILE_SET (header files for the user to include in their projects)
install(TARGETS ${CMAKE_PROJECT_NAME}
  EXPORT complexNumbersTargets
  FILE_SET HEADERS
)

# provide some basic meta data about the library to generate the library's config file 
install(
  EXPORT complexNumbersTargets
  FILE complexNumbersTargets.cmake
  DESTINATION "lib/cmake/complexNumbers"
  NAMESPACE complexNumbers::
)

# generate the library's config file
include(CMakePackageConfigHelpers)
configure_package_config_file(
  cmake/complexNumbersConfig.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
  INSTALL_DESTINATION "lib/cmake/complexNumbers"
)

# install the config file
install(
  FILES "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
  DESTINATION "lib/cmake/complexNumbers"
)

The first part of the CMakeLists.txt file is hopefully not a surprise anymore. We set the minimum CMake version here to version 3.23 as we want to use the FILE_SET command, which was introduced in 3.23 (and the newest feature we use in this CMake file). We also set some project-specific metadata and, then, on line 14, proceed to include the CMakeOptions.txt file to make library-specific options available.

The content of the CMakeOptions.txt file is shown below:

option(ENABLE_SHARED "Build shared (dynamic) library? If set to off, a static library will be built" OFF)

It contains a single option labelled ENABLE_SHARED, which allows us to set whether we want to build the library as a static or dynamic library. By default, the library will be built as a static library.

On line 17-19, we ask CMake to add the letter d to the name of our library if we are on Windows and if we are building a debug version of our library. This is a Windows convention and is required to allow for debug and release libraries to sit in the same folder. We need that as Windows strongly discriminates between debug and release builds, and we must pick the correct one, something which is rather relaxed on UNIX systems. We’ll see that later in action where it will become clearer.

Returning to the CMakeLists.txt file, we see that we use this ENABLE_SHARED option on lines 21-37 to either create a new library through the add_library() command, where we either use the STATIC or SHARED keyword to indicate whether we want to build a static or dynamic library. The name of the library is derived from the project name we set on line 7, which is available to us through the ${CMAKE_PROJECT_NAME} variable.

We also set the pre-processor to either COMPILEDLL or COMPILELIB, which will pass that information in a platform-independent manner to the compiler, i.e. on Windows using the cl.exe compiler, we either get the /DCOMPILEDLL or /DCOMPILELIB compiler flag added to the compilation, and on UNIX, using either the GNU or clang compiler, we, nothing. The pre-processor directives are only defined for Windows (see line 3 in the header file), but if we wanted something to be added to our UNIX compilers, we would get -D instead of /D as a compiler flag.

On lines 39-43, we include the root folder using a generator expression, something that we did discuss in the previous article as well. Let’s explore that in more detail. If we are building the library, then we are including ${CMAKE_CURRENT_SOURCE_DIR} during the compilation. This is nothing else than specifying during the compilation that we want to include the project’s root folder using the include flag -I. or /I.

This allows us to make relative imports from the root folder in our source files, such as complexNumberLib/complexNumbers.hpp. This is what we do in the complexNumberLib/complexNumbers.cpp file at the top. If we do not include ${CMAKE_CURRENT_SOURCE_DIR}, then we would have to rewrite this to include as simply complexNumbers.hpp, i.e. look for the header file in the current directory. For a simple project like this, it would be fine to leave it, but for more complex projects it makes navigating your library a lot easier, if all includes are relative to the root.

During the installation, though, we do not want to have the same include path. The reason is that CMake works with absolute paths in the background, and having an absolute path point to your project folder will make no sense. You want to point to the installation directory instead, and using the generator expression to distinguish between the build and install phase allows us to conditionally set the include path here for our library. The good news is that lines 39-43 are pretty much boilerplate instructions, i.e. you can copy and paste it into your own project, and it will work.

On line 46, we are using the add_subdirectory() command to instruct CMake to look for an additional CMakeLists.txt file within the complexNumberLib/ folder. This file has the following content:

target_sources(${CMAKE_PROJECT_NAME}
  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.cpp
  PUBLIC FILE_SET HEADERS
  BASE_DIRS ${PROJECT_SOURCE_DIR}
  FILES ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.hpp
)

We simply add the source file (e.g. complexNumbers.cpp) to our library target. It is set as PRIVATE, as only our library will need to know about this file. Then, we are using the PUBLIC FILE_SET HEADERS instruction to let CMake know that we want to include some header files as well in our target, which will not be used during compilation but rather be made available later when we install the project. All header files marked under the FILE_SET command will get exported and land in the <cmake-install-prefix>/include folder.

And now, we get to the juice bit, the part that we did not cover in detail in our first article about CMake, and looking through it, you can understand why (we needed a few more advanced concepts which we covered in the previous article). To make things simple, I have copied lines 50-76 below again, so we don’t have to scroll up and down. I’ll be referring to the line numbers shown below to make things simple but keep in mind that these are different to the line numbers in the CMakeLists.txt file shown above.

# install the headers that we specified with FILE_SET (header files for the user to include in their projects)
install(TARGETS ${CMAKE_PROJECT_NAME}
  EXPORT complexNumbersTargets
  FILE_SET HEADERS
)

# provide some basic meta data about the library to generate the library's config file 
install(
  EXPORT complexNumbersTargets
  FILE complexNumbersTargets.cmake
  DESTINATION "lib/cmake/complexNumbers"
  NAMESPACE complexNumbers::
)

# generate the library's config file
include(CMakePackageConfigHelpers)
configure_package_config_file(
  cmake/complexNumbersConfig.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
  INSTALL_DESTINATION "lib/cmake/complexNumbers"
)

# install the config file
install(
  FILES "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
  DESTINATION "lib/cmake/complexNumbers"
)

On lines 2-5, we set some basic information about the library. In particular, we use the EXPORT command to define an installation target. It is a bit like the add_library() or add_executable() command, where we define a name for the library or executable, which we can use to add files and include directories to this target specifically. In a similar manner, the EXPORT command allows us to associate any installation information with this target name, such as install directories and files that should get installed (copied into the installation folder).

The FILE_SET command on line 4 makes sure that all header files marked under the FILE_SET command (i.e. see the complexNumberLib/CMakeLists.txt file) will get installed into the installation directory. We now bind this FILE_SET instruction with the exported install target.

Lines 8-13 define some additional meta-information, which, again, are associated with the EXPORT target. We specify that we want to create a <library-name>Targets.cmake file, which will be automatically generated by CMake based on the information provided within all the install() commands as seen above. This file name is conventional, and you should use the same wording.

The DESTINATION specifies where this <library-name>Targets.cmake file should be located, and again, by convention, we use lib/cmake/<library-name>. I have seen permutations of that in the wild, e.g. cmake/lib/<library-name>, which probably should work just as well, but the former makes sure that all *.cmake files from all libraries will be located in the same folder.

Then, we have the NAMESPACE complexNumber:: instruction. A namespace is similar to a C++ namespace in that we can provide a dedicated namespace for all of our libraries. Sometimes, you may want to provide a set of libraries under a common library name. Boost may be one such example, which probably most C++ programmers will have at least heard about. Then, you would identify your libraries later as <namespace>::libraryName. By convention, we use a namespace even for single libraries, where the library and namespace are the same.

Lines 16-21 take the configuration file cmake/complexNumberConfig.cmake.in and produce the corresponding complexNumbersConfig.cmake file. It will be located in the ${CMAKE_CURRENT_BINARY_DIR}, i.e. the build/ folder, but then installed into the same lib/cmake/complexNumbers folder, just like the complexNumbersTargets.cmake file. Let’s have a look at the configuration file; the content is given below:

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)

# any additional libraries we depend on should go here, e.g.
# find_dependency(Gtest::Gtest)
# notice that we use find_dependency(), not find_package() here

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

We can see the @PACKAGE_INIT@ variable at the beginning of the file, which will be replaced by CMake during the installation. I.e. if you later look into the file <cmake-install-prefix>/lib/cmake/complexNumbers/complexNumbersConfig.cmake, you will see this variable replaced by some CMake content. The include() directives on lines 3 and 9 are the only other requirement that we have to put here and are mostly boilerplate as well, we would just have to change the library name on line 9 if we wanted to use this file in another project.

Essentially, what this file offers us is the opportunity to look for additional dependencies, i.e., libraries, on which our project depends. In our case, we have none. We can see in the comments that we use the find_dependency() syntax to look for a library. Notice that in the example, we use Google Test (GTest), which is provided by its own namespace. So to find GTest, we would have to write find_dependency(GTest::Gtest). Later, we will see that this command simply wraps around another command called find_package(), that we will use to find libraries.

Returning back to the CMakeLists.txt excerpt above, on lines 24-27 we simply provide additional instructions to install the generated configuration file, and this is all the information required to get a clean installation. So, with all of our CMake files now ready and prepared, let us go ahead and compile and install the library next.

Command line instructions

The command line instructions for compiling the library are provided next. Make sure that you have a clean build/ folder available, which does not contain any files and that you have changed into the build/ folder on the command line. Then, you can go ahead and build a static library first using the following command (I am using Ninja here as my preferred build generator, but you can omit this and use the system’s default if you prefer or don’t have Ninja installed):

cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_SHARED=OFF -G Ninja ..

With the build file generated, proceed to generate the library using the following command:

cmake --build . --config Release

And finally, once the library is available and compiled, we want to install it into a specific directory. I am on Windows here and will use C:\temp as my installation prefix. This is to show that even if we build in a non-standard path (i.e. one that is not in the environmental variables), we can still discover these libraries later, thanks to the C:\temp\lib\cmake\complexNumbers\*.cmake files that we have generated. The install command becomes:

cmake --install . --prefix "C:\temp" --config Release

These three steps have generated a release build of the static library. If you are on UNIX, hooray, go ahead and use the library. If you are on Windows, hold on; we need to talk. Remember ABI compatibility we mentioned in the introduction? Remember that I said that this includes communication with the operating system? It turns out, on Windows, you can’t mix and match debug and release builds, this breaks the ABI (but not on UNIX, testing it on Ubuntu shows that you can use library compiled in release mode within a project that is compiled in debug mode).

If you tried to use a library on Windows that was compiled in release mode, but then your project is compiled in debug mode, you will likely see an error message like the following:

complexNumbers.lib(complexNumbers.cpp.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' do
esn't match value '2' in main.obj [C:\Users\tom\code\buildSystems\Build-Systems-Part-4-static-shared-Testing\build\
complexNumberTest.vcxproj]

complexNumbers.lib(complexNumbers.cpp.obj) : error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MD_DynamicRe
lease' doesn't match value 'MDd_DynamicDebug' in main.obj [C:\Users\tom\code\buildSystems\Build-Systems-Part-4-stat
ic-shared-Testing\build\complexNumberTest.vcxproj]

Both _ITERATOR_DEBUG_LEVEL and MD_DynamicRelease are variables set by the linker, and it checks that all dependencies have the same entry (i.e. either debug or release). So, if you want to use your library on Windows, make sure that you either provide both a debug and release build, or be consistent in the use of either a debug/release compilation.

Since both release and debug libraries have the same name, we can only provide one or the other. Thus, on Windows, the convention is that we provide two versions of a library with the name <lib-name>.lib|dll for a release build of a static|dynamic library and <lib-name>d.lib|dll for a debug build. Notice the additional d after <lib-name>. In this way, we can have both libraries compiled and sitting next to each other in the same folder.

This is why we included line 17-19 within the CMakeLists.txt file, i.e. we had:

if(WIN32)
  set(CMAKE_DEBUG_POSTFIX d)
endif()

Since this is a common convention (I would label it as a hack or workaround, though!), CMake has support out of the box for this, and we don’t have to write messy CMake generator expressions to check if we have a release or debug build. Instead, CMake does that for us automatically. By the way, if you are wondering why we use WIN32 (aren’t we firmly established in the 64-bit era?), well, CMake has been around for some time, and it is just showing its age. But again, I remain hopeful for changes to be made by the 22nd century!

If you wanted to build the dynamic library, then you would simply have to change the first command in this section. i.e. we then have

cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_SHARED=ON -G Ninja ..

The rest of the commands remain the same. You will notice, though, that the dynamic library will be installed into the <cmake-install-prefix>/bin directory for Windows, with the wrapper library still located in the <cmake-install-prefix>/lib folder (on UNIX, you get everything in the lib/ folder as expected). The location doesn’t matter, though, as CMake will expose the file locations through the <cmake-install-prefix>/lib/cmake/complexNumber/*.cmake files to any other project that wants to use this complex number library.

However, as you may remember as well from previous articles, if we are working with Windows and dynamic libraries, we’ll need to copy any *.dll file into the same folder as our executable. This may sound trivial, but now we have potentially different library names (thanks to the d suffix), as well as different locations of executables depending on the build script used (some will be in the build/ folder, others will be in build/Debug or build/Release). There is no uniform handling of this, and we’ll need to hack that into our CMake scripts later when we test the library.

At this point, though, we are done with compiling and installing, and we can use our library within another project. Before we do, though, let us quickly look at the changes we would have to make if we wanted to provide a header-only library.

Header-only libraries are a weird niche topic for me in the context of C++. The fact that header-only libraries work at all is somewhat surprising (not from a technical point but from a design philosophy point of view). C++ requires a compiler to translate code into machine instructions, but nothing gets compiled within a header-only library, so you don’t need a compiler (technically) to write your own header-only library.

Of course, you may want to provide some tests to ensure your code is working, and you probably want others to use your code. A header-only library will eventually get compiled, but not by you as the library developer, but rather by the end-user when they include it in their project and then compile it.

Having said that, apart from perhaps compiling test executables, a build script is just as important for a header-only library, as it can keep track of all header files that need to be copied during the installation, as well as generate all required *.cmake files that other projects can use to find this header-only library in a non-standard path.

Compared to our static and dynamic library example above, there are very few differences between the two projects, but they are important enough to highlight so that you can see what needs to be changed should you wish to do so. I’ll concentrate on the differences, rather than going through all steps again, so we can wrap up this section fairly quickly.

Project structure

The project itself remains largely unchanged in terms of its structure, and the only change here is that we have lost the complexNumberLib/complexNumbers.cpp source file. All of that content has now migrated into the complexNumberLib/complexNumbers.hpp header file. All pre-processor directives are gone now, as these are only relevant for dynamic libraries, which we can’t compile now and so we don’t need these directives anymore.

As a consequence, we don’t have any options anymore, and so we can remove the CMakeOptions.txt file. A real header-only library, though, likely would still have some configuration options, such as enabling testing as part of the build process, so you would likely still require the support of some options to be set during the configuration.

root
├── build/
├── cmake/
│   └── complexNumbersConfig.cmake.in
├── complexNumberLib/
│   ├── CMakeLists.txt
│   └── complexNumbers.hpp
└── CMakeLists.txt

Source files

For completeness, I have decided to reproduce the entire header file here so you can directly see the differences to the previous header file. As alluded to above, the pre-processor directives are gone, and all implementation of the functions is now within the header file itself.

#pragma once

#include <iostream>
#include <cmath>
#include <stdexcept>
#include <limits>

class ComplexNumber {
public:
	ComplexNumber(double real, double imaginary) : _real(real), _imaginary(imaginary) {
    isNan(real, imaginary);
  }

	~ComplexNumber() = default;

	double Re() const { return _real; }
	double Im() const { return _imaginary; }
  void setRe(double real) { _real = real; }
  void setIm(double imaginary) { _imaginary = imaginary; }

  void conjugate() { _imaginary *= -1.0; }
  double magnitude() const { return std::sqrt(std::pow(_real, 2) + std::pow(_imaginary, 2)); }
	
	ComplexNumber operator+(const ComplexNumber &other) {
    isNan(_real, _imaginary);
    isNan(other._real, other._imaginary);
		
    ComplexNumber c(0, 0);
		c._real = _real + other._real;
		c._imaginary = _imaginary + other._imaginary;
		return c;
	}

  ComplexNumber operator-(const ComplexNumber &other) {
    isNan(_real, _imaginary);
    isNan(other._real, other._imaginary);
		
    ComplexNumber c(0, 0);
    c._real = _real - other._real;
    c._imaginary = _imaginary - other._imaginary;
    return c;
  }

  ComplexNumber operator*(const ComplexNumber &other) {
    isNan(_real, _imaginary);
    isNan(other._real, other._imaginary);
    
    ComplexNumber c(0, 0);
    c._real = _real * other._real - _imaginary * other._imaginary;
    c._imaginary = _real * other._imaginary + _imaginary * other._real;
    return c;
  }

  ComplexNumber operator/(const ComplexNumber &other) {
    isNan(_real, _imaginary);
    isNan(other._real, other._imaginary);

    double denominator = other._real * other._real + other._imaginary * other._imaginary;
    if (std::abs(denominator) < std::numeric_limits<double>::epsilon())
      throw std::runtime_error("Complex number division by zero");
    
    ComplexNumber c(0, 0);
    c._real = (_real * other._real + _imaginary * other._imaginary) / denominator;
    c._imaginary = (_imaginary * other._real - _real * other._imaginary) / denominator;
    return c;
  }

  friend std::ostream &operator<<(std::ostream &os, const ComplexNumber &c) {
    os << "(" << c._real << ", " << c._imaginary << ")";
    return os;
  }

private:
  void isNan(double real, double imaginary) const {
  if (std::isnan(real) || std::isnan(imaginary))
    throw std::runtime_error("Complex number is NaN");
}
	
private:
	double _real;
	double _imaginary;
};

CMake build scripts

The root-level CMakeLists.txt file also changes slightly. Most of the file remains unchanged, especially the part around the installation, but then we have to change the type of the library from either STATIC or SHARED to INTERFACE. In CMake, if we are working with a header-only library, it is identified as INTERFACE internally.

The target_include_directories() call also lost its generator expression, we no longer need to differentiate between the build and install target, as we no longer have a build target. Since we only have the install target left, we can use that to simplify our build instructions.

Both these changes are shown in the file excerpt below:

# Create a header-only library for the complex number class
add_library(${CMAKE_PROJECT_NAME} INTERFACE)

target_include_directories(${CMAKE_PROJECT_NAME}
  INTERFACE
    ${CMAKE_INSTALL_INCLUDEDIR}
)

Notice also that we can remove calls to set(CMAKE_DEBUG_POSTFIX d) and target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL), as we are no longer building a library. Thus, there is no need for a d library name suffix or pre-processor directive that is applicable to dynamic libraries only.

The complexNumberLib/CMakeLists.txt file also has to change since we no longer have a source file to compile. Though, we still want to associate the header file with the library (target), so that it gets installed as part of the installation step. This is shown below as well:

target_sources(${CMAKE_PROJECT_NAME}
  PUBLIC FILE_SET HEADERS
  BASE_DIRS ${PROJECT_SOURCE_DIR}
  FILES ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.hpp
)

And these were all changes required to CMake! We simply change the library target, remove the project directory for the build stage (though technically we could have left it in, the generator expression would have always evaluated to the install step, never the build step), and removed the source file from the sources as well. Now, let’s look at the changes on the command line to get this library installed.

Command line instructions

While we do not have to build this library, we still need to perform the configuration step. Since we are not building anything, we do not need a generator like Ninja, and we don’t have to specify a build type such as debug or release (that is up to decide for the project that uses our library). Since we also removed the CMakeOptions.txt file, and there are no further variables for us to set for the configuration, we can simply run CMake with the following command:

As already mentioned, we skip the build stage, and this is pretty much the only difference on the command line. The next step is the install step, which still requires a prefix (i.e. install location) to be specified. I am using C:\temp in this example again (though I have also tested ~/temp on UNIX, which is working fine for the header-only and static/dynamic library compilation). The command is:

cmake --install . --prefix "C:\temp"

This will have installed the header-only library on your system. With that done, let us have a look how to use all three types of libraries within another project.

Using the installed libraries in other projects

Up until this point, we haven’t actually achieved anything more than what we did when introducing CMake for the first time, where we looked at the complex number library as well. We did compile and install it (though we didn’t look at the differences between header-only and compiled libraries). What I want to do in this section is to emulate what another project would have to do to use our library. For that, we will develop a very simple project that simply tests the arithmetic overloaded operators within the complex number class.

Project structure

This is the simplest project you can have; it consists of our build/ folder, a main.cpp source file that tests some of the features of the complex number class, as well as the CMakeLists.txt file, which is where all the magic is happening. In addition, we retain a CMakeOptions.txt file, which could have, however, been integrated into the main CMakeLists.txt file. It keeps the options separated, though, from the main build logic, and this is retained here. The structure is given below as well.

root
├── build/
├── main.cpp
├── CMakeLists.txt
└── CMakeOptions.txt

Source file of a simple test program for complex numbers

Let’s start by looking at the content of the main.cpp file. This simple test program includes the header file of our library and then defines two complex numbers that are combined through the various arithmetic operators, e.g. addition, subtraction, multiplication, and division. These are stored in different complex numbers, and the result is then printed to the console for verification. There is no assertion going on, this is not a replacement of a unit test, it is just to show that we can fidn the library and work with it. The code is given below:

#include <iostream>
#include "complexNumberLib/complexNumbers.hpp"

int main() {
  ComplexNumber a(2, 3);
  ComplexNumber b(4, 5);

  ComplexNumber c = a + b;
  ComplexNumber d = a - b;
  ComplexNumber e = a * b;
  ComplexNumber f = a / b;

  std::cout << c << std::endl;
  std::cout << d << std::endl;
  std::cout << e << std::endl;
  std::cout << f << std::endl;

  return 0;
}

CMake build scripts

If you don’t care about dynamic libraries, or you do but only for UNIX systems, you can reduce your CMakeLists.txt file down to only 5 commands. However, we are good CFD developers, and we take cross-platform development seriously. So, our CMake file will now contain 48 lines instead of 5. The excess in lines required is not just a Windows fault; there are still rather hacky commands we need from CMake, which are cross-platform but just not very precise.

Be that as it may, working with libraries in CMake is actually rather straight forward, and there is excellent support for finding libraries, mainly thanks to the generated *.cmake files that we generated and installed as part of our complex number library generation step. Let’s look at the CMaekLists.txt file, and then discuss it below.

cmake_minimum_required(VERSION 3.23)

project(
  complexNumberTest
  VERSION 1.0
  LANGUAGES CXX
)

# include custom options
include(CMakeOptions.txt)

# Find packages will search for a specific library. CMake will fail if the package is REQUIRED but not found
find_package(complexNumbers REQUIRED)

# Add an executable and provide the source file directly.
add_executable(${CMAKE_PROJECT_NAME} main.cpp)

# Make sure you link your executable with your library. Notice the namespace that we defined earlier.
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE complexNumbers::complexNumbers)

if (MSVC)
  if(${ENABLE_SHARED})
    target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL)
    else()
    target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILELIB)
  endif()
endif()

# On Windows, we need the *.dll file in the same folder as the executable to be able to run it
if(WIN32)
  if(EXISTS "${CMAKE_PREFIX_PATH}/bin/complexNumbers.dll" OR EXISTS "${CMAKE_PREFIX_PATH}/bin/complexNumbersd.dll")
    add_custom_command(
      TARGET ${CMAKE_PROJECT_NAME}
      POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_if_different
      "${CMAKE_PREFIX_PATH}/bin/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
      "${CMAKE_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
    )
    add_custom_command(
      TARGET ${CMAKE_PROJECT_NAME}
      POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_if_different
      "${CMAKE_PREFIX_PATH}/bin/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
      "${CMAKE_BINARY_DIR}/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>"
    )
  endif()
endif()

Lines 1-10 should hopefully now be self-explanatory, as we have looked at this before a few times. A new command for us is the find_package() command on line 13, which is similar to the find_dependency() command that I mentioned before when we wrote our <library-name>Config.cmake.in configuration file.

find_package() is a bit of magic (not really, but as someone who has spent quite a lot of time manually compiling, installing, and linking libraries on the command line, it feels like cheating!). It will look for a given library and then has two options if it is not found. If we specify that the library is REQUIRED, CMake will abort the configuration step. If we omit this keyword, CMake will configure without issues and only print a warning to the console that a package was not found.

Some packages (libraries) are essential for your code to work. For example, when we developed our CGNS-based mesh reading library for structured and unstructured grids, we require the CGSN library to be available, otherwise we can’t build the library. In this case, REQUIRED would need to be specified, e.g. find_package(cgns REQUIRED). In other cases, we can proceed with the build process, even if a package is not found. For example, we can still build a library even if the test framework like Google test (gtest) is not found (it won’t affect the build of the library itself).

We then proceeded to line 16 to generate an executable, where I specified the source file here as the second argument. For a larger project, I would omit that source file and add it instead by calling target_sources(), but let’s keep things simple here.

Then, the real magic happens on line 19; we are now instructing CMake to link our executable from line 16 with the complex number library we found on line 13 (if it wasn’t found, CMake would have never gotten to lines 13 and 16 in the first place). Notice that we now have to use the namespace complexNumber:: that we have specified before in the install() command within the root level CMakeLists.txt script of the complex number project.

We link against the library using PRIVATE. There may be circumstances where you need to link here using PUBLIC, for example, if you use a dynamic (SHARED) library and you are developing a library yourself, and you know that whoever uses your library also needs to have access to the library you are linking against. Take the CGNS mesh reader library we worked on as an example; if we were to link the dynamic CGNS library against our mesh reader library, using PUBLIC here would allow anyone using our library to also access CGNS-specific functions/definitions.

On lines 21-27, we check if we want to compile a static or dynamic version of our library. The ENABLE_SHARED variable is set similar to the complex number library itself within the CMakeOptions.txt file and defaults to a static library build if nothing is specified. To overwrite this, we would pass -DENABLE_SHARED=ON|OFF to CMake during configuration.

And then we have lines 30-47. Any comment starting with # On Windows, ... should make you nervous and induce anxiety. It does for me, as I have written many comments starting just like this. I know, from personal experience, that every comment starting with # On Windows, ... or similar, is responsible for a small nervous breakdown on the developer’s side. So let’s go through it.

First, we check if we are on Windows, and if so, if the complex number library exists as a dynamic library within the CMake prefix path. We check for both release (complexNumbers.dll) and debug (complexNumbersd.dll) versions of this library. If it does exist, we invoke the add_custom_command() function in CMake, which allows us to perform some routine tasks (like copying files) in a platform indepdendent manner (i.e. no need to write simple bash or PowerShell scripts). Let’s break this command down in the form that we use it:

add_custom_command(
  TARGET ${CMAKE_PROJECT_NAME}
  POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  <path-and-file-to-copy>
  <file-destination>
)

We say that whenever we invoke the ${CMAKE_PROJECT_NAME} target, which we do when we type cmake --build . into our console (as it will look to build any executables or libraries that were defined with add_executable() or add_library()), it will run this command. Which command you ask? This is specified by the COMMAND keyword on line 4, which can be any command you could type in a terminal. Here, we use ${CMAKE_COMMAND} -E, which will be replaced by cmake -E. There are several internal commands that you can provide here as an argument.

We are using the copy_if_different command, that will look for a file and only copy it if it has changed since the last copy was done. It has two arguments, where the first points to the path and filename that we want to copy, while the second points to the location where we want to store the file.

What we are trying to achieve here is to copy the complexNumbers(d).dll file into our build/, build/Debug, or build/Release directory, so that we can invoke any executable that will be generated in any of these folders. Thus, we see in our CMakeLists.txt file two commands to copy the dynamic library. The <path-and-file-to-copy> for the first command is given as:

${CMAKE_PREFIX_PATH}/bin/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>

First, we check that within the CMake prefix path (e.g. C:\temp here, which is the install directory for our library), if we have a file called bin/complexNumbersd.dll if we are building a debug version of our executable or bin/complexNumbers.dll if we are building a release version. This is what the convoluted generator expression is checking for. Then, on line 37, we specify the path to copy this file to:

${CMAKE_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/$<IF:$<CONFIG:Debug>,complexNumbersd.dll,complexNumbers.dll>

Here, ${CMAKE_BINARY_DIR} is the build/ folder, and then we dynamically construct a string based on the build type, the following paths are possible:

  • build/Debug/complexNumbersd.dll
  • build/Release/complexNumbers.dll

However, the executable may not necessarily be generated in a build/Debug or build/Release folder (in-fact, this is only the case for MSBuild, XCode, and Ninja multi-config). Instead, we may have our executable located just in the build/ folder. In this case, we also need to copy the dynamic library into the build/ folder itself, and this is what lines 39-45 are doing. Having done this, we are now able to consume not just static and header-only libraries with our project, but dynamic libraries as well.

Command line instructions to build and execute the test program

The same is true for the command line. It doesn’t matter which of three types of library you are consuming, nor on which operating system you are. The build instructions will always be the same. Before we start, make sure the build/ folder is present in your project, and if it is, go ahead and configure the project.

The configure step is shown below. The build type is set to release simply because we used a release build for our complex number above. You may have decided to build a debug version instead, in which case you have to change this instruction to debug (that is, if you are in Windows, UNIX users can, again, mix and match build types to their heart’s content).

cmake -DCMAKE_PREFIX_PATH=C:\temp -DCMAKE_BUILD_TYPE=Release -G Ninja ..

I’m using Ninja as my generator again, but you can safely omit this flag. And then there is -DCMAKE_PREFIX_PATH=C:\temp. This is the folder where CMake will look for any lib/cmake/*/*.cmake files that could give CMake an indication of where to find any library in a non-standard search path, including any header files that were installed.

Next, we need to compile and link our executable to our library. This is achieved with the usual build command:

cmake --build . --config Release

We specify again that we want to build in release mode but adjust that as you need based on your previous CMAKE_BUILD_TYPE settings. Once completed, you should now have a new executable in your build/ folder called either complexNumberTest.exe on Windows or complexNumberTest on UNIX, i.e. no file extension.

On Windows, if you are already within the build/ folder, you can execute this file using the following command:

And on UNIX, use:

This should print the following messages to your console:

(6, 8)
(-2, -2)
(-7, 22)
(0.560976, 0.0487805)

This is just the output from the different arithmetic operations, so seeing that we are getting some complex numbers printed to the screen means the library was successfully linked to our executable. Congratulations, you are now able to develop your own libraries and make them available to other projects in a sensible manner.

Summary

In this article, we explored how to work with static, dynamic, and header-only libraries and showcased how to use a simple, complex number library to compile and install it, as well as how to consume this installed library within another project. We have seen that CMake provides a clean interface to us and that we achieve cross-platform compilation for different types of libraries with just a few lines of build script and a handful of commands on the console.

If you have made it all the way to the end of this article and are reading it, this means that you are serious about developing code that is easy for other people/projects to consume. Please make use of this knowledge. Library development can be hard if you have to start from scratch, but most of what you will need is covered in this article. And anything else you may need will be covered in the next articles. So stay tuned for that, but in the meantime, always think of how you can export your work as a library and move away from executables. It makes using your code that much easier!


Tom-Robin Teschner is a senior lecturer in computational fluid dynamics and course director for the MSc in computational fluid dynamics and the MSc in aerospace computational engineering at Cranfield University.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Эмулятор приложений ios для windows
  • Dhcp не включен на сетевом адаптере беспроводная сеть что делать windows 10 ноутбук
  • Asus chipset drivers windows 10
  • Автоматическая установка программ при установке windows
  • Мониторинг процессов windows 10 как открыть