CMake快速上手
前言
CMake 不再使你在构建项目时郁闷地想自杀了。
插曲:之前在研究所做项目的时候,刘总总是暴力的使用sh脚本编译[狗头保命一波],狠狠的暴力美学。但是,这对写lab的我有点难泵,于是乎,全给重写CMakeLists.txt文件的形式编译。
好了,基本文件的CMakeList.txt学习清楚后,就应该学习实际项目中CMakeLists.txt是如何规范书写,到实践中学习。
介绍
跨平台设计原理
在大型C/C++项目中,跨平台设计是一个重要的考虑因素。跨平台设计的目标是使得源代码能够在多种操作系统和硬件架构上编译和运行,而无需进行大量的修改。这样可以大大提高代码的可移植性和复用性,降低维护成本。
CMake(Cross-platform Make)是一个开源的、跨平台的自动化建构系统,它允许开发者编写一份通用的CMakeList.txt文件来控制编译过程,而不需要修改特定平台下的编译配置,从而实现真正意义上的跨平台编译。
CMake支持多种编译器,包括GCC,Clang,Visual Studio等,并且可以生成各种类型的项目文件,如Makefile,Ninja,Visual Studio解决方案等。这使得CMake成为了跨平台C/C++项目的首选构建工具。
在CMake中,跨平台设计的实现主要依赖于以下几个原理:
- 抽象层:CMake为各种操作系统和编译器提供了一套抽象层,开发者只需要关注源代码和依赖库,而无需关心具体的编译器和操作系统。这是通过在CMakeList.txt文件中设置目标(target)和属性(property)来实现的。
- 模块系统:CMake提供了一套模块系统,用于查找库和包,检查编译器和系统特性,以及管理测试等。这些模块大大简化了跨平台开发的复杂性。
- 生成器:CMake通过生成器(generator)将CMakeList.txt文件转换为特定平台下的构建文件。生成器根据目标系统的特性,自动处理平台相关的编译和链接问题。
- 变量和条件:CMake支持变量和条件语句,使得开发者可以根据不同的平台和编译器,选择不同的源文件和编译选项。
以上就是CMake实现跨平台设计的基本原理,接下来我们将深入探讨CMake在跨平台设计中的应用。
跨平台设计
在大型C/C++项目中,跨平台设计是必不可少的一环。这主要涉及到如何使用CMake来配置和管理不同平台的编译环境。
CMake的跨平台特性
CMake本身就是一个跨平台的构建工具,它可以在Windows、Linux、Mac等多种操作系统上运行。CMake通过生成平台相关的构建文件(如Unix的Makefile,Windows的nmake文件或Visual Studio项目文件等)来实现跨平台构建。这意味着,我们可以编写一套CMake构建脚本,然后在不同的平台上生成相应的构建文件,从而实现跨平台构建。
使用CMake进行跨编译
跨编译是指在一个平台上生成另一个平台的可执行代码。例如,我们可能需要在Linux平台上编译出运行在嵌入式设备上的ARM架构的代码。CMake支持跨编译,我们可以通过设置CMake的工具链文件(Toolchain File)来指定交叉编译器和相关的编译选项。
在CMake的工具链文件中,我们可以设置如下变量:
CMAKE_SYSTEM_NAME
:目标系统的名称,如Linux、Windows、Android等。CMAKE_SYSTEM_PROCESSOR
:目标系统的处理器架构,如x86、arm等。CMAKE_C_COMPILER
、CMAKE_CXX_COMPILER
:C和C++的交叉编译器的路径。CMAKE_FIND_ROOT_PATH
:在查找库和头文件时,CMake应该查找的路径。
通过设置这些变量,我们可以告诉CMake我们要编译的目标平台是什么,以及应该使用哪些工具进行编译。
处理平台相关的代码
在大型C/C++项目中,通常会有一些平台相关的代码。例如,Windows平台和Linux平台的系统调用是不同的,处理文件路径的方式也是不同的。我们需要在CMake构建脚本中检测目标平台,然后根据目标平台来决定编译哪些源文件。
CMake提供了if
命令来进行条件判断。我们可以使用CMAKE_SYSTEM_NAME
变量来判断目标平台。例如:
1 | if(CMAKE_SYSTEM_NAME STREQUAL "Linux") |
以下是跨平台设计的流程图:
在大型C/C++项目中,我们需要考虑到跨平台设计。这主要涉及到如何使用CMake来配置和管理不同平台的编译环境。CMake本身就是一个跨平台的构建工具,它可以在Windows、Linux、Mac等多种操作系统上运行。CMake通过生成平台相关的构建文件(如Unix的Makefile,Windows的nmake文件或Visual Studio项目文件等)来实现跨平台构建。
跨编译是指在一个平台上生成另一个平台的可执行代码。例如,我们可能需要在Linux平台上编译出运行在嵌入式设备上的ARM架构的代码。CMake支持跨编译,我们可以通过设置CMake的工具链文件(Toolchain File)来指定交叉编译器和相关的编译选项。
在大型C/C++项目中,通常会有一些平台相关的代码。例如,Windows平台和Linux平台的系统调用是不同的,处理文件路径的方式也是不同的。我们需要在CMake构建脚本中检测目标平台,然后根据目标平台来决定编译哪些源文件。CMake提供了if
命令来进行条件判断。我们可以使用CMAKE_SYSTEM_NAME
变量来判断目标平台。
跨平台设计案例
在实践中,跨平台设计是一个复杂的过程,需要考虑到各种因素。以下是一些实践和案例,帮助我们更好地理解跨平台设计的过程和挑战。
首先,我们需要理解平台差异(Understanding Platform Differences)。不同的操作系统和硬件平台有不同的特性和限制。例如,Windows和Linux在文件系统、线程管理和网络编程等方面有显著的差异。理解这些差异是设计跨平台应用的第一步。
其次,选择合适的工具和库(Choosing Appropriate Tools and Libraries)也是非常重要的。有些工具和库是跨平台的,可以在多种操作系统和硬件平台上运行。例如,CMake就是一个跨平台的构建工具,可以在Windows、Linux和MacOS上使用。使用这些工具和库可以大大简化跨平台设计的复杂性。
然后,编写可移植的代码(Writing Portable Code)是另一个关键步骤。可移植的代码是指可以在多种平台上编译和运行的代码。为了实现代码的可移植性,我们需要避免使用特定平台的特性和API,或者使用预处理器指令来处理平台差异。
最后,进行全面的测试(Comprehensive Testing)是确保跨平台应用正确运行的重要步骤。我们需要在所有目标平台上测试应用,确保它在各种环境中都能正常工作。
以上就是跨平台设计的一些基本步骤和实践。在实际的项目中,我们可能还需要考虑到其他的因素,如性能、安全性和用户体验等。但是,只要我们遵循这些基本原则,我们就可以设计出高质量的跨平台应用。
基本使用
常用命令
1. 指定 cmake 的最小版本
1 | cmake_minimum_required(VERSION 3.4.1) |
这行命令是可选的,当然可以不写这句话,但在有些情况下,如果 CMakeLists.txt 文件中使用了一些高版本 cmake 特有的一些命令的时候,就需要加上这样一行,提醒用户升级到该版本之后再执行 cmake。
2. 设置项目名称
1 | project(demo) |
这个命令不是强制性的,但最好都加上。它会引入两个变量 demo_BINARY_DIR 和 demo_SOURCE_DIR,同时,cmake 自动定义了两个等价的变量 PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR。
3. 设置编译类型
1 | add_executable(demo demo.cpp) # 生成可执行文件 |
add_library 默认生成是静态库,通过以上命令生成文件名字,
- 在 Linux 下是:
demo
libcommon.a
libcommon.so - 在 Windows 下是:
demo.exe
common.lib
common.dll
4. 指定编译包含的源文件
4.1 明确指定包含哪些源文件
1 | add_library(demo demo.cpp test.cpp util.cpp) |
4.2 搜索所有的 cpp 文件
aux_source_directory(dir VAR) 发现一个目录下所有的源代码文件并将列表存储在一个变量中。
1 | aux_source_directory(. SRC_LIST) # 搜索当前目录下的所有.cpp文件 |
4.3 自定义搜索规则
1 | file(GLOB SRC_LIST "*.cpp" "protocol/*.cpp") |
5. 查找指定的库文件
find_library(VAR name path)查找到指定的预编译库,并将它的路径存储在变量中。
默认的搜索路径为 cmake 包含的系统库,因此如果是 NDK 的公共库只需要指定库的 name 即可。
1 | find_library( # Sets the name of the path variable. |
类似的命令还有 find_file()、find_path()、find_program()、find_package()。
6. 设置包含的目录
1 | include_directories( |
Linux 下还可以通过如下方式设置包含的目录
1 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_CURRENT_SOURCE_DIR}") |
7. 设置链接库搜索目录
1 | link_directories( |
Linux 下还可以通过如下方式设置包含的目录
1 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_CURRENT_SOURCE_DIR}/libs") |
8. 设置 target 需要链接的库
1 | target_link_libraries( # 目标库 |
在 Windows 下,系统会根据链接库目录,搜索xxx.lib 文件,Linux 下会搜索 xxx.so 或者 xxx.a 文件,如果都存在会优先链接动态库(so 后缀)。
8.1 指定链接动态库或静态库
1 | target_link_libraries(demo libface.a) # 链接libface.a |
8.2 指定全路径
1 | target_link_libraries(demo ${CMAKE_CURRENT_SOURCE_DIR}/libs/libface.a) |
8.3 指定链接多个库
1 | target_link_libraries(demo |
9. 设置变量
9.1 set 直接设置变量的值
1 | set(SRC_LIST main.cpp test.cpp) |
9.2 set 追加设置变量的值
1 | set(SRC_LIST main.cpp) |
9.3 list 追加或者删除变量的值
1 | set(SRC_LIST main.cpp) |
10. 条件控制
10.1 if…elseif…else…endif
逻辑判断和比较:
1 | if (expression):expression 不为空(0,N,NO,OFF,FALSE,NOTFOUND)时为真 |
数字比较:
1 | if (variable LESS number):LESS 小于 |
字母表顺序比较:
1 | if (variable STRLESS string) |
1 | if(MSVC) |
10.2 while…endwhile
1 | while(condition) |
10.3 foreach…endforeach
1 | foreach(loop_var RANGE start stop [step]) |
start 表示起始数,stop 表示终止数,step 表示步长,示例:
1 | foreach(i RANGE 1 9 2) |
11. 打印信息
1 | message(${PROJECT_SOURCE_DIR}) |
12. 包含其它 cmake 文件
1 | include(./common.cmake) # 指定包含文件的全路径 |
常用信息
1.预定义变量
- PROJECT_SOURCE_DIR:工程的根目录
- PROJECT_BINARY_DIR:运行 cmake 命令的目录,通常是 ${PROJECT_SOURCE_DIR}/build
- PROJECT_NAME:返回通过 project 命令定义的项目名称
- CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 所在的路径
- CMAKE_CURRENT_BINARY_DIR:target 编译目录
- CMAKE_CURRENT_LIST_DIR:CMakeLists.txt 的完整路径
- CMAKE_CURRENT_LIST_LINE:当前所在的行
- CMAKE_MODULE_PATH:定义自己的 cmake 模块所在的路径,SET(CMAKE_MODULE_PATH
- ${PROJECT_SOURCE_DIR}/cmake),然后可以用INCLUDE命令来调用自己的模块
- EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
- LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
2. 环境变量
使用环境变量
1 | $ENV{Name} |
写入环境变量
1 | set(ENV{Name} value) |
3.系统信息
- CMAKE_MAJOR_VERSION:cmake 主版本号,比如 3.4.1 中的 3
- CMAKE_MINOR_VERSION:cmake 次版本号,比如 3.4.1 中的 4
- CMAKE_PATCH_VERSION:cmake 补丁等级,比如 3.4.1 中的 1
- CMAKE_SYSTEM:系统名称,比如 Linux-2.6.22
- CMAKE_SYSTEM_NAME:不包含版本的系统名,比如 Linux
- CMAKE_SYSTEM_VERSION:系统版本,比如 2.6.22
- CMAKE_SYSTEM_PROCESSOR:处理器名称,比如 i686
- UNIX:在所有的类 UNIX 平台下该值为 TRUE,包括 OS X 和 cygwin
- WIN32:在所有的 win32 平台下该值为 TRUE,包括 cygwin
4.主要开关选项
- BUILD_SHARED_LIBS:这个开关用来控制默认的库编译方式,如果不进行设置,使用 add_library 又没有指定库类型的情况下,默认编译生成的库都是静态库。如果 set(BUILD_SHARED_LIBS ON) 后,默认生成的为动态库
- CMAKE_C_FLAGS:设置 C 编译选项,也可以通过指令 add_definitions() 添加
- CMAKE_CXX_FLAGS:设置 C++ 编译选项,也可以通过指令 add_definitions() 添加
1 | add_definitions(-DENABLE_DEBUG -DABC) # 参数之间用空格分隔 |
实战
在实践之前,先看份去年写的CMakeList.txt,截取部分。
1 | # 打印CMake模块路径 |
单个源文件
(源代码所在目录:Demo1)
假设现在我们的项目中只有一个源文件http://main.cc,该程序的用途是计算一个数的指数幂。
1 | #include <stdio.h> |
首先编写 CMakeLists.txt 文件,并保存在与http://mian.cc源文件同个目录下:
1 | # CMake 最低版本号要求 |
CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的。符号 #
后面的内容被认为是注释。命令由命令名称、小括号和参数组成,参数之间使用空格进行间隔。
对于上面的 CMakeLists.txt 文件,依次出现了几个命令:
cmake_minimum_required
:指定运行此配置文件所需的 CMake 的最低版本;project
:参数值是Demo1
,该命令表示项目的名称是Demo1
。add_executable
: 将名为 main.cc 的源文件编译成一个名称为 Demo 的可执行文件。
之后,在当前目录执行cmake .
,得到 Makefile 后再使用make
命令编译得到 Demo1 可执行文件。
多个源文件
(源代码所在目录Demo2)
上面的例子只有单个源文件。现在假如把power
函数单独写进一个名为MathFunctions.c
的源文件里,使得这个工程变成如下的形式:
1 | /Demo2 |
这个时候,CMakeLists.txt 可以改成如下的形式:
1 | # CMake 最低版本号要求 |
唯一的改动只是在add_executable
命令中增加了一个MathFunctions.cc
源文件。这样写当然没什么问题,但是如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作。更省事的方法是使用aux_source_directory
命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。其语法如下:
1 | aux_source_directory(<dir> <variable>) |
因此,可以修改 CMakeLists.txt 如下:
1 | # CMake 最低版本号要求 |
这样,CMake 会将当前目录所有源文件的文件名赋值给变量DIR_SRCS
,再指示变量DIR_SRCS
中的源文件需要编译成一个名称为 Demo 的可执行文件。
多个目录,多个源文件
(源代码所在目录Demo3)
现在进一步将 MathFunctions.h 和http://MathFunctions.cc文件移动到 math 目录下。
1 | ./Demo3 |
对于这种情况,需要分别在项目根目录 Demo3 和 math 目录里各编写一个 CMakeLists.txt 文件。为了方便,我们可以先将 math 目录里的文件编译成静态库再由 main 函数调用。
根目录中的 CMakeLists.txt :
1 | # CMake 最低版本号要求 |
该文件添加了下面的内容: 第3行,使用命令 add_subdirectory
指明本项目包含一个子目录 math,这样 math 目录下的 CMakeLists.txt 文件和源代码也会被处理 。第6行,使用命令 target_link_libraries
指明可执行文件 main 需要连接一个名为 MathFunctions 的链接库 。
子目录中的 CMakeLists.txt:
1 | # 查找当前目录下的所有源文件 |
在该文件中使用命令add_library
将 src 目录中的源文件编译为静态链接库。
总结
理清概念:
CMake:CMake是一个跨平台的构建系统生成器,它并不直接构建项目,而是生成适合目标平台的构建文件(例如Makefile、Visual Studio项目文件等)。【然后再通过这个平台的构建项目的命令作用于构建文件生成项目】
以下是一些选择CMake和Makefile的考虑因素:
- 跨平台支持:如果您需要在不同的操作系统上构建项目,则CMake是更好的选择,因为它可以自动生成适合不同平台的Makefile。而Makefile只能在
GNU
环境下使用。 - 项目规模:对于小型项目,使用Makefile可能更加简单和方便。但是对于大型项目,使用CMake可以更好地组织和管理代码。
- 自动化构建:如果您需要自动化构建过程,则CMake是更好的选择,因为它可以自动生成Makefile并自动化构建过程。而Makefile需要手动编写和维护。
- 可维护性:CMake可以更好地组织代码和依赖关系,从而提高代码的可维护性。而Makefile可能更加难以维护,特别是对于大型项目。
总之,选择CMake还是Makefile取决于您的需求和偏好。如果您需要跨平台支持和自动化构建,则CMake是更好的选择。如果您只是需要管理小型项目,则使用Makefile可能更加简单和方便。
工具/文件 | 类别 | 主要用途 |
---|---|---|
Make | 构建工具 | 根据 Makefile 自动化编译和构建项目 |
CMake | 构建系统生成器 | 生成适合不同平台的构建系统文件(如 Makefile ) |
CMakeLists.txt | CMake 的配置文件 | 定义项目结构和构建规则,用于生成构建文件 |
- Make 是一个传统的构建工具,依赖
Makefile
。 - CMake 是一个更高级的工具,可以生成
Makefile
或其他平台的构建文件。 - CMakeLists.txt 是 CMake 的配置文件,描述了项目的编译规则。