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
💡 关联知识:[[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 出厂开发环境,读者需按自己的实际安装路径修改。
| 阶段 | 工具 | 说明 |
|---|---|---|
| 手动编译 | gcc main.c hello.c -o hello |
文件一多就要手敲一长串,改一个文件全量重编 |
| 脚本化 | Makefile + make |
描述依赖关系与规则,支持增量编译;但语法复杂、各平台不通用 |
| 跨平台构建 | CMake | 写与平台无关的 CMakeLists.txt,自动生成本地化 Makefile / 工程文件 |
CMake 的主要优点(教材总结):
CMakeLists.txt,按当前平台生成 Makefile 和工程文件,最终还是调用 make 编译,但 CMake 本身跨平台;CMakeLists.txt 语法与平台无关,比 Makefile 简单易懂,由 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
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(培训教程)。
// 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.txt、CMakeFiles/、cmake_install.cmake、Makefile 等文件。
上面的做法把中间文件和源码混在一起,清理麻烦。正确做法是源码与构建分离(out-of-source):
├── build/ # 构建目录
├── CMakeLists.txt
└── main.c
cd build/
cmake ../
make
所有中间文件和可执行文件都落在 build/,清理时直接删掉 build/ 即可。
// 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)。
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_executable、add_library定义的目标名在整个工程内必须唯一,所以不能直接用add_library(hello hello.c)改名,只能用OUTPUT_NAME属性。
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.txt;PROJECT_SOURCE_DIR 指工程顶层源码目录。
# 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
# 这是单行注释
cmake_minimum_required(VERSION 3.5)
project(HELLO)
set(MY_VAL "Hello World!") # 设置变量
message(${MY_VAL}) # 用 ${} 引用变量
command(参数1 参数2 ...),参数用空格分隔(不是逗号)。命令名大小写不敏感(project 与 PROJECT 等价),内置变量习惯用大写以示区分。<参数> 表示,可选参数(选项)用 [参数] 表示。${变量名}。| 命令 | 说明 |
|---|---|
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 |
设置指定目标的库搜索路径 |
add_executableadd_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
add_executable(hello 1.c 2.c 3.c) # 生成可执行文件 hello
源文件路径可用相对路径(相对当前源码路径)或绝对路径。
add_libraryadd_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
add_subdirectoryadd_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,不是当前源码路径。
aux_source_directoryaux_source_directory(<dir> <variable>)
扫描目录下所有源文件,存进变量,各元素用分号 ; 分隔。
aux_source_directory(src SRC_LIST)
message("${SRC_LIST}") # 加双引号才能看到列表全貌
include_directoriesinclude_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
相当于 gcc -I。默认添加到头文件搜索列表末尾,可用 BEFORE/AFTER 调整;设置 CMAKE_INCLUDE_DIRECTORIES_BEFORE=ON 可改变默认行为。调用 add_subdirectory 时,该列表会向下传递给子源码。
link_directories 与 link_librarieslink_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) # 绝对路径
库文件搜索列表同样会向下传递给子源码。
listlist(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) # 排序
messagemessage([<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})
project 与 setproject(HELLO) # 设置工程名称
project(HELLO VERSION 1.1.0) # 同时设置版本号
执行 project(HELLO) 后引入 HELLO_SOURCE_DIR、HELLO_BINARY_DIR 两个变量(前缀就是工程名)。CMake 还定义了两个等价的 PROJECT_SOURCE_DIR、PROJECT_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 默认行为
target_include_directories 与 target_link_librariestarget_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)
| 变量 | 说明 |
|---|---|
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 |
工程名(二者等价) |
| 变量 | 说明 |
|---|---|
BUILD_SHARED_LIBS |
控制 add_library 未显式指定时是否生成动态库 |
CMAKE_BUILD_TYPE |
构建类型,Debug 或 Release |
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})
| 变量 | 说明 |
|---|---|
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
| 变量 | 说明 |
|---|---|
EXECUTABLE_OUTPUT_PATH |
可执行程序的输出路径 |
LIBRARY_OUTPUT_PATH |
库文件的输出路径 |
默认情况下,最终目标文件的输出目录就是源码的 BINARY_DIR。
命令参数:双引号把内容当成一个整体参数。
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
ifif(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) |
常量真假规则:1、ON、YES、TRUE、Y、非零数字为真;0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串、以 -NOTFOUND 结尾为假,大小写不敏感。不匹配这些常量时,才当作变量或字符串处理。
foreachforeach(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) # 遍历显式元素
while、break、continue 与 mathset(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")
functionfunction(<name> [arg1 [arg2 ...]])
# ...
return() # 可提前退出,但 return 不能返回参数
endfunction()
调用函数时实际参数个数可以多于定义个数,甚至定义 0 个也行。函数内部内置变量:
| 内部变量 | 说明 |
|---|---|
ARGVX |
第 X 个参数,如 ARGV0、ARGV1 |
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)
函数作用域是全局的:父源码定义的函数子源码能用,子源码定义的函数父源码也能用(调用前需已定义)。
macromacro(<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()
区别:宏无作用域概念、纯文本替换;函数有自己的作用域。
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 中定义的同名变量。
属性分为全局属性、目录属性(源码属性)、目标属性等。常用命令:
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_DIRECTORIES(include_directories() 添加的目录)、LINK_DIRECTORIES(link_directories() 添加的目录)、CACHE_VARIABLES、VARIABLES、MACROS、PARENT_DIRECTORY 等。用 set_directory_properties 设置时必须用绝对路径。目标属性:OUTPUT_NAME、TYPE(STATIC_LIBRARY/SHARED_LIBRARY/EXECUTABLE 等)、INCLUDE_DIRECTORIES、INTERFACE_INCLUDE_DIRECTORIES、INTERFACE_LINK_LIBRARIES、LINK_LIBRARIES、BINARY_DIR、SOURCE_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
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_DIR。STRINGS的常用选项包括LENGTH_MAXIMUM、LENGTH_MINIMUM、LIMIT_COUNT、LIMIT_INPUT、LIMIT_OUTPUT、NEWLINE_CONSUME、NO_HEX_CONVERSION、REGEX、ENCODING。
# 方式一:命令中显式指定
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
不配置交叉编译时,CMake 默认用主机的 gcc,产物只能在 Ubuntu 上运行。要让产物跑在 ARM 开发板上,需要设置若干变量。I.MX6U 使用的交叉编译器为:
arm-poky-linux-gnueabi-gcc # C 编译器
arm-poky-linux-gnueabi-g++ # C++ 编译器
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,即用主机工具 |
方式一(不推荐):把配置直接写进 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 会执行它来配置交叉编译。
file main # 应显示 ARM 架构,而不是 x86-64
下面是一个多目录工程,综合运用 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.h 与 hello.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.h 与 math.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)
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make
tree .
预期产物:
build/
├── bin/cmake_demo
└── lib/
├── libhello.a
└── libmath.a
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 下运行即可。
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 在新增文件时不一定触发重新配置。
| 现象 | 原因 | 处理 |
|---|---|---|
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
| 维度 | 手写 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
要点:CMake 不直接编译,而是解析 CMakeLists.txt 生成本地化的 Makefile(或工程文件),最终仍由 make 编译。
详解:CMakeLists.txt 与平台无关,CMake 根据当前平台/工具链生成对应构建脚本;Makefile 的语法和规则在不同平台往往不同,不能通用。因此跨平台的本质是"用统一描述文件 + 生成器适配各平台",而真正干活的仍是 make。
追问:
cmake 和 make 分别负责什么?(cmake 生成构建文件,make 依据构建文件执行编译)include_directories 与 target_include_directories 有什么区别?该用哪个?要点:前者对当前源码所有目标生效并向下传递;后者只对指定目标生效,作用域由 PRIVATE/INTERFACE/PUBLIC 控制。
详解:target_include_directories(t PRIVATE d) 只给 t 用;INTERFACE 只传给依赖 t 的目标;PUBLIC 两者都传。全局命令在大工程里容易污染其它目标、产生隐蔽错误,因此推荐目标级命令以保持目录清晰。
追问:
PRIVATE 和 PUBLIC 的差别在哪?(头文件目录是否传递给依赖该目标的目标)PUBLIC 头文件目录会怎样传给可执行文件?(通过链接关系自动带入)要点:add_library(name [STATIC|SHARED] src);用 set_target_properties(NAME PROPERTIES OUTPUT_NAME ...) 改名。
详解:add_library 默认生成静态库,SHARED 生成动态库;库名不含 lib 前缀与 .a/.so 后缀,由 CMake 自动补齐。目标名工程内必须唯一,所以不能用目标名去改文件名,只能用 OUTPUT_NAME 属性。
追问:
BUILD_SHARED_LIBS 的作用?(设为 on 时 add_library 默认生成动态库).so 放到 /usr/lib 或设置 LD_LIBRARY_PATH)要点:编写工具链文件,设置 CMAKE_SYSTEM_NAME、CMAKE_SYSTEM_PROCESSOR、CMAKE_SYSROOT、CMAKE_C_COMPILER 等,再用 -DCMAKE_TOOLCHAIN_FILE= 指定。
详解:配置不要写进 CMakeLists.txt,而是放到独立工具链文件,且必须在 project() 之前生效。示例见本篇第 5.1 节。CMAKE_SYSROOT 会作为 --sysroot 传给 gcc,CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/INCLUDE 设为 ONLY 可保证只搜 sysroot。
追问:
-D 选项创建的是什么变量?(缓存变量,全局生效,覆盖同名普通变量)file 命令查看产物架构,应为 ARM 而非 x86-64)要点:函数作用域、目录作用域(值拷贝、向下有效)、全局作用域(缓存变量);函数内用 PARENT_SCOPE 修改上层变量。
详解:函数内 set 默认创建局部变量,不改变外层同名变量;加 PARENT_SCOPE 后设置到上一层作用域,可借"传入变量名"的技巧实现返回值。目录之间是值拷贝,子目录改父目录变量无效。-D 定义的缓存变量全局有效。
追问:
PARENT_SCOPE 在嵌套函数中写到哪一层?(调用者的作用域,即上一层)cmake 都要清理 build?(缓存变量会保留并覆盖新配置,可能导致旧设置残留)内容来源:《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMake入门与进阶;例程 33_mqtt/mqtt_prj/CMakeLists.txt、33_mqtt/mqtt_prj/cmake/arm-linux-setup.cmake。第 6.3 节 option()/install()/file(GLOB) 为扩展知识。