1+1=10

记记笔记,放松一下...

现代CMake学习笔记(二)

现代CMake学习笔记(一),捋一捋CMake中的一些容易被忽略的问题。先列一下本文内容,然后再就用一个最简单的C++例子,看看能发散多少

知识点:

  • cmake -h 查看生成器列表与默认值。-G 用于选择生成器
  • cmake --build . 隐藏不同生成器的执行差异
  • -S -B--build 实现了在源码目录下直接执行out-of-source-build,不需要切换目录
  • 务必为 cmake指定构建类型 ,如果你不喜欢薛定谔的猫的话。它默认既不是Release也不是Debug
  • Debug和Release的选择,区分单配置和多配置的生成器。单配置在config时指定,多配置在build时指定

以上几点合并起来——如果我要编译release版本程序,且生成产物放置到my-release-dir下,只需要在源码目录下执行如下命令:

  • 对单配置生成器(以Ninja为例)
1
2
cmake -S . -B my-release-dir -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build my-release-dir
  • 对多配置生成器(以Visual Studio 16 2019为例)
1
2
cmake -S . -B my-release-dir -G "Visual Studio 16 2019"
cmake --build my-release-dir --config Release

言归正传,如下正文:

CMake简单使用?使用简单?

CMake用起来很简单,只需要写一个代码文件和工程文件:

  • main.cpp 文件
1
2
3
4
5
#include <iostream>
int main() {
    std::cout<<"Hi 1+1=10 ...\n";
    return 0;
}
  • CMakeLists.txt
1
2
project(simple-test)
add_executable(hello main.cpp)

然后就可以放飞自我,在众多平台下构建了:

1
2
cmake .
cmake --build .

这时得到一个名为hello可执行程序,任务完成。

  • 第一条命令:为make生成Makefile文件或为其他工具生成工程文件,生成在当前目录下
  • 第二条命令:调用make或其他工具,在当前目录进行构建

就这么简单!

就这么简单??不妨看看这里面到底有多少注意点...

make? cmake --build?

在上面的例子中,我们使用了cmake --build . 而不是直接 make。明明下面这样更简单啊。

而且很多资料也是这么用的,不是么?

1
2
cmake .
make                       # 或者 nmake 或 jom 或 ...

因为cmake 有不同的生成器,针对不同的生成器,需要我们需调用不同的命令make/nmake/jom/ninja/msbuild/...

最终写起来很费心,用--build这个选项,就不用分情况讨论了。

注:构建时使用 -v--verbose参数,有时候很有用 cmake --build -v .

生成器(Generator)

可用的的生成器有哪些,默认是哪个?可如下命令可以查看。

1
cmake -h

比如,cmake在Ubuntu下的结果如下:(从中可见,该配置下默认是Unix Makefiles)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Generators

The following generators are available on this platform (* marks default):
  Green Hills MULTI            = Generates Green Hills MULTI files
                                 (experimental, work-in-progress).
* Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Ninja Multi-Config           = Generates build-<Config>.ninja files.
  Watcom WMake                 = Generates Watcom WMake makefiles.
  CodeBlocks - Ninja           = Generates CodeBlocks project files.
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
  CodeLite - Ninja             = Generates CodeLite project files.
  CodeLite - Unix Makefiles    = Generates CodeLite project files.
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
  Kate - Ninja                 = Generates Kate project files.
  Kate - Unix Makefiles        = Generates Kate project files.
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files.

要切换不同的Generator,可使用 -G 参数,比如,工程可以这么构建:

1
2
cmake . -G Ninja
cmake --build .

另外,如果要改变默认的生成器,可以设置环境变量(就不用每次用-G指定了):

1
CMAKE_GENERATOR=Ninja

注:如果用conan的话,使用如下环境变量

1
CONAN_CMAKE_GENERATOR=Ninja

只用一个生成器?

CMake本身是跨平台的方案,如果只考虑一个环境。cmake不一定简单了。

而且如果只用一个平台,且用的Visual Studio或XCode的话,也确实没必要碰cmake。

对上面例子来说,我们只要写一个 main.cpp 文件,然后:

1
g++ main.cpp

或者

1
cl main.cpp

不就得了?? 既不需要工程文件,也不需要执行执行两次命令。多简单

即使文件比较多,要高级一点,也就写个Makefile文件,比如

1
2
main:
    g++ main.cpp

每次构建也只需要调用下面一条命令就够了

1
make

嗯,这样确实不需要cmake,且似乎更简单。

尽管如此,cmake还是有一定优势,毕竟可以逃避学更底层的一些东西,比如Makefile的规则。

In-source build?Out-of-source build?

在上面例子中,构建的中间产物和目标产物都和源码在一个目录下(即In-source-build)。

尽管命令简单,但缺点也比较明显。

  • 污染源码目录,不方便源码管控。需要精细配置 .gitignore等文件
  • 如果一套源码在不同的编译器下编译,不便于隔离。
  • ...

建议的操作方式是,构建和源码目录隔离,单独创建构建目录(Out-of-source-build)。

其实操作不复杂

1
2
cmake -S . -B build-dir
cmake --build build-dir
  • 第一条命令:为make生成Makefile文件或为其他工具生成工程文件。使用-S指定源码目录,-B指定构建目录
  • 第二条命令:调用make或其他工具,在指定目录下进行构建

这样源码目录没那么乱了:

1
2
3
4
5
6
7
8
9
.
├── CMakeLists.txt
├── main.cpp
├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
...
│   ├── cmake_install.cmake
│   ├── hello

当前,可以构建目录和源码目录平行,这样...

1
2
3
4
5
6
7
8
.
├── src
│   ├── CMakeLists.txt
│   ├── main.cpp
│ 
├── build-msvc2019
├── build-mingw
├── build-clang

注意,我们在这儿没有显式切换目录,如果你看过老版本的CMake,操作主要在构建目录下进行,写下来会像下面这样:

1
2
3
4
mkdir build-dir
cd build-dir
cmake ..
cmake --build .

Debug? Release ?(单配置生成器 single-configuration)

这个一个坑!!!

我们某个程序Linux下某程序运行很慢,怀疑没用release模式构建。同事排查后:里面没有Debug信息,肯定是Release。

对于单配置的生成器(比如Makefile 和 Ninja)来说,构建类型通过CMAKE_BUILD_TYPE来指定,它的典型值如下:

  • Debug,
  • Release,
  • RelWithDebInfo
  • MinSizeRel

比如我们要构建release和debug:

1
2
3
4
5
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release

cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build-debug

如果不加type,默认是那个呢?

默认是什么东西?

  • https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html

手册中说:

1
This variable is initialized by the first project() or enable_language() command called in a project when a new build tree is first created. If the CMAKE_BUILD_TYPE environment variable is set, its value is used. Otherwise, a toolchain-specific default is chosen when a language is enabled. The default value is often an empty string, but this is usually not desirable and one of the other standard build types is usually more appropriate.

手册中说,默认经常是空值,通常不符合预期。

以Ubuntu为例,默认时,编译选项中既没有-g来生成调试信息,也没有-O3等优化选项。

完全就是玩具,不要再生产环境用!!(其实这也不全是cmake的锅,前面我们直接调g++不也没有加选项嘛,但是cmake这样弄还是有点接受不了)

另外,Debug、Release 到底是大写、小写还是首字符大写。官方也没有确定的说法。

查看不同类型编译选项

要查看编译选项,可以在CMakeLists.txt中添加

1
2
3
message("debug flags:" ${CMAKE_CXX_FLAGS_DEBUG})
message("release flags:" ${CMAKE_CXX_FLAGS_RELEASE})
message("min size flags:" ${CMAKE_CXX_FLAGS_MINSIZEREL})

运行cmake,在Linux下大致是下面这样的结果

1
2
3
debug flags:-g
release flags:-O3 -DNDEBUG
min size flags:-Os -DNDEBUG

或者直接运行 cmake -LA . 来查看当前项目所有变量值

1
2
3
4
5
6
7
...
CMAKE_CXX_FLAGS:STRING=
CMAKE_CXX_FLAGS_DEBUG:STRING=-g
CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG
CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
...

Debug? Release ?(多配置生成器 Multi-configuration)

对于 Visual StudioXcodeNinja Multi-Config这些多配置生成器来说,在config阶段生效的 CMAKE_BUILD_TYPE 变量没有任何作用,会直接被忽略掉

配置时,不需要指定类型,但是构建时,需要通过--config来指定。

1
2
cmake -S . -B build-dir
cmake --build build-dir --config Debug

那么config后面可以跟什么呢,手册中说预定义这么几个

  • Debug
  • Release
  • RelWithDebInfo
  • MinSizeRel

这个列表可以使用CMAKE_CONFIGURATION_TYPES可以手动修改/指定。

1
CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo

注意:单配置生成器会直接忽略 CMAKE_CONFIGURATION_TYPES 选项。

对于config值,同前面单配置的坑一样,手册中强调

1
The default value will often be none of the above standard configurations and will instead be an empty string. A common misunderstanding is that this is the same as Debug, but that is not the case. Users should always explicitly specify the build type instead to avoid this common problem.
  • 默认值通常不是上面几个值中的一个,而是一个空值。
  • 不要将默认值 等同于 Debug
  • 你应该始终显式指定构建类型

比如我们要构建release和debug:

1
2
3
cmake -S . -B build-dir
cmake --build build-dir --config Release
cmake --build build-dir --config Debug

如何默认Release构建?

如果只是自己用,设置如下环境变量就行了:

1
CMAKE_BUILD_TYPE=Release

如果不想让项目每个参与者都设置环境变量,那么,可以像下面这样改项目的CMakeLists.txt文件:

1
2
3
4
get_property(isMultiConfig  GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(NOT isMultiConfig AND NOT (CMAKE_BUILD_TYPE OR DEFINED ENV{CMAKE_BUILD_TYPE}))
  set(CMAKE_BUILD_TYPE Release CACHE STRING "I prefer Release to Debug")
endif()

注意:里面用到了全局属性GENERATOR_IS_MULTI_CONFIG来判定生成器类型,这一属性是CMAKE 3.9 引入的。

参考

  • https://blog.feabhas.com/2021/07/cmake-part-2-release-and-debug-builds/
  • https://stackoverflow.com/questions/23995019/what-is-the-modern-method-for-setting-general-compile-flags-in-cmake
  • https://stackoverflow.com/questions/16851084/how-to-list-all-cmake-build-options-and-their-default-values
  • https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html
  • https://cmake.org/cmake/help/latest/variable/CMAKE_CONFIGURATION_TYPES.html
  • https://blog.csdn.net/weixin_39766005/article/details/122439200
  • https://www.kitware.com/cmake-and-the-default-build-type/
  • https://www.scivision.dev/cmake-default-build-type/

Tools cmake