假设我们在目录chp1下面有一个简单的文件main.cpp
#include <iostream>
int main()
{
std::cout<<"你好"<<std::endl;
return 0;
}
我们同时在chp1下面写一个文件CMakeLists.txt
,内容如下
cmake_minimum_required(VERSION 3.20)
project(chp1)
add_executable(chp1 main.cpp)
然后我们打开一个terminal,切换到chp1目录,就可以编译生成了。首先输入下面命令:
cmake -B build -S .
会有如下输出信息
-- The C compiler identification is AppleClang 14.0.3.14030022
-- The CXX compiler identification is AppleClang 14.0.3.14030022
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /xxx/chp1/chp1-1/build
这时就会在chp1下面新增加出一个build目录,这个目录里面有一些编译信息
然后在输入下面命令进行编译
cmake --build ./build
命令行有如下信息输出
[ 50%] Building CXX object CMakeFiles/chp1.dir/main.cpp.o
[100%] Linking CXX executable chp1
[100%] Built target chp1
这样就可以顺利在build目录生成出可执行文件
执行文件,可以有对应的输出
chp1-1 % ./build/chp1
你好
这样,我们就使用cmake完成了第一个c++工程的编译。
一、cmake版本管理
项目指定CMake版本行为的详细信息是使用 cmake_minimum_required()
命令。这应该是CMakeLists.txt文件的第 一行,这样项目的需求就会放在在其他事情之前。
这个命令做了两件事:
指定了项目所需的CMake的最低版本。如果CMakeLists.txt文件使用的CMake版本比指定的版本低,将 立即停止构建,并出现错误报告。
强制设置将CMake行为匹配到对应版本。
cmake_minimum_required()
命令很简单:
cmake_minimum_required(VERSION major.minor[.patch[.tweak]])
VERSION
关键字必须出现,提供的版本详细信息必须有 major.minor
。大多数项目中, patch 和 tweak 没有必 要,因为新特性通常只在 minor 版本更新中出现。只有当修复某个错误时,项目 才应该指定 patch 部分。3.x的CMake使用了 tweak ,但项目不需要进行指定。
一般建议指定3.20以上。
二、创建一个project
每个CMake项目都应该包含一个 project()
命令,它应该在 cmake_minimum_required()
之后出现。
该命令常见的 选项如下所示:
project(projectName
[VERSION major[.minor[.patch[.tweak]]]]
[LANGUAGES languageName ...]
)
项目名称是必需的,只能是字母、数字、下划线(_)和连字符(-),字母和下划线比较常用。由于不允许使用空 格,所以项目名称不必用引号括起来。这个名字用于项目的生成器(如Xcode和Visual Studio),也可以运用于 各种项目的其他部分,比如默认打包名称和文档元数据,项目名称等等。 projectName
是 project()
命令惟一 强制性参数。 VERSION
只在CMake 3.0之后的版本支持。与 projectName
一样,CMake使用版本细节来填充变量并作为默认元数据,除此之外版本信息没有任何意义。LANGUAGES
参数定义了应该为项目启用的编程语言,支持的语言包括 C 、 CXX 、 Fortran 、 ASM 、 Java、Object-C、Object-C++ 等。 如果指定了多种语言,请用空格分隔。如果没有 语言选项,CMake将默认为 C 和 CXX 。
三、构建可执行文件
add_executable()
命令告诉CMake为一组源文件创建一个可执行文件。
此命令的形式为:
add_executable(targetName [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...]
)
WIN32
在Windows平台上构建可执行文件时,此选项表示CMake将该可执行文件构建为Windows GUI应用程序。实 际上,会使用 WinMain() 创建入口,而不是 main() ,并且会添加链接选项 /SUBSYSTEM:WINDOWS 。其他平台会忽 略 WIN32 选项。
MACOSX_BUNDLE
它会引导CMake在苹果平台上构建应用程序包。与选项名称不同,不仅适用于macOS,也适用于其他苹果平 台,如iOS。此选项的确切效果在不同平台间略有不同。例如,在macOS上,app bundle有一个非常特定的 目录结构,而iOS的目录结构是扁平的。CMake还会生成一个 Info.plist文件。非Apple平台上,会忽略 MACOSX_BUNDLE 关键字。
EXCLUDE_FROM_ALL
项目定义了许多目标,但默认情况下只需要构建一部分。当在构建没有指定目标时,将构建默认的 ALL 目标 (根据使用CMake生成器的不同,名称略有不同,比如:Xcode的 ALL_BUILD )。如果可执行文件用
EXCLUDE_FROM_ALL
定义,就不会包含在默认的 ALL 目标中。然后,只有当构建命令显式地请求该可执行文 件,或者作为默认构建对其有依赖时,可执行文件才会构建。
调用add_executable
将创建一个可执行文件,可以在CMake项目中作为 targetName
引用。这个名称可以包含字母、数字、下划 线和连字符。构建项目时,在构建目录中创建一个与平台相关名称的可执行文件。
通过使用不同的名称多次调用 add_executable()
,可以在 CMakeLists.txt文件中定义多个可执行文件。
四、生成工程
cmake [<options>] -B <path-to-build> [-S <path-to-source>]
从cmake 3.13版本开始,我们更加推荐使用这种方法。直接用-B
指定工程生成的目录,用-S
指定工程根目录,也就是顶层CMakeLists.txt所在的地方。
我们上面的例子中使用
cmake -B build -S .
也就是使用当前目录作为源码目录,在当前目录下生成build
目录作为生成的工程所在的目录。
五、编译工程
cmake --build <dir> [<options>] [-- <build-tool-options>]
我们前面的例子使用
cmake --build ./build
也就是指定当前目录下的build目录为生成所在的工程的目录。
看得出生成工程和编译工程是非常简单的。里面的难点在于如何处理生成程序的类型(Debug, Release),这部分的解释我们后续文章会做详细解释。
如果经常编程序的同学就会问一个问题:编译出来的程序到底是Debug版本还是Release版本?
其实在Cmake内部是通过CMAKE_BUILD_TYPE
来标识到底现在编译的是什么类型。我们把CMakeLists.txt
修改一下
cmake_minimum_required(VERSION 3.20)
project(chp1)
if(CMAKE_BUILD_TYPE MATCHES Debug)
add_definitions(-DDEBUG)
else()
add_definitions(-DRELEASE)
endif()
add_executable(chp1 main.cpp)
也就是
当类型为
Debug
的时候添加宏DEBUG
;当类型为
Release
的时候添加宏RELEASE
同时我们把main.cpp
修改为
#include <iostream>
int main()
{
#ifdef DEBUG
std::cout << "Debug version" << std::endl;
#elif defined(RELEASE)
std::cout << "Release version" << std::endl;
#else
std::cout << "Other version" << std::endl;
#endif
return 0;
}
编译运行
chp1-2 % cmake -B build -S .
...
chp1-2 % cmake --build ./build
...
chp1-2 % ./build/chp1
Release version
可以看出默认情况下生成的是Release
版本。
对于cmake来说,从CMakeLists.txt
生成工程文件的时候,是可以传入编译类型的,比如下面方式
chp1-2 % cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
...
chp1-2 % cmake --build ./build
[ 50%] Building CXX object CMakeFiles/chp1.dir/main.cpp.o
[100%] Linking CXX executable chp1
[100%] Built target chp1
chp1-2 % ./build/chp1
Debug version
也就是生成工程文件的时候传入了Debug
版本,那么最终生成出了Debug
版本的程序。
上面我们没有单独选择生成器,cmake默认生成了Unix Makefiles。我们可以使用cmake --help
来查看cmake可以有多少种generator。
chp1 % cmake --help
...
Generators
The following generators are available on this platform (* marks default):
* 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.
Xcode = Generate Xcode project files.
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.
这是我的mac笔记本上支持的generator。查看官方文档我们可以看到
CMake Generators
Command-Line Build Tool Generators
These generators support command-line build tools. In order to use them, one must launch CMake from a command-line prompt whose environment is already configured for the chosen compiler and build tool.
Makefile Generators
Ninja Generators
IDE Build Tool Generators
These generators support Integrated Development Environment (IDE) project files. Since the IDEs configure their own environment one may launch CMake from any environment.
Visual Studio Generators
Other Generators
Extra Generators
Deprecated since version 3.27: Support for "Extra Generators" is deprecated and will be removed from a future version of CMake. IDEs may use the
cmake-file-api(7)
to view CMake-generated project build trees.Some of the CMake Generators listed in the
cmake(1)
command-line tool--help
output may have variants that specify an extra generator for an auxiliary IDE tool. Such generator names have the form<extra-generator> - <main-generator>
. The following extra generators are known to CMake.
其实我们比较常用generator主要有:
Visual Studio
Xcode
Ninja
Unix Makefiles
generator还会引入一个新的概念:multi-config。这个概念我们以前其实经常用到,比如在Visual Studio里面,我们可以选择Debug
还是Release
编译,在Xcode里面也可以配置。也就是说,vs和xcode的工程都同时支持多种编译配置。事实上,以下三种generator都支持multi-config:
Ninja Multi-Config
Visual Studio Generators
Xcode
那我们使用一下Xcode,看上面的代码是否能正常检测编译类型。
chp1-2 % cmake -B build -S . -GXcode
...
chp1-2 % cmake --build ./build
在build目录下面生成了一个Debug目录,里面生成了可执行文件。
chp1-2 % ./build/Debug/chp1
Debug version
我们删除build目录,尝试传入编译类型
chp1-2 % cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -GXcode
...
chp1-2 % cmake --build ./build
...
依然只是在build目录下面生成了一个Debug目录,并且在Debug目录里面生成了可执行文件。
chp1-2 % ./build/Debug/chp1
Release version
出现了很诡异的现象,就是在Debug的目录生成的可执行程序,打印出了Release版本的信息。
其实,在使用multi-config的generator的情况下,生成的程序类型不是由CMAKE_BUILD_TYPE
控制的,而是由CMAKE_CONFIGURATION_TYPES
控制的,会忽略掉CMAKE_BUILD_TYPE
的值。
我们使用下面命令试一下
cmake -B build -S . -GXcode
然后查看build目录下面的CMakeCache.txt
,会有两个发现
没有
CMAKE_BUILD_TYPE
相关内容CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo
而使用
cmake -B build -S .
的时候,build目录下面的CMakeCache.txt
只有
//Choose the type of build, options are: None Debug Release RelWithDebInfo
// MinSizeRel ...
CMAKE_BUILD_TYPE:STRING=
也就是说:
使用非
multi-config
的generator的时候,是CMAKE_BUILD_TYPE
控制程序生成的类型,默认是None
,CMAKE_CONFIGURATION_TYPES
不生效;使用
multi-config
的generator的时候,CMAKE_CONFIGURATION_TYPES
不生效,程序生成类型完全是CMAKE_BUILD_TYPE
控制。
需要把CMakeLists.txt
修改成下面这样才可以
cmake_minimum_required(VERSION 3.20)
project(chp1)
# if(CMAKE_BUILD_TYPE MATCHES Debug)
# add_definitions(-DDEBUG)
# else()
# add_definitions(-DRELEASE)
# endif()
add_executable(chp1 main.cpp)
# Add this compile definition for debug builds, this same logic works for
# target_compile_options, target_link_options, etc.
target_compile_definitions(chp1 PRIVATE
$<$<CONFIG:Debug>:DEBUG=1>
$<$<CONFIG:Release>:RELEASE=1>
)
$<$<xxx>:yyy>
这种表达式我们后面会详细介绍,这里只需要知道上面的意思就是
如果
CMAKE_CONFIGURATION_TYPES
是Debug
,那么定义DEBUG=1
;如果
CMAKE_CONFIGURATION_TYPES
是Release
,那么定义RELEASE=1
;否则什么都不定义
对于multi-config的generator,可以在生成工程阶段使用CMAKE_CONFIGURATION_TYPES
来配置默认的生成类型,也可以编译的时候使用--config
来执行到底选用Debug
还是Release
。
我们先来测试一下使用CMAKE_CONFIGURATION_TYPES
来控制。
首先测试默认config的情况
chp1-2 % cmake -B build -S . -GXcode -DCMAKE_CONFIGURATION_TYPES="Release;Debug" ... chp1-2 % cmake --build ./build ... chp1-2 % ./build/Debug/chp1 Debug version
在build目录下面生成了Debug目录,并没有Release目录。也就是说,默认config选择了Debug版本。但是这个默认的值对于不同的generator是不一样的。
Ninja Multi-Config
也是一个multi-config的generator,我们可以测试一下chp1-2 % cmake -B build -S . -G"Ninja Multi-Config" -DCMAKE_CONFIGURATION_TYPES="Release;Debug" ... chp1-2 % cmake --build ./build [2/2] Linking CXX executable Release/chp1 chp1-2 % ./build/Release/chp1 Release version
也就是说,用除了generator之外同样的配置去生成工程,Xcode在不指定config的情况下默认生成的是
Debug
版本而Ninja Multi-Config
生成的确是Release
版本。这说明我们不能依靠默认的config去生成我们想要的版本,需要用--config显示指定。测试使用config修改配置
chp1-2 % cmake -B build -S . -GXcode -DCMAKE_CONFIGURATION_TYPES="Release;Debug" ... chp1-2 % cmake --build ./build --config Release ... chp1-2 % ./build/Release/chp1 Release version
也比较符合预期,在build目录下面只生成了Release目录,在该目录下面生成了可执行程序。
继续执行
chp1-2 % cmake --build ./build --config Debug ... chp1-2 % ./build/Debug/chp1 Debug version
因为该工程支持multi-config,所以可以切换编译目标。
我们再测试只指定一个
CMAKE_CONFIGURATION_TYPES
的情况chp1-2 % cmake -B build -S . -GXcode -DCMAKE_CONFIGURATION_TYPES="Release" ... chp1-2 % cmake --build ./build --config Debug ... chp1-2 % ./build/Release/chp1 Release version chp1-2 % cmake --build ./build --config Release ... chp1-2 % ./build/Release/chp1 Release version
也就是说,在显式指定只有一种configuration的情况下,无论我们使用
--config
如何切换,得到的都只会有一种生成的程序类型。这个其实也很好理解,--config
只能从给定的config中去选择,既然只指定了一个,那么怎么选择也没有办法选择其它的类型。最后我们再测试一下使用
CMAKE_CONFIGURATION_TYPES
控制single-config的情况chp1-2 % cmake -B build -S . -G"Unix Makefiles" -DCMAKE_CONFIGURATION_TYPES="Release;Debug" -DCMAKE_BUILD_TYPE="Release" ... CMake Warning: Manually-specified variables were not used by the project: CMAKE_CONFIGURATION_TYPES
这跟在multi-config使用
CMAKE_BUILD_TYPE
传参一样,cmake警告说CMAKE_CONFIGURATION_TYPES
没有被工程使用。实际上,DCMAKE_CONFIGURATION_TYPES
只能在multi-config中使用,CMAKE_BUILD_TYPE
只能在single-config中使用。
multi-config的总结:
multi-config
的数量是由CMAKE_CONFIGURATION_TYPES
决定的,默认情况下有Debug
,Release
,RelWithDebInfo
,MinSizeRel
,用户可以通过-DCMAKE_CONFIGURATION_TYPES=xxx
显式指定需要的类型;用户可以通过
--config
显示指定需要的类型,但是指定的类型只能在CMAKE_CONFIGURATION_TYPES
范围内才会生效;不同generator在不使用
--config
显式指定生成类型的时候,不同的generator表现是不一致的,尽量避免这种情况;CMAKE_CONFIGURATION_TYPES
只能在multi-config中使用,就像CMAKE_BUILD_TYPE
只能在single-config中使用一样。
六、总结
single-config
意味着工程只可能生成出一种类型的程序,类型在工程生成的时候就已经确定了;multi-config
意味着cmake生成的工程是有能力生成多种类型的程序,具体生成的类型可以通过--config
在编译的时候进行选择。multi-config
未来也许有更多的类型,但是目前就只有Visual Studio、XCode以及Ninja multi-config是这种类型,其它的都是single-config
类型。
综合single-config
和multi-config
的情况,我们有如下结论:
cmake同时支持
single-config
和multi-config
类型;single-config
类型最后生成的程序的类型是通过CMAKE_BUILD_TYPE
在生成工程的时候控制的;而multi-config
是通过CMAKE_CONFIGURATION_TYPES
在生成工程的时候以及--config xxx
在编译的时候选择来共同控制的;multi-config
的默认config对于不同的generator是不一致的,最佳做法是显式指定。
我们再使用single-config
的时候是使用
if(CMAKE_BUILD_TYPE MATCHES Debug)
add_definitions(-DDEBUG)
else()
add_definitions(-DRELEASE)
endif()
来给最终的程序添加需要的宏。在使用multi-config
的时候是通过
target_compile_definitions(chp1 PRIVATE
$<$<CONFIG:Debug>:DEBUG=1>
$<$<CONFIG:Release>:RELEASE=1>
)
来给最终的程序添加需要的宏。
那如何添加宏才能使得无论在single-config
还是multi-config
下都生效呢?
其实CMAKE给我们提供了一个只读的GENERATOR_IS_MULTI_CONFIG
来帮助判断,这样我们就可以把上面加宏的代码修改为:
if(GENERATOR_IS_MULTI_CONFIG)
target_compile_definitions(chp1 PRIVATE
$<$<CONFIG:Debug>:DEBUG=1>
$<$<CONFIG:Release>:RELEASE=1>
)
else()
if(CMAKE_BUILD_TYPE MATCHES Debug)
add_definitions(-DDEBUG)
else()
add_definitions(-DRELEASE)
endif()
endif()
也就是在使用multi-config
的时候使用Config判断,否则使用CMAKE_BUILD_TYPE
判断。
除此之外,cmake保证在编译Release
的版本的时候会加入NDEBUG
宏,我们也可以在代码里面直接使用这个宏来判断,这也是一个非常准确的判断条件。
七、
结语
使用cmake生成和编译工程的时候一定要注意区分single-config
和multi-config
,因为两种不同的config中用来控制最终程序的版本的条件是不一样的,不注意的话很容易造成编译出来的程序的效率不符合预期。
评论区