器→工具, 工具软件

编译工具cmake入门教程

钱魏Way · · 15 次浏览

CMake简介

CMake 是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有Makefile(通过 make 命令进行项目的构建),大多是IDE软件都集成了make,比如:VS 的 nmake、linux 下的 GNU make、Qt 的 qmake等,如果自己动手写 makefile,会发现,makefile 通常依赖于当前的编译平台,而且编写 makefile 的工作量比较大,解决依赖关系时也容易出错。而 CMake 恰好能解决上述问题, 其允许开发者指定整个工程的编译流程,在根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需make编译即可,所以可以把CMake看成一款自动生成 Makefile的工具。

CMake的主要特点:

  • 跨平台支持: CMake 支持多种操作系统,包括 Windows、Linux、macOS,以及各种嵌入式系统。
  • 生成器支持: CMake 可以生成多种构建系统文件,如 Unix Makefiles、Ninja 文件、Visual Studio 工程文件等。
  • 模块化和可扩展性: 通过模块和脚本,可以扩展和自定义 CMake 的功能。
  • 易于集成: CMake 很容易与外部库和工具集成,如 BOOST、Qt、OpenCV 等。

CMake和make、Makefile的关系

CMake、make 和 Makefile 是构建系统中密切相关但各自有不同角色的组成部分。

以下是它们之间的关系和作用:

CMake

  • 角色: CMake 是一个跨平台的构建系统生成器,它的主要作用是生成适合于特定平台和编译器的构建文件。
  • 输入: CMake 使用txt 文件作为输入,这些文件包含了项目的构建配置、依赖关系、编译选项等。
  • 输出: CMake 根据配置生成平台和工具链特定的构建文件,例如 Makefile、Ninja 文件、Visual Studio 解决方案等。

Make 和 Makefile

  • 角色:make 是一个构建自动化工具,使用 Makefile 文件来指导构建过程。它广泛用于 Unix/Linux 系统。
  • 输入:make 读取 Makefile 文件,Makefile 文件定义了构建目标、依赖关系和构建规则。
  • 输出:make 根据 Makefile 中的规则执行相应的命令,最终生成可执行文件、库文件等目标文件。

CMake 和 Make 的关系

  • 生成 执行: CMake 的主要作用是生成构建系统文件,而make 的作用是读取这些文件并执行构建过程。当 CMake 生成 Makefile 时,make 执行这些 Makefile 来完成实际的编译和链接。
  • 抽象层次: CMake 提供了一个更高层次的抽象,隐藏了具体平台和工具链的细节,而 Makefile 是更低层次的具体构建规则。

通过 CMake 生成 Makefile,然后使用 make 执行构建,可以实现跨平台和高效的构建过程。这种组合利用了 CMake 的高层次抽象和 make 的实际执行能力,是许多项目中常见的构建方式。

CMake的使用

CMake的工作流程:

  • 编写txt 文件: 这是 CMake 的配置文件,用于描述项目的构建设置、依赖关系、编译选项等。
  • 配置项目: 使用cmake 命令或图形工具(如 cmake-gui)配置项目,这一步会生成构建系统特定的文件。
  • 生成项目: 在配置完成后,使用生成的构建系统文件(如 Makefile 或 Visual Studio 工程)来编译项目。

假设有一个项目目录结构如下:

/HelloWorld
  - CMakeLists.txt
  - main.cpp

CMakeLists.txt 内容如下:

cmake_minimum_required(VERSION 3.10)
project(HelloWorld)

add_executable(hello main.cpp)

main.cpp 内容如下:

#include <iostream>

int main() {
  std::cout << "Hello, World!" << std::endl;
  return 0;
}

在该目录下,运行以下命令配置和生成项目:

mkdir build
cd build
cmake ..
cmake --build .

生成的可执行文件 hello 将位于 build 目录下。

CMakeLists.txt的编写

CMakeLists.txt 是 CMake 的核心配置文件,用于描述一个项目的构建信息。它包含了项目的元数据、源文件、生成目标、依赖关系以及各种构建选项。以下是对 CMakeLists.txt 文件的详细介绍和一些常用命令的说明。

以下是一个简单的 CMakeLists.txt 示例文件,用于构建一个包含单个源文件的可执行程序:

cmake_minimum_required(VERSION 3.10)  # 设置最低要求的 CMake 版本
project(HelloWorld)  # 定义项目名称
add_executable(hello main.cpp)  # 定义可执行文件及其源文件

常用命令

  • cmake_minimum_required:这个命令用于设置要求的最低 CMake 版本,从而确保使用该txt 文件时的兼容性。cmake_minimum_required(VERSION 3.10)
  • project:定义项目的名称、版本信息等。这些信息可以用于后续配置中。project(HelloWorld VERSION 1.0 LANGUAGES CXX)
  • add_executable:定义一个可执行文件及其源文件。add_executable(hello main.cpp)
  • add_library:定义一个库文件及其源文件。add_library(MyLibrary STATIC mylibrary.cpp)
  • target_link_libraries:将指定的库链接到目标(可执行文件或者库)。target_link_libraries(hello MyLibrary)
  • include_directories:为项目添加额外的头文件搜索路径。include_directories(${PROJECT_SOURCE_DIR}/include)
  • find_package:查找外部包或库,并设置相应的变量以便后续使用。find_package(Boost 1.70 REQUIRED COMPONENTS filesystem)
  • set:设置变量的值,可以是简单变量或列表。set(CMAKE_CXX_STANDARD 11),set(SOURCES main.cpp helper.cpp)
  • add_subdirectory:添加子目录,这样可以在一个项目中包含多个txt 文件,适用于大型项目。add_subdirectory(src)
  • file: 文件操作命令,如 GLOB、GLOB_RECURSE 等。

查找和使用包

在CMake中,查找和使用外部包(libraries和modules)是一个常见且重要的任务。CMake 提供了多种方法来查找和使用这些包,包括 find_package() 和 find_library() 等指令。以下是一些常见的查找和使用包的方法及示例。

使用 find_package()

find_package() 是查找和使用外部包的主要指令。CMake 提供了许多内置的模块,可以查找常见的库,如 Boost、Qt 和 OpenCV。使用 find_package() 可以查找这些库,并自动设置相关的编译和链接标志。

基本用法:

find_package(<PackageName> [version] [REQUIRED] [components])
  • <PackageName>: 包的名称。
  • [version]: 可选,指定所需的版本。
  • [REQUIRED]: 可选,如果指定则表示查找失败将导致配置失败。
  • [components]: 可选,指定要查找的组件。

示例:

cmake_minimum_required(VERSION 3.10)
project(FindPackageExample)

# 查找 Boost 库
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)

# 包含 Boost 头文件目录
include_directories(${Boost_INCLUDE_DIRS})

# 添加源文件
add_executable(my_app main.cpp)

# 链接 Boost 库
target_link_libraries(my_app ${Boost_LIBRARIES})

# 查找 Qt 库
find_package(Qt5 COMPONENTS Widgets REQUIRED)

# 包含 Qt 头文件目录
include_directories(${Qt5Widgets_INCLUDE_DIRS})

# 添加源文件
add_executable(my_app main.cpp)

# 链接 Qt 库
target_link_libraries(my_app Qt5::Widgets)

使用 find_library()

find_library() 指令用于查找特定的库文件。例如,你可以使用它查找一个自定义的或不常见的库。

基本用法:

find_library(<VAR> <name> [path1 path2 ...] [NO_DEFAULT_PATH])
  • <VAR>: 存储查找到的库路径的变量名称。
  • <name>: 要查找的库的名称。
  • [path1 path2 …]: 可选,指定要查找的路径。
  • [NO_DEFAULT_PATH]: 可选,仅在指定路径中查找,不使用默认路径。

示例,查找自定义库:

假设你有一个自定义库 libmylib.so,位于 /usr/local/lib 目录下。

cmake_minimum_required(VERSION 3.10)
project(FindLibraryExample)

# 查找自定义库
find_library(MYLIB_LIBRARY NAMES mylib PATHS /usr/local/lib)

# 包含库头文件目录
include_directories(/usr/local/include)

# 添加源文件
add_executable(my_app main.cpp)

# 链接自定义库
target_link_libraries(my_app ${MYLIB_LIBRARY})

使用 pkg_check_modules()

对于一些使用 pkg-config 的库,可以使用 pkg_check_modules() 查找并使用库。这需要先包含 FindPkgConfig 模块。

基本用法:

find_package(PkgConfig REQUIRED)
pkg_check_modules(<prefix> [REQUIRED] <module1> [<module2> ...])

示例:查找使用 pkg-config 的库,假设你要查找并使用 glib-2.0 库。

cmake_minimum_required(VERSION 3.10)
project(PkgConfigExample)

# 包含 FindPkgConfig 模块
find_package(PkgConfig REQUIRED)

# 使用 pkg-config 查找 glib-2.0 库
pkg_check_modules(GLIB REQUIRED glib-2.0)

# 包含 GLib 头文件目录
include_directories(${GLIB_INCLUDE_DIRS})

# 添加源文件
add_executable(my_app main.cpp)

# 链接 GLib 库
target_link_libraries(my_app ${GLIB_LIBRARIES})

在 CMake 中,除了 find_package() 和 find_library() 之外,还有 find_path() 和 find_file() 指令,用于查找头文件路径和任意文件路径。这些指令非常有用,特别是在查找自定义或不常见的文件时。

find_path()

find_path() 指令用于查找包含特定文件(如头文件)的目录路径。

基本用法:

find_path(<VAR> name1 [path1 path2 ...] [PATHS path1 path2 ...] [HINTS path1 path2 ...] [DOC "documentation string"] [NO_DEFAULT_PATH])
  • <VAR>: 用于存储查找到的路径的变量名称。
  • name1: 要查找的文件名。
  • [path1 path2 …]: 可选,指定要查找的路径。
  • [PATHS path1 path2 …]: 可选,指定额外的查找路径。
  • [HINTS path1 path2 …]: 可选,给出一些提示路径,优先级高于PATHS。
  • [DOC “documentation string”]: 可选,提供关于变量的文档字符串。
  • [NO_DEFAULT_PATH]: 可选,仅在指定路径中查找,不使用默认路径。

示例:假设你有一个头文件 mylib.h,位于 /usr/local/include 目录下。

cmake_minimum_required(VERSION 3.10)
project(FindPathExample)

# 查找头文件路径
find_path(MYLIB_INCLUDE_DIR mylib.h PATHS /usr/local/include)

# 包含头文件目录
include_directories(${MYLIB_INCLUDE_DIR})

# 添加源文件
add_executable(my_app main.cpp)

find_file()

find_file() 指令用于查找特定文件的路径。

基本用法:

find_file(<VAR> name1 [path1 path2 ...] [PATHS path1 path2 ...] [HINTS path1 path2 ...] [DOC "documentation string"] [NO_DEFAULT_PATH])
  • <VAR>: 用于存储查找到的文件路径的变量名称。
  • name1: 要查找的文件名。
  • [path1 path2 …]: 可选,指定要查找的路径。
  • [PATHS path1 path2 …]: 可选,指定额外的查找路径。
  • [HINTS path1 path2 …]: 可选,给出一些提示路径,优先级高于PATHS。
  • [DOC “documentation string”]: 可选,提供关于变量的文档字符串。
  • [NO_DEFAULT_PATH]: 可选,仅在指定路径中查找,不使用默认路径。

示例:假设你有一个配置文件 config.json,位于 /etc/myapp 目录下。

cmake_minimum_required(VERSION 3.10)
project(FindFileExample)

# 查找配置文件路径
find_file(MY_CONFIG_FILE config.json PATHS /etc/myapp)

# 打印配置文件路径
message("Config file found at: ${MY_CONFIG_FILE}")

# 添加源文件
add_executable(my_app main.cpp)

依赖管理

在现代软件开发中,管理项目的依赖关系非常重要。CMake 提供了多种方法来管理依赖关系,包括但不限于外部库、内部库和第三方库。以下是一些常见的依赖管理策略和示例。

依赖管理策略:

  • 使用find_package() 查找和配置外部库: 适用于常见的库,如 Boost、Qt、OpenCV 等。
  • 使用ExternalProject 下载和构建外部项目: 适用于需要从外部来源(如 Git、URL)获取并构建的库。
  • 使用FetchContent 下载和包含外部内容: 更简单的方式来下载和包含外部依赖项。
  • 使用add_subdirectory() 添加子目录: 适用于项目中的内部库或直接包含在项目中的第三方库。

使用 ExternalProject

ExternalProject 模块提供了一种灵活的方法来下载、配置和构建外部项目。

示例:下载和构建 GoogleTest

cmake_minimum_required(VERSION 3.10)
project(ExternalProjectExample)

include(ExternalProject)

ExternalProject_Add(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        release-1.10.0
  CMAKE_ARGS     -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
  UPDATE_COMMAND ""
)

# 添加包含目录
include_directories(${CMAKE_BINARY_DIR}/include)

# 添加测试可执行文件
add_executable(test_app test_main.cpp)

# 链接 GoogleTest 库
add_dependencies(test_app googletest)
target_link_libraries(test_app ${CMAKE_BINARY_DIR}/lib/libgtest.a ${CMAKE_BINARY_DIR}/lib/libgtest_main.a)

使用 FetchContent

FetchContent 模块提供了一种更简单的方法来下载和包含外部内容。

示例:使用 GoogleTest

cmake_minimum_required(VERSION 3.10)
project(FetchContentExample)

include(FetchContent)

FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        release-1.10.0
)

FetchContent_MakeAvailable(googletest)

# 添加测试可执行文件
add_executable(test_app test_main.cpp)

# 链接 GoogleTest 库
target_link_libraries(test_app gtest gtest_main)

使用 add_subdirectory()

add_subdirectory() 是将子目录添加到构建中,并适用于直接包含在项目中的内部库或第三方库。

示例:添加内部库

目录结构

/MyProject
  - CMakeLists.txt
  - src
    - main.cpp
  - lib
    - MyLib
      - CMakeLists.txt
      - mylib.h
      - mylib.cpp

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 添加子目录
add_subdirectory(lib/MyLib)

# 包含头文件目录
include_directories(lib/MyLib)

# 添加源文件
add_executable(my_app src/main.cpp)

# 链接内部库
target_link_libraries(my_app MyLib)

子目录 CMakeLists.txt (lib/MyLib/CMakeLists.txt)

cmake_minimum_required(VERSION 3.10)
project(MyLib)

# 添加库
add_library(MyLib mylib.cpp)

# 包含头文件目录
target_include_directories(MyLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

target_include_directories

target_include_directories 用于为指定的目标添加头文件搜索路径。这对于确保编译器能够找到头文件至关重要。

基本用法:

target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...])
  • <target>: 目标名称。
  • [SYSTEM]: 可选,将目录标识为系统包含路径,可能会抑制某些编译器警告。
  • [AFTER|BEFORE]: 可选,指定包含路径的优先级。
  • <INTERFACE|PUBLIC|PRIVATE>:
    • INTERFACE: 仅在链接到该目标的目标中使用。
    • PUBLIC: 在当前目标和链接到该目标的目标中使用。
    • PRIVATE: 仅在当前目标中使用。
  • [items1…]: 要添加的包含路径。

示例:

add_executable(my_app main.cpp)
target_include_directories(my_app PUBLIC ${CMAKE_SOURCE_DIR}/include)

target_compile_definitions

target_compile_definitions 用于为指定的目标添加编译定义(宏)。这些定义通常用于条件编译代码。

基本用法:

target_compile_definitions(<target> [INTERFACE|PUBLIC|PRIVATE] [definitions1...])
  • <target>: 目标名称。
  • <INTERFACE|PUBLIC|PRIVATE>:
    • INTERFACE: 仅在链接到该目标的目标中使用。
    • PUBLIC: 在当前目标和链接到该目标的目标中使用。
    • PRIVATE: 仅在当前目标中使用。
  • [definitions1…]: 要添加的编译定义。

示例:

add_executable(my_app main.cpp)
target_compile_definitions(my_app PRIVATE MY_DEFINE=1)

target_compile_options

target_compile_options 用于为指定的目标添加编译选项。这些选项可以是编译器特定的标志,用于控制编译行为。

基本用法:

target_compile_options(<target> [INTERFACE|PUBLIC|PRIVATE] [options1...])
  • <target>: 目标名称。
  • <INTERFACE|PUBLIC|PRIVATE>:
    • INTERFACE: 仅在链接到该目标的目标中使用。
    • PUBLIC: 在当前目标和链接到该目标的目标中使用。
    • PRIVATE: 仅在当前目标中使用。
  • [options1…]: 要添加的编译选项。

示例:

add_executable(my_app main.cpp)
target_compile_options(my_app PRIVATE -Wall -Wextra)

条件控制和循环

CMake 提供了一系列用于条件控制和循环控制的指令,类似于编程语言中的条件语句和循环语句。这些指令使得 CMakeLists.txt 文件可以在不同的配置和生成环境中表现出灵活性和可定制性,从而实现更复杂的构建逻辑。如果你熟练掌握这些指令,可以大幅度提升 CMake 文件的可读性和可维护性。if/else/endif: 用于条件判断,执行不同的配置逻辑。

if/else/endif

if、else 和 endif 指令用于在 CMakeLists.txt 中进行条件判断,根据不同的条件执行不同的配置逻辑。

语法:

if(CONDITION)
  # Commands to execute if CONDITION is true
elseif(OTHER_CONDITION)
  # Commands to execute if OTHER_CONDITION is true
else()
  # Commands to execute if none of the above conditions are true
endif()

示例:

set(MY_VAR TRUE)

if(MY_VAR)
  message("MY_VAR is TRUE")
else()
  message("MY_VAR is FALSE")
endif()

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  message("Building in Debug mode")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
  message("Building in Release mode")
else()
  message("Unknown build type")
endif()

foreach

foreach 指令用于遍历列表中的每一个元素,并对每个元素执行一系列命令。

语法:

foreach(VAR IN LISTS LIST_VARIABLE)
  # Commands to execute for each item in LIST_VARIABLE
endforeach()

foreach(VAR RANGE START STOP [STEP])
  # Commands to execute for each number in the range from START to STOP
endforeach()

示例:

set(MY_LIST "item1" "item2" "item3")

foreach(ITEM IN LISTS MY_LIST)
  message("Item: ${ITEM}")
endforeach()

foreach(NUM RANGE 1 10 2)
  message("Number: ${NUM}")
endforeach()

while

while 指令用于在条件为真时重复执行一系列命令,直到条件变为假。

语法:

while(CONDITION)
  # Commands to execute while CONDITION is true
endwhile()

示例:

set(COUNTER 0)

while(COUNTER LESS 5)
  message("Counter: ${COUNTER}")
  math(EXPR COUNTER "${COUNTER} + 1")
endwhile()

break/continue

break 和 continue 指令用于控制循环的执行流程。

  • break: 立即终止循环,不再执行循环体中的命令。
  • continue: 跳过本次循环的剩余命令,直接开始下一个循环。

语法:

foreach(VAR IN LISTS LIST_VARIABLE)
  if(CONDITION)
    break()
  endif()
  # Commands to execute if CONDITION is false
endforeach()

while(CONDITION)
  if(OTHER_CONDITION)
    continue()
  endif()
  # Commands to execute if OTHER_CONDITION is false
endwhile()

示例:

set(MY_LIST "item1" "item2" "stop" "item3")

foreach(ITEM IN LISTS MY_LIST)
  if(ITEM STREQUAL "stop")
    break()
  endif()
  message("Item: ${ITEM}")
endforeach()

set(COUNTER 0)

while(COUNTER LESS 5)
  math(EXPR COUNTER "${COUNTER} + 1")
  if(COUNTER EQUAL 3)
    continue()
  endif()
  message("Counter: ${COUNTER}")
endwhile()

自定义命令和目标

add_custom_command

添加自定义命令,这些命令可以在构建过程中执行,如生成文件、拷贝文件等。

add_custom_command(
  OUTPUT ${CMAKE_BINARY_DIR}/generated.cpp
  COMMAND python3 ${CMAKE_SOURCE_DIR}/generate_code.py
  DEPENDS ${CMAKE_SOURCE_DIR}/generate_code.py
)

add_custom_target

添加自定义目标,这些目标可以执行自定义命令,且不一定与构建项目直接相关。

add_custom_target(
  RunAllTests
  COMMAND ${CMAKE_CTEST_COMMAND} --verbose
)

配置选项

在 CMake 中,set 和 option 是两个常用的指令,用于定义和配置变量。这两个指令用于不同的目的,掌握它们的用法可以帮助你更灵活地配置项目并控制构建流程。

set 指令

set 指令用于定义变量并赋值,这些变量可以是字符串、布尔值或列表。你可以使用 set 指令来定义在整个 CMakeLists.txt 文件中使用的变量。

# 字符串变量:
set(MY_VAR "Hello, World!")
message("MY_VAR: ${MY_VAR}")

# 布尔变量:
set(ENABLE_FEATURE ON)
if(ENABLE_FEATURE)
  message("Feature is enabled")
endif()

# 列表变量:
set(MY_LIST "item1" "item2" "item3")
foreach(ITEM IN LISTS MY_LIST)
  message("List Item: ${ITEM}")
endforeach()
# CACHE 变量:
# CACHE 选项用于将变量添加到 CMake 的缓存中,这样可以在后续的 CMake 配置过程中保留它们的值。
set(MY_CACHE_VAR "Default Value" CACHE STRING "Description of my cache variable")
message("MY_CACHE_VAR: ${MY_CACHE_VAR}")

option 指令

option 指令用于定义布尔选项,这些选项可以在 CMake 配置过程中通过命令行传递。这使得用户可以在配置时启用或禁用特定功能或特性。

基本用法:

option(ENABLE_FEATURE "Enable the special feature" ON)

if(ENABLE_FEATURE)
  message("Special feature is enabled")
else()
  message("Special feature is disabled")
endif()

使用命令行传递配置选项

可以在命令行通过 -D 选项传递 set 和 option 变量的值。

# 设置缓存变量和布尔选项
cmake -S . -B build -DMY_CACHE_VAR="New Value" -DBUILD_TESTS=OFF

常见 CMake 变量

CMake 提供了许多内置变量和用户定义变量,用于控制和定制构建过程。这些变量涵盖了编译器设置、构建类型、路径设置等许多方面。以下是一些常见的 CMake 变量及其用途。

编译器和工具链相关变量

  • CMAKE_CXX_COMPILER: 指定 C++ 编译器。set(CMAKE_CXX_COMPILER “/usr/bin/g++”)
  • CMAKE_C_COMPILER: 指定 C 编译器。set(CMAKE_C_COMPILER “/usr/bin/gcc”)
  • CMAKE_LINKER: 指定链接器。set(CMAKE_LINKER “/usr/bin/ld”)
  • CMAKE_AR: 指定静态库管理工具。set(CMAKE_AR “/usr/bin/ar”)
  • CMAKE_RANLIB: 指定库索引生成工具。set(CMAKE_RANLIB “/usr/bin/ranlib”)

构建类型相关变量

  • CMAKE_BUILD_TYPE: 指定构建类型(仅适用于单配置生成器,如 Makefile)。常见值:Debug、Release、RelWithDebInfo、MinSizeRel。set(CMAKE_BUILD_TYPE “Release”)
  • CMAKE_CONFIGURATION_TYPES: 指定多配置生成器(如 Visual Studio)支持的构建类型。set(CMAKE_CONFIGURATION_TYPES “Debug;Release;MinSizeRel;RelWithDebInfo”)

编译选项相关变量

  • CMAKE_CXX_FLAGS: 为所有 C++ 编译器设置附加编译标志。set(CMAKE_CXX_FLAGS “-Wall -Wextra”)
  • CMAKE_C_FLAGS: 为所有 C 编译器设置附加编译标志。set(CMAKE_C_FLAGS “-Wall -Wextra”)
  • CMAKE_EXE_LINKER_FLAGS: 设置链接可执行文件时的附加标志。set(CMAKE_EXE_LINKER_FLAGS “-s”)

路径相关变量

  • CMAKE_SOURCE_DIR: 项目根目录下的源代码路径。message(“Source Directory: ${CMAKE_SOURCE_DIR}”)
  • CMAKE_BINARY_DIR: 构建目录的根路径。message(“Binary Directory: ${CMAKE_BINARY_DIR}”)
  • CMAKE_INSTALL_PREFIX: 指定 make install 安装的前缀路径。set(CMAKE_INSTALL_PREFIX “/usr/local”)

项目和目标相关变量

  • PROJECT_NAME: 当前项目的名称。message(“Project Name: ${PROJECT_NAME}”)
  • PROJECT_SOURCE_DIR: 当前项目的源代码路径。message(“Project Source Directory: ${PROJECT_SOURCE_DIR}”)
  • CMAKE_PROJECT_NAME: 根项目的名称(当有多个项目时)。message(“CMake Project Name: ${CMAKE_PROJECT_NAME}”)

系统相关变量

  • CMAKE_SYSTEM_NAME: 操作系统的名称。message(“System Name: ${CMAKE_SYSTEM_NAME}”)
  • CMAKE_SYSTEM_VERSION: 操作系统的版本。message(“System Version: ${CMAKE_SYSTEM_VERSION}”)
  • CMAKE_HOST_SYSTEM_NAME: 生成系统的操作系统名称。message(“Host System Name: ${CMAKE_HOST_SYSTEM_NAME}”)
  • CMAKE_HOST_SYSTEM_VERSION: 生成系统的操作系统版本。message(“Host System Version: ${CMAKE_HOST_SYSTEM_VERSION}”)

CTest 相关变量

  • BUILD_TESTING: 指示是否构建测试。option(BUILD_TESTING “Build the testing tree” ON)
  • CMAKE_CTEST_COMMAND: CTest 的路径。message(“CTest Command: ${CMAKE_CTEST_COMMAND}”)

模块和宏

在 CMake 中,模块和宏是两种非常有用的工具,它们可以帮助你组织和简化构建脚本。了解如何编写和使用它们,可以极大地提高你的 CMakeLists.txt 文件的可读性和可维护性。

CMake 模块

CMake 模块是一种用于封装查找和配置外部包或执行特定任务的脚本。CMake 自带许多常用的模块,如 FindBoost.cmake、FindOpenCV.cmake 等。你也可以编写自定义模块来满足特定需求。

假设你想编写一个自定义模块 FindMyLib.cmake,用于查找和配置一个名为 MyLib 的库。

目录结构

/MyProject
  - CMakeLists.txt
  - cmake
    - FindMyLib.cmake
  - src
    - main.cpp
  - include
    - mylib.h
  - lib
    - libmylib.so

FindMyLib.cmake

# 查找头文件
find_path(MYLIB_INCLUDE_DIR mylib.h PATHS /usr/local/include /usr/include)

# 查找库文件
find_library(MYLIB_LIBRARY NAMES mylib PATHS /usr/local/lib /usr/lib)

# 如果找到头文件和库文件,设置变量 MYLIB_FOUND
if(MYLIB_INCLUDE_DIR AND MYLIB_LIBRARY)
    set(MYLIB_FOUND TRUE)
    set(MYLIB_INCLUDE_DIRS ${MYLIB_INCLUDE_DIR})
    set(MYLIB_LIBRARIES ${MYLIB_LIBRARY})
else()
    set(MYLIB_FOUND FALSE)
endif()

# 提供给 find_package() 使用的宏
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib DEFAULT_MSG MYLIB_INCLUDE_DIR MYLIB_LIBRARY)

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 设置模块路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

# 查找 MyLib
find_package(MyLib REQUIRED)

# 包含头文件目录
include_directories(${MYLIB_INCLUDE_DIRS})

# 添加可执行文件
add_executable(my_app src/main.cpp)

# 链接 MyLib 库
target_link_libraries(my_app ${MYLIB_LIBRARIES})

CMake 宏

CMake 宏是一种用于封装常用命令和逻辑的脚本,类似于函数,但没有返回值。宏可以帮助你简化重复的任务,并使 CMakeLists.txt 文件更具可读性。

假设你想编写一个宏,用于简化添加可执行文件并链接公共库的过程。

目录结构

/MyProject
  - CMakeLists.txt
  - cmake
    - MyMacros.cmake
  - src
    - main.cpp
    - other.cpp

MyMacros.cmake

macro(add_executable_with_libs exe_name src_files)
    # 添加可执行文件
    add_executable(${exe_name} ${src_files})

    # 链接公共库(假设公共库为 pthread 和 m)
    target_link_libraries(${exe_name} pthread m)
endmacro()

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 包含自定义宏文件
include(${CMAKE_SOURCE_DIR}/cmake/MyMacros.cmake)

# 使用宏添加可执行文件
add_executable_with_libs(my_app "src/main.cpp;src/other.cpp")

宏与函数的区别

CMake 中也有函数,与宏类似,但有一些关键区别:

  • 作用域: 宏在调用它的作用域内执行,而函数有自己的作用域。
  • 参数处理: 宏的参数直接替换文本,而函数的参数在局部变量中处理。

自定义函数

function(add_executable_with_libs exe_name src_files)
    # 添加可执行文件
    add_executable(${exe_name} ${src_files})

    # 链接公共库(假设公共库为 pthread 和 m)
    target_link_libraries(${exe_name} pthread m)
endfunction()

你可以像使用宏一样使用这个函数,但请注意它们的作用域和参数处理方式不同。

小结

  • 模块: 用于封装查找和配置外部包或执行复杂任务。将模块保存在一个专用的目录中,如cmake/,并通过 list(APPEND CMAKE_MODULE_PATH …) 将该目录添加到模块搜索路径。
  • 宏与函数: 用于简化和重用构建逻辑。将宏和函数定义保存在一个专用的文件中,如cmake,并通过 include() 指令包含该文件。

测试和打包

在 CMake 中,测试和打包是两个非常重要的功能。测试可以确保你的代码在各种情况下都能正确运行,而打包可以方便地分发你的应用程序或库。CMake 提供了一套强大的工具来支持这两个功能。

测试

CMake 提供了 CTest 模块,用于集成测试。CTest 可以与各种测试框架(如 GoogleTest 和 Catch2)配合使用。

基本用法:

  • 启用测试: 使用enable_testing() 指令启用测试。
  • 添加测试: 使用add_test() 指令添加测试。

示例:使用 CTest 和 GoogleTest

目录结构

/MyProject
  - CMakeLists.txt
  - src
    - main.cpp
    - mylib.cpp
    - mylib.h
  - tests
    - CMakeLists.txt
    - test_mylib.cpp

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 启用测试
enable_testing()

# 添加源文件
add_library(MyLib src/mylib.cpp)

# 添加可执行文件
add_executable(my_app src/main.cpp)
target_link_libraries(my_app MyLib)

# 添加子目录
add_subdirectory(tests)
tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

# 查找 GoogleTest
find_package(GTest REQUIRED)
include_directories(${GTest_INCLUDE_DIRS})

# 添加测试可执行文件
add_executable(runUnitTests test_mylib.cpp)
target_link_libraries(runUnitTests ${GTest_LIBRARIES} MyLib)

# 添加测试
add_test(NAME MyLibTest COMMAND runUnitTests)

tests/test_mylib.cpp

#include <gtest/gtest.h>
#include "mylib.h"

TEST(MyLibTest, SampleTest) {
    EXPECT_EQ(1, 1);
}

src/main.cpp
#include <iostream>
#include "mylib.h"

int main() {
    std::cout << "Hello, World!" << std::endl;
    mylib_function();
    return 0;
}

src/mylib.cpp

#include "mylib.h"
#include <iostream>

void mylib_function() {
    std::cout << "mylib_function called" << std::endl;
}

src/mylib.h
#ifndef MYLIB_H
#define MYLIB_H

void mylib_function();

#endif // MYLIB_H

打包

CMake 提供了 CPack 模块,用于生成各种格式的安装包(如 tar.gz、zip、DEB、RPM 等)。

基本用法:

  • 设置打包变量: 设置项目名称、版本号、维护者等信息。
  • 启用打包: 使用include(CPack) 包含 CPack 模块。

示例

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)

# 启用测试
enable_testing()

# 添加源文件
add_library(MyLib src/mylib.cpp)

# 添加可执行文件
add_executable(my_app src/main.cpp)
target_link_libraries(my_app MyLib)

# 添加子目录
add_subdirectory(tests)

# 设置打包变量
set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_CONTACT "maintainer@example.com")
set(CPACK_PACKAGE_DESCRIPTION "MyProject is a sample project")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_FILE_NAME "MyProject-1.0.0-Linux")

# 启用打包
include(CPack)

在构建目录中运行以下命令生成安装包:

cmake --build . --target package

这将生成一个包含所有目标文件的安装包。

总结

  • CTest: 用于集成测试,支持与各种测试框架配合使用。
    • 通过enable_testing() 启用测试。
    • 使用add_test() 添加测试。
  • CPack: 用于生成各种格式的安装包。
    • 设置打包变量,如项目名称、版本号、维护者等信息。
    • 通过include(CPack) 启用打包。

通过合理使用 CTest 和 CPack,可以确保你的项目在开发、测试和发布阶段都能顺利进行。测试可以保证代码的质量,而打包可以方便地分发应用程序或库。

CMake的配置和生成

CMake 通过配置(Configuration)和生成(Generation)两个主要阶段来创建项目构建系统。这两个阶段分别处理不同的任务,共同确保项目能够正确编译和链接。以下是对这两个阶段的详细介绍。

配置(Configuration)

配置阶段的主要目标是读取和解析 CMakeLists.txt 文件,设置项目的构建选项和依赖关系,并生成一个内部的构建描述。

步骤:

  • 读取txt 文件:CMake 会从项目的根目录开始读取 CMakeLists.txt 文件,并递归读取所有子目录中的 CMakeLists.txt 文件。
  • 解析配置指令:解析 cmake_minimum_required、project 等基本指令。设置编译选项、添加源文件、定义库和可执行目标等。
  • 查找依赖库和包:使用 find_package、find_library 等指令找到外部库和包,并设置相应的变量。
  • 处理条件和循环:根据条件语句(如 if、elseif、else)和循环语句(如 foreach、while)执行相应的配置逻辑。
  • 生成中间构建文件:在配置阶段结束时,CMake 会生成一个txt 文件,其中包含了所有配置选项和变量的值。

示例:

# 在项目根目录下运行 CMake 配置
cmake -S . -B build
  • -S . 指定源目录(当前目录)。
  • -B build 指定构建目录(build 目录)。

生成(Generation)

生成阶段的主要目标是根据配置阶段生成的中间构建描述,生成实际的构建系统文件(例如 Makefile、Ninja 文件、Visual Studio 工程文件等)。

步骤:

  • 选择生成器:根据用户指定或系统默认的生成器,选择适当的生成器(如 Makefile、Ninja、Visual Studio 等)。
  • 生成平台特定的构建文件:使用配置阶段的信息,生成适用于特定平台和编译器的构建文件。例如:如果选择的是 Unix Makefiles 生成器,CMake 会生成 Makefile 文件;如果选择的是 Visual Studio 生成器,CMake 会生成对应的 .sln 和 .vcxproj 文件。
  • 输出构建文件:生成的构建文件会放置在指定的构建目录中(如 build 目录)。

示例

# 在指定的构建目录下运行生成命令
cmake --build build

这将根据生成的构建文件(例如 Makefile)实际编译和链接项目。

CMake生成器

CMake 生成器(Generator)是 CMake 用来生成特定平台和构建工具的构建文件的组件。不同的生成器可以生成适用于不同环境的构建系统文件,例如 Makefile、Ninja 文件、Visual Studio 解决方案等。选择合适的生成器可以显著简化项目的构建和开发过程。

主要生成器类型:

  • UNIX Makefiles
    • 描述: 生成标准的 Makefile 文件,适用于大多数 Unix/Linux 系统和基于 Make 的构建流程。
    • 使用场景: 适用于大多数 Unix/Linux 系统,特别是那些已经有 Make 工具的环境。
    • 指定方式:cmake -G “Unix Makefiles” ..
  • Ninja
    • 描述: 生成 Ninja 构建文件。Ninja 是一个小而快速的构建系统,可以显著加速大型项目的构建过程。
    • 使用场景: 适用于需要高速构建的环境,尤其是大型项目。
    • 指定方式:cmake -G “Ninja” ..
  • Visual Studio
    • 描述: 生成 Visual Studio 解决方案和项目文件,适用于 Windows 平台的开发环境。
    • 使用场景: 适用于 Windows 平台的开发,特别是那些使用 Visual Studio 作为主要开发工具的团队。
    • 指定方式:cmake -G “Visual Studio 16 2019” ..
  • Xcode
    • 描述: 生成 Xcode 项目文件,适用于 macOS 平台的开发环境。
    • 使用场景: 适用于 macOS 平台的开发,特别是那些使用 Xcode 作为主要开发工具的团队。
    • 指定方式:cmake -G “Xcode” ..

生成器的选择通常取决于开发环境、构建工具和项目需求。以下是一些常见的选择策略:

  • 平台特定:
    • Windows: Visual Studio 生成器。
    • Linux/Unix: Unix Makefiles 或 Ninja。
    • macOS: Xcode 或 Unix Makefiles。
  • 构建速度:
    • Ninja 通常比 Makefile 更快,适用于大型项目。
  • 集成开发环境(IDE):
    • 使用 Visual Studio 或 Xcode 作为主要开发工具时,选择相应的生成器更为合适。

CMakeCache.txt

在使用 CMake 构建系统时,理解 CMakeCache.txt 文件和如何使用命令行参数来配置 CMake 是非常重要的。这些工具和技术可以帮助你更好地管理和控制构建过程。

CMakeCache.txt 文件是 CMake 用来存储项目配置选项和变量的缓存文件。它通常位于构建目录中,并包含所有通过命令行参数或者 CMakeLists.txt 文件设置的变量。这个文件的作用是使得 CMake 在重新运行时保持配置的一致性,而不需要重复输入相同的参数。

查看 CMakeCache.txt

你可以直接打开 CMakeCache.txt 文件查看其中的内容。每个变量都有一个注释说明,并以 key:type=value 的格式存储。例如:

//The C compiler identification is GNU 9.3.0
CMAKE_C_COMPILER:FILEPATH=/usr/bin/gcc

//The CXX compiler identification is GNU 9.3.0
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/g++

修改 CMakeCache.txt

虽然你可以手动编辑 CMakeCache.txt,但更推荐使用 CMake 提供的命令行工具或者 GUI 工具来配置和修改缓存变量。

使用命令行参数设置变量

在运行 CMake 时,你可以使用 -D 选项来设置变量。这是一个方便的方式来配置构建选项,而不需要修改 CMakeLists.txt 文件。

基本语法:

cmake -D<variable>=<value> <path-to-source>
  • <variable>: 变量名。
  • <value>: 变量值。
  • <path-to-source>: 源代码目录的路径。

示例,假设你要设置安装路径和构建类型:

cmake -DCMAKE_INSTALL_PREFIX=/opt/myproject -DCMAKE_BUILD_TYPE=Release ..

这将设置安装路径为 /opt/myproject,并将构建类型设置为 Release。

以下是一些常用的 CMake 变量及其说明:

  • CMAKE_BUILD_TYPE: 指定构建类型(Debug、Release、RelWithDebInfo、MinSizeRel)。
  • CMAKE_INSTALL_PREFIX: 指定安装路径。
  • CMAKE_C_COMPILER: 指定 C 编译器。
  • CMAKE_CXX_COMPILER: 指定 C++ 编译器。
  • CMAKE_MODULE_PATH: 指定自定义模块搜索路径。
  • BUILD_SHARED_LIBS: 设置为ON 或 OFF,指定是否构建共享库。

使用 ccmake 或 cmake-gui

如果你更喜欢图形化的方式配置 CMake,可以使用 ccmake(基于终端的交互界面)或者 cmake-gui(图形用户界面)。

  • 在 ccmake 界面中,你可以浏览和修改所有的缓存变量。
  • cmake-gui 提供了一个图形界面,可以更方便地配置和修改缓存变量。

如果你需要重新配置项目,可以使用 CMake 的 -U 选项清除某些变量,或者直接删除 CMakeCache.txt 文件。

# 清除特定变量
cmake -U<variable> ..

# 删除缓存文件
rm CMakeCache.txt

# 然后重新运行 CMake:
cmake ..

CMake构建和安装

在 CMake 中,构建和安装项目通常涉及以下几个步骤。首先,你需要配置项目,然后生成构建文件,最后编译和安装项目。以下是详细的过程:

生成构建文件

配置完成后,CMake 会生成适用于所选生成器的构建文件。这些文件可以是 Makefile(对于 Unix Makefiles 生成器)或其他特定于生成器的文件。

# 生成构建文件
cmake --build .

编译项目

使用生成的构建文件编译项目。对于 Makefile,使用 make 命令;对于 Ninja,使用 ninja 命令。

# 使用 Makefile 编译
make

# 使用 Ninja 编译
ninja

安装项目

编译完成后,可以使用 make install 或 cmake –install 命令将项目安装到指定的路径。安装路径通常在配置阶段通过 CMAKE_INSTALL_PREFIX 设置。

# 使用 Makefile 安装
make install

# 使用 Ninja 安装
ninja install

# 或者使用 CMake 的 --install 选项
cmake --install .

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注