1. 简介
相信学习或者从事过C、C++程序开发的同学都清楚,make是一种管理c或者c++代码构建(编译、安装、部署等)的构建系统,而它通过Makefile脚本文件进行编程。通常我们会为每一个子模块(功能)编写一个makefile文件,如果这个工程很大,而且相关性比较强的话,makefile的书写就会变得相对繁琐。如果以后需要添加新的功能或者是新人需要修改现有功能的话,维护成本较高;为了解决这样的问题,CMake便出现了,CMake的入门相当容易,它提供了相比Makefile更加高层级的原语,使得对源代码的管理这件事情变得简单起来。
CMake本身并不直执行源代码的构建,它依赖于底层构建系统(如make),在配置过程中生成它们所需要的配置文件,然后通过调用底层构建系统来完成实际的构建过程。最初,CMake是为Makefile的各种方言设计的生成器,现在CMak支持生成现代的构建系统,比如Ninja,以及IDE的项目描述文件,比如Visual Studio和Xcode。
所有控制CMake配置过程的命令都写在一个CMakeLists.txt的文件中。当CMakeLists.txt文件编写完成后,便使用cmake命令并告知CMakeLists.txt所在目录便启动了配置过程。假设我们底层使用的是make构建系统,那么在CMake的配置过程会产生makefile文件,然后我们便可以执行make,启动真正的代码构建过程,最终得到编译的结果。如此一来,我们不再需要去理解纷繁复杂的Makefile命令,而只需要写简单的CMakeLists.txt即可。
因此,想要快速入门CMake,最简单高效的方式就是学会如何编写CMakeLists.txt文件。下面便通过几个简单的例子来学习CMake的使用。
本文中的例子均在如下的软件环境中验证:
- OS:Windows 11
- CMake:3.26.4
- Generator:Visual Studio 17 2022
- Compiler:MSVC 19.32.31332.
稍微剧透一下,这篇博文是后续CMake系列博文的开端,这篇文章只会提到最基础的概念,目的是让读者先对CMake有一个感性的认识,并且可以通过例子快速地将CMake用起来。更加详细的原理,以及高级的使用方法将会在后续的博文中逐步揭露。
2. 例1:编译单个源文件
最简单的例子莫过于编译一个输出hello cmake字符串的单个源文件的例子了。在这个例子中,我们假设这个源文件名字为main.c,其源代码内容如下:
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("Hello CMake!\n");
return 0;
}
为了使用CMake来管理这个源文件的构建,我们需要在与它相同的目录下创建一个CMakeLists.txt文件,并编写如下内容:
cmake_minimum_required(VERSION 3.5)
project(hello_cmake)
# print message
message("hello_cmake_BINARY_DIR = ${hello_cmake_BINARY_DIR}, PROJECT_BINARY_DIR=${PROJECT_BINARY_DIR}")
add_executable(${PROJECT_NAME} main.c)
下面会给CMakeList.txt中的语法解释:CMakeLists.txt中的命令不区分大小写,但是参数和变量区分。
cmake_minimum_required()
:这是一个可选的命令。它指出解析该CMakeLists.txt所需的CMake的最低版本,因为CMake本身是一个在不断迭代的软件,每次更新都可能会新增一些命令,因此,如果我们使用了某些依赖于版本的命令时,最好使用这条命令来声明所需的CMake最低版本。例子中的3.5是一个拍脑袋的值。project()
:非强制,但建议加上。因为它会生许多有用的变量,例如下面几个:hello_cmake_BINARY_DIR
:与PROJECT_BINARY_DIR
等价,指向生成目标所在目录hello_cmake_SOURCE_DIR
:与PROJECT_SOURCE_DIR
等价,指向源代码所在路径PROJECT_NAME
:项目的名称,在这个例子中为hello_cmake
。
message()
:正如其名,该命令用于打印一条消息。该命令可以引用脚本中定义的或者系统预定义的变量,这里我们打印了上面提到的两个目录以进行验证。add_executable()
:指示cmake,这个工程将生成一个可执行文件作为目标。它需要指出目标的名称,已经生成这个目标所依赖的源文件。聪明的你一定会问,想到是不是还有生成函数库的命令?对的,add_library()
便是,这会在后面的例子中介绍。
因为直接在当前目录运行cmake进行配置时,会在当前目录下产生很多临时文件和目录。因此,我们通常会在源文件所在目录新建一个build目录,然后进入到build目录,执行命令cmake ..
,这样产生的所有临时文件都会生成在build目录下,而不影响源码目录的代码。我们进入到build目录,执行命令cmake ..
,得到如下日志:
D:\Workspace\CMake\hello_cmake\build>cmake ..
-- Building for: Visual Studio 17 2022
-- The C compiler identification is MSVC 19.32.31332.0
-- The CXX compiler identification is MSVC 19.32.31332.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: D:/ProgramFiles/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.32.31326/bin/Hostx64/x64/cl.exe - 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: D:/ProgramFiles/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.32.31326/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
hello_cmake_BINARY_DIR = D:/Workspace/CMake/hello_cmake/build, PROJECT_BINARY_DIR=D:/Workspace/CMake/hello_cmake/build
-- Configuring done (5.5s)
-- Generating done (0.0s)
-- Build files have been written to: D:/Workspace/CMake/hello_cmake/build
我们可以看到,hello_cmake_BINARY_DIR
和PROJECT_BINARY_DIR
符合预期。最终生成的底层构建系统的文件被放在了D:/Workspace/CMake/hello_cmake/build
目录下。使用dir
查看该目录,得到如下文件列表:
2023/06/03 11:15 43,653 ALL_BUILD.vcxproj
2023/06/03 11:15 285 ALL_BUILD.vcxproj.filters
2023/06/03 11:15 14,166 CMakeCache.txt
2023/06/03 11:15 <DIR> CMakeFiles
2023/06/03 11:15 1,448 cmake_install.cmake
2023/06/03 11:15 3,122 hello_cmake.sln
2023/06/03 11:15 52,854 hello_cmake.vcxproj
2023/06/03 11:15 571 hello_cmake.vcxproj.filters
2023/06/03 11:15 43,325 ZERO_CHECK.vcxproj
2023/06/03 11:15 528 ZERO_CHECK.vcxproj.filters
使用过Visual Studio的朋友一定不会对hello_cmake.sln文件感到陌生。我们可以使用这个文件将生成的文件导入到Visual Studio IDE中进行最终的构建。但还有更加方便的方式,即在该目录下执行cmake --build .
,这样就可生成相应的可执行程序。执行这个命令输入的日志如下:
D:\Workspace\CMake\hello_cmake\build>cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 17.2.1+52cd2da31
版权所有(C) Microsoft Corporation。保留所有权利。
Checking Build System
Building Custom Rule D:/Workspace/CMake/hello_cmake/CMakeLists.txt
main.c
hello_cmake.vcxproj -> D:\Workspace\CMake\hello_cmake\build\Debug\hello_cmake.exe
Building Custom Rule D:/Workspace/CMake/hello_cmake/CMakeLists.txt
好了,生成的可执行文件被放在了D:\Workspace\CMake\hello_cmake\build\Debug\hello_cmake.exe,执行这个文件,毫无疑问,我们将得到如下输出:
D:\Workspace\CMake\hello_cmake\build>Debug\hello_cmake.exe
Hello CMake!
小结一下,通过这个简单的例子,我们掌握了通过CMake,如何从CMakeLists.txt开始,一直到生成可执行文件的流程。看起来,整个流程似乎并不那么简单,甚至还有点繁琐。这就是“用牛刀杀鸡”的副作用。因为CMake真正的魅力并不在于管理这种玩具一般的C/C++项目,而是实际项目中复杂的源代码构建。此外,这个例子之所以显得繁琐,是因为出于教学目的,使用的是控制台命令的操作方式,并且故意将配置和构建过程独立开。实际上,我们在使用时,通常会使用IDE+插件的方式,例如VSCode + CMake Tools插件,通过简单的F7快捷键就可以一件完成配置、编译的过程。但是为了说明整个流程,后续的例子还是采用这种控制台的操作方式。
3. 例2:编译多个源文件
在前面例子的基础上,我们增加一个两个文件:say_hello.h, say_hello.c。并在main.c中对它们进行调用。
say_hello.h的内容如下:
#pragma once
void say_hello(const char* name);
say_hello.c的内容如下:
#include "hello.h"
#include <stdio.h>
void say_hello(const char* name)
{
printf("Hello, my name is %s\n",name);
}
修改main.c,增加对say_hello的调用:
#include "say_hello.h"
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("Hello CMake!\n");
say_hello("Duran");
return 0;
}
而CMakeLists.txt的内容需要做出相应的修改,如下所示:
cmake_minimum_required(VERSION 3.5)
project(hello_cmake)
# print message
message("hello_cmake_BINARY_DIR = ${hello_cmake_BINARY_DIR}, PROJECT_BINARY_DIR=${PROJECT_BINARY_DIR}")
set(PROJECT_SOURCES main.c say_hello.c)
add_executable(${PROJECT_NAME} ${PROJECT_SOURCES})
细心的你一定发现了,与例子1中的不同就是新增了一条set()
命令,并修改了add_executable()
对源文件的描述。
set()
:该命令用于定义一个可在后续的CMakeLists.txt引用的变量。这里把由两个源文件名组成的字符串列表定义为变量PROJECT_SOURCES
。当然,这个变量的定义并不是必须,而是为了让CMakeLists.txt可读性更好而引入的。add_executable()
:这里引用了由set()
命令定义的变量PROJECT_SOURCES
。有人可能会问:为何不需要将头文件加入到这里的源文件列表中?原因是这里的头文件并不参与真正的构建过程,并且大部分情况下,都不需要将头文件纳入到源文件列表。但是,对于一些需要特殊情况,例如Qt,它特殊的meta object系统需要对头文件做特殊的处理,在这种情况下,就需要将头文件纳入到源文件列表中了。
后续的过程跟单个源文件的例子一致,这里不再赘述,直接贴出程序的执行结果如下:
D:\Workspace\CMake\hello_cmake\build>Debug\hello_cmake.exe
Hello CMake!
Hello, my name is Duran!
在这个例子中,似乎可以稍微感受到CMake的便利了,在项目增加源文件的情况下,我们仅需要稍微修改一下CMakeLists.txt的源文件列表即可。
4. 例3:生成一个函数库和一个可执行文件
在实际的项目开发中,通常会有许多模块(功能)是相对通用的,实现这些模块的源文件会被项目内的其他代码重复使用(调用)。为了实现这种用例,我们不应该再像上面提到的这个例子一样,每当某个模块需要使用这些源文件时,都将它们添加到自己的依赖源文件列表中,因为这样太繁琐,且容易出错。相反,我们可以将这些需要被复用的源文件打包成函数库,而使用者仅需要引用这个函数库即可。
继续基于上一个例子,这里我们将say_hello打包成一个静态函数库,由main来调用。要实现这个需求,只需要在例子2的基础上修改一下CMakeLists.txt文件。修改后的CMakeLists.txt的内容如下:
cmake_minimum_required(VERSION 3.5)
project(hello_cmake)
message("hello_cmake_BINARY_DIR = ${hello_cmake_BINARY_DIR}, PROJECT_BINARY_DIR=${PROJECT_BINARY_DIR}")
set(LIB_NAME say_hello)
add_library(${LIB_NAME} say_hello.c)
add_executable(${PROJECT_NAME} main.c)
target_link_libraries(${PROJECT_NAME} ${LIB_NAME})
相比2,这里有两点不同:
- 使用
add_library()
命令生成了一条静态函数库生成策略。而生成函数库所需的源文件为say_hello.c。add_library()
还可以用于生成动态库,这会在以后的博文中介绍。 - 使用
target_link_libraries()
命令告知CMake${PROJECT_NAME}
目标的生成依赖于函数库${LIB_NAME}
。
其中,target_link_libraries()
搜索函数库的路径是系统的环境变量,以及当前路径。还可以通过指定函数库的绝对路径来进行链接。另外,还可以通过link_directories()
命令来添加CMake搜索函数库的路径,如下所示:
link_directories(<path_to_be_add>)
配置和构建过程跟之前一样。当我们执行cmake --build .
时,会得到下列日志:
D:\Workspace\CMake\hello_cmake\build>cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 17.2.1+52cd2da31
版权所有(C) Microsoft Corporation。保留所有权利。
Checking Build System
Building Custom Rule D:/Workspace/CMake/hello_cmake/CMakeLists.txt
say_hello.vcxproj -> D:\Workspace\CMake\hello_cmake\build\Debug\say_hello.lib
Building Custom Rule D:/Workspace/CMake/hello_cmake/CMakeLists.txt
main.c
hello_cmake.vcxproj -> D:\Workspace\CMake\hello_cmake\build\Debug\hello_cmake.exe
Building Custom Rule D:/Workspace/CMake/hello_cmake/CMakeLists.txt
可以看到,在编译过程中,MSVC为我们生成了say_hello.lib静态库和hello_cmake.exe。
看到这里,你一定会发现,我们这个例子并没有很好地解决前面提到的需求,因为我们把构建函数库所需的源文件和CMakeLists.txt与使用者(main.c)杂糅到一起了。别着急,下个例子就可以解决你的疑问。
5. 例4:模块化编译
在上一个例子中,虽然我们使用了函数库,但是源代码都是在同一个路径下面。但是,在真实的项目开发中,功能比我们的例子复杂得多,源代码文件数量相应也会比例子中多得多。因此,我们常常会使用分而治之的思想,按照模块(功能)对这些源文件进行分类,形成多个子文件夹。基于例子3,我们把main.c划分为主程序,并使用app目录存放;然后将say_helloc函数库放入子目录lib中。为了让CMake可以正常地配置这个源代码树,我们至少需要在根目录下编写一个CMakeLists.txt文件,目录结构如下;
2023/06/03 16:14 <DIR> app
2023/06/03 16:05 <DIR> build
2023/06/03 16:04 368 CMakeLists.txt
2023/06/03 16:14 <DIR> lib
如前文所述,build
目录是CMake的工作目录,这里不需要理会。我可以只使用根目录下CMakeLists.txt文件就实现上述需求。但是,并不建议这么做,因为app和lib之间是独立开发的,我们不应该把相互独立的两个模块强行地揉到一起。更正确和常见的做法是,分别为app和lib文件夹编写独立的CMakeLists.txt文件,最后通过顶层的CMakeLists.txt关联起来。下面就分别介绍这3个文件的内容。
app文件夹下的CMakeLists.txt的内容如下:
add_executable(${PROJECT_NAME} main.c)
target_link_libraries(${PROJECT_NAME} say_hello)
这部分没有新增语法,不明白的同学可以参看前面的例子。
lib文件夹的CMakeLists.txt的内容如下:
set(LIB_NAME say_hello)
add_library(${LIB_NAME} say_hello.c)
target_include_directories(${LIB_NAME} PUBLIC .)
注意,这里使用到了target_include_directories()
命令,这是告知CMake这个say_hello函数库的源文件所引用的头文件的路径。而PUBLIC
选项是可选的,例子中使用了它,意味着任何依赖了say_hello函数库的模块,也可以引用它所需要的头文件。在这个例子中,最终的结果就是让main.c可以通过#include "say_hello.h"
的方式直接引用say_hello函数库的头文件。
最后,根目录下的CMakeLists.txt的内容如下:
cmake_minimum_required(VERSION 3.5)
project(hello_cmake)
add_subdirectory(lib)
add_subdirectory(app)
这里使用了两个add_subdirectory
命令,它们则指示CMake需要到相应的子目录下去寻找CMakeLists.txt文件并进行配置,因此,在这个例子中,CMake在配置过程中,需要到app和lib这两个子目录下继续执行配置。你或许会问:这两个命令的顺序重要吗?即add_subdirectory(lib)
必须要在add_subdirectory(app)
之前吗?答案是否定的,因为CMake的配置过程并不真正地执行编译,因此在配置app时,并不一定需要say_hello函数库真的存在。
后续的配置和构建过程与之前的例子一样。配置过程结束后,我们得到的build目录结构是下面这样的:
2023/06/03 16:39 43,870 ALL_BUILD.vcxproj
2023/06/03 16:39 285 ALL_BUILD.vcxproj.filters
2023/06/03 16:39 <DIR> app
2023/06/03 16:39 14,166 CMakeCache.txt
2023/06/03 16:39 <DIR> CMakeFiles
2023/06/03 16:39 1,700 cmake_install.cmake
2023/06/03 16:39 4,239 hello_cmake.sln
2023/06/03 16:39 <DIR> lib
2023/06/03 16:39 44,261 ZERO_CHECK.vcxproj
2023/06/03 16:39 528 ZERO_CHECK.vcxproj.filters
可以看到,CMake在build目录中,为我们的app和lib模块创建了对应的子目录。
查看构建日志:
D:\Workspace\CMake\hello_cmake\build>cmake --build .
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 17.2.1+52cd2da31
版权所有(C) Microsoft Corporation。保留所有权利。
Checking Build System
Building Custom Rule D:/Workspace/CMake/hello_cmake/lib/CMakeLists.txt
say_hello.c
say_hello.vcxproj -> D:\Workspace\CMake\hello_cmake\build\lib\Debug\say_hello.lib
Building Custom Rule D:/Workspace/CMake/hello_cmake/app/CMakeLists.txt
main.c
hello_cmake.vcxproj -> D:\Workspace\CMake\hello_cmake\build\app\Debug\hello_cmake.exe
Building Custom Rule D:/Workspace/CMake/hello_cmake/CMakeLists.txt
可以看到,构建系统为我们分别在lib目录下生成了say_hello.lib,在app下生成了hello_cmake.exe。运行hello_cmake.exe的结果当然也是正常的:
D:\Workspace\CMake\hello_cmake\build>app\Debug\hello_cmake.exe
Hello CMake!
Hello, my name is Duran!
看到这,不知道你是否会感觉哪里不太舒服?对,作为项目最终输出的可执行文件hello_cmake.exe,却被存放于app子目录下了。我希望它放在build目录的bin文件夹下,这样更符合直觉。请继续阅读下一个例子。
6. 例5:install简单用例
在例子4之后,我们产生了一个新的需求,即把生成的最终可执行文件部署到指定的目录下,例如build目录的bin中。这时,我们就可以使用CMake中的install()
命令了。它将生成一条install编译目标,将编译得到的可执行文件、库文件、数据文件等放到我们指定的路径。
为了达到这个目的,我们仅需要在例子4的基础上修改根目录和app目录下的CMakeLists.txt文件,在根目录的CMakeLists.txt中,添加设置CMAKE_INSTALL_PREFIX
变量:
cmake_minimum_required(VERSION 3.5)
project(hello_cmake)
set(CMAKE_INSTALL_PREFIX ${PROJECT_BINARY_DIR})
add_subdirectory(app)
add_subdirectory(lib)
我们使用set()
命令将CMAKE_INSTALL_PREFIX
设置为当前项目的生成路径。CMAKE_INSTALL_PREFIX
变量会被install()
引用,当install()
的目标路径使用的是相对路径时,指的是就是相对于CMAKE_INSTALL_PREFIX
变量指向的路径。
修改app目录下的CMakeLists.txt如下:
add_executable(${PROJECT_NAME} main.c)
target_link_libraries(${PROJECT_NAME} say_hello)
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
install()
命令的将生成一条install策略,在构建install目标时,便会将可执行文件hello_cmake.exe复制到${CMAKE_INSTALL_PREFIX}/bin
目录中。
我们执行cmake --build . --target install
命令来触发部署过程,日志如下:
D:\Workspace\CMake\hello_cmake\build>cmake --build . --target install
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 17.2.1+52cd2da31
版权所有(C) Microsoft Corporation。保留所有权利。
say_hello.vcxproj -> D:\Workspace\CMake\hello_cmake\build\lib\Debug\say_hello.lib
hello_cmake.vcxproj -> D:\Workspace\CMake\hello_cmake\build\app\Debug\hello_cmake.exe
-- Install configuration: "Debug"
-- Installing: D:/Workspace/CMake/hello_cmake/build/bin/hello_cmake.exe
可以看到,hello_cmake.exe被部署到D:/Workspace/CMake/hello_cmake/build/bin/目录中了。并且,该程序可以正常运行:
D:\Workspace\CMake\hello_cmake\build>bin\hello_cmake.exe
Hello CMake!
Hello, my name is Duran!
7. 小结
本文从最简单的单源文件编译切入,逐步深入,通过5个可拿来就用的实例,简要地介绍了如何使用CMake进行源代码的构建和部署。至此,相信大家已经建立对CMake的感性认识了。后续的博文将会逐步深挖CMake的功能。