CMake 快速入门

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_DIRPROJECT_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,这里有两点不同:

  1. 使用add_library()命令生成了一条静态函数库生成策略。而生成函数库所需的源文件为say_hello.c。add_library()还可以用于生成动态库,这会在以后的博文中介绍。
  2. 使用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的功能。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CMake是一个跨平台的开源构建工具,它能够自动生成各种不同平台的构建脚本,简化了项目的构建过程。下面是一个300字快速入门CMake的实战教程。 首先,安装CMake并确保它被正确添加到系统的环境变量中。 接下来,在你的项目根目录下创建一个CMakeLists.txt文件,这个文件是CMake的配置文件,用来指导CMake生成项目的构建脚本。在CMakeLists.txt文件中,首先指定项目的名称和最低版本要求,例如: ``` cmake_minimum_required(VERSION 3.10) project(MyProject) ``` 然后,添加你的项目的源文件和头文件,使用add_executable或add_library命令指定它们的路径,例如: ``` add_executable(MyApp main.cpp foo.cpp bar.cpp) ``` 接下来,你可以添加依赖库,使用target_link_libraries命令指定它们的路径,例如: ``` target_link_libraries(MyApp ${CMAKE_DL_LIBS}) ``` 此外,你还可以指定编译选项,例如: ``` set(CMAKE_CXX_FLAGS "-std=c++11 -Wall") ``` 最后,你可以通过命令行执行cmake命令来生成构建脚本,并执行构建过程,例如: ``` mkdir build cd build cmake .. make ``` 在build目录中,你会找到生成的构建脚本及其生成的可执行文件(或库文件)。 通过这个简单的实战教程,你可以快速入门CMake,并且开始使用它来管理你的项目的构建过程。当然,在实际应用中,你还可以进行更多高级配置,例如添加条件编译、安装目标等。希望这个简短的回答能够给你一个基本的CMake入门指导。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值