【问题标题】:Modern way to set compiler flags in cross-platform cmake project在跨平台 cmake 项目中设置编译器标志的现代方法
【发布时间】:2018-02-07 20:44:55
【问题描述】:

我想编写一个 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 这样的命令可以让我用append(CMAKE_CXX_FLAGS "Foo") 替换set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo")?
  2. 我读过很多遍,一开始不应该手动设置CMAKE_CXX_FLAGS 和类似的变量,但我不确定要使用什么其他机制。
  3. 最重要的是:按照我在这里的方式,我需要为每个编译器和配置创建一个单独的构建目录 理想情况下,我想将其转换为在同一个目录中拥有多个目标,这样我就可以例如致电make foo_debug_clang。

所以我的问题是

  • a) 是否有更好的方法来编写解决我的“痛点”的 cmake 脚本? 解决上述几点?
  • b) 对于如何建立此类项目,是否存在类似公认的现代最佳实践?

我可以在互联网上找到的大多数参考资料要么已过时,要么仅显示琐碎的示例。我目前使用 cmake3.8,但如果这有什么不同,我对更新版本的答案更感兴趣。

【问题讨论】:

  • @AI.G.谢谢你。是否还有单独的调试集并将其发布给我必须通过另一个级别的 if_else 来解决这个问题?我对生成器表达式进行了一些实验,但我觉得这变得更不可读了。
  • 在我看来,您可以使用肮脏的生成器表达式或 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} --flags") 的相当冗长的语法。在这种情况下(考虑到这两种情况都不令人满意),您可以尝试编写一些辅助函数,将脏代码包装成更令人赏心悦目的东西。
  • I've read multiple times, that one should not manually set CMAKE_CXX_FLAGS - 第一次听说这个。虽然在项目中设置此变量的 cached 版本可能会给用户带来不便(但在某些情况下很有用),但将标志附加到非缓存变量并没有错。 I need a separate build directory for each compiler - 这在 CMake 和它使用的许多构建工具(例如 MSVC)中都是不可避免的。对于多个配置的单个构建目录,multiconfig 构建工具(如 MSVC)能够以这种方式工作。
  • 整个问题对我来说似乎“过于宽泛”,因为点 1,2 与 3 的相关性较弱。可能是,专门询问 3,并添加 1,2 作为旁注(所以 1,2不需要在答案中描述)?

标签: c++ cmake


【解决方案1】:

您的方法 - 正如@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.已评论-“使用肮脏的generator expressions”。

生成器表达式有一些缺点:

  1. 非常有用的 $&lt;IF:...,...,...&gt; 表达式仅在 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++ 标准,您只需使用 target_compile_features() 调用命名您的代码/目标确实依赖的 C++ feature。

对于这个例子,我选择了cxx_lambda_init_captures,例如较旧的 GCC 编译器会出现以下错误(例如,如果编译器不支持此功能会发生什么情况):

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

"GNU"

version 4.8.4.

并且您需要编写一个包装脚本来使用"single configuration" makefile generator 构建多个配置或使用"multi configuration" IDE 作为Visual Studio。

以下是对示例的引用:

因此,我使用 Open Folder Visual Studio 2017 CMake 支持测试了以下内容,在此示例中结合了 、 和 编译器:

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

因此,您可以使用 Visual Studio 2017 中的任何 CMake 生成器,存在一些不健康的引用(至于 2017 年 9 月,可能稍后修复),需要 mingw32-make.cmd 中介(删除引号)。

【讨论】:

  • 我严格反对指定单独的标准语言功能要求,而不是指定我编码所针对的标准,否则感谢您提供全面的答案。
  • @MikeMB 不客气。您也可以将一般声明与 target_compile_features(cxx_std_17) 一起使用。
  • 我对结果并不完全满意,但这绝对是 cmake 的错,而不是你的错。顺便说一句:指定cxx_std_XX 比指定每个功能要好,但是afaik cmake 将此解决为-std=gnuc++XX 开关而不是c++XX,这对于跨平台开发来说是个问题。通常我会尝试使所有编译器尽可能接近标准一致性(-std=c++XX, /permissive-, -pedantic 等),然后如果某些 3rd 方库无法在它们下编译,则退避,但这超出了问题的范围。
  • @MikeMB 通过将CMAKE_CXX_EXTENSIONS 设置为OFF,您可以获得更严格的-std=c++XX 选项。
  • 谢谢,不知道那个
【解决方案2】:

别这样!

尤其是在 CMake 3.19+ 中,presets 是一个选项。将警告标志等可选设置放入预设中,并仅将硬构建要求放入 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编译器。英特尔编译器还可以在 MSVC 和 GCC 兼容模式之间切换。
  3. 编译器版本太多。警告标志的含义因版本而异。从一个版本到下一个版本,给定的警告可能或多或少敏感,尤其是执行数据流分析的更高级的警告。启用 warnings-as-errors 后,这将为您的用户转化为损坏的构建。

您不想为您的构建维护标志兼容性表。幸运的是,有一个简单的解决方案:不要。

将您想要的标志存储在预设(CMake 3.19+)或工具链文件(CMake 3.0+,可能更早)中,并让您的用户选择加入这些设置如果他们愿意。

使用预设,就像编写 CMakePresets.json 文件一样简单。有一些extensive examples in the documentation。然后你让你的用户告诉你他们想要使用哪组标志:

# 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

等等。

最重要的是:我在这里做的方式,我需要为每个编译器和配置一个单独的构建目录理想情况下,我想将它转换为在同一个目录中拥有多个目标,这样我就可以例如拨打make foo_debug_clang。

这是一个奇怪的要求,因为无论如何,每个人都必须完全构建所有内容。老实说,我只是建议设置一个包装器 Makefile 来支持这个工作流,因为 (a) CMake 本身不支持它,并且 (b) 扩展 CMake 这样做并没有真正的优势。

  • b) 对于如何建立此类项目,是否存在类似于公认的现代最佳实践?

我不知道“已接受”,但我绝对发现将硬构建要求从理想构建设置中拆分为 CMakeLists.txt 和预设(或工具链文件)分别解决(或回避)许多此类问题。最终结果是更强大的构建更易于使用。

【讨论】:

  • 据我从documentation 了解到,presets 仅适用于 developers 和 CI。但是-Werror 选项对最终用户 很有用。此外,预设适用于开发人员知道他/她的环境的情况。但是-Werror 选项对于在未知环境 中运行,使用“未知版本”的编译器很有用:当这样的编译器发现项目代码有什么奇怪的地方时,最好终止构建而不是允许编译器创建可能错误的程序。
  • @Tsyvarev - 文档没有这么说。我不知道你为什么选择编造这个。 CI 是作为示例 给出的,但它没有说“仅用于”之类的东西。请不要恶意与我交往。 -Werror 选项最大程度地反用户。旧代码可能有错误,新编译器更有可能捕获它们。有些警告是虚假的。让最终用户编辑构建文件只是为了让构建通过是构建的绝对最坏情况。
  • 如果我编写了一个库,而其他人想使用比我使用的更早的编译器版本从源代码构建它,那应该是允许的。如果我对它们强制使用-Werror,那么我将负责在无数旧编译器版本上测试我的代码,即使错误只是带有警告。或者,我强迫他们修补我的构建,这是不行的。-Werror 是令人震惊和激进用户敌对的,没有硬编码的地方。
  • “如果我对它们强制使用-Werror,那么我将负责在无数旧编译器版本上测试我的代码,即使错误只是带有警告。” - 项目的作者负责在许多编译器上测试代码无论如何。是否允许用户在未经测试的配置上构建项目始终是一个权衡。在编译器警告后是否继续构建始终是一个权衡。参见例如that comment 您引用的博文。
【解决方案3】:

解决前两点,而不是第三点:

  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)

对于 gcc、clang 和 MSVC 将导致 -DHELLO -DWORLD 但对于奇怪的编译器可能会使用其他开关。

真的没有像 append 这样的命令可以让我将set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} Foo") 替换为append(CMAKE_CXX_FLAGS "Foo")?

set_property 既可以在设置模式下使用,也可以在附加模式下使用(参见上面的示例)。

不过,我不能说这是否比 add_compile_options 或 target_compile_features 更可取。

【讨论】:

    【解决方案4】:

    另一种方法是使用 .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}")
    

    这可能会使包含多个和深奥的选项更容易管理。

    【讨论】:

    • 我必须承认,我从未听说过 rsp_files - 你能举个例子吗?无论如何,我不确定将现在集中在一个位置的信息拆分为多个单独的文件是否会使流程变得更好。
    • @MikeMB rsp 文件(响应文件)只是将命令行选项放入文件而不是命令行的一种方式。我能想到的所有编译器都支持它们
    【解决方案5】:

    您可以使用target_compile_options() 来“附加”编译选项。

    【讨论】:

      猜你喜欢
      • 2022-09-27
      • 2014-07-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-07
      • 1970-01-01
      相关资源
      最近更新 更多