03-CMake入门与进阶.md 49 KB


title: CMake入门与进阶 tags: [

嵌入式Linux,
Linux应用编程,
CMake,
CMakeLists,
Makefile,
构建系统,
add_executable,
add_library,
target_link_libraries,
target_include_directories,
静态库,
动态库,
交叉编译,
工具链文件,
arm-linux-setup.cmake,
install,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMake入门与进阶"

CMake入门与进阶

💡 关联知识:[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]、[[04-网络编程与项目实战/01-网络基础与socket编程]];延伸阅读:[[Linux+C+C++技术体系梳理/3. 编译调试与驱动预留/2. Makefile]]

直接敲 gcc 编译一两个文件还行,工程一大就会遇到"源文件太多、依赖太乱、换个平台就得重写 Makefile"的问题。CMake 用一个与平台无关的 CMakeLists.txt 描述整个工程的编译流程,再由它根据当前平台生成对应的 Makefile,最后仍由 make 完成编译。本篇从零开始,用五个递进示例讲清 CMake 的用法,再系统整理命令、变量、作用域与交叉编译配置,最后给出一个可直接复制的多目录工程。

约定:本篇命令基于 Ubuntu 主机;示例中的交叉编译工具链路径(/opt/fsl-imx-x11/...)来自正点原子 I.MX6U 出厂开发环境,读者需按自己的实际安装路径修改。


1. 为什么需要 CMake

1.1 构建系统的演进

阶段 工具 说明
手动编译 gcc main.c hello.c -o hello 文件一多就要手敲一长串,改一个文件全量重编
脚本化 Makefile + make 描述依赖关系与规则,支持增量编译;但语法复杂、各平台不通用
跨平台构建 CMake 写与平台无关的 CMakeLists.txt,自动生成本地化 Makefile / 工程文件

CMake 的主要优点(教材总结):

  • 开放源代码:官网 https://cmake.org/ 可下载源码;
  • 跨平台:CMake 不直接编译出可执行文件/库,而是解析 CMakeLists.txt,按当前平台生成 Makefile 和工程文件,最终还是调用 make 编译,但 CMake 本身跨平台;
  • 语法规则简单CMakeLists.txt 语法与平台无关,比 Makefile 简单易懂,由 CMake 自动生成 Makefile,无需手写。

1.2 CMake 与 Makefile 对比

维度 Makefile CMake
编写者 开发者手工编写 开发者写 CMakeLists.txt,Makefile 由 CMake 生成
语法 复杂、易错,制表符敏感 命令 + 变量,类脚本,简单
跨平台 各平台规则往往不同 CMakeLists.txt 与平台无关
依赖扫描 需手工维护或配合工具 自动处理头文件依赖
产物 直接给 make 中间层,再生成 Makefile
适合规模 小工程、学习原理 中大型工程、跨平台工程
flowchart LR
    A["CMakeLists.txt<br/>(平台无关)"] --> B["cmake 工具"]
    B -->|Linux| C["Makefile"]
    B -->|其它平台| D["本地化工程文件"]
    C --> E["make"]
    E --> F["可执行文件 / 库文件"]

    classDef cfg fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
    classDef tool fill:#fef9c3,stroke:#ca8a04,color:#713f12
    classDef out fill:#dcfce7,stroke:#16a34a,color:#14532d
    class A cfg
    class B,E tool
    class F out

1.3 安装与查看版本

sudo apt-get install cmake    # Ubuntu 在线安装
cmake --version               # 查看版本号

实测提示:Ubuntu 自带的 CMake 可能是 3.5.1 这类老版本,在配置交叉编译时会报错。建议改用 CMake 3.16.0(GitHub Releases 下载 cmake-3.16.0-Linux-x86_64.tar.gz,解压即用,无需自己编译),后文示例均以该版本为准。

官方文档:https://cmake.org/documentation/(总链接)、https://cmake.org/cmake/help/latest/guide/tutorial/index.html(培训教程)。


2. 从零开始:五个递进示例

2.1 示例一:单个源文件

// main.c
#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}
# CMakeLists.txt
project(HELLO)
add_executable(hello ./main.c)
  • project(HELLO):设置工程名称(非强制,但推荐)。
  • add_executable(hello ./main.c):生成名为 hello 的可执行文件,源文件为 ./main.c

在工程目录执行:

cmake ./      # 解析当前目录的 CMakeLists.txt
make          # 编译,得到可执行文件 hello
file hello    # 查看架构,x86-64 说明只能在 Ubuntu 上跑
./hello

执行 cmake 后会额外生成 CMakeCache.txtCMakeFiles/cmake_install.cmakeMakefile 等文件。

2.2 out-of-source 构建

上面的做法把中间文件和源码混在一起,清理麻烦。正确做法是源码与构建分离(out-of-source):

├── build/            # 构建目录
├── CMakeLists.txt
└── main.c
cd build/
cmake ../
make

所有中间文件和可执行文件都落在 build/,清理时直接删掉 build/ 即可。

2.3 示例二:多个源文件

// hello.h
#ifndef __TEST_HELLO_
#define __TEST_HELLO_

void hello(const char *name);

#endif
// hello.c
#include <stdio.h>
#include "hello.h"

void hello(const char *name)
{
    printf("Hello %s!\n", name);
}
// main.c
#include "hello.h"

int main(void)
{
    hello("World");
    return 0;
}
project(HELLO)
set(SRC_LIST main.c hello.c)
add_executable(hello ${SRC_LIST})

set(SRC_LIST main.c hello.c) 定义变量 SRC_LIST,用 ${SRC_LIST} 引用;也可直接写成 add_executable(hello main.c hello.c)

2.4 示例三:生成库文件

project(HELLO)
add_library(libhello hello.c)
add_executable(hello main.c)
target_link_libraries(hello libhello)

编译后在 build/ 下同时得到可执行文件 hello 和静态库 liblibhello.a

  • add_library(libhello hello.c):生成库文件。第一个参数是不含前后缀的库名,Linux 下静态库自动加 lib + .a,动态库自动加 lib + .so
  • 指定库类型:

    add_library(libhello SHARED hello.c)   # 生成动态库 liblibhello.so
    add_library(libhello STATIC hello.c)   # 生成静态库 liblibhello.a
    
  • 想得到 libhello.a 而不是 liblibhello.a,用 set_target_properties 改输出名:

    set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
    

目标名唯一add_executableadd_library 定义的目标名在整个工程内必须唯一,所以不能直接用 add_library(hello hello.c) 改名,只能用 OUTPUT_NAME 属性。

2.5 示例四:源文件分目录 + add_subdirectory

├── build/
├── CMakeLists.txt
├── libhello/
│   ├── CMakeLists.txt
│   ├── hello.c
│   └── hello.h
└── src/
    ├── CMakeLists.txt
    └── main.c
# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(HELLO)
add_subdirectory(libhello)
add_subdirectory(src)
# src/CMakeLists.txt
include_directories(${PROJECT_SOURCE_DIR}/libhello)
add_executable(hello main.c)
target_link_libraries(hello libhello)
# libhello/CMakeLists.txt
add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")

add_subdirectory(dir) 告诉 CMake 去子目录寻找并解析新的 CMakeLists.txtPROJECT_SOURCE_DIR 指工程顶层源码目录。

2.6 示例五:把产物放到单独的目录

# src/CMakeLists.txt
include_directories(${PROJECT_SOURCE_DIR}/libhello)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)   # 可执行文件输出路径
add_executable(hello main.c)
target_link_libraries(hello libhello)
# libhello/CMakeLists.txt
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)      # 库文件输出路径
add_library(libhello hello.c)
set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
变量 作用
EXECUTABLE_OUTPUT_PATH 控制可执行文件输出目录
LIBRARY_OUTPUT_PATH 控制库文件输出目录

最终目录结构:

build/
├── bin/hello
└── lib/libhello.a

3. CMakeLists.txt 语法规则

3.1 注释、命令、变量

# 这是单行注释
cmake_minimum_required(VERSION 3.5)
project(HELLO)

set(MY_VAL "Hello World!")   # 设置变量
message(${MY_VAL})           # 用 ${} 引用变量
  • 命令格式:command(参数1 参数2 ...),参数用空格分隔(不是逗号)。命令名大小写不敏感(projectPROJECT 等价),内置变量习惯用大写以示区分。
  • 必要参数用 <参数> 表示,可选参数(选项)用 [参数] 表示。
  • 变量分为内置变量与自定义变量,引用一律用 ${变量名}

3.2 常用命令一览

命令 说明
add_executable 定义可执行程序目标
add_library 定义库文件目标
add_subdirectory 去指定目录中寻找新的 CMakeLists.txt
aux_source_directory 收集目录中的源文件名并赋值给变量
cmake_minimum_required 设置 CMake 最低版本要求
get_target_property 获取目标属性
include_directories 设置所有目标的头文件搜索路径(≈ gcc -I
link_directories 设置所有目标的库文件搜索路径(≈ gcc -L
link_libraries 设置所有目标需要链接的库(≈ gcc -l
list 列表相关操作
message 打印输出信息
project 设置工程名称
set 设置变量
set_target_properties 设置目标属性
target_include_directories 设置指定目标的头文件搜索路径
target_link_libraries 设置指定目标链接的库
target_sources 设置指定目标所需的源文件
target_link_directories 设置指定目标的库搜索路径

3.3 常用命令详解

3.3.1 add_executable

add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
add_executable(hello 1.c 2.c 3.c)   # 生成可执行文件 hello

源文件路径可用相对路径(相对当前源码路径)或绝对路径。

3.3.2 add_library

add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
add_library(mylib STATIC 1.c 2.c 3.c)   # 静态库 libmylib.a
add_library(mylib SHARED 1.c 2.c 3.c)   # 动态库 libmylib.so

3.3.3 add_subdirectory

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir:子源码目录(必须有 CMakeLists.txt)。
  • binary_dir:子源码的输出文件目录(BINARY_DIR),可选。不指定时,默认在当前源码的 BINARY_DIR 下创建与子目录同名的文件夹。

    add_subdirectory(src)          # 子目录在源码树内,可省略 binary_dir
    add_subdirectory(src output)   # 指定子源码 BINARY_DIR 为 build/output
    add_subdirectory(../lib output)# 加载平级目录,必须显式指定 binary_dir
    

注意:加载非当前源码子目录(平级、上级)时,如果不显式指定 binary_dir,执行 cmake 会报错。相对路径的 binary_dir 是相对于当前源码的 BINARY_DIR,不是当前源码路径。

3.3.4 aux_source_directory

aux_source_directory(<dir> <variable>)

扫描目录下所有源文件,存进变量,各元素用分号 ; 分隔。

aux_source_directory(src SRC_LIST)
message("${SRC_LIST}")   # 加双引号才能看到列表全貌

3.3.5 include_directories

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

相当于 gcc -I。默认添加到头文件搜索列表末尾,可用 BEFORE/AFTER 调整;设置 CMAKE_INCLUDE_DIRECTORIES_BEFORE=ON 可改变默认行为。调用 add_subdirectory 时,该列表会向下传递给子源码。

3.3.6 link_directorieslink_libraries

link_directories(directory1 directory2 ...)
link_libraries([item1 [item2 [...]]] [[debug|optimized|general] <item>] ...)
include_directories(include)
link_directories(lib)
link_libraries(hello)                 # 简写
link_libraries(libhello.so)           # 全称
link_libraries(${PROJECT_SOURCE_DIR}/lib/libhello.so)   # 绝对路径

库文件搜索列表同样会向下传递给子源码。

3.3.7 list

list(LENGTH <list> <output variable>)
list(GET <list> <element index> [...] <output variable>)
list(APPEND <list> [<element> ...])
list(FIND <list> <value> <output variable>)
list(INSERT <list> <element_index> <element> [...])
list(REMOVE_ITEM <list> <value> [...])
list(REMOVE_AT <list> <index> [...])
list(REMOVE_DUPLICATES <list>)
list(REVERSE <list>)
list(SORT <list>)

示例:

set(SRC_LIST main.c world.c hello.c)
message("SRC_LIST: ${SRC_LIST}")

list(LENGTH SRC_LIST L_LEN)
message("列表长度: ${L_LEN}")

list(GET SRC_LIST 1 VAR1)     # 取 index=1 的元素
message("index=1: ${VAR1}")

list(APPEND SRC_LIST hello_world.c)   # 追加
list(SORT SRC_LIST)                   # 排序

3.3.8 message

message([<mode>] "message to display" ...)
mode 说明
重要信息、普通信息
STATUS 附带信息
WARNING CMake 警告,继续处理
AUTHOR_WARNING CMake 警告(开发),继续处理
SEND_ERROR CMake 错误,继续处理,但跳过生成
FATAL_ERROR CMake 错误,停止处理和生成
DEPRECATION 弃用错误/警告
message("Hello World!")
message(STATUS "CMake version: " ${CMAKE_VERSION})

3.3.9 projectset

project(HELLO)                 # 设置工程名称
project(HELLO VERSION 1.1.0)   # 同时设置版本号

执行 project(HELLO) 后引入 HELLO_SOURCE_DIRHELLO_BINARY_DIR 两个变量(前缀就是工程名)。CMake 还定义了两个等价的 PROJECT_SOURCE_DIRPROJECT_BINARY_DIR,通常只用这两个,且只在顶层调用一次 project

set(<variable> <value>... [PARENT_SCOPE])   # 设置变量
set(SRC_LIST 1.c 2.c 3.c 4.c 5.c)           # 字符串列表,元素以 ; 分隔
set(BUILD_SHARED_LIBS on)                   # 改变 add_library 默认行为

3.3.10 target_include_directoriestarget_link_libraries

target_include_directories(<target> [SYSTEM] [BEFORE]
    <INTERFACE|PUBLIC|PRIVATE> [items1...]
    [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

target_link_libraries(<target>
    <PRIVATE|PUBLIC|INTERFACE> <item>...
    [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)

它们与 include_directories/link_libraries 功能相同,但作用范围可控,只影响指定目标。关键是三个作用域关键字:

关键字 当前目标是否使用 是否传递给依赖目标 等价关系
PRIVATE 使用 不传递 私有
INTERFACE 不使用 传递 只给依赖者用
PUBLIC 使用 传递 PRIVATE + INTERFACE
# hello_world 内部用 hello,且 hello_world.h 也对外暴露 hello.h
target_link_libraries(hello_world PUBLIC hello)
target_include_directories(hello_world PUBLIC hello)

强烈建议统一使用 target_include_directories() / target_link_libraries(),而不是全局的 include_directories() / link_libraries()——后者作用于当前源码所有目标并向下传递,大工程里容易混乱、出错。保持工程目录清晰。

target_link_directories() 则为指定目标设置库文件搜索路径(对应 gcc -L),只对该目标生效:

target_link_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/lib)
target_link_libraries(mqttClient PRIVATE paho-mqtt3c)

3.4 部分常用变量

3.4.1 提供信息的变量

变量 说明
PROJECT_SOURCE_DIR 工程顶层目录(顶层 CMakeLists.txt 所在目录)
PROJECT_BINARY_DIR 工程 BINARY_DIR(顶层源码的输出目录)
CMAKE_SOURCE_DIR PROJECT_SOURCE_DIR 等价
CMAKE_BINARY_DIR PROJECT_BINARY_DIR 等价
CMAKE_CURRENT_SOURCE_DIR 当前源码所在路径
CMAKE_CURRENT_BINARY_DIR 当前源码的 BINARY_DIR
CMAKE_MAJOR_VERSION / CMAKE_MINOR_VERSION / CMAKE_VERSION CMake 主/次/完整版本号
PROJECT_VERSION / PROJECT_VERSION_MAJOR / PROJECT_VERSION_MINOR 工程版本号
CMAKE_PROJECT_NAME / PROJECT_NAME 工程名(二者等价)

3.4.2 改变行为的变量

变量 说明
BUILD_SHARED_LIBS 控制 add_library 未显式指定时是否生成动态库
CMAKE_BUILD_TYPE 构建类型,DebugRelease
CMAKE_SYSROOT 对应编译器 --sysroot 选项,交叉编译时使用
CMAKE_IGNORE_PATH find_xxx 忽略的目录列表
CMAKE_INCLUDE_PATH find_file() / find_path() 的搜索路径
CMAKE_INCLUDE_DIRECTORIES_BEFORE 控制 include_directories() 默认行为
CMAKE_LIBRARY_PATH find_library() 的搜索路径
CMAKE_MODULE_PATH include() / find_package() 加载模块的搜索路径
CMAKE_PROGRAM_PATH find_program() 的搜索路径
set(BUILD_SHARED_LIBS on)        # add_library 默认生成动态库
set(CMAKE_BUILD_TYPE Debug)      # 带调试信息,可用 GDB
set(CMAKE_INCLUDE_PATH ${PROJECT_SOURCE_DIR}/src)

CMAKE_INCLUDE_PATH 实例:

set(CMAKE_INCLUDE_PATH ${PROJECT_SOURCE_DIR}/src)
find_file(P_VAR hello.c)   # 找到后返回 hello.c 的全路径
message(${P_VAR})

3.4.3 描述系统的变量

变量 说明
CMAKE_HOST_SYSTEM_NAME 运行 CMake 的操作系统名(uname -s
CMAKE_HOST_SYSTEM_PROCESSOR 运行 CMake 的处理器名(uname -p
CMAKE_HOST_SYSTEM 运行 CMake 的系统(复合信息)
CMAKE_HOST_SYSTEM_VERSION 运行 CMake 的系统版本(uname -r
CMAKE_HOST_UNIX / UNIX 主机是类 UNIX 时为真
CMAKE_HOST_WIN32 / WIN32 主机是 Windows 时为真
CMAKE_SYSTEM_NAME 目标主机的操作系统名
CMAKE_SYSTEM_PROCESSOR 目标主机的处理器名
CMAKE_SYSTEM / CMAKE_SYSTEM_VERSION 目标主机的复合信息 / 版本号
ENV 访问环境变量,用法 $ENV{VAR}
message(${CMAKE_HOST_SYSTEM_NAME})
message(${CMAKE_SYSTEM_NAME})
message($ENV{XXX})              # 读取环境变量 XXX

3.4.4 控制编译的变量

变量 说明
EXECUTABLE_OUTPUT_PATH 可执行程序的输出路径
LIBRARY_OUTPUT_PATH 库文件的输出路径

默认情况下,最终目标文件的输出目录就是源码的 BINARY_DIR

3.5 双引号的作用

命令参数:双引号把内容当成一个整体参数。

message(Hello World)     # 两个参数,打印 HelloWorld
message("Hello World")   # 一个参数,打印 Hello World

引用变量${VAR} 不加引号时,列表元素被拆开、无分隔地打印;加引号时按整体处理,CMake 用分号保持列表语义。

set(MY_LIST Hello World China)
message(${MY_LIST})     # HelloWorldChina
message("${MY_LIST}")   # Hello;World;China

3.6 条件判断 if

if(expression)
  # ...
elseif(expression2)
  # ...
else()
  # ...
endif()

else / endif 括号中的表达式可写可不写,写了必须与 if 一致。常用表达式:

表达式 为真条件 常用写法示例
<constant> 1/ON/YES/TRUE/Y/非零数字 if(ON)
<variable\|string> 变量已定义且不为假常量 if(GG)
NOT <expr> expr 为假 if(NOT GG)
<e1> AND <e2> 两者同为真 if(yes AND on)
<e1> OR <e2> 至少一个为真 if(yes OR no)
COMMAND name name 是已定义的命令/宏/函数 if(COMMAND project)
TARGET name name 是已定义的目标 if(TARGET hello)
EXISTS path 文件或目录存在(需绝对路径) if(EXISTS ${PROJECT_BINARY_DIR})
IS_DIRECTORY path path 是目录 if(IS_DIRECTORY ${PROJECT_BINARY_DIR}/hello)
IS_ABSOLUTE path path 是绝对路径 if(IS_ABSOLUTE ${PROJECT_BINARY_DIR})
<var\|str> MATCHES regex 正则匹配成功 if(MY_STR MATCHES "Hello World")
<var\|str> IN_LIST <var> 元素在列表中 if(Hello IN_LIST MY_LIST)
DEFINED <var> 变量已定义(值真假无关) if(DEFINED yyds)
<a> LESS/GREATER/EQUAL <b> 数值比较 if(20 LESS 100)

常量真假规则:1ONYESTRUEY、非零数字为真;0OFFNOFALSENIGNORENOTFOUND、空字符串、以 -NOTFOUND 结尾为假,大小写不敏感。不匹配这些常量时,才当作变量或字符串处理。

3.7 循环与数学运算

3.7.1 foreach

foreach(loop_var arg1 arg2 ...)     # 遍历参数列表
    message("${loop_var}")
endforeach()

set(my_list A B C D)
foreach(loop_var ${my_list})        # 遍历列表
endforeach()

foreach(loop_var RANGE 4)           # 0..4
foreach(loop_var RANGE 1 4 1)       # start stop step

foreach(loop_var IN LISTS my_list)  # 遍历列表
foreach(loop_var IN ITEMS A B C D)  # 遍历显式元素

3.7.2 whilebreakcontinuemath

set(loop_var 4)
while(loop_var GREATER 0)
    message("${loop_var}")
    math(EXPR loop_var "${loop_var} - 1")
endwhile()

# break / continue
while(loop_var GREATER 0)
    if(loop_var LESS 6)
        break()        # 跳出循环
    endif()
    math(EXPR loop_var "${loop_var} - 1")
endwhile()

# 打印偶数
while(loop_var GREATER 0)
    math(EXPR var "${loop_var} % 2")
    if(var EQUAL 0)
        message("${loop_var}")
        math(EXPR loop_var "${loop_var} - 1")
        continue()     # 进入下一次循环
    endif()
    math(EXPR loop_var "${loop_var} - 1")
endwhile()

math() 支持 + - * / %| & ^ ~ << >> 及组合运算,含义与 C 语言相同:

math(EXPR out_var "100 + 100")
math(EXPR out_var "(100 & 100) * 50 - 2")

3.8 函数与宏

3.8.1 function

function(<name> [arg1 [arg2 ...]])
    # ...
    return()      # 可提前退出,但 return 不能返回参数
endfunction()

调用函数时实际参数个数可以多于定义个数,甚至定义 0 个也行。函数内部内置变量:

内部变量 说明
ARGVX 第 X 个参数,如 ARGV0ARGV1
ARGV 实际传入的所有参数(列表)
ARGN 超出形参个数的剩余参数(列表)
ARGC 实际传入的参数个数
function(xyz arg1 arg2)
    message("ARGC: ${ARGC}")
    message("ARGV: ${ARGV}")
    message("ARGN: ${ARGN}")
    message("ARGV0: ${ARGV0}")
endfunction()

xyz(A B C D E F G)

函数作用域是全局的:父源码定义的函数子源码能用,子源码定义的函数父源码也能用(调用前需已定义)。

3.8.2 macro

macro(<name> [arg1 [arg2 ...]])
    # ...
endmacro()

宏与函数用法相似,也支持 ARGVX/ARGC/ARGV/ARGN,但宏是字符串替换,其参数与这些值不是变量:

macro(abc arg1 arg2)
    if(DEFINED ARGC)     # 宏里 ARGC 被替换成数字 4,故不成立
        message(true)
    else()
        message(false)
    endif()
endmacro()

function(xyz arg1 arg2)
    if(DEFINED ARGC)     # 函数里 ARGC 是变量,成立
        message(true)
    else()
        message(false)
    endif()
endfunction()

区别:宏无作用域概念、纯文本替换;函数有自己的作用域。

3.9 变量的作用域

CMake 有三种作用域:

flowchart TB
    G["全局作用域<br/>缓存变量 / -D 定义的变量"]
    D1["目录作用域<br/>顶层 CMakeLists"]
    D2["目录作用域<br/>子目录 CMakeLists<br/>(值拷贝)"]
    F["函数作用域<br/>function 内部"]

    G --> D1 --> D2
    D1 --> F

    classDef scope fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
    class G,D1,D2,F scope

函数作用域:函数内 set 只创建函数内的变量;引用未在函数内定义的变量时,逐层向外查找。要在函数内修改外部变量,set 末尾加 PARENT_SCOPE(设置到上一层作用域):

function(xyz)
    set(ABC "Hello China!" PARENT_SCOPE)
endfunction()

set(ABC "Hello World!")
xyz()
message("${ABC}")   # Hello China!

利用 PARENT_SCOPE 可以实现函数"返回值"——把变量名当参数传入,在函数里用该名字 set 到上层:

function(xyz out var1 var2)
    math(EXPR temp "${var1} + ${var2}")
    set(${out} ${temp} PARENT_SCOPE)
endfunction()

xyz(out_var 5 10)
message("${out_var}")   # 15

目录作用域:子目录会把父目录变量值拷贝一份,子目录内 set 不影响父目录(向下有效、值拷贝)。

全局作用域:缓存变量在整个工程生命周期有效,可用 set(... CACHE ...) 或命令行 -D 定义:

cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake ..

-D 创建的缓存变量是全局变量,会覆盖 CMakeLists.txt 中定义的同名变量。

3.10 属性

属性分为全局属性、目录属性(源码属性)、目标属性等。常用命令:

get_directory_property(<variable> [DIRECTORY <dir>] <prop-name>)
set_directory_properties(PROPERTIES prop1 value1 prop2 value2)
get_target_property(<variable> <target> <prop-name>)
set_target_properties(<target> PROPERTIES prop1 value1 ...)
  • 目录属性:INCLUDE_DIRECTORIESinclude_directories() 添加的目录)、LINK_DIRECTORIESlink_directories() 添加的目录)、CACHE_VARIABLESVARIABLESMACROSPARENT_DIRECTORY 等。用 set_directory_properties 设置时必须用绝对路径
  • 目标属性:OUTPUT_NAMETYPESTATIC_LIBRARY/SHARED_LIBRARY/EXECUTABLE 等)、INCLUDE_DIRECTORIESINTERFACE_INCLUDE_DIRECTORIESINTERFACE_LINK_LIBRARIESLINK_LIBRARIESBINARY_DIRSOURCE_DIR 等。

    include_directories(include)
    get_directory_property(out_var INCLUDE_DIRECTORIES)
    message("${out_var}")
    
    add_library(mylib STATIC mylib.c)
    get_target_property(type mylib TYPE)
    message("${type}")     # STATIC_LIBRARY
    

3.11 file() 文件操作

用法 说明
file(WRITE <file> <content>...) 写文件,存在则覆盖
file(APPEND <file> <content>...) 追加到文件末尾
file(GENERATE OUTPUT <out> INPUT <in> \| CONTENT <c> [CONDITION <expr>]) 由内容/输入文件生成文件
file(READ <file> <var> [OFFSET <o>] [LIMIT <n>] [HEX]) 按字节读取
file(STRINGS <file> <var> [options...]) 按字符串列表读取(忽略二进制、CR)
file(<MD5\|SHA1\|SHA256\|...> <file> <var>) 计算 hash 值
file(RENAME <old> <new>) 重命名
file(REMOVE <files>...) 删除文件
file(REMOVE_RECURSE <files>...) 删除文件或目录(含非空目录)
file(WRITE wtest.txt "Hello World!")
file(APPEND wtest.txt " China")

file(GENERATE OUTPUT out1.txt INPUT "${PROJECT_SOURCE_DIR}/wtest.txt")
file(GENERATE OUTPUT out2.txt CONTENT "This is the out2.txt file")

file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var)
file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var OFFSET 0 LIMIT 10)
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LENGTH_MAXIMUM 5)
file(SHA256 "${PROJECT_SOURCE_DIR}/input.txt" out_var)
file(RENAME "${PROJECT_SOURCE_DIR}/input.txt" "${PROJECT_SOURCE_DIR}/output.txt")
file(REMOVE "${PROJECT_SOURCE_DIR}/out1.txt")
file(REMOVE_RECURSE "${PROJECT_SOURCE_DIR}/Non_empty-dir")

路径规则:WRITE/APPEND/READ/STRINGS/RENAME/REMOVE 的相对路径相对于当前源码路径GENERATE 与 hash 计算的相对路径相对于当前源码的 BINARY_DIRSTRINGS 的常用选项包括 LENGTH_MAXIMUMLENGTH_MINIMUMLIMIT_COUNTLIMIT_INPUTLIMIT_OUTPUTNEWLINE_CONSUMENO_HEX_CONVERSIONREGEXENCODING


4. 生成静态库与动态库

# 方式一:命令中显式指定
add_library(mylib STATIC 1.c 2.c 3.c)   # libmylib.a
add_library(mylib SHARED 1.c 2.c 3.c)   # libmylib.so

# 方式二:用 BUILD_SHARED_LIBS 改变默认行为
set(BUILD_SHARED_LIBS on)
add_library(hello hello/hello.c)        # 生成 libhello.so
add_library(world world/world.c)        # 生成 libworld.so
库类型 选项 产物前缀/后缀 链接方式
静态库 STATIC(默认) lib + .a 链接时把代码复制进可执行文件
动态库 SHARED lib + .so 运行时加载,需保证目标机有对应 .so
模块库 MODULE 视平台而定 供运行时动态加载,一般不参与链接

想要自定义输出文件名,用属性而非目标名:

set_target_properties(mylib PROPERTIES OUTPUT_NAME "hello")   # libhello.a / libhello.so

5. 交叉编译

不配置交叉编译时,CMake 默认用主机的 gcc,产物只能在 Ubuntu 上运行。要让产物跑在 ARM 开发板上,需要设置若干变量。I.MX6U 使用的交叉编译器为:

arm-poky-linux-gnueabi-gcc     # C 编译器
arm-poky-linux-gnueabi-g++     # C++ 编译器

5.1 工具链文件 arm-linux-setup.cmake

##################################
# 配置 ARM 交叉编译
#################################
set(CMAKE_SYSTEM_NAME Linux)    # 设置目标系统名字
set(CMAKE_SYSTEM_PROCESSOR arm) # 设置目标处理器架构

# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)

# 指定交叉编译器 arm-linux-gcc 和 arm-linux-g++
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-g++)

# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
#################################
# end
##################################

各变量含义:

变量 含义
CMAKE_SYSTEM_NAME 目标主机操作系统名,Linux 表示目标是 Linux 系统
CMAKE_SYSTEM_PROCESSOR 目标架构名,arm
CMAKE_SYSROOT 传给 gcc 的 --sysroot 选项,编译时去该目录找标准库与头文件
CMAKE_C_COMPILER C 编译器(交叉编译时指向 arm-gcc)
CMAKE_CXX_COMPILER C++ 编译器(交叉编译时指向 arm-g++)
CMAKE_C_FLAGS / CMAKE_CXX_FLAGS 分别为 C/C++ 编译器追加编译选项
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY 表示 find_library() 只在 CMAKE_SYSROOT 中搜索;NEVER 只用主机路径;BOTH 都搜
CMAKE_FIND_ROOT_PATH_MODE_INCLUDE 控制 find_file()/find_path() 是否使用 CMAKE_SYSROOT,取值同上
CMAKE_FIND_ROOT_PATH_MODE_PROGRAM 控制查找可执行程序时使用主机还是 sysroot 路径;通常设为 NEVER,即用主机工具

5.2 两种使用方式

方式一(不推荐):把配置直接写进 CMakeLists.txt,且必须放在 project() 之前,否则不生效。

方式二(推荐):单独写成工具链文件,用 -DCMAKE_TOOLCHAIN_FILE 指定:

cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DCMAKE_BUILD_TYPE=Release ..
make

-D 创建缓存变量,CMAKE_TOOLCHAIN_FILE 的值就是工具链文件路径,CMake 会执行它来配置交叉编译。

5.3 验证

file main    # 应显示 ARM 架构,而不是 x86-64

6. 完整可复制工程示例

下面是一个多目录工程,综合运用 add_subdirectory、库目标、目标级属性、输出路径与交叉编译工具链。

cmake_demo/
├── CMakeLists.txt              # 顶层
├── cmake/
│   └── arm-linux-setup.cmake   # 交叉编译工具链文件(第 5.1 节)
├── include/
│   └── demo.h                  # 对外公共头文件
├── src/
│   ├── CMakeLists.txt
│   └── main.c
└── lib/
    ├── hello/
    │   ├── CMakeLists.txt
    │   ├── hello.c
    │   └── hello.h
    └── math/
        ├── CMakeLists.txt
        ├── math.c
        └── math.h

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(cmake_demo C VERSION 1.0.0)

# 输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH    ${PROJECT_BINARY_DIR}/lib)

# 公共头文件目录(只影响下面的目标,靠目标级命令传递)
include_directories(${PROJECT_SOURCE_DIR}/include)

message(STATUS "CMake version: " ${CMAKE_VERSION})
message(STATUS "Project source: " ${PROJECT_SOURCE_DIR})
message(STATUS "Project binary: " ${PROJECT_BINARY_DIR})

# 两个库子目录 + 一个应用子目录
add_subdirectory(lib/hello)
add_subdirectory(lib/math)
add_subdirectory(src)

include/demo.h

#ifndef __DEMO_H_
#define __DEMO_H_

#define DEMO_VERSION "1.0.0"

#endif

lib/hello/hello.hhello.c

#ifndef __HELLO_H_
#define __HELLO_H_

void hello(const char *name);

#endif
#include <stdio.h>
#include "hello.h"

void hello(const char *name)
{
    printf("Hello %s!\n", name);
}

lib/hello/CMakeLists.txt

# 生成静态库 libhello.a
add_library(hello STATIC hello.c)

# 头文件目录:本目标自己用,也传给依赖者
target_include_directories(hello PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

lib/math/math.hmath.c

#ifndef __MATH_H_
#define __MATH_H_

int add(int a, int b);
int mul(int a, int b);

#endif
#include "math.h"

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

lib/math/CMakeLists.txt

add_library(math STATIC math.c)
target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

src/main.c

#include <stdio.h>
#include "demo.h"
#include "hello.h"
#include "math.h"

int main(void)
{
    hello("CMake");
    printf("demo version: %s\n", DEMO_VERSION);
    printf("3 + 5 = %d\n", add(3, 5));
    printf("3 * 5 = %d\n", mul(3, 5));
    return 0;
}

src/CMakeLists.txt

add_executable(cmake_demo main.c)

# 链接两个库,库的 PUBLIC 头文件目录会自动带过来
target_link_libraries(cmake_demo PRIVATE hello math)

# 如需额外头文件搜索路径(示例,仅本目标生效):
# target_include_directories(cmake_demo PRIVATE ${PROJECT_SOURCE_DIR}/include)

6.1 构建(主机验证)

mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make
tree .

预期产物:

build/
├── bin/cmake_demo
└── lib/
    ├── libhello.a
    └── libmath.a

6.2 交叉编译(ARM 开发板)

cd build
rm -rf *
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
      -DCMAKE_BUILD_TYPE=Release ..
make
file bin/cmake_demo      # 应为 ARM 架构

bin/cmake_demo 拷贝到开发板 /home/root 下运行即可。

6.3 扩展:option()install()

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

# option() 定义一个布尔缓存变量,默认值 OFF;命令行 -DENABLE_DEBUG=ON 可覆盖
option(ENABLE_DEBUG "Enable debug build" OFF)
if(ENABLE_DEBUG)
    set(CMAKE_BUILD_TYPE Debug)
    add_definitions(-DDEBUG)
else()
    set(CMAKE_BUILD_TYPE Release)
endif()

# install() 定义安装规则,配合 make install 使用
# CMAKE_INSTALL_PREFIX 默认 /usr/local,可用 -DCMAKE_INSTALL_PREFIX=... 覆盖
install(TARGETS cmake_demo RUNTIME DESTINATION bin)
install(TARGETS hello math ARCHIVE DESTINATION lib)
install(FILES include/demo.h DESTINATION include)

# file(GLOB) 通配收集源文件(不推荐用于正式工程)
file(GLOB SRC_FILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/src/*.c)
# 注意:CMake 官方建议显式列出源文件,GLOB 在新增文件时不一定触发重新配置。

7. 实验步骤与调试

现象 原因 处理
cmake 报交叉编译配置相关错误 Ubuntu 自带 CMake 版本太旧(如 3.5.1) 换用 CMake 3.16.0(解压即用的二进制包)
交叉编译不生效 配置写进了 CMakeLists.txt 但放在了 project() 之后 交叉编译配置必须在 project() 之前,或改用工具链文件
add_subdirectory(../lib) 报错 加载平级/上级目录未指定 binary_dir 显式写 add_subdirectory(../lib output)
找不到头文件 未设置头文件搜索路径,或未向下/向上传递 target_include_directories(... PUBLIC ...)
生成的库名是 liblibhello.a 库目标名与 OUTPUT_NAME 混用 set_target_properties(... PROPERTIES OUTPUT_NAME "hello")
make 找不到目标 没在 build 目录执行 cmake 回到 build 目录,先 cmake ..make
想彻底重来 缓存了旧变量 删除 build 目录(或 CMakeCache.txt)后重新 cmake

推荐构建流程:

cd build && rm -rf ./*
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DCMAKE_BUILD_TYPE=Release ..
make

8. 跨平台 / 工具对比

维度 手写 gcc 命令 Makefile CMake Qt qmake
跨平台 一般
学习成本 低(Qt 专用)
依赖管理 手工 自动扫描 + 目标属性 QT += 模块
大型工程 不可行 尚可 推荐 Qt 工程推荐
与 IDE 集成 一般 好(生成工程文件)
典型场景 单文件验证 小型 C 工程 中大型跨平台 C/C++ Qt 应用

对同一份多文件源码,三种方式的对比:

# 1) 手写 gcc
gcc -Iinclude src/main.c lib/hello/hello.c lib/math/math.c -o demo

# 2) Makefile(节选)
demo: src/main.o lib/hello/hello.o lib/math/math.o
	$(CC) $^ -o $@

# 3) CMake
cmake -B build && cmake --build build

9. 面试精选(5 题)

Q1 CMake 和 Makefile 的关系是什么?为什么说 CMake 跨平台?

要点:CMake 不直接编译,而是解析 CMakeLists.txt 生成本地化的 Makefile(或工程文件),最终仍由 make 编译。

详解CMakeLists.txt 与平台无关,CMake 根据当前平台/工具链生成对应构建脚本;Makefile 的语法和规则在不同平台往往不同,不能通用。因此跨平台的本质是"用统一描述文件 + 生成器适配各平台",而真正干活的仍是 make

追问

  1. 为什么不直接写 Makefile?(语法复杂、跨平台差、大工程依赖难维护)
  2. cmakemake 分别负责什么?(cmake 生成构建文件,make 依据构建文件执行编译)

Q2 include_directoriestarget_include_directories 有什么区别?该用哪个?

要点:前者对当前源码所有目标生效并向下传递;后者只对指定目标生效,作用域由 PRIVATE/INTERFACE/PUBLIC 控制。

详解target_include_directories(t PRIVATE d) 只给 t 用;INTERFACE 只传给依赖 t 的目标;PUBLIC 两者都传。全局命令在大工程里容易污染其它目标、产生隐蔽错误,因此推荐目标级命令以保持目录清晰。

追问

  1. PRIVATEPUBLIC 的差别在哪?(头文件目录是否传递给依赖该目标的目标)
  2. 静态库的 PUBLIC 头文件目录会怎样传给可执行文件?(通过链接关系自动带入)

Q3 CMake 中如何生成静态库和动态库?如何自定义库文件名?

要点add_library(name [STATIC|SHARED] src);用 set_target_properties(NAME PROPERTIES OUTPUT_NAME ...) 改名。

详解add_library 默认生成静态库,SHARED 生成动态库;库名不含 lib 前缀与 .a/.so 后缀,由 CMake 自动补齐。目标名工程内必须唯一,所以不能用目标名去改文件名,只能用 OUTPUT_NAME 属性。

追问

  1. BUILD_SHARED_LIBS 的作用?(设为 onadd_library 默认生成动态库)
  2. 动态库拷到开发板后程序运行报 "cannot open shared object file" 怎么办?(把 .so 放到 /usr/lib 或设置 LD_LIBRARY_PATH

Q4 如何为 CMake 工程配置 ARM 交叉编译?

要点:编写工具链文件,设置 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_PROCESSORCMAKE_SYSROOTCMAKE_C_COMPILER 等,再用 -DCMAKE_TOOLCHAIN_FILE= 指定。

详解:配置不要写进 CMakeLists.txt,而是放到独立工具链文件,且必须在 project() 之前生效。示例见本篇第 5.1 节。CMAKE_SYSROOT 会作为 --sysroot 传给 gcc,CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/INCLUDE 设为 ONLY 可保证只搜 sysroot。

追问

  1. -D 选项创建的是什么变量?(缓存变量,全局生效,覆盖同名普通变量)
  2. 怎么验证交叉编译成功?(file 命令查看产物架构,应为 ARM 而非 x86-64)

Q5 CMake 变量有哪些作用域?函数内如何修改外部变量?

要点:函数作用域、目录作用域(值拷贝、向下有效)、全局作用域(缓存变量);函数内用 PARENT_SCOPE 修改上层变量。

详解:函数内 set 默认创建局部变量,不改变外层同名变量;加 PARENT_SCOPE 后设置到上一层作用域,可借"传入变量名"的技巧实现返回值。目录之间是值拷贝,子目录改父目录变量无效。-D 定义的缓存变量全局有效。

追问

  1. PARENT_SCOPE 在嵌套函数中写到哪一层?(调用者的作用域,即上一层)
  2. 为什么每次 cmake 都要清理 build?(缓存变量会保留并覆盖新配置,可能导致旧设置残留)

内容来源:《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMake入门与进阶;例程 33_mqtt/mqtt_prj/CMakeLists.txt33_mqtt/mqtt_prj/cmake/arm-linux-setup.cmake。第 6.3 节 option()/install()/file(GLOB) 为扩展知识。