Современный способ установки флагов компилятора в кроссплатформенном проекте cmake

Я хочу написать файл cmake, который устанавливает различные параметры компилятора для clang++, g++ и MSVC в сборках отладки и выпуска. То, что я сейчас делаю, выглядит примерно так:

if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++latest /W4")
    # Default debug flags are OK 
    set(CMAKE_CXX_FLAGS_RELEASE "{CMAKE_CXX_FLAGS_RELEASE} /O2")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1z -Wall -Wextra -Werror")
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} some other flags")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

    if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
    else()
        # nothing special for gcc at the moment
    endif()
endif()

Но у меня есть пара проблем с этим:

  1. Во-первых, тривиально: не существует ли такой команды, как appen, которая позволила бы мне заменить set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo") с append(CMAKE_CXX_FLAGS "Foo")?
  2. Я прочитал несколько раз, что не следует устанавливать вручную CMAKE_CXX_FLAGS и подобные переменные в первую очередь, но я не уверен, какой другой механизм использовать.
  3. Самое главное: как я делаю это здесь, мне нужен отдельный каталог сборки для каждого компилятора и конфигурации. В идеале я хотел бы преобразовать это в несколько объектов в одном каталоге, чтобы я мог, например, вызвать make foo_debug_clang,

Так что мои вопросы

  • а) Есть ли лучший способ написать сценарий cmake, который решает мои "болевые точки"? решение вопросов, упомянутых выше?
  • б) Есть ли что-то вроде общепринятой современной практики создания таких проектов?

Большинство ссылок, которые я мог найти в Интернете, либо устарели, либо показывают только тривиальные примеры. В настоящее время я использую cmake3.8, но если это что-то меняет, меня еще больше интересует ответ для более свежих версий.

5 ответов

Решение

Ваш подход - как прокомментировал @Tsyvarev - был бы абсолютно нормальным, поскольку ваш запрос на "новый" подход в CMake будет следующим:

cmake_minimum_required(VERSION 3.8)

project(HelloWorld)

string(
    APPEND _opts
    "$<IF:$<CXX_COMPILER_ID:MSVC>,"
        "/W4;$<$<CONFIG:RELEASE>:/O2>,"
        "-Wall;-Wextra;-Werror;"
            "$<$<CONFIG:RELEASE>:-O3>"
            "$<$<CXX_COMPILER_ID:Clang>:-stdlib=libc++>"
    ">"
)

add_compile_options("${_opts}")

add_executable(HelloWorld "main.cpp")

target_compile_features(HelloWorld PUBLIC cxx_lambda_init_captures)

Ты взял add_compile_options() и - как @Al.G. прокомментировал - "используйте выражения грязного генератора ".

Есть несколько недостатков выражений генератора:

  1. Очень полезно $<IF:...,...,...> выражение доступно только в версии CMake>= 3.8
  2. Вы должны написать это в одну строку. Чтобы избежать этого, я использовал string(APPEND ...), который вы также можете использовать для "оптимизации" вашего set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ... звонки.
  3. Это трудно читать и понимать. Например, точка с запятой необходима, чтобы сделать ее списком параметров компиляции (в противном случае CMake процитирует его).

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

if(MSVC)
    add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
else()
    add_compile_options("-Wall" "-Wextra" "-Werror" "$<$<CONFIG:RELEASE>:-O3>")
    if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
        add_compile_options("-stdlib=libc++")
    else()
        # nothing special for gcc at the moment
    endif()
endif()

И да, вы больше не указываете явно стандарт C++, вы просто называете функцию C++, от которой зависит ваш код / ​​цель target_compile_features() звонки.

Для этого примера я выбрал cxx_lambda_init_captures например, более старый компилятор GCC выдает следующую ошибку (например, что происходит, если компилятор не поддерживает эту функцию):

The compiler feature "cxx_lambda_init_captures" is not known to CXX compiler

"GNU"

version 4.8.4.

И вам нужно написать скрипт-обертку для создания нескольких конфигураций с помощью генератора make-файлов "с одной конфигурацией" или использовать IDE "с несколькими конфигурациями" в качестве Visual Studio.

Вот ссылки на примеры:

Итак, я проверил следующее с Open Folder Поддержка Visual Studio 2017 CMake для объединения в этом примере компиляторов cl, clang и mingw:

Конфигурации

CMakeSettings.json

{
    // See https://go.microsoft.com//fwlink//?linkid=834763 for more information about this file.
    "configurations": [
        {
            "name": "x86-Debug",
            "generator": "Visual Studio 15 2017",
            "configurationType": "Debug",
            "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
            "buildCommandArgs": "-m -v:minimal",
        },
        {
            "name": "x86-Release",
            "generator": "Visual Studio 15 2017",
            "configurationType": "Release",
            "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
            "buildCommandArgs": "-m -v:minimal",
        },
        {
            "name": "Clang-Debug",
            "generator": "Visual Studio 15 2017",
            "configurationType": "Debug",
            "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
            "cmakeCommandArgs": "-T\"LLVM-vs2014\"",
            "buildCommandArgs": "-m -v:minimal",
        },
        {
            "name": "Clang-Release",
            "generator": "Visual Studio 15 2017",
            "configurationType": "Release",
            "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
            "cmakeCommandArgs": "-T\"LLVM-vs2014\"",
            "buildCommandArgs": "-m -v:minimal",
        },
        {
            "name": "GNU-Debug",
            "generator": "MinGW Makefiles",
            "configurationType": "Debug",
            "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
            "variables": [
                {
                    "name": "CMAKE_MAKE_PROGRAM",
                    "value": "${projectDir}\\mingw32-make.cmd"
                }
            ]
        },
        {
            "name": "GNU-Release",
            "generator": "Unix Makefiles",
            "configurationType": "Release",
            "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuild\\${workspaceHash}\\build\\${name}",
            "variables": [
                {
                    "name": "CMAKE_MAKE_PROGRAM",
                    "value": "${projectDir}\\mingw32-make.cmd"
                }
            ]
        }
    ]
}

mingw32-make.cmd

@echo off
mingw32-make.exe %~1 %~2 %~3 %~4

Таким образом, вы можете использовать любой генератор CMake из Visual Studio 2017, происходит нездоровое цитирование (например, сентябрь 2017 года, может быть исправлено позже), которое требует mingw32-make.cmd посредник (удаление кавычек).

Не делай этого!

Особенно в CMake 3.19+, где являются опцией. Поместите дополнительные настройки, такие как предупреждающие флажки, в предустановки и поместите только жесткие требования к сборке в CMakeLists.txt.

Читайте объяснение почему .


Я хочу написать файл cmake, который устанавливает различные параметры компилятора для clang ++, g ++ и MSVC в сборках отладки и выпуска.

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

То, что я делаю сейчас, выглядит примерно так:

       if(MSVC)
    # ...
else()
    # ...
    if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
        # ...
    else()
        # ...
    endif()
endif()

[...] Я читал несколько раз, что не следует вручную устанавливать CMAKE_CXX_FLAGS и подобные переменные в первую очередь, но я не уверен, какой еще механизм использовать.

Вот проблемы с этой структурой:

  1. Поставщиков компиляторов слишком много . Да, есть MSVC, Clang и GCC, но есть также компилятор Intel, компилятор PGI и так далее.
  2. Слишком много вариантов компилятора . Есть не только Clang, но также ClangCL и компилятор Clang CUDA. Компилятор Intel также может переключаться между режимами совместимости с MSVC и GCC.
  3. Слишком много версий компилятора . Значения предупреждающих флажков меняются в зависимости от версии. От одной версии к другой данное предупреждение может быть более или менее чувствительным, особенно более сложные, которые выполняют анализ потока данных. При включении предупреждений как ошибок это приводит к неработающим сборкам для ваших пользователей.

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

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

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

      # Using presets:
$ cmake --preset=msvc
$ cmake --preset=gcc
$ cmake --preset=clang

# Using toolchains:
$ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/msvc.cmake
$ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/gcc.cmake
$ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$PWD/cmake/clang.cmake

и так далее.

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

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

  • б) Есть ли что-то вроде общепринятой современной передовой практики создания таких проектов?

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

Обращаясь к первым двум пунктам, но не к третьему:

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

Команда, которую вы хотите set_property, CMake поддерживает множество свойств - не все, а множество - таким образом, что избавляет вас от необходимости выполнять работу, специфичную для компилятора. Например:

set_property(TARGET foo PROPERTY CXX_STANDARD 17)

что для некоторых компиляторов приведет к --std=c++17 но для более ранних с --std=c++1z (до завершения C++17). или же:

set_property(TARGET foo APPEND PROPERTY COMPILE_DEFINITIONS HELLO WORLD)

приведет это -DHELLO-DWORLD для gcc, clang и MSVC, но для странных компиляторов могут использоваться другие ключи.

Неужели нет такой команды, как append, которая позволила бы мне заменить set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo") с append(CMAKE_CXX_FLAGS "Foo")?

set_property может использоваться как в режиме установки, так и в режиме добавления (см. примеры выше).

Я не могу сказать, предпочтительнее ли это add_compile_options или же target_compile_features, хоть.

Вы можете использовать target_compile_options() для "добавления" параметров компиляции.

Другой способ - использовать файлы.rsp.

set(rsp_file "${CMAKE_CURRENT_BINARY_DIR}/my.rsp")
configure_file(my.rsp.in ${rsp_file} @ONLY)
target_compile_options(mytarget PUBLIC "@${rsp_file}")

что может упростить управление множественными и эзотерическими вариантами.

Другие вопросы по тегам