Make 与 CMake 构建笔记
面向 Linux / C/C++ / 交叉编译(ARM、MIPS uClibc 工具链)的构建笔记。
Make 基础概念
Make 通过比较**目标(target)与其依赖(prerequisite)的修改时间(mtime)**决定是否重建:只要任一依赖比目标新,就执行该目标的命令,并沿依赖图递归向上。
- Makefile 中第一个 target 是默认目标(下文示例里是
all),make不带参数时构建它。 - 常用调试开关:
make -n:dry-run,只打印将执行的命令,不真正执行。make -p:打印 make 内部的规则与变量数据库。make -d:打印为什么某个目标被判定需要重建。
- NFS/时钟偏差可能导致 mtime 误判,必要时
make clean重来。
交叉编译静态库示例
一个完整的交叉编译静态库 Makefile(已修正原示例中的 ar 修饰符与缺失的 .PHONY):
# 交叉编译器前缀
CROSS_COMPILER := arm-mol-linux-uclibcgnueabihf-
CXX := $(CROSS_COMPILER)g++
AR := $(CROSS_COMPILER)ar
PROJECT_ROOT := /home/meian/work/SVN/nvr/plh/eavs_FH_list/eavs_mc6830/project
# 编译选项:只放编译期标志。
# -L ./lib -lfdk-aac 属于“链接期”选项,不应放进 CXXFLAGS(编译 .o 阶段用不到)
CXXFLAGS = -std=c++11 -Wall -O0
FDK_AAC_INC = -I./include -I./include/fdk-aac -I../mc6830/include
# 静态库名称
LIBNAME = libG711toAAC.a
# 源文件列表
CPPSRC := AACEncoder.cpp G711toAAC.cpp ../mc6830/G711.cpp
# 目标文件列表(把 .cpp 后缀替换成 .o)
CPPOBJ = $(CPPSRC:.cpp=.o)
# 默认目标
all: lib
# 模式规则:编译每个 .cpp -> .o
%.o: %.cpp
$(CXX) $(CXXFLAGS) $(FDK_AAC_INC) -c $< -o $@
# 归档静态库
# 说明:原示例用的是 rcu,其中 u(仅更新更新的成员)在现代 binutils 默认的
# “确定性归档”模式(D)下会被忽略并告警,故改为 rcs:s 显式表明写入符号索引
lib: $(CPPOBJ)
$(AR) rcs $(LIBNAME) $(CPPOBJ)
# 仅当使用该 ARM 交叉编译器时,把生成的静态库拷到工程 lib 目录
# 该 ifeq 为解析期条件,紧跟 lib 规则,其下的 cp 会追加到 lib 的 recipe
ifeq ($(CROSS_COMPILER),arm-mol-linux-uclibcgnueabihf-)
cp $(LIBNAME) $(PROJECT_ROOT)/lib_mc6830/
endif
# 清理生成文件
clean:
rm -f $(LIBNAME) $(CPPOBJ)
# 声明伪目标:避免与同名文件冲突(见“伪目标”一节)
.PHONY: all lib clean
关于
ar修饰符:GNU ar 的r操作会自动写入符号索引(armap),rc/rcs均可直接链接,无需再跑ranlib;只有大写S才会抑制索引。rcs里的s只是把“写索引”的意图显式化,并对非 GNU 的 ar 更具可移植性。
Makefile 变量与赋值
四种赋值运算符的差异会直接影响标志拼接与工具链切换,是最容易踩的坑:
| 运算符 | 名称 | 展开时机 | 典型用途 |
|---|---|---|---|
= | 递归 / 延迟展开 | 每次引用时重新求值(可引用后面才定义的变量) | 一般变量;注意 X = $(X)... 会无限递归 |
:= | 立即 / 简单展开 | 定义时求值一次 | 路径、CROSS_COMPILE 前缀、$(shell ...) 结果,避免反复求值 |
?= | 条件赋值 | 仅当变量未定义时赋值 | 允许命令行/环境覆盖,如 CROSS_COMPILE ?= arm-mol-...- |
+= | 追加 | 继承左侧原有的展开语义 | CXXFLAGS += -g |
其它要点:
$(VAR)与${VAR}等价。- 命令行传入的变量优先级最高:
make CROSS_COMPILE=mips-linux-会覆盖 Makefile 里的同名赋值(除非用override)。 - 用
:=冻结$(shell ...)结果,用=则每次引用都会重新执行 shell:
GITVER := $(shell git rev-parse --short HEAD) # 只执行一次
GITVER2 = $(shell git rev-parse --short HEAD) # 每次引用都执行一次
- recipe 里访问 shell 变量要写两个
$:$$HOME(单个$会被 make 当成自己的变量)。
Target、Prerequisite、Recipe
规则的三要素:
target: prerequisites
recipe # 必须以 TAB 开头
- recipe 行必须以真实 TAB 开头,用空格会报
*** missing separator. Stop.——编辑器把 TAB 自动转空格是最高频踩坑。 - 每条 recipe 行在独立的 shell 中执行;跨行要么行尾加
\续行,要么用;连接(如cd build && make)。 - order-only 依赖:
target: normal-prereqs | order-only-prereqs。竖线右侧的依赖只保证“先存在”,其 mtime 变化不会触发重建,常用于先建输出目录:
OBJDIR := build
$(OBJDIR)/%.o: %.cpp | $(OBJDIR) # $(OBJDIR) 是 order-only 依赖
$(CXX) $(CXXFLAGS) -c $< -o $@
$(OBJDIR):
mkdir -p $@
模式规则与自动变量
自动变量表
| 变量 | 含义 |
|---|---|
$@ | 目标名 |
$< | 第一个依赖(编译规则里就是源文件) |
$^ | 全部依赖,去重(写链接行的对象列表用它) |
$+ | 全部依赖,不去重、保持顺序(链接顺序敏感时有用) |
$? | 比目标新的依赖 |
$* | 模式规则中 % 匹配到的茎(stem) |
$(@D) / $(@F) | 目标的目录部分 / 文件名部分 |
链接规则示例:
app: $(CPPOBJ)
$(CXX) $^ -o $@ $(LDFLAGS) # $^ = 所有 .o
静态模式规则与 VPATH
- 静态模式规则:只对指定目标列表套用模式,比通配
%.o: %.cpp更可控:
$(CPPOBJ): %.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
- 源文件散落在多目录(如本例的
../mc6830/)时,用vpath让 make 跨目录搜索:
vpath %.cpp ../mc6830 # 只对 .cpp 生效
# VPATH = ../mc6830:../common # 对所有类型生效
自动头文件依赖生成
%.o: %.cpp 只把 .cpp 列为依赖,头文件对 make 不可见——改了 .h 不会触发重编,会链接进过期的 .o,是典型的隐蔽 bug。让编译器生成依赖文件即可解决:
CXXFLAGS += -MMD -MP
-include $(CPPOBJ:.o=.d) # 放在 Makefile 末尾
-MMD:生成每个.o对应的.d依赖文件,只记录用户头文件(-MD连系统头也记)。-MP:为每个头文件生成一个空的伪目标,避免头文件被删后 make 报“无规则可用”。
编译和链接选项
编译分阶段
-E:只做预处理,输出到 stdout。-S:编译到汇编(.s)。-c:编译+汇编到目标文件.o,不链接(编译多文件工程的常态)。- 不带上述选项:一路编译并链接成可执行文件。
gcc -c hello.c -o hello.o # 只编译,产出 .o
gcc hello.o -o hello # 链接
编译期 vs 链接期选项
| 阶段 | 常用选项 |
|---|---|
| 编译期 | -I<dir> -D<宏> -std= -O0/-O2/-Os -g -Wall -Wextra -Werror -f*(如 -fPIC) |
| 链接期 | -L<dir> -l<name> -Wl,<原样传给 ld 的选项> -shared -static -pthread |
-I(大写 i):头文件搜索目录——把<dir>加入预处理时#include的搜索路径(参数是目录,不是某个头文件)。-L(大写 L):库文件搜索目录。-l(小写 L):链接指定库(-lworld找libworld.so或libworld.a)。
gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld
- 嵌入式常用体积/优化选项:
-Os(为体积优化,flash 受限时常用)、-Og(可调试的优化);配合-ffunction-sections -fdata-sections+ 链接期-Wl,--gc-sections剔除未用段;strip(或链接加-s)去符号进一步减小体积。
-fpic、-fPIC、-fpie、-fPIE 的区别
(原为空标题,此处补齐。)
-fPIC:生成位置无关代码(Position-Independent Code),共享库(.so)必需——凡是要进.so的.o都得用它编译。-fpic(小写):同样是位置无关代码,但用更小/更快的 GOT 寻址序列,受平台 GOT 表大小限制;MIPS/ARM 上大型库容易溢出,拿不准就用-fPIC。-fPIE/-fpie:位置无关可执行文件(Position-Independent Executable),配合 ASLR;-fPIE是编译期标志,链接期还要加-pie。- PIC vs PIE:PIC 允许符号插入(interposition,用于库);PIE 面向主程序、可做更激进优化。
- 交叉/嵌入式注意:很多 uClibc 工具链默认构建非 PIE;把用
-fPIC编的.o混进非 PIC 的静态库再链进.so,会报recompile with -fPIC的重定位错误。
静态库与动态库
创建
- 静态库(.a):把多个
.o打包,链接时只抽取被引用到的成员,代码进最终可执行文件(体积大、无运行期依赖)。
$(CROSS)gcc -c a.c b.c
$(CROSS)ar rcs libfoo.a a.o b.o # s = 写符号索引
- 动态库(.so):编译加
-fPIC,链接加-shared,运行期由动态加载器解析(体积小、多进程共享)。
$(CROSS)gcc -fPIC -c a.c b.c
$(CROSS)gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 a.o b.o
soname 与三级命名
- 用
-Wl,-soname,libfoo.so.1记录 soname。 - 磁盘惯例:真实文件
libfoo.so.1.0.0← 运行期符号链接libfoo.so.1(= soname)← 开发期符号链接libfoo.so(-lfoo链接时找它)。
链接顺序敏感(高频 undefined reference 根源)
链接器从左到右单遍处理,库必须出现在引用它符号的对象/库之后,否则报 undefined reference:
# G711toAAC 用到 fdk-aac,所以 fdk-aac 放在它右边
$(CXX) main.o libG711toAAC.a -L./lib -lfdk-aac -o app
- 两个静态库相互依赖时:重复列出,或用
-Wl,--start-group libA.a libB.a -Wl,--end-group。 -Wl,-Bstatic/-Wl,-Bdynamic可强制某段按静态/动态链接。
运行期库查找与排查
- 加载器查找顺序:
DT_RPATH→LD_LIBRARY_PATH→DT_RUNPATH→/etc/ld.so.cache(ldconfig)→ 默认目录。 - 嵌入搜索路径:
-Wl,-rpath,/usr/local/lib,或用$ORIGIN相对可执行文件定位-Wl,-rpath,'$ORIGIN/../lib'。 - 交叉编译时为传递性
.so依赖指定仅链接期搜索路径:-Wl,-rpath-link,<dir>。 - 全静态可执行文件(最小 rootfs 常用):
-static;只静态化工具链运行时:-static-libgcc -static-libstdc++。 - 排查:
arm-...-readelf -d a.out看NEEDED/RPATH;readelf -h a.out看架构与 ABI。
Linux 下的符号可见性(对应 Windows 的 dllexport/dllimport)
下面这段 xlog_API 宏是 Windows 的库导出/导入写法(__declspec(dllexport/dllimport));在 Linux 分支上它展开为空,也就没法控制 .so 到底导出哪些符号。
#ifndef XLOG_H
#define XLOG_H
// __declspec(dllexport) 导出 xLog 类的函数到 lib 文件中
// __declspec(dllimport) 导入 xLog 类的函数到 exe 文件中
#ifndef _WIN32 // linux
# define xlog_API
#else
# ifdef xlog_STATIC // 静态库
# define xlog_API
# else
# ifdef xlog_EXPORTS
# define xlog_API __declspec(dllexport) // 库项目
# else
# define xlog_API __declspec(dllimport) // 调用方
# endif
# endif
#endif
class xlog_API XLog
{
public:
XLog();
~XLog() = default;
};
#endif // XLOG_H
Linux/GCC 的对应机制是 visibility 属性:
- 用
__attribute__((visibility("default")))标记要导出的符号,编译.so时加-fvisibility=hidden把其余符号默认隐藏。 - 即把上面 Linux 分支从
#define xlog_API(空)改为构建共享库时定义为__attribute__((visibility("default")))。 - 好处:减小
.so符号表、加快目标机加载,是 dllexport/dllimport 的跨平台对应物。
伪目标
避免和同名文件冲突
如果一个规则并不产生与目标同名的文件,则其命令在每次 make 该目标时都应执行。例如:
clean:
rm *.o temp
因为 rm 命令并不产生名为 clean 的文件,正常情况下每次 make clean 都会执行。但如果目录里恰好出现了名为 clean 的文件,规则就失效了:它没有依赖,clean 文件永远被视为“最新”,命令永远不执行。用 .PHONY 声明可避免此问题:
.PHONY: clean
clean:
rm *.o temp
这样 make clean 会无视是否存在 clean 文件,始终执行。
常备伪目标清单
工程里常声明为伪目标的:all clean distclean install uninstall test。可集中声明:
.PHONY: all clean install
.PHONY 除避免与同名文件冲突外,还能让 make 跳过对该名字的文件系统 stat 检查,略微提速。
递归 Make
内核模块编译
-C <dir>:进入<dir>目录后再执行其 Makefile。M=:传给内核 Makefile 的变量,指定外部模块源码目录。
make -C $(KDIR) M=$(PWD) modules
跨目录递归要点
- 子目录调用用
$(MAKE)而非裸make——这样-jN、MAKEFLAGS、jobserver 才能正确下传,并行才生效:
sub:
$(MAKE) -C subdir
export VAR把变量下传给子 make;命令行传入的变量(如make CROSS_COMPILE=...)默认自动下传。make -jN并行构建;个别有竞态的目标用.NOTPARALLEL强制串行,或用 order-only 依赖保证顺序。- 递归 make 的固有缺点是跨目录依赖不完整(Recursive Make Considered Harmful);大工程可考虑非递归单 Makefile,或改用 CMake。
常见问题
典型编译/链接/交叉错误排查
| 报错 | 原因与处理 |
|---|---|
*** missing separator. Stop. | recipe 用了空格而非 TAB |
undefined reference to X | 缺库,或库顺序错(库要放在引用它的对象右侧),循环依赖用 --start-group |
cannot find -lfoo | -L 路径或库名不对 |
wrong ELF class / file 显示架构不符 | 误用了主机 gcc 或错的工具链 |
version 'GLIBC_2.xx' not found / ABI 报错 | 目标机 libc 与工具链不匹配(uClibc 与 glibc 混用尤甚) |
fatal error: xxx.h: No such file or directory | 缺 -I,或交叉编译未设 --sysroot 导致系统头缺失 |
找不到头文件(CMake)
main.cpp:10:10: fatal error: gpio.h: No such file or directory
#include "gpio.h"
^~~~~~~~
排查思路:确认头文件实际所在目录,然后用 target_include_directories(见“CMake Target 化用法”)把该目录加入目标的头文件搜索路径;交叉编译还要确认系统头是否在 sysroot 内。
待确认:原笔记此处「目录结构」示意图缺失,具体工程的目录布局需本人补充。
设置 gdb 调试模式(CMake)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -Wall")
如果不把 CMAKE_BUILD_TYPE 设成 Debug,CMake 根本不会去读 CMAKE_CXX_FLAGS_DEBUG,只会用无后缀的 CMAKE_CXX_FLAGS。
CMake 基础
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
生成 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"
]
}
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。
第一个 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 # 构建
路径相关变量
| 变量 | 含义 |
|---|---|
CMAKE_SOURCE_DIR | 整个构建的顶层源码目录(第一次进入的 CMakeLists.txt 所在目录),不随 add_subdirectory() 或子目录的 project() 改变 |
PROJECT_SOURCE_DIR | 当前 project() 的源码目录。顶层时等于 CMAKE_SOURCE_DIR;子目录再次调用 project() 后,在其作用域内变为子项目根 |
CMAKE_CURRENT_SOURCE_DIR | 当前 CMakeLists.txt 所在目录(写相对本目录路径最稳) |
CMAKE_CURRENT_LIST_DIR | 当前被处理文件(含被 include 的 .cmake 模块)所在目录 |
用法建议:
# 本模块内的路径用 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
编译类型与 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)
追加编译选项
# 注意:必须加引号,否则会存成分号列表 "-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)
# 头文件搜索路径(全局,不递归子目录)
include_directories(${PROJECT_SOURCE_DIR}/include)
收集源文件
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 FILE ...) 中的 FILE 不是合法关键字,
# 会被当成匹配不到的 glob 模式静默吞掉。删掉即可:
file(GLOB SRC2 ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
FILE(GLOB H_FILE "${INCLUDE_PATH}/xcpp/*.h")
file(GLOB)合法选项只有LIST_DIRECTORIES/RELATIVE/CONFIGURE_DEPENDS。且 GLOB 结果在配置阶段固定,增删文件不会自动重配(除非用CONFIGURE_DEPENDS);单个已知源文件建议直接set(SRC ...)。
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 才显示


设置显示级别与重定向:
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,多出的第二个>是 bash 语法错误,命令根本无法执行;正确写法是> log.txt 2>&1。

查找库日志(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}")
前景色 / 背景色 / 颜色(修正:黄色背景码为 43,原表误写作 34)
前景色 背景色 颜色
30 40 黑色
31 41 红色
32 42 绿色
33 43 黄色
34 44 蓝色
35 45 紫红色
36 46 青蓝色
37 47 白色

变量使用
- 变量引用是值替换,未设置的变量返回空字符串。
- 变量引用可嵌套,从内向外求值。
- 变量名大小写敏感。
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")

if 流程控制
if(<conditional>)
elseif(<conditional>)
else()
endif()
if(<constant>) 常量:
ON,YES,TRUE,Y或非零数(含浮点数)→ 真。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()

一元 / 二元 / 正则匹配
一元判断:DEFINED(变量是否定义)、COMMAND(是否为已定义的命令/宏/函数)、EXISTS(路径是否存在)。
二元判断:EQUAL(数字相等)、STREQUAL(字符串相等)。
set(VAR1 123)
if(VAR1 EQUAL 123) message("VAR1 EQUAL 123") endif()
if(VAR1 STREQUAL "123") message("VAR1 STREQUAL 123") endif() # 推荐用 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()

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 强制修改

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交互修改。


普通变量与缓存变量重名(CACHE 覆盖)
set(normalVar1 "normal var1")
set(normalVar1 "Cache var1" CACHE STRING "cache doc")
message("normalVar1 = ${normalVar1}")
message("CACHE{normalVar1} = $CACHE{normalVar1}")

# CMP0126 策略:OLD 删除同名普通变量;NEW 保留普通变量(读 cache 需用 $CACHE{VAR})
cmake_policy(SET CMP0126 OLD)

用 -D 传递缓存变量
cmake -S . -B b5 -D PARA1=para00001
第二次不带 -D 运行仍会保留 PARA1(已写入 cache),除非再次 -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}") # 找到则为绝对路径

CMake 自带变量
- 提供信息:
PROJECT_NAME(对应project()的名字)。 - 改变行为:
BUILD_SHARED_LIBS(ON 动态库 / OFF 静态库,默认 OFF)。 - 描述系统:
MSVC、WIN32(=1 为 Windows)、UNIX(=1 为 Linux)、CMAKE_SYSTEM_NAME。 - 控制构建:
CMAKE_COLOR_MAKEFILE、输出路径等。
set(BUILD_SHARED_LIBS ON) # 后续 add_library 默认建动态库
add_definitions(-Dxlog_STATIC) # 向 C++ 传宏(现代写法见下节 target_compile_definitions)
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)


分步编译与清理指定 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 # 清理

调试:打印生成的具体指令
set(CMAKE_VERBOSE_MAKEFILE ON) # 默认 OFF
- 或
cmake --build . -v(第一次生成就要加-v,否则日志不完整,可清理后重生成)。
输出路径设置
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 静态库
括号注释
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)

作用域类型:GLOBAL(全局)、DIRECTORY [<dir>](目录)、TARGET <target>(目标)、SOURCE <src>(源文件)、INSTALL <file>、TEST <test>、CACHE <entry>、VARIABLE。
CMake Target 化用法
现代 CMake 提倡以「目标(target)」为中心组织依赖,而非全局的 include_directories / add_definitions / link_libraries。
target_include_directories
为特定目标设置头文件搜索目录,支持三种作用域:
target_include_directories(<target> [PRIVATE|PUBLIC|INTERFACE] <dirs>)
PRIVATE:仅当前目标可见,下游不继承。PUBLIC:当前目标 + 依赖它的下游都可见。INTERFACE:当前目标不用,只有下游用。
target_link_libraries(依赖传播)
组织多库交叉编译工程的核心,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 链接静态库」一节只有 add_executable + include_directories,缺真正的链接命令,补齐如下:
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 告诉 cmake 库在磁盘的位置)
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)

优先用
target_link_libraries而非全局的link_directories()/link_libraries()。
set_target_properties
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输出确认后再补。