CMake 学习笔记

目录

快速入门

CMake 工作原理

CMake 生成构建文件并调用构建工具的流程图

CMake、构建工具、编译器与最终产物之间的关系。

安装指定版本 CMake(源码编译)

Linux:

wget https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1.tar.gz
tar -xvf cmake-3.23.1.tar.gz
cd cmake-3.23.1
./configure
make -j16
sudo make install
  • 默认前缀 /usr/local:可执行文件(cmake/ctest/cpack/ccmake)装到 /usr/local/bin,模块/模板数据装到 /usr/local/share/cmake-3.23。
  • /usr/local/bin 通常已在 PATH 中,一般无需再 export;若不在,用 export PATH=/usr/local/bin:$PATH(不要把 share/cmake-3.23 加进 PATH,那里没有可执行文件)。

Windows: 官网 https://cmake.org/files/v3.23/ 下载,把 ...\cmake-3.23.1-windows-x86_64\bin 加入 PATH。

out-of-source 构建规范

cmake -S . -B build          # 配置:源码在 .,构建产物放 build/
cmake --build build -jN      # 构建(-jN 并行)
  • -S <dir>:源码目录(含顶层 CMakeLists.txt 的目录,不是 CMakeLists.txt 文件本身)。
  • -B <dir>:构建目录,不存在会自动创建,所有中间文件/生成的 makefile 都放这里。
  • 选项大小写敏感:是大写 -S/-B,-s/-b 无效;注意 -S . -B b 中间的空格不能少(-S .-B b 会把 .-B 当成 -S 的值)。
  • 好处:产物与源码隔离,清理只需删 build/。

Windows(需先打开 Developer Command Prompt):

cmake -S . -B build -G "NMake Makefiles"
cmake --build build

第一个 CMakeLists.txt

# 指定 cmake 最低版本
cmake_minimum_required(VERSION 3.20)

# 项目名称
project(first_cmake)

# 生成可执行程序
add_executable(first_cmake 101first_cmake.cpp)

配置 + 构建:

cmake -S . -B build          # 配置
cmake --build build         # 构建

变量与流程控制

变量使用

  • 变量引用是值替换,未设置的变量返回空字符串。
  • 变量引用可嵌套,从内向外求值。
  • 变量名大小写敏感。
  • message() 拼接多个字符串参数时无分隔符。
set(VAR1 "测试变量VAR1的值")
message("VAR1= " ${VAR1})
message("VAR1 in string ${VAR1}")
message("\${VAR1} = ${VAR1}")     # 想打印字面 ${} 用 \ 转义

set(VAR2 "VAR1")
message("VAR2= " ${${VAR2}})      # 嵌套访问,取到 VAR1 的值

unset(VAR1)                        # 取消变量
message("\${VAR1} = ${VAR1}")

# 注意:多参数无分隔符,下面输出为“参数1参数2参数3”,不是带空格的
message("参数1" "参数2" "参数3")

变量展开、嵌套引用和 unset 后的输出

变量引用与取消变量后的输出示例。

路径相关变量

变量含义
CMAKE_SOURCE_DIR整个构建的顶层源码目录
PROJECT_SOURCE_DIR当前 project() 的源码目录
CMAKE_CURRENT_SOURCE_DIR当前 CMakeLists.txt 所在目录
CMAKE_CURRENT_LIST_DIR当前被处理文件所在目录,包含被 include 的 .cmake 模块

作用域区别:

  • CMAKE_SOURCE_DIR 是第一次进入的 CMakeLists.txt 所在目录,不随 add_subdirectory() 或子目录的 project() 改变。
  • 顶层的 PROJECT_SOURCE_DIR 等于 CMAKE_SOURCE_DIR;子目录再次调用 project() 后,在其作用域内变为子项目根。
  • 写相对当前 CMakeLists.txt 的路径时,使用 CMAKE_CURRENT_SOURCE_DIR 最稳妥。

用法建议:

# 本模块内的路径用 CMAKE_CURRENT_LIST_DIR(推荐)
${CMAKE_CURRENT_LIST_DIR}/include
# 引用其他模块用 PROJECT_SOURCE_DIR / CMAKE_SOURCE_DIR
${PROJECT_SOURCE_DIR}/modules/parameter/include
${PROJECT_SOURCE_DIR}/3rdpart/tinyxml2

CMake 自带变量

  • 提供信息:PROJECT_NAME(对应 project() 的名字)。
  • 改变行为:BUILD_SHARED_LIBS(ON 动态库 / OFF 静态库,默认 OFF)。
  • 描述系统:MSVC、WIN32(=1 为 Windows)、UNIX(Unix 或类 Unix,包含 macOS 等,不等于仅 Linux)、CMAKE_SYSTEM_NAME。
  • 控制构建:CMAKE_COLOR_MAKEFILE、输出路径等。
set(BUILD_SHARED_LIBS ON)          # 后续 add_library 默认建动态库
add_definitions(-Dxlog_STATIC)     # 向 C++ 传宏;现代写法见“Target 与依赖管理”

message(" MSVC = " ${MSVC})
message(" WIN32 = " ${WIN32})
message(" UNIX = " ${UNIX})
message("CMAKE_SYSTEM_NAME = " ${CMAKE_SYSTEM_NAME})

# 把头文件也列进来:作用是让 IDE/工程生成器在项目树里显示(头文件本身不编译)
# 注意:并不是“列了头文件才会在头文件改动时重编”——重编靠编译器/CMake 的依赖扫描自动完成
add_library(${PROJECT_NAME} xlog.cpp xlog.h)

构建动态库 libxlog.so 的日志

动态库构建输出:libxlog.so。

构建静态库 libxlog.a 的日志

静态库构建输出:libxlog.a。

if 流程控制

if(<conditional>)
elseif(<conditional>)
else()
endif()

if(<constant>) 常量:

  • 真:ON、YES、TRUE、Y 或非零数(含浮点数)。
  • 假:0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串,或以 -NOTFOUND 结尾的字符串。

if(<variable>) 变量: 非假值常量为真;未定义变量为假;环境变量总为假。

if(1)
    message("1 is true")
endif()

if(0)
    message("0 is true")
else()
    message("0 is false")
endif()

if(VAR_NOT_DEF)                 # 未定义变量默认为假
    message("VAR_NOT_DEF is true")
else()
    message("VAR_NOT_DEF is false")
endif()

if("ON")
    message("String ON is true")
endif()

if 对常量、字符串和未定义变量的判断结果

常量、字符串与未定义变量的条件判断示例。

一元 / 二元 / 正则匹配

一元判断:DEFINED(变量是否定义)、COMMAND(是否为已定义的命令/宏/函数)、EXISTS(路径是否存在)。

二元判断:EQUAL(数字相等)、STREQUAL(字符串相等)。

set(VAR1 123)

if(VAR1 EQUAL 123)
    message("VAR1 EQUAL 123")
endif()

# 推荐用 STREQUAL
if(VAR1 STREQUAL "123")
    message("VAR1 STREQUAL 123")
endif()

EQUAL 与 STREQUAL 比较结果

数字相等与字符串相等的判断结果对比。

MATCHES 正则:if(<variable|string> MATCHES regex)

set(STR1 "abcd1234")

if(STR1 MATCHES "[a-z]+")
    message("MATCHES [a-z]+ true")
endif()

if(STR1 MATCHES "^[a-z]+$")
    message("full match")
else()
    message("no full match")
endif()

MATCHES 部分匹配与完整匹配的结果

正则匹配对比:包含匹配与使用 ^、$ 限定的完整匹配。

cache 缓存变量

语法:set(<var> <value>... CACHE <type> <docstring> [FORCE])

  • type:BOOL(ON/OFF 勾选)、FILEPATH(选文件)、PATH(选目录)、STRING、INTERNAL(内部,不显示在 GUI)。
  • docstring:说明文档。
set(VAR1 "VALUE1-1" CACHE STRING "cache doc")
set(VAR1 "VALUE1-2" CACHE STRING "cache doc")        # 缓存变量二次赋值不生效
message("VAR1 = ${VAR1}")
set(VAR1 "VALUE1-3" CACHE STRING "cache doc" FORCE)  # FORCE 强制修改

缓存变量修改前后的终端输出

缓存变量取值与强制修改后的输出示例。

不同类型的缓存变量与 option() 示例:

set(VAR_BOOL1 "ON"  CACHE BOOL "bool 001")
set(VAR_FILE "filepath" CACHE FILEPATH "var file path")
set(VAR_PATH "path"     CACHE PATH "var path")
set(VAR_INTERNAL "internal var" CACHE INTERNAL "internal var")
option(OPT1 "opt1 doc" OFF)
option(OPT2 "opt2 doc" ON)
  • Windows:cmake-gui。
  • Linux:先 cmake -S . -B build 生成 cache,再 ccmake build 交互修改。

Windows cmake-gui 中的源码目录、构建目录与缓存变量

Windows:在 cmake-gui 中查看和修改缓存变量。

Linux ccmake 中的缓存变量和操作提示

Linux:在 ccmake 中查看和修改缓存变量。

普通变量与缓存变量重名(CACHE 覆盖)

set(normalVar1 "normal var1")
set(normalVar1 "Cache var1" CACHE STRING "cache doc")
message("normalVar1 = ${normalVar1}")
message("CACHE{normalVar1} = $CACHE{normalVar1}")

同名普通变量与缓存变量分别取值的结果

同名普通变量与缓存变量保留不同取值的示例。

# CMP0126:NEW 保留同名普通变量;OLD 仅在新建缓存项、原缓存项无类型、FORCE/INTERNAL 等条件下移除普通变量
cmake_policy(SET CMP0126 OLD)

CMP0126 OLD 下同名变量的输出

设置 CMP0126 OLD 后的输出示例。

用 -D 传递缓存变量

cmake -S . -B b5 -D PARA1=para00001

第二次不带 -D 运行仍会保留 PARA1(已写入 cache),除非再次 -D PARA1=new 覆盖。

通过 -D 设置 PARA1 为 para00001 后的输出

通过 -D PARA1=para00001 设置缓存变量。

通过 -D 将 PARA1 更新为 new 后的输出

通过 -D PARA1=new 覆盖已有缓存值。

include 用法

include("cmake/test_cmake.cmake")
include("cmake/test_cmake1.cmake" OPTIONAL)                   # 找不到不报错
include("cmake/test_cmake1.cmake" OPTIONAL RESULT_VARIABLE ret)
message("ret = ${ret}")                                       # 未找到为 NOTFOUND
include("cmake/test_cmake.cmake" OPTIONAL RESULT_VARIABLE ret)
message("ret = ${ret}")                                       # 找到则为绝对路径

include 查找结果为 NOTFOUND 或模块绝对路径

RESULT_VARIABLE 返回未找到标记或模块绝对路径的示例。

括号注释

3.0 之后支持括号注释 #[[ ... ]],可跨行、可行内:

message("参数1" #[[中间注释]] "参数2")   # 输出:参数1参数2

属性

属性是作用域为特定对象(全局/目录/目标/源文件/测试/缓存)的变量。

  • set_property / get_property:设置 / 读取属性。
  • define_property:定义自定义属性。
set_property(GLOBAL PROPERTY TEST_GLOBAL "test global 001")
set_property(SOURCE myfile.cpp PROPERTY COMPILE_FLAGS "-Wall")
get_property(result TARGET my_target PROPERTY CXX_STANDARD)

CMake 属性作用域与 SOURCE 附加选项对照

属性作用域类型及 SOURCE 附加选项说明。

作用域类型:GLOBAL(全局)、DIRECTORY [<dir>](目录)、TARGET <target>(目标)、SOURCE <src>(源文件)、INSTALL <file>、TEST <test>、CACHE <entry>、VARIABLE。

构建配置与日志

编译类型与 C++ 标准

  • CMAKE_BUILD_TYPE:Debug / Release / RelWithDebInfo / MinSizeRel。单配置生成器(Unix Makefiles、Ninja)需显式设置才生效;嵌入式常用 MinSizeRel(→ -Os)。
  • C++ 标准优先用标准变量或 target 级命令,而非手写 -std=:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 或 target 级:target_compile_features(app PUBLIC cxx_std_11)

追加编译选项

注意:给 CMAKE_CXX_FLAGS 追加内容时必须加引号,否则会存成分号列表(如 "-O2;-g"),传给编译器时成为畸形单参数,原值非空时会编译失败。

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")

# 更稳妥的等价写法
string(APPEND CMAKE_CXX_FLAGS " -g")

# 正确处理列表语义
add_compile_options(-g)

其它示例中出现的变量:

cmake_minimum_required(VERSION 3.0)
project(chat)
# 最终可执行文件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 头文件搜索路径(目录作用域,可被后续 add_subdirectory 的子目录继承;不会递归搜索磁盘上的子目录)
include_directories(${PROJECT_SOURCE_DIR}/include)

收集源文件

aux_source_directory:收集目录下所有源文件,语法为 aux_source_directory(<dir> <var>)。

aux_source_directory("./src" LIB_SRCS)

file(GLOB ...):按通配收集。语法 file(<operation> <output_variable> <args...>),常用 operation:GLOB(收集)、GLOB_RECURSE(递归收集)、COPY、REMOVE、WRITE。

file(GLOB SRC2 ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB H_FILE "${INCLUDE_PATH}/xcpp/*.h")

常见错误:file(GLOB SRC2 FILE ...) 中的 FILE 不是合法关键字,会被当成匹配不到的 glob 模式静默吞掉,应删去。

file(GLOB) 合法选项只有 LIST_DIRECTORIES / RELATIVE / CONFIGURE_DEPENDS。

注意:GLOB 结果在配置阶段固定,增删文件不会自动重配(除非用 CONFIGURE_DEPENDS);单个已知源文件建议直接 set(SRC ...)。

输出路径设置

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/lib")   # .so
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/bin")   # 可执行/dll/pdb
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/lib")   # .a 静态库

生成 compile_commands.json

供 clangd / VS Code 等做代码索引:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

VS Code CMake Tools 也可通过 .vscode/settings.json 配置:

{
  "cmake.configureArgs": [
    "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
  ]
}

分步编译与清理指定 target

cmake --build . --target help          # 查看所有目标
cmake --build . --target 101first_cmake.i   # 预处理
cmake --build . --target 101first_cmake.s   # 编译到汇编
cmake --build . --target 101first_cmake.o   # 汇编到目标文件
cmake --build . --target clean              # 清理

cmake –build 的 help 目标列出可用构建目标

--target help 列出清理、预处理、汇编等可用目标。

调试:打印生成的具体指令

set(CMAKE_VERBOSE_MAKEFILE ON)     # 默认 OFF
  • 或 cmake --build . -v(第一次生成就要加 -v,否则日志不完整,可清理后重生成)。

message 用法

日志级别

级别说明输出流
FATAL_ERROR终止 CMake 过程stderr
SEND_ERROR进程继续但最终不生成(不会执行 add_executable/add_library),打印行号stderr
WARNING打印代码路径和行号stderr
无关键字(NOTICE)等同于不加级别 message("...")stderr
STATUS加前缀 --,用户可能感兴趣stdout
VERBOSE默认不显示的详细信息stdout
DEBUG加前缀 --stdout
TRACE加前缀 --stdout
message(FATAL_ERROR "TEST FATAL ERROR") # 终止;其后不执行
message(SEND_ERROR  "TEST SEND ERROR")  # 继续但不生成目标
message(WARNING "TEST WARNING")
message("TEST NOTICE")                  # 等同无级别
message(STATUS  "TEST STATUS")
message(VERBOSE "TEST VERBOSE")         # 需 --log-level=VERBOSE 才显示

message WARNING 输出代码位置和警告信息

WARNING 输出示例:包含文件位置与警告内容。

message NOTICE 与 STATUS 输出对比

NOTICE 与 STATUS 输出对比:STATUS 带有 -- 前缀。

设置显示级别与重定向:

cmake -S . -B build --log-level=VERBOSE
cmake -S . -B build --log-level=VERBOSE > log.txt        # 仅 stdout 重定向到文件
cmake -S . -B build --log-level=VERBOSE > log.txt 2>&1   # stdout 和 stderr 都写入 log.txt

常见错误:重定向应写成 > log.txt 2>&1,不要在 2>&1 前再加一个 >。

启用 VERBOSE 日志级别后的终端输出

使用 --log-level=VERBOSE 后显示详细日志。

查找库日志(CHECK_START / CHECK_PASS / CHECK_FAIL)

message(CHECK_START "查找 xcpp")
set(CMAKE_MESSAGE_INDENT "--")     # 嵌套查找的缩进
message(CHECK_START "查找 xlog")
message(CHECK_PASS  "成功")
message(CHECK_START "查找 xtheadpool")
message(CHECK_FAIL  "失败")
set(CMAKE_MESSAGE_INDENT "")       # 取消缩进
message(CHECK_FAIL "失败")

嵌套库查找的成功与失败日志

嵌套查找日志:展示缩进及成功、失败状态。

让 message 输出颜色

# \033[显示方式;前景色;背景色m ... \033[0m
#   显示方式:0-默认 1-高亮 2-低亮 3-下划线 4-闪烁 5-慢闪 6-快闪 7-反白 8-隐藏 9-划掉
string(ASCII 27 Esc)
set(Red  "${Esc}[0;31m")     # 红色
set(Blue "${Esc}[1;34m")     # 蓝色高亮
set(RedB "${Esc}[1;31;40m")  # 红字黑底
set(End  "${Esc}[0m")

message("${Red} 红色内容${End}")
message("${Blue} 蓝色内容${End}")
message("${RedB} 红色+背景内容${End}")

前景色与背景色对照:

颜色前景色背景色
黑色3040
红色3141
绿色3242
黄色3343
蓝色3444
紫红色3545
青蓝色3646
白色3747

常见错误:黄色的背景色代码为 43,不要误写成 34。

message 输出红色、蓝色与带背景色的文本

终端中的文字颜色与背景色效果。

Target 与依赖管理

现代 CMake 提倡以「目标(target)」为中心组织依赖,而非全局的 include_directories / add_definitions / link_libraries。

add_library(创建库目标)

add_library() 可以从源码创建库目标,也可以用 IMPORTED 引用已编译的外部库。下面先介绍从源码创建库的语法,实际编译在 cmake --build 时执行。

add_library(<name> [STATIC|SHARED|MODULE] <sources>...)
  • <name> 是项目内唯一的目标名,例如 xlog;链接该目标时也使用这个名字。
  • <sources> 是源文件列表,例如 src/xlog.cpp。Linux 默认会为库文件添加 lib 前缀及相应扩展名,目标名无需写成 libxlog.a。

静态库、动态库与插件库

类型用途Linux 默认产物(目标名为 xlog)
STATIC静态库,在链接时把需要的目标代码纳入使用它的程序或库libxlog.a
SHARED动态库,可被其他目标链接,并在运行时加载libxlog.so
MODULE插件库,通常通过 dlopen() 等方式加载,不能作为其他目标的链接依赖libxlog.so

分别创建静态库和动态库时,使用不同的目标名:

add_library(xlog_static STATIC src/xlog.cpp)  # libxlog_static.a
add_library(xlog_shared SHARED src/xlog.cpp)  # libxlog_shared.so

库类型及命名规则参见 add_library 官方文档。

省略类型与 BUILD_SHARED_LIBS

省略类型时,BUILD_SHARED_LIBS 为真则创建 SHARED,否则创建 STATIC。CMake 本身不预设这个变量;未定义时按静态库处理。

通常在顶层、创建库目标之前提供一个开关:

option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
add_library(xlog src/xlog.cpp)

配置时切换为动态库:

cmake -S . -B build-shared -DBUILD_SHARED_LIBS=ON
cmake --build build-shared

注意:这个开关只影响省略类型的 add_library() 调用;显式写了 STATIC 或 SHARED 的目标不受影响。参见 BUILD_SHARED_LIBS 官方文档。

示例:创建库并链接到程序

假设已有以下源文件,main.cpp 通过 xlog.h 调用 xlog.cpp 中的函数:

library_demo/
├── CMakeLists.txt
├── include/
│   └── xlog.h
├── src/
│   └── xlog.cpp
└── main.cpp

CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(library_demo LANGUAGES CXX)

add_library(xlog STATIC
    src/xlog.cpp
)

target_include_directories(xlog PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE xlog)

PUBLIC 使 xlog 和使用它的 app 都能找到 include/ 下的头文件;链接目标 xlog 也建立了构建依赖,无需手写 .a 文件路径。参见 CMake 目标与依赖说明及 target_include_directories 官方文档。

在项目根目录执行:

cmake -S . -B build
cmake --build build
./build/app

在 Linux 的 Unix Makefiles 生成器下,默认得到 build/libxlog.a 和 build/app。把示例中的 STATIC 改为 SHARED,即可构建动态库 build/libxlog.so。

如果在 Linux 中将静态库继续链接进动态库,通常需要在创建静态库后为它开启 PIC:

set_target_properties(xlog PROPERTIES POSITION_INDEPENDENT_CODE ON)

SHARED、MODULE 目标默认开启 PIC。参见 POSITION_INDEPENDENT_CODE 官方文档。

导入外部库:IMPORTED

已有第三方提供的 .a 或 .so 时,可以用 IMPORTED 将它注册为 CMake 目标,再通过目标名链接。CMake 不会为该库生成编译规则;库文件和头文件需要事先准备好。

add_library(<name> <type> IMPORTED [GLOBAL])
参数含义
<name>导入目标名,例如 xlog::xlog,不必与库文件名相同
<type>导入 .a 使用 STATIC,导入 Linux .so 使用 SHARED
GLOBAL可选;默认仅在创建目标的目录及其子目录可见,加上后整个项目可见

导入静态库

假设 main.cpp 通过 xlog.h 调用外部库中的函数,目录如下:

imported_library_demo/
├── CMakeLists.txt
├── main.cpp
└── third_party/
    └── xlog/
        ├── include/
        │   └── xlog.h
        └── lib/
            └── libxlog.a

CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(imported_library_demo LANGUAGES CXX)

set(XLOG_ROOT "${CMAKE_CURRENT_LIST_DIR}/third_party/xlog")

add_library(xlog::xlog STATIC IMPORTED)
set_target_properties(xlog::xlog PROPERTIES
    IMPORTED_LOCATION "${XLOG_ROOT}/lib/libxlog.a"
    INTERFACE_INCLUDE_DIRECTORIES "${XLOG_ROOT}/include"
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE xlog::xlog)
属性作用
IMPORTED_LOCATION库文件的完整路径,应指向 libxlog.a 文件,而非 lib/ 目录
INTERFACE_INCLUDE_DIRECTORIES使用该库的目标所需的头文件目录;app 链接 xlog::xlog 后会自动获得它

头文件目录也可以通过 target_include_directories(xlog::xlog INTERFACE "${XLOG_ROOT}/include") 设置,用来替代上面的 INTERFACE_INCLUDE_DIRECTORIES 属性。参见 target_include_directories 官方文档。

构建并运行:

cmake -S . -B build
cmake --build build
./build/app

导入动态库

准备好 third_party/xlog/lib/libxlog.so 后,用下面的声明和属性设置替换静态库示例中的对应部分;创建 app 和链接目标的写法保持一致:

add_library(xlog::xlog SHARED IMPORTED)
set_target_properties(xlog::xlog PROPERTIES
    IMPORTED_LOCATION "${XLOG_ROOT}/lib/libxlog.so"
    INTERFACE_INCLUDE_DIRECTORIES "${XLOG_ROOT}/include"
)

动态库在运行时需要由加载器找到。部署程序时,还要部署所需的 .so 及其依赖,并配置相应的运行时搜索路径。参见 CMake 动态库说明。

声明外部库的额外依赖

如果外部库还依赖其他库,应把这些依赖记录到导入目标的接口中。例如,xlog 需要线程库时,在创建 xlog::xlog 后添加:

find_package(Threads REQUIRED)
target_link_libraries(xlog::xlog INTERFACE Threads::Threads)

这样 app 仍只需链接 xlog::xlog,就会同时获得它声明的线程依赖。参见 INTERFACE_LINK_LIBRARIES 官方文档。

导入目标与 GLOBAL 的完整规则参见 CMake 导入库指南;库文件路径要求参见 IMPORTED_LOCATION 官方文档。

其他常用形式

形式声明示例用途
OBJECTadd_library(xlog_obj OBJECT src/xlog.cpp)只编译出目标文件,不打包成 .a 或链接成 .so,供其他目标复用
INTERFACEadd_library(xlog_headers INTERFACE)不编译、不生成库文件,用于传递头文件目录、宏等使用要求,常用于纯头文件库
IMPORTEDadd_library(vendor STATIC IMPORTED)声明已有的预编译库,不重新编译;还需设置库文件位置等属性
ALIASadd_library(demo::xlog ALIAS xlog)为已有的 xlog 目标创建别名;配置属性时使用原目标

这些形式使用各自的参数格式,参见 add_library 官方文档。IMPORTED 的完整示例见本节导入外部库:IMPORTED,库文件的搜索方式见链接预编译的第三方库。

target_include_directories

为特定目标设置头文件搜索目录,支持三种作用域:

target_include_directories(<target> [PRIVATE|PUBLIC|INTERFACE] <dirs>)
  • PRIVATE:仅当前目标可见,下游不继承。
  • PUBLIC:当前目标 + 依赖它的下游都可见。
  • INTERFACE:当前目标不用,只有下游用。

组织多库交叉编译工程的核心,PUBLIC/PRIVATE/INTERFACE 在这里真正发挥作用——它会自动传播被链接目标的 usage requirements(头文件目录、宏、编译/链接选项等):

target_link_libraries(app PRIVATE foo)   # foo 仅 app 自己用
target_link_libraries(app PUBLIC  foo)   # app 及其下游都继承 foo 的 usage requirements
target_link_libraries(app INTERFACE foo) # app 自己不用,只有下游用

同理,用 target_compile_options / target_compile_definitions / target_compile_features 取代全局的 add_definitions / CMAKE_CXX_FLAGS。

链接预编译的第三方库(.a / .so)

先创建可执行目标并设置头文件路径,再从下面两种方式中选择一种链接库。

公共配置:

cmake_minimum_required(VERSION 3.20)
project(test_xlog)
add_executable(test_xlog test_log.cpp)
include_directories("../xlog")            # 头文件路径

方式一:find_library 搜索后链接

find_library(FDK fdk-aac PATHS ${CMAKE_CURRENT_LIST_DIR}/lib)
target_link_libraries(test_xlog PRIVATE ${FDK})

方式二:使用导入目标

IMPORTED_LOCATION 指定库在磁盘上的位置;包含头文件配置和动态库导入的完整示例见导入外部库:IMPORTED。

add_library(fdkaac STATIC IMPORTED)
set_target_properties(fdkaac PROPERTIES
    IMPORTED_LOCATION ${CMAKE_CURRENT_LIST_DIR}/lib/libfdk-aac.a
)
target_link_libraries(test_xlog PRIVATE fdkaac)

编译时找不到 xlog.h 的错误信息

头文件路径未配置正确时的错误示例:找不到 xlog.h。

优先用 target_link_libraries 而非全局的 link_directories() / link_libraries()。

set_target_properties

设置目标(target)属性的命令,目标可以是可执行文件、库、自定义目标,也可以是 IMPORTED 目标。它的作用是修改一个已创建目标的各项属性,从而控制编译、链接、输出路径、使用要求等行为

set_target_properties(<目标1> <目标2> ...
    PROPERTIES
        <属性1> <值1>
        <属性2> <值2>
)
  • 必须在目标创建之后(add_executable/add_library 之后)才能设置。

常用属性:

IMPORTED_LOCATION(导入目标在磁盘的位置)、VERSION / SOVERSION(.so 版本与 soname)、OUTPUT_NAME 等。

find_package 与导入目标

find_package(Threads REQUIRED)
target_link_libraries(app PRIVATE Threads::Threads)   # 导入目标自带 usage requirements

生成器表达式基础

在生成阶段(而非配置阶段)求值,用于按配置/构建期或安装期切换:

target_compile_options(app PRIVATE $<$<CONFIG:Debug>:-O0 -g>)
target_include_directories(lib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>)

install

install(TARGETS app
    RUNTIME DESTINATION bin      # 可执行文件
    LIBRARY DESTINATION lib      # .so
    ARCHIVE DESTINATION lib)     # .a

配合 CMAKE_INSTALL_PREFIX(交叉编译常指向 sysroot / rootfs 打包目录)。

交叉编译与 Toolchain

CMake toolchain 文件

用 -DCMAKE_TOOLCHAIN_FILE 指定,是让 CMake「真正做交叉编译」的关键:

cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=arm.cmake
# arm.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER   arm-mol-linux-uclibcgnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-mol-linux-uclibcgnueabihf-g++)
set(CMAKE_SYSROOT      /path/to/arm-mol-linux-uclibcgnueabihf/sysroot)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
# 让 find_* 只在 sysroot 里找库/头,程序仍用主机的
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Makefile 交叉编译要点

  • 用 CROSS_COMPILE ?= arm-mol-linux-uclibcgnueabihf- + CC=$(CROSS_COMPILE)gcc,?= 便于命令行覆盖切换工具链。
  • 带 gnueabihf 后缀的工具链默认硬浮点 ABI,通常无需再显式加 -mfloat-abi=hard(与工具链默认不符反而会报错)。soft/hard-float ABI 不匹配会在链接或运行期报错。
  • 前缀式交叉 gcc 通常已内置 sysroot,无需显式 --sysroot;确需覆盖时再加 --sysroot=<path>。

pkg-config 交叉编译

设 PKG_CONFIG_SYSROOT_DIR + PKG_CONFIG_LIBDIR/PKG_CONFIG_PATH 指向 sysroot,否则 pkg-config 会吐出主机的 -I/-L。

产物校验

file a.out                       # 确认目标架构
arm-...-readelf -h a.out         # 架构、ABI
arm-...-readelf -d a.out         # NEEDED 动态库依赖、RPATH

工具链目录结构

MIPS 工具链(mips-gcc720-uclibc229)

mips-linux-uclibc/
├── include/c++/        # C++ 标准库头文件
├── libc/               # 默认 sysroot 根目录 ⭐
│   ├── usr/include/    # 系统头文件
│   ├── usr/lib/        # 系统库文件
│   ├── glibc/          # glibc 变体
│   ├── soft-float/     # 软浮点变体
│   └── mfp32/          # mfp32 浮点变体

默认 sysroot:mips-linux-uclibc/libc/

ARM 工具链(molchipv500-armgcc-uclibc)

arm-mol-linux-uclibcgnueabihf/
├── bin/
├── include/            # 工具链头文件
├── lib/                # 工具链库文件
├── debug-root/
└── sysroot/            # 默认 sysroot 根目录 ⭐
    ├── lib/            # 系统库文件
    ├── sbin/
    └── usr/
        ├── include/    # 系统头文件
        └── lib/        # 系统库文件

默认 sysroot:arm-mol-linux-uclibcgnueabihf/sysroot/

  • MIPS 工具链使用 multilib 架构,支持多种 C 库变体,sysroot 在 libc/ 子目录。
  • ARM 工具链使用标准 sysroot 架构,所有系统文件在 sysroot/ 子目录。

待确认:MIPS multilib 各浮点/ C 库变体(soft-float/、mfp32/、glibc/)对应的具体选择标志(如 -mfloat-abi / -march / -mfp32 组合)与该工具链的实际 multilib 映射有关,需按本人工具链 gcc -print-multi-lib 输出确认后再补。

参考:UNIX 变量;CMP0126。