Как изменить компилятор cmake

I have embedded project using cross compiler. I would like to introduce Google test, compiled with native GCC compiler. Additionally build some unit test targets with CTC compiler. Briefly: I ha...

I just had the same issue right now, but the other answer didn’t help me. I’m also cross-compiling, and I need some utility programs to be compiled with GCC, but my core code to be compiled with avr-gcc.

Basically, if you have a CMakeLists.txt, and you want all targets in this file to be compiled with another compiler, you can just set the variables by hand.

Define these macros somewhere:

macro(use_host_compiler)
  if (${CURRENT_COMPILER} STREQUAL "NATIVE")
    # Save current native flags
    set(NATIVE_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "GCC flags for the native compiler." FORCE)

    # Change compiler
    set(CMAKE_SYSTEM_NAME ${CMAKE_HOST_SYSTEM_NAME})
    set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_HOST_SYSTEM_PROCESSOR})
    set(CMAKE_C_COMPILER ${HOST_C_COMPILER})
    set(CMAKE_C_FLAGS ${HOST_C_FLAGS})
    set(CURRENT_COMPILER "HOST" CACHE STRING "Which compiler we are using." FORCE)
  endif()
endmacro()


macro(use_native_compiler)
  if (CMAKE_CROSSCOMPILING AND ${CURRENT_COMPILER} STREQUAL "HOST")
    # Save current host flags
    set(HOST_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "GCC flags for the host compiler." FORCE)

    # Change compiler
    set(CMAKE_SYSTEM_NAME ${NATIVE_SYSTEM_NAME})
    set(CMAKE_SYSTEM_PROCESSOR ${NATIVE_SYSTEM_PROCESSOR})
    set(CMAKE_C_COMPILER ${NATIVE_C_COMPILER})
    set(CMAKE_C_FLAGS ${NATIVE_C_FLAGS})
    set(CURRENT_COMPILER "NATIVE" CACHE STRING "Which compiler we are using." FORCE)
  endif()
endmacro()

At the very beginning of your CMakeLists.txt script (or in a toolchain file), set the following variables according to what you need:

  • CURRENT_COMPILER
  • HOST_C_COMPILER
  • HOST_C_FLAGS
  • NATIVE_SYSTEM_NAME
  • NATIVE_C_COMPILER
  • NATIVE_C_FLAGS

The idea is that CMAKE_C_COMPILER (and company) is a variable like any other, so setting it inside a certain scope will only leave it changed within that scope.


Example usage:

use_host_compiler()
add_executable(foo foo.c) # Compiled with your host (computer)'s compiler.
use_native_compiler()
add_executable(bar bar.c) # Compiled with your native compiler (e.g. `avr-gcc`).

I just had the same issue right now, but the other answer didn’t help me. I’m also cross-compiling, and I need some utility programs to be compiled with GCC, but my core code to be compiled with avr-gcc.

Basically, if you have a CMakeLists.txt, and you want all targets in this file to be compiled with another compiler, you can just set the variables by hand.

Define these macros somewhere:

macro(use_host_compiler)
  if (${CURRENT_COMPILER} STREQUAL "NATIVE")
    # Save current native flags
    set(NATIVE_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "GCC flags for the native compiler." FORCE)

    # Change compiler
    set(CMAKE_SYSTEM_NAME ${CMAKE_HOST_SYSTEM_NAME})
    set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_HOST_SYSTEM_PROCESSOR})
    set(CMAKE_C_COMPILER ${HOST_C_COMPILER})
    set(CMAKE_C_FLAGS ${HOST_C_FLAGS})
    set(CURRENT_COMPILER "HOST" CACHE STRING "Which compiler we are using." FORCE)
  endif()
endmacro()


macro(use_native_compiler)
  if (CMAKE_CROSSCOMPILING AND ${CURRENT_COMPILER} STREQUAL "HOST")
    # Save current host flags
    set(HOST_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "GCC flags for the host compiler." FORCE)

    # Change compiler
    set(CMAKE_SYSTEM_NAME ${NATIVE_SYSTEM_NAME})
    set(CMAKE_SYSTEM_PROCESSOR ${NATIVE_SYSTEM_PROCESSOR})
    set(CMAKE_C_COMPILER ${NATIVE_C_COMPILER})
    set(CMAKE_C_FLAGS ${NATIVE_C_FLAGS})
    set(CURRENT_COMPILER "NATIVE" CACHE STRING "Which compiler we are using." FORCE)
  endif()
endmacro()

At the very beginning of your CMakeLists.txt script (or in a toolchain file), set the following variables according to what you need:

  • CURRENT_COMPILER
  • HOST_C_COMPILER
  • HOST_C_FLAGS
  • NATIVE_SYSTEM_NAME
  • NATIVE_C_COMPILER
  • NATIVE_C_FLAGS

The idea is that CMAKE_C_COMPILER (and company) is a variable like any other, so setting it inside a certain scope will only leave it changed within that scope.


Example usage:

use_host_compiler()
add_executable(foo foo.c) # Compiled with your host (computer)'s compiler.
use_native_compiler()
add_executable(bar bar.c) # Compiled with your native compiler (e.g. `avr-gcc`).

I see more and more people who set CMAKE_C_COMPILER and other compiler-related variables in the CMakeLists.txt after the project call and wonder why this approach breaks sometimes.

What happens actually

When CMake executes the project() call, it looks for a default compiler executable and determines the way for use it: default compiler flags, default linker flags, compile features, etc.

And CMake stores path to that default compiler executable in the CMAKE_C_COMPILER variable.

When one sets CMAKE_C_COMPILER variable after the project() call, this only changes the compiler executable: default flags, features all remains set for the default compiler.

AS RESULT: When the project is built, a build system calls the project-specified compiler executable but with parameters suitable for the default compiler.

As one could guess, this approach would work only when one replaces a default compiler with a highly compatible one. E.g. replacement of gcc with clang could work sometimes.

This approach will never work for replacement of cl compiler (used in Visual Studio) with gcc one. Nor this will work when replacing a native compiler with a cross-compiler.

What to do

Never set a compiler in CMakeLists.txt.

If you want, e.g., to use clang instead of defaulted gcc, then either:

  1. Pass -DCMAKE_C_COMPILER=<compiler> to cmake when configure the project. That way CMake will use this compiler instead of default one and on the project() call it will adjust all flags for the specified compiler.

  2. Set CC environment variable (CXX for C++ compiler). CMake checks this variable when selects a default compiler.

  3. (Only in rare cases) Set CMAKE_C_COMPILER variable before the project() call. This approach is similar to the first one, but makes the project less flexible.

If the ways above do not work

If on setting CMAKE_C_COMPILER in the command line CMake errors that a compiler cannot «compile a simple project», then something wrong in your environment.. or you specify a compiler incompatible for chosen generator or platform.

Examples:

  • Visual Studio generators work with cl compiler but cannot work with gcc.
  • A MinGW compiler usually requires MinGW Makefiles generator.

Incompatible generator cannot be fixed in CMakeLists.txt. One need to pass the proper -G option to the cmake executable (or select the proper generator in CMake GUI).

Cross-compiling

Cross-compiling usually requires setting CMAKE_SYSTEM_NAME variable, and this setting should normally be done in the toolchain file. That toolchain file is also responsible for set a compiler.

Setting CMAKE_SYSTEM_NAME in the CMakeLists.txt is almost always an error.

Учебное пособие по использованию CMake для настройки компиляторов и их флагов для проектов разработки

Вступление

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

В этом руководстве подробно описано, как проверять, настраивать и выводить параметры компилятора проекта с помощью CMake. Мы будем продвигаться постепенно, как описано ниже:

  1. Проверка компилятора по умолчанию
    В этом разделе подробно описано, как узнать, какие компиляторы и флаги компилятора CMake использует в вашей системе по умолчанию. Эти настройки затем используются для компиляции образца проекта, включенного в это руководство.
  2. Выбор компилятора и проверка его свойств
    Затем мы рассмотрим, как выбрать другой компилятор C ++ в системе для сборки нашего проекта, а также как выводить такие свойства, как его путь, идентификатор и версия.
  3. Проверка типов сборки по умолчанию
    Идентифицируются и обсуждаются флаги компилятора по умолчанию, которые CMake использует для своих конфигураций сборки.
  4. Условное добавление флагов компилятора
    Наша конфигурация изменена для условной установки флагов компилятора в зависимости от выбранного компилятора и типа сборки.

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

Исходные файлы

Небольшой пример проекта C ++ был настроен на GitHub, чтобы сопровождать это руководство. Выполните следующую команду git, чтобы загрузить ее в свою систему:

$ git clone https://github.com/danebulat/cmake-compiler-flags.git

Проект содержит файл начальной конфигурации CMake вместе с некоторыми исходными файлами в одном каталоге:

> root
   ---> CMakeLists.txt      # Initial CMake configuration
   ---> interpolate.cpp     # Source file for 'interp' library
   ---> interpolate.h       # Header file for 'interp' library
   ---> main.cpp            # Source file for the executable
   ---> steps/
  • interpolate.cpp и interpolate.h
    Эти файлы реализуют некоторые стандартные функции плавности, которые обычно используются для создания промежуточной анимации. Для выполнения этого руководства не обязательно понимать, как работают функции плавности. CMake просто компилирует эти файлы в общую библиотеку под названием interp.
  • main.cpp
    Простая программа, которая вызывает некоторые функции замедления и выводит результаты. CMake компилирует этот файл в исполняемый файл и связывает его с общей библиотекой interp.
  • CMakeLists.txt
    Файл конфигурации CMake, описывающий, как построен проект.
  • steps/
    Каталог, содержащий моментальные снимки файлов проекта, соответствующих каждому этапу обучения.

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

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

Для компиляции целей проекта требуются функции стандарта C ++ 14. Мы также предпочитаем не запрашивать расширения, специфичные для компилятора.

set(BUILD_SHARED_LIBS ON)

Переменная BUILD_SHARED_LIBS включена, что означает, что разделяемая библиотека всегда создается, если мы не передаем тип библиотеки команде add_library.

add_library(interp interpolate.h interpolate.cpp)

Исходные файлы interpolate.h и interpolate.cpp скомпилированы в общую библиотеку под названием interp. CMake будет использовать предпочтительную схему именования основной операционной системы для разделяемых библиотек. Например, общая библиотека с именем libinterp.so будет создана в операционной системе Linux.

add_executable(main main.cpp)
target_link_libraries(main interp)

Файл main.cpp компилируется в исполняемый файл с именем main, который впоследствии связывается с общей библиотекой interp.

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

1. Проверка компилятора по умолчанию

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

Давайте создадим эти файлы, создав каталог сборки в нашем примере проекта и вызвав CMake для чтения файла конфигурации:

$ cd cmake-compiler-flags
$ mkdir build && cd build
$ cmake ..
    -| The CXX compiler identification is GNU 10.2.0
   ... Check for working CXX compiler: /usr/bin/c++ - skipped

Информация о выбранном компиляторе выводится при чтении файла конфигурации. Идентификатор компилятора и номер версии завершают первую строку вывода. Расположение компилятора в файловой системе также выводится на несколько строк ниже. В моем случае выбрана версия GCC 10.2.0, которая находится по адресу /usr/bin/c++.

Эти значения могут отличаться в вашей системе. Например, компилятор Clang, вероятно, будет выбран в Mac OS с соответствующим двоичным файлом с именем clang++. Мы рассмотрим способы переключения компилятора в следующем разделе.

Стоит отметить, что в вашей файловой системе может быть несколько копий одного и того же компилятора, сопоставленных с разными именами. Например, общие двоичные имена для компилятора GCC включают c++, gcc и g++. Это может произойти, когда вы загружаете группу разработчиков дистрибутива Linux, которая содержит различные пакеты для облегчения разработки.

Если вы не знаете, какие компиляторы установлены в вашей системе, я рекомендую передать флаги --version и --help в соответствующие двоичные файлы:

# A path is returned if the compiler is installed on your system
$ which c++ g++ gcc
# Check compiler name and version
$ c++ --version
$ gcc --version
$ g++ --version
# Review the help listing
$ g++ --help | less
# Compare file size
$ ls -l $(which c++ g++ gcc)

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

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

На данный момент нам нужно знать, что все типы сборки используют флаги компилятора, определенные в переменной CMAKE_CXX_FLAGS. Проекты, для которых не выбран тип сборки, например наша собственная, передают компилятору флаги, определенные в этой переменной. По умолчанию он определяется как пустая строка, а флаги добавляются либо через интерфейс кеша, либо путем изменения CMakeLists.txt. Еще одна важная переменная, о которой следует помнить, — это CMAKE_CXX_COMPILER, в которой хранится путь к выбранному компилятору.

Прежде чем компилировать наш пример проекта, давайте проверим его файл кеша, чтобы подтвердить наличие CMAKE_CXX_FLAGS в дополнение к выбранному компилятору:

# Open cache in build directory
$ ccmake .

Программа ccmake представляет собой интерфейс командной строки, который позволяет нам взаимодействовать с файлом кэша нашего проекта. Самая верхняя переменная с именем CMAKE_BUILD_TYPE — это пустая строка, что означает, что наш проект еще не выбрал тип сборки. Интересующие нас переменные открываются после нажатия клавиши t для переключения режима продвижения — теперь будут отображаться CMAKE_CXX_COMPILER и CMAKE_CXX_FLAGS. Когда вы закончите проверку кеша, нажмите q для выхода:

Теперь мы знаем, какой компилятор выбран и какие флаги (отсутствие) он получит, давайте создадим проект и запустим нашу программу:

# Build project
$ cmake --build .
# Run program
$ ./main
    -| Begin...
       Result 0: 1 ...

2. Выбор компилятора и проверка его свойств

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

$ cmake --system-information information.txt

Отсюда мы можем открыть information.txt в текстовом редакторе, таком как Vim, и искать конкретную информацию о нашей среде. Идите вперед и найдите следующие переменные, чтобы проверить, какие значения присвоены по умолчанию:

  • CMAKE_CXX_COMPILER_LOADED: Истина, если для проекта включен C ++.
  • CMAKE_CXX_COMPILER_ID: Уникальная идентификационная строка компилятора.
  • CMAKE_COMPILER_IS_GNUCXX: Истина, если компилятор C ++ является частью GCC.
  • CMAKE_CXX_COMPILER_VERSION: строка версии компилятора C ++.
  • CMAKE_CXX_COMPILER: путь к выбранному компилятору C ++.
  • CMAKE_C_COMPILER: путь к выбранному компилятору C.

Значения, присвоенные каждой переменной в information.txt, будут различаться в зависимости от операционной системы хоста. Также обратите внимание, что идентичные переменные определены для других языков, поддерживаемых CMake. Например, в именах переменных языка C указано C вместо CXX.

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

Выбор другого компилятора

Давайте обсудим, как выбрать другой компилятор C ++ для сборки проекта. Возможно, вы являетесь поклонником компилятора Clang / LLVM и предпочли бы использовать его вместо GCC. Как вы могли заметить, CMake хранит путь к выбранному компилятору внутри переменной с именем CMAKE_CXX_COMPILER. Эту переменную можно установить двумя способами:

  • С помощью переключателя в командной строке:
    cmake -DCMAKE_CXX_COMPILER=<compiler name> ..
  • Использование переменной среды:
    export CXX=<compiler name>

Если вы хотите, чтобы флаг --system-information обнаруживал другой компилятор, вам нужно будет экспортировать переменную среды с именем CXX:

# Create CXX environment variable
$ export CXX=clang++
$ echo $CXX
    -| clang++
# Overwrite information.txt with updated system information
$ cmake --system-information information.txt

Теперь будет выбран Clang, если CMake повторно вызывается для чтения CMakeLists.txt. В качестве альтернативы компилятор можно указать в командной строке, явно задав CMAKE_CXX_COMPILER. Это будет иметь приоритет перед любым значением, хранящимся в переменной среды CXX:

# Enter build directory and remove all files (clean)
$ cd build && rm -fr *
# Read configuration and set g++ compiler
$ cmake -DCMAKE_CXX_COMPILER=clang++ ..
    -| The CXX compiler identification is Clang 11.0.0
   ... Check for working CXX compiler: /usr/bin/clang++ - skipped

На этом этапе образец проекта можно построить и запустить так же, как и раньше. Однако этапы компиляции и компоновки будут обрабатываться Clang вместо GCC:

$ cmake --build .
$ ./main
    -| Begin...
       Result 0: 1 ...

Вывод информации о компиляторе во время настройки

Дополнительная информация может быть отправлена ​​на стандартный вывод, когда CMake вызывается для чтения файла конфигурации. Например, приложение может иметь параметр конфигурации для управления тем, будет ли оно связываться с разделяемыми библиотеками или статическими библиотеками во время компиляции. Было бы неплохо увидеть визуальное подтверждение этого параметра во время настройки, прежде чем мы приступим к процессу сборки.

Имея это в виду, мы обновим наш файл конфигурации, чтобы вывести информацию о выбранном компиляторе. Фактически, мы будем ссылаться на те же переменные, которые искали ранее в файле information.txt. Добавьте следующий код в CMakeLists.txt, чтобы увидеть это в действии:

   set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
>> if(CMAKE_CXX_COMPILER_LOADED)
     message(STATUS "Compiler path: ${CMAKE_CXX_COMPILER}")
     message(STATUS "Compiler ID: ${CMAKE_CXX_COMPILER_ID}")
     message(STATUS "Compiler version:    
             ${CMAKE_CXX_COMPILER_VERSION}")
     message(STATUS "Compiler is part of GCC: 
             ${CMAKE_COMPILER_IS_GNUCXX}")
   endif()

Наша конфигурация сначала проверяет, включен ли компилятор C ++ через переменную CMAKE_CXX_COMPILER_LOADED. Если компилятор включен, команда message вызывается для каждой переменной, которую мы хотим зарегистрировать. Обратите внимание, что для ссылки на переменные CMake внутри строки указывается знак доллара, за которым следует фигурные скобки вокруг имени переменной.

Теперь наши сообщения выводятся всякий раз, когда CMake вызывается для чтения CMakeLists.txt:

$ cmake ..
    -| Compiler path: /usr/bin/clang++
       Compiler ID: Clang
       Compiler version: 11.0.0
       Compiler is part of GCC:  ...

Вывод информации о компиляторе во время выполнения

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

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

   set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
   
>> option(OUTPUT_COMPILER_INFO
     "Output compiler information when launching the main
      executable." ON)

Еще одна проблема, с которой мы сталкиваемся, — это поведение переменной CMAKE_COMPILER_IS_GNUCXX. Поскольку CMAKE_COMPILER_IS_GNUCXX либо содержит значение 1, либо вообще не содержит значения, сопоставление его с определением препроцессора подвержено ошибкам. Вместо этого мы создадим новую переменную с именем CXX_COMPILER_IS_GNU и присвоим ей 1 или 0 в зависимости от того, содержит ли CMAKE_COMPILER_IS_GNUCXX значение:

   option(OUTPUT_COMPILER_INFO "..." ON)
>> if(CMAKE_COMPILER_IS_GNUCXX MATCHES 1)
     set(CXX_COMPILER_IS_GNU 1)
   else()
     set(CXX_COMPILER_IS_GNU 0)
   endif()

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

configure_file(<input> <output>)

Подобно шаблону, входной файл содержит заполнители для переменных, определенных в CMakeLists.txt. Синтаксис объявления заполнителя переменной — @[email protected]. Выходной файл скопирует входной файл и установит для заполнителей фактические значения, определенные в нашей конфигурации сборки. Давайте сначала создадим входной файл в корневом каталоге демонстрационного проекта с именем config.h.in и добавим в него несколько определений:

#pragma once
#define COMPILER_LOADED @[email protected]
#define COMPILER_IS_GNU @[email protected]
#define COMPILER_NAME "@[email protected]"
#define COMPILER_ID "@[email protected]"
#define COMPILER_VERSION "@[email protected]"
#cmakedefine OUTPUT_COMPILER_INFO

Мы продолжаем, вызывая configure_file в нашем файле конфигурации. Сразу после этого вызов target_include_directories указывает место, где будет сгенерирован config.h, чтобы main мог найти его в файловой системе:

   target_link_libraries(main interp)
 
>> # generate configuration file
   configure_file(config.h.in config.h @ONLY)
   # include binary directory to find config.h
   target_include_directories(main PRIVATE ${PROJECT_BINARY_DIR})
  • @ONLY указывает, что заполнители могут быть объявлены только с символом @.
  • Ключевое слово PRIVATE означает, что только цель main будет искать в двоичном каталоге включаемые файлы. Любая цель, которая ссылается на main, не будет искать каталоги, перечисленные в этом вызове.

Наконец, обновите main.cpp, чтобы включить config.h. На определения препроцессора, определенные в config.h, теперь можно ссылаться в приложении для выполнения таких операций, как условная компиляция и отладка. В нашем случае мы определяем функцию, которая выводит свойства выбранного компилятора и впоследствии вызывает ее в функции main. Кроме того, OUTPUT_COMPILER_INFO используется для выполнения условной компиляции — если он содержит ложное значение, наш новый код не компилируется:

Когда наша новая конфигурация сборки завершена, давайте посмотрим, что произойдет, когда мы выберем другой компилятор:

$ cd build && rm -fr *
$ cmake -DCMAKE_CXX_COMPILER=g++ ..
$ cmake --build .
$ ./main
   -| Compiler name: /usr/bin/g++
      Compiler ID: GNU
      Compiler version: 10.2.0
      GCC compiler: 1

Более того, если вы установите OUTPUT_COMPILER_INFO в OFF в кэше и перестроите, наша функция вывода не будет компилироваться.

3. Проверка типов сборки по умолчанию

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

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

  • Debug: Быстрое время компиляции, без оптимизации, символы отладки поддерживаются.
  • Release: Компилируется с высоким уровнем оптимизации. Отдает приоритет производительности времени выполнения над временем компиляции.
  • RelWithDebugInfo: Компилируется с хорошим уровнем оптимизации, символы отладки сохранены.
  • MinSizeRel: Компилируется с оптимизацией, которая ставит размер и скорость исполняемого файла в приоритет над временем компиляции.

Для каждого типа сборки, упомянутого выше, CMake создает соответствующую переменную для хранения флагов компилятора. Имена переменных вместе с их флагами подробно описаны ниже:

  • CMAKE_CXX_FLAGS_DEBUG: -g
  • CMAKE_CXX_FLAGS_MINSIZEREL: -Os -DNDEBUG
  • CMAKE_CXX_FLAGS_RELEASE: -O3 -DNDEBUG
  • CMAKE_CXX_FLAGS_RELWITHDEBUGINFO: -O2 -g -DNDEBUG

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

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

Доступ к переменным типа сборки и их соответствующим флагам компилятора можно получить из кеша. Переключите расширенный режим после запуска ccmake или cmake-gui, чтобы проверить их.

Выбор типа сборки

Выбрать тип сборки можно несколькими способами. Один из способов — явно установить переменную CMAKE_BUILD_TYPE в вашей конфигурации:

   set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
>> if(NOT CMAKE_BUILD_TYPE)
     set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
   endif()

Этот код устанавливает тип сборки Release, при этом значение для CMAKE_BUILD_TYPE не передается в командной строке. Это означает, что проект всегда будет наследовать встроенный тип при его настройке. Имейте в виду, что новое значение всегда можно установить через интерфейс кеша перед запуском процесса сборки.

Другой метод, обычно применяемый для установки переменной CMAKE_BUILD_TYPE, — это использование командной строки во время настройки:

$ cmake -D CMAKE_BUILD_TYPE=Debug ..

В этом случае вместо Release выбирается конфигурация Debug.

Чтобы продемонстрировать типы сборки в действии, в main.cpp можно добавить некоторый код для условной компиляции некоторого кода. Мы выведем сообщение, которое будет меняться в зависимости от того, выбран ли тип сборки отладки или нет. Помните символ -NDEBUG, который определяют типы сборки выпуска? Мы будем использовать это, чтобы определить, какой тип сборки выбран.

Давайте продолжим, добавив код в main.cpp:

>> #ifdef NDEBUG
       std::cout << "NDEBUG Defined (Release)" 
        << std::endl << std::endl;
   #else
    std::cout << "NDEBUG Not Defined (Debug)" 
                 << std::endl << std::endl;
   #endif
   std::cout << "Begin..." << std::endl;

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

# Clean generated files
$ cmake --build . --target clean
# Configure a debug build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ ..
$ cmake --build .
# Run exectuable
$ ./main
   -| Compiler name: /usr/bin/clang++
      ...
      NDEBUG Defined (Release)

4. Условное добавление флагов компилятора

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

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

     message(STATUS "Compiler is part of GCC: ${ ... }")
   endif()
>> set(CXX_FLAGS)
   set(CXX_FLAGS_DEBUG)
   set(CXX_FLAGS_RELEASE)

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

   set(CXX_FLAGS_RELEASE)
>> list(APPEND CXX_FLAGS "-fPIC" "-Wall")

Отсюда мы создаем пару if операторов, которые проверяют, какой компилятор выбран во время сборки. Наша конфигурация проверяет наличие компилятора GNU, а также компилятора Clang. В зависимости от выбранного компилятора к нашим переменным добавляются соответствующие флаги:

   list(APPEND CXX_FLAGS "-fPIC" "-Wall")
   
>> if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
     list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions")
     list(APPEND CXX_FLAGS_DEBUG "-Wsuggest-final-types" 
          "-Wsuggest-final-methods" "-Wsuggest-override")
     list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
   endif()
   if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
     list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions" 
          "-Qunused-arguments" "-fcolor-diagnostics")
     list(APPEND CXX_FLAGS_DEBUG "-Wdocumentation")
     list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
   endif()

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

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

>> target_compile_options(interp PRIVATE ${CXX_FLAGS})
   add_executable(main main.cpp)

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

>> target_compile_options(main PRIVATE ${CXX_FLAGS}
     "$<$<CONFIG:Debug>:${CXX_FLAGS_DEBUG}>"
     "$<$<CONFIG:Release>:${CXX_FLAGS_RELEASE}>")
   target_link_libraries(main interp)

Выражение генератора CONFIG возвращает выбранный тип сборки. Мы используем его здесь, чтобы назначить правильные флаги компилятора в зависимости от выбранного типа сборки. Например, если CONFIG возвращает Debug, $<CONFIG:Debug> расширится до $<Debug:Debug>, что приведет к истинному значению. В этом случае используются флаги компилятора, определенные в CXX_FLAGS_DEBUG. С другой стороны, CMake выберет флаги, определенные в CXX_FLAGS_RELEASED, если CONFIG вернет Release.

Также важно знать, что параметры компиляции могут быть добавлены с тремя уровнями видимости:

  • PRIVATE: параметры будут применены только к данной цели, а не к другим потребляющим ее целям.
  • INTERFACE: параметры для данной цели будут применяться только к целям, потребляющим ее.
  • PUBLIC: параметры будут применены к данной цели и ко всем другим целям, потребляющим ее.

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

Снова соберите образец проекта несколько раз, но на этот раз добавьте -- VERBOSE=1 в конце команды сборки. Эта опция позволит нам подтвердить, какие флаги компилятора были получены компилятором:

$ cd build && rm -fr *
$ cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug ..
$ cmake --build . -- VERBOSE=1

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

Вывод выбранных флагов компилятора

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

Обновите файл конфигурации, чтобы выводить флаги компилятора непосредственно перед сборкой общей библиотеки:

>> include(CMakePrintHelpers)
   cmake_print_variables(CXX_FLAGS)
   cmake_print_variables(CXX_FLAGS_DEBUG)
   cmake_print_variables(CXX_FLAGS_RELEASE)
   
   add_library(interp interpolate.h interpolate.cpp)

Мы включаем модуль CMakePrintHelpers и немедленно вызываем cmake_print_variables для вывода содержимого каждой переменной в одной строке. Также можно указать несколько переменных за один вызов.

Страница руководства для любого модуля CMake доступна из командной строки с помощью параметра --help-module. Например, следующая команда открывает страницу руководства для модуля CMakePrintHelpers:

$ cmake --help-module CMakePrintHelpers

Выбранные флаги теперь выводятся для конкретной сборки во время настройки:

В заключение

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

Статьи по Теме

Другие написанные мной статьи, посвященные разработке с помощью CMake:

description title ms.date helpviewer_keywords

Learn more about: Customize CMake build settings

Customize CMake build settings in Visual Studio

12/15/2021

CMake build settings

Customize CMake build settings

::: moniker range=»>=msvc-160″

Visual Studio uses a CMake configuration file to drive CMake generation and build. CMakePresets.json is supported by Visual Studio 2019 version 16.10 or later and is the recommended CMake configuration file. CMakePresets.json is supported directly by CMake and can be used to drive CMake generation and build from Visual Studio, from VS Code, in a Continuous Integration pipeline, and from the command line on Windows, Linux, and Mac. For more information on CMakePresets.json, see Configure and build with CMake Presets.

If you maintain projects that use a CMakeSettings.json file for CMake build configuration, Visual Studio 2019 and later versions provide a CMake settings editor. The editor lets you add CMake configurations and customize their settings easily. It’s intended to be a simpler alternative to manually editing the CMakeSettings.json file. However, if you prefer to edit the file directly, you can select the Edit JSON link in the upper right of the editor.

To open the CMake settings editor, select the Configuration drop-down in the main toolbar and choose Manage Configurations.

Screenshot of the CMake configuration drop-down that highlights the Manage Configurations selection.

Now you see the Settings Editor with the installed configurations on the left.

Screenshot of the CMake settings editor.

Visual Studio provides one x64-Debug configuration by default. You can add more configurations by choosing the green plus sign. The settings that you see in the editor might vary depending on which configuration is selected.

The options that you choose in the editor are written to a file called CMakeSettings.json. This file provides command-line arguments and environment variables that are passed to CMake when you build the projects. Visual Studio never modifies CMakeLists.txt automatically; by using CMakeSettings.json you can customize the build through Visual Studio while leaving the CMake project files untouched so that others on your team can consume them with whatever tools they’re using.

CMake General Settings

The following settings are available under the General heading:

Configuration name

Corresponds to the name setting. This name appears in the C++ configuration dropdown. You can use the ${name} macro to compose other property values such as paths.

Configuration type

Corresponds to the configurationType setting. Defines the build configuration type for the selected generator. Currently supported values are «Debug», «MinSizeRel», «Release», and «RelWithDebInfo». It maps to CMAKE_BUILD_TYPE.

Toolset

Corresponds to the inheritedEnvironments setting. Defines the compiler environment that’s used to build the selected configuration. Supported values depend on the type of configuration. To create a custom environment, choose the Edit JSON link in the upper right corner of the Settings editor, and edit the CMakeSettings.json file directly.

CMake toolchain file

Path to the CMake toolchain file. This path is passed to CMake as «-DCMAKE_TOOLCHAIN_FILE = <filepath>». Toolchain files specify locations of compilers and toolchain utilities, and other target platform and compiler-related information. By default, Visual Studio uses the vcpkg toolchain file if this setting is unspecified.

Build root

Corresponds to buildRoot. Maps to CMAKE_BINARY_DIR, and specifies where to create the CMake cache. The specified folder is created if it doesn’t exist.

Command arguments

The following settings are available under the Command arguments heading:

CMake command arguments

Corresponds to cmakeCommandArgs. Specifies any more command-line options passed to CMake.

Build command arguments

Corresponds to buildCommandArgs. Specifies more switches to pass to the underlying build system. For example, passing -v when using the Ninja generator forces Ninja to output command lines.

CTest command arguments

Corresponds to ctestCommandArgs. Specifies more command-line options to pass to CTest when running tests.

General settings for remote builds

For configurations such as Linux that use remote builds, the following settings are also available:

rsync command arguments

Extra command-line options passed to rsync, a fast, versatile file-copying tool.

CMake variables and cache

These settings enable you to set CMake variables and save them in CMakeSettings.json. They’re passed to CMake at build time, and override whatever values are in the CMakeLists.txt file. You can use this section in the same way that you might use the CMakeGUI to view a list of all the CMake variables available to edit. Choose the Save and generate cache button to view a list of all CMake variables available to edit, including advanced variables (per the CMakeGUI). You can filter the list by variable name.

Corresponds to variables. Contains a name-value pair of CMake variables passed as -D name=value to CMake. If your CMake project build instructions specify the addition of any variables directly to the CMake cache file, we recommend you add them here instead.

Advanced settings

CMake generator

Corresponds to generator. Maps to the CMake -G switch, and specifies the CMake generator to use. This property can also be used as a macro, ${generator}, when composing other property values. Visual Studio currently supports the following CMake generators:

  • «Ninja»
  • «Unix Makefiles»
  • «Visual Studio 16 2019»
  • «Visual Studio 16 2019 Win64»
  • «Visual Studio 16 2019 ARM»
  • «Visual Studio 15 2017»
  • «Visual Studio 15 2017 Win64»
  • «Visual Studio 15 2017 ARM»
  • «Visual Studio 14 2015»
  • «Visual Studio 14 2015 Win64»
  • «Visual Studio 14 2015 ARM»

Because Ninja is designed for fast build speeds instead of flexibility and function, it’s set as the default. However, some CMake projects may be unable to correctly build using Ninja. If that occurs, you can instruct CMake to generate a Visual Studio project instead.

IntelliSense mode

The IntelliSense mode used by the IntelliSense engine. If no mode is selected, Visual Studio inherits the mode from the specified toolset.

Install directory

The directory in which CMake installs targets. Maps to CMAKE_INSTALL_PREFIX.

CMake executable

The full path to the CMake program executable, including the file name and extension. It allows you to use a custom version of CMake with Visual Studio. For remote builds, specify the CMake location on the remote machine.

For configurations such as Linux that use remote builds, the following settings are also available:

Remote CMakeLists.txt root

The directory on the remote machine that contains the root CMakeLists.txt file.

Remote install root

The directory on the remote machine in which CMake installs targets. Maps to CMAKE_INSTALL_PREFIX.

Remote copy sources

Specifies whether to copy source files to the remote machine, and lets you specify whether to use rsync or sftp.

Directly edit CMakeSettings.json

You can also directly edit CMakeSettings.json to create custom configurations. The Settings Editor has an Edit JSON button in the upper right that opens the file for editing.

The following example shows a sample configuration, which you can use as a starting point:

    {
      "name": "x86-Debug",
      "generator": "Ninja",
      "configurationType": "Debug",
      "inheritEnvironments": [ "msvc_x86" ],
      "buildRoot": "${env.USERPROFILE}\CMakeBuilds\${workspaceHash}\build\${name}",
      "installRoot": "${env.USERPROFILE}\CMakeBuilds\${workspaceHash}\install\${name}",
      "cmakeCommandArgs": "",
      "buildCommandArgs": "-v",
      "ctestCommandArgs": ""
    },

JSON IntelliSense helps you edit the CMakeSettings.json file:

Screenshot of the CMake JSON IntelliSense pop-up in the editor.

The JSON editor also informs you when you choose incompatible settings.

For more information about each of the properties in the file, see CMakeSettings.json schema reference.

::: moniker-end

::: moniker range=»<=msvc-150″

Visual Studio 2017 provides several CMake configurations that define how CMake is invoked to create the CMake cache for a given project. To add a new configuration, select the configuration drop-down in the toolbar and choose Manage Configurations:

Screenshot of Manage configurations selected in the drop-down.

You can choose from the list of predefined configurations:

Add Configuration to CMake Settings dialog list of predefined configurations.

The first time you select a configuration, Visual Studio creates a CMakeSettings.json file in your project’s root folder. This file is used to re-create the CMake cache file, for example after a Clean operation.

To add another configuration, right-click CMakeSettings.json and choose Add Configuration.

Screenshot of the shortcut menu with Add configuration selected.

You can also edit the file using the CMake Settings Editor. Right-click on CMakeSettings.json in Solution Explorer and choose Edit CMake Settings. Or, select Manage Configurations from the configuration drop-down at the top of the editor window.

You can also directly edit CMakeSettings.json to create custom configurations. The following example shows a sample configuration, which you can use as a starting point:

    {
      "name": "x86-Debug",
      "generator": "Ninja",
      "configurationType": "Debug",
      "inheritEnvironments": [ "msvc_x86" ],
      "buildRoot": "${env.USERPROFILE}\CMakeBuilds\${workspaceHash}\build\${name}",
      "installRoot": "${env.USERPROFILE}\CMakeBuilds\${workspaceHash}\install\${name}",
      "cmakeCommandArgs": "",
      "buildCommandArgs": "-v",
      "ctestCommandArgs": ""
    },

JSON IntelliSense helps you edit the CMakeSettings.json file:

Screenshot of the CMake JSON IntelliSense pop-up in the editor.

For more information about each of the properties in the file, see CMakeSettings.json schema reference.

::: moniker-end

See also

CMake Projects in Visual Studio
Configure a Linux CMake project
Connect to your remote Linux computer
Configure CMake debugging sessions
Deploy, run, and debug your Linux project
CMake predefined configuration reference

Дружба навек

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

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

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

  1. Сборку;
  2. Автозапуск тестов;
  3. Замер покрытия кода;
  4. Установку;
  5. Автодокументирование;
  6. Генерацию онлайн-песочницы;
  7. Статический анализ.

Кто и так разбирается в плюсах и си-мейке может просто скачать шаблон проекта и начать им пользоваться.

Содержание

  1. Проект изнутри
    1. Структура проекта
    2. Главный CMake-файл (./CMakeLists.txt)
      1. Информация о проекте
      2. Опции проекта
      3. Опции компиляции
      4. Основная цель
      5. Установка
      6. Тесты
      7. Документация
      8. Онлайн-песочница
    3. Скрипт для тестов (test/CMakeLists.txt)
      1. Тестирование
      2. Покрытие
    4. Скрипт для документации (doc/CMakeLists.txt)
    5. Скрипт для онлайн-песочницы (online/CMakeLists.txt)
  2. Проект снаружи
    1. Сборка
      1. Генерация
      2. Сборка
    2. Опции
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Сборочные цели
      1. По умолчанию
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Примеры
  3. Инструменты
  4. Статический анализ
  5. Послесловие

Проект изнутри

Структура проекта

.
├── CMakeLists.txt
├── README.en.md
├── README.md
├── doc
│   ├── CMakeLists.txt
│   └── Doxyfile.in
├── include
│   └── mylib
│       └── myfeature.hpp
├── online
│   ├── CMakeLists.txt
│   ├── mylib-example.cpp
│   └── wandbox.py
└── test
    ├── CMakeLists.txt
    ├── mylib
    │   └── myfeature.cpp
    └── test_main.cpp

Главным образом речь пойдёт о том, как организовать CMake-скрипты, поэтому они будут разобраны подробно. Остальные файлы каждый желающий может посмотреть непосредственно на странице проекта-шаблона.

Главный CMake-файл (./CMakeLists.txt)

Информация о проекте

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

cmake_minimum_required(VERSION 3.13)

Затем обозначим наш проект, его название, версию, используемые языки и прочее (см. команду project).

В данном случае указываем язык CXX (а это значит C++), чтобы CMake не напрягался и не искал компилятор языка C (по умолчанию в CMake включены два языка: C и C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Здесь же можно сразу проверить, включён ли наш проект в другой проект в качестве подпроекта. Это сильно поможет в дальнейшем.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Опции проекта

Предусмотрим две опции.

Первая опция — MYLIB_TESTING — для выключения модульных тестов. Это может понадобиться, если мы уверены, что с тестами всё в порядке, а мы хотим, например, только установить или запакетировать наш проект. Или наш проект включён в качестве подпроекта — в этом случае пользователю нашего проекта не интересно запускать наши тесты. Вы же не тестируете зависимости, которыми пользуетесь?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

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

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Опции компиляции

Разумеется, мы крутые программисты-плюсовики, поэтому хотим от компилятора максимального уровня диагностик времени компиляции. Ни одна мышь не проскочит.

add_compile_options(
    -Werror

    -Wall
    -Wextra
    -Wpedantic

    -Wcast-align
    -Wcast-qual
    -Wconversion
    -Wctor-dtor-privacy
    -Wenum-compare
    -Wfloat-equal
    -Wnon-virtual-dtor
    -Wold-style-cast
    -Woverloaded-virtual
    -Wredundant-decls
    -Wsign-conversion
    -Wsign-promo
)

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

if(NOT CMAKE_CXX_EXTENSIONS)
    set(CMAKE_CXX_EXTENSIONS OFF)
endif()

Основная цель

Наша библиотека состоит только из заголовочных файлов, а значит, мы не располагаем каким-либо выхлопом в виде статических или динамических библиотек. С другой стороны, чтобы использовать нашу библиотеку снаружи, её нужно установить, нужно, чтобы её можно было обнаружить в системе и подключить к своему проекту, и при этом вместе с ней были привязаны эти самые заголовки, а также, возможно, какие-то дополнительные свойства.

Для этой цели создаём интерфейсную библиотеку.

add_library(mylib INTERFACE)

Привязываем заголовки к нашей интерфейсной библиотеке.

Современное, модное, молодёжное использование CMake подразумевает, что заголовки, свойства и т.п. передаются через одну единственную цель. Таким образом, достаточно сказать target_link_libraries(target PRIVATE dependency), и все заголовки, которые ассоциированы с целью dependency, будут доступны для исходников, принадлежащих цели target. И не требуется никаких [target_]include_directories. Это будет продемонстрировано ниже при разборе CMake-скрипта для модульных тестов.

Также стоит обратить внимание на т.н. выражения-генераторы: $<...>.

Данная команда ассоциирует нужные нам заголовки с нашей интерфейсной библиотекой, причём, в случае, если наша библиотека будет подключена к какой-либо цели в рамках одной иерархии CMake, то с ней будут ассоциированы заголовки из директории ${CMAKE_CURRENT_SOURCE_DIR}/include, а если наша библиотека установлена в систему и подключена в другой проект с помощью команды find_package, то с ней будут ассоциированы заголовки из директории include относительно директории установки.

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Установим стандарт языка. Разумеется, самый последний. При этом не просто включаем стандарт, но и распространяем его на тех, кто будет использовать нашу библиотеку. Это достигается за счёт того, что установленное свойство имеет категорию INTERFACE (см. команду target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Заводим псевдоним для нашей библиотеки. Причём для красоты он будет в специальном «пространстве имён». Это будет полезно, когда в нашей библиотеке появятся разные модули, и мы заходим подключать их независимо друг от друга. Как в Бусте, например.

add_library(Mylib::mylib ALIAS mylib)

Установка

Установка наших заголовков в систему. Тут всё просто. Говорим, что папка со всеми заголовками должна попасть в директорию include относительно места установки.

install(DIRECTORY include/mylib DESTINATION include)

Далее сообщаем системе сборки о том, что мы хотим иметь возможность в сторонних проектах звать команду find_package(Mylib) и получать цель Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

Следующее заклинание нужно понимать так. Когда в стороннем проекте мы вызовем команду find_package(Mylib 1.2.3 REQUIRED), и при этом реальная версия установленной библиотеки окажется несовместимой с версией 1.2.3, CMake автоматически сгенерирует ошибку. То есть не нужно будет следить за версиями вручную.

include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake"
    VERSION
        ${PROJECT_VERSION}
    COMPATIBILITY
        AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)

Тесты

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

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

Документация

Документация также не будет генерироваться в случае подпроекта.

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

Онлайн-песочница

Аналогично, онлайн-песочницы у подпроекта тоже не будет.

if(NOT IS_SUBPROJECT)
    add_subdirectory(online)
endif()

Скрипт для тестов (test/CMakeLists.txt)

Тестирование

Первым делом находим пакет с нужным тестовым фреймворком (замените на свой любимый).

find_package(doctest 2.3.3 REQUIRED)

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

add_executable(mylib-unit-tests test_main.cpp)

А файлы, в которых описаны сами тесты, добавляю позже. Но так делать не обязательно.

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

Подключаем зависимости. Обратите внимание, что к нашему бинарнику мы привязали только нужные нам CMake-цели, и не вызывали команду target_include_directories. Заголовки из тестового фреймворка и из нашей Mylib::mylib, а также параметры сборки (в нашем случае это стандарт языка C++) пролезли вместе с этими целями.

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

Наконец, создаём фиктивную цель, «сборка» которой эквивалентна запуску тестов, и добавляем эту цель в сборку по умолчанию (за это отвечает атрибут ALL). Это значит, что сборка по умолчанию инициирует запуск тестов, то есть мы никогда не забудем их запустить.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Покрытие

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

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Измерение покрытия кода тестами включено")

    target_compile_options(mylib-unit-tests PRIVATE --coverage)
    target_link_libraries(mylib-unit-tests PRIVATE gcov)

    add_custom_target(coverage
        COMMAND
            ${GCOVR_EXECUTABLE}
                --root=${PROJECT_SOURCE_DIR}/include/
                --object-directory=${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS
            check
    )
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE)
    set(MYLIB_COVERAGE OFF)
    message(WARNING "Для замеров покрытия кода тестами требуется программа gcovr")
endif()

Скрипт для документации (doc/CMakeLists.txt)

Нашли Doxygen.

find_package(Doxygen)

Дальше проверяем, установлена ли пользователем переменная с языком. Если да, то не трогаем, если нет, то берём русский. Затем конфигурируем файлы системы Doxygen. Все нужные переменные, в том числе и язык попадают туда в процессе конфигурации (см. команду configure_file).

После чего создаём цель doc, которая будет запускать генерирование документации. Поскольку генерирование документации — не самая большая необходимость в процессе разработки, то по умолчанию цель включена не будет, её придётся запускать явно.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Скрипт для онлайн-песочницы (online/CMakeLists.txt)

Тут находим третий Питон и создаём цель wandbox, которая генерирует запрос, соответствующий API сервиса Wandbox, и отсылает его. В ответ приходит ссылка на готовую песочницу.

find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE)
    set(WANDBOX_URL "https://wandbox.org/api/compile.json")

    add_custom_target(wandbox
        COMMAND
            ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include |
            curl -H "Content-type: application/json" -d @- ${WANDBOX_URL}
        WORKING_DIRECTORY
            ${CMAKE_CURRENT_SOURCE_DIR}
        DEPENDS
            mylib-unit-tests
    )
else()
    message(WARNING "Для создания онлайн-песочницы требуется интерпретатор ЯП python 3-й версии")
endif()

Проект снаружи

Теперь рассмотрим, как этим всем пользоваться.

Сборка

Сборка данного проекта, как и любого другого проекта на системе сборки CMake, состоит из двух этапов:

Генерация

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Если команда выше не сработала из-за старой версии CMake, попробуйте опустить -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Подробнее про опции.

Сборка проекта

cmake --build путь/к/сборочной/директории [--target target]

Подробнее про сборочные цели.

Опции

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Включает цель coverage, с помощью которой можно запустить замер покрытия кода тестами.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

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

Также тестирование автоматически отключается в случае, если проект подключается в другой проект качестве подпроекта с помощью команды add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Переключает язык документации, которую генерирует цель doc на заданный. Список доступных языков см. на сайте системы Doxygen.

По умолчанию включён русский.

Сборочные цели

По умолчанию

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

Если цель не указана (что эквивалентно цели all), собирает всё, что можно, а также вызывает цель check.

mylib-unit-tests

cmake --build path/to/build/directory --target mylib-unit-tests

Компилирует модульные тесты. Включено по умолчанию.

check

cmake --build путь/к/сборочной/директории --target check

Запускает собранные (собирает, если ещё не) модульные тесты. Включено по умолчанию.

См. также mylib-unit-tests.

coverage

cmake --build путь/к/сборочной/директории --target coverage

Анализирует запущенные (запускает, если ещё не) модульные тесты на предмет покрытия кода тестами при помощи программы gcovr.

Выхлоп покрытия будет выглядеть примерно так:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

Цель доступна только при включённой опции MYLIB_COVERAGE.

См. также check.

doc

cmake --build путь/к/сборочной/директории --target doc

Запускает генерацию документации к коду при помощи системы Doxygen.

wandbox

cmake --build путь/к/сборочной/директории --target wandbox

Ответ от сервиса выглядит примерно так:

{
    "permlink" :    "QElvxuMzHgL9fqci",
    "status" :  "0",
    "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

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

Примеры

Сборка проекта в отладочном режиме с замером покрытия

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

Установка проекта без предварительной сборки и тестирования

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Сборка в выпускном режиме заданным компилятором

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Генерирование документации на английском

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

Инструменты

  1. CMake 3.13

    На самом деле версия CMake 3.13 требуется только для запуска некоторых консольных команд, описанных в данной справке. С точки зрения синтаксиса CMake-скриптов достаточно версии 3.8, если генерацию вызывать другими способами.

  2. Библиотека тестирования doctest

    Тестирование можно отключать (см. опцию MYLIB_TESTING).

  3. Doxygen

    Для переключения языка, на котором будет сгенерирована документация, предусмотрена опция MYLIB_DOXYGEN_LANGUAGE.

  4. Интерпретатор ЯП Python 3

    Для автоматической генерации онлайн-песочницы.

Статический анализ

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

Cppcheck

В CMake встроена поддержка инструмента для статического анализа Cppcheck.

Для этого нужно воспользоваться опцией CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

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

Clang

При помощи чудесного инструмента scan-build тоже можно запускать статический анализ в два счёта:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Здесь, в отличие от случая с Cppcheck, требуется каждый раз запускать сборку через scan-build.

Послесловие

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


→ Скачать шаблон проекта

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Как изменить компас на пдф
  • Как изменить компанию на физ лицо на авито
  • Как изменить компанию доставки на алиэкспресс
  • Как изменить комнату до неузнаваемости
  • Как изменить коммит git после пуша

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии