一、CMake 常用函数简析
1. 条件判断 if() / elseif() / else()
在 CMake 脚本中,条件判断是控制逻辑的重要工具。if()
支持多种比较语句,包括数值、字符串、布尔值和变量存在性等。在条件满足时执行特定逻辑代码,下面是典型语法:
if(VARIABLE)
# 当 VARIABLE 存在且为非空时执行
elseif(${VARIABLE} STREQUAL "value")
# 当 VARIABLE 等于指定字符串 "value" 时执行
else()
# 以上都不满足时执行
endif()
1.1 基本语句表达
if(<constant>)
如果常量是
1
、ON
、YES
、TRUE
、Y
或非零数字(包括浮点数),则为 True 。如果常量是0
、OFF
、NO
、FALSE
、N
、IGNORE
、NOTFOUND
、空字符串或以后缀-NOTFOUND
结尾,则为 False 。if(<variable>)
如果给定的变量被定义为非 false 常量的值,则为 True 。否则为 False ,包括变量未定义的情况。
if(<string>)
引用的字符串始终计算为 false,除了下面这些情况:
- 字符串的值是 true 常量之一
- 策略
CMP0054
未设置为NEW
,并且字符串的值恰好是受CMP0054
行为影响的变量名。(本章末尾将对CMP0054
策略进行介绍)
那么有个问题:
set(IS_STRING "This is string") if(IS_STRING) message(OK) endif()
请问会输出“OK”吗?
1.2 逻辑运算符
在 CMake 中,逻辑运算符被广泛应用于条件判断,帮助我们根据变量状态或构建条件调整逻辑流转。主要运算符如下:
运算符 | 含义 | 示例 |
---|---|---|
NOT |
逻辑非,取反 | NOT IS_EMPTY_STRING |
AND |
逻辑与,所有条件为真 | IS_TRUE AND NOT IS_FALSE |
OR |
逻辑或,任一条件为真 | IS_TRUE OR IS_FALSE |
我们直接通过代码来介绍:
option(IS_TRUE "This is true" ON) # True
option(IS_FALSE "This is false" OFF) # False
set(IS_EMPTY_STRING "") # False
set(IS_STRING "This is a string") # True
if(NOT IS_EMPTY_STRING) # True
message("IS_EMPTY_STRING is empty")
endif()
if(IS_STRING) # True
message("IS_STRING is: \"${IS_STRING}\"")
endif()
if(IS_TRUE AND NOT IS_FALSE) # True
message("IS_TRUE is true and IS_FALSE is false")
endif()
if(IS_FALSE OR NOT IS_TRUE) # False
message("IS_FALSE is true or IS_TRUE is false")
elseif(IS_TRUE) # True
message("IS_TRUE is true")
else() # 这句到不了
message("IS_FALSE is true")
endif()
1.3 存在检查
下面列举几种比较常用的检查:
1.3.1 if(DEFINED <name>|CACHE{<name>}|ENV{<name>})
如果定义了具有给定 <name>
的变量、缓存变量或环境变量,则为 True 。变量的值无关紧要。
1.3.2 if(TARGET <target-name>)
用于判断指定目标是否已通过 add_executable()
、add_library()
或 add_custom_target()
定义,并且无论目标是在哪个目录(包括子目录或父目录)中定义的都能检测到。
1.3.3 if(<variable|string> IN_LIST <variable>)
(3.3版本后才加入的)
如果给定元素包含在命名列表变量中,则为 True 。
1.4 文件操作
操作不少,这里只列出常用的(至少我在生产级别项目中常用的),想了解更多可以到官方教程看。
1.4.1 if(EXISTS <path-to-file-or-directory>)
如果指定的文件或目录存在且可读,则返回 True 。Linux 下,~/
不会被解析为主目录。
为了准确,尽可能使用绝对路径,例如:if(EXISTS ${CMAKE_BINARY_DIR}/Debug/VarApp.exe)
。
1.4.2 if(IS_DIRECTORY <path>)
如果 path
是目录,则为 True 。同样,path
传入绝对路径,避免出问题。
1.4.3 if(IS_ABSOLUTE <path>)
如果给定路径是绝对路径,则为 True 。注意以下特殊情况:
- 空的
path
计算结果为 False。 - 在 Windows 主机上,任何以驱动器号和冒号(例如
C:
)、正斜杠或反斜杠开头的path
都将计算为 True。这意味着像C:no\base\dir
这样的路径将计算为 True,即使路径的非驱动器部分是相对的。 - 在非 Windows 主机上,任何以波浪号开头的
path
(~
)都会被计算为 True。
1.5 比较
我直接把所有情况列出来吧,大家应该都能看懂:
if(<variable|string> MATCHES )
if(<variable|string> LESS <variable|string>)
if(<variable|string> GREATER <variable|string>)
if(<variable|string> EQUAL <variable|string>)
if(<variable|string> LESS_EQUAL <variable|string>)(在 3.7 版本中添加)
if(<variable|string> GREATER_EQUAL <variable|string>)(在 3.7 版本中添加)
if(<variable|string> STRLESS <variable|string>)
if(<variable|string> STRGREATER <variable|string>)
if(<variable|string> STREQUAL <variable|string>)
if(<variable|string> STRLESS_EQUAL <variable|string>)
if(<variable|string> STRGREATER_EQUAL <variable|string>)
要注意的是 if(<variable|string> MATCHES <regex>)
,大家在官方文档这里看支持的正则。
1.6 版本比较
用的很少,真想用就看官方文档吧。
3.24 版本中加入了
if(<variable|string> PATH_EQUAL <variable|string>)
,可以用来更准确地比较两个地址。我没用过,不敢胡乱写,大家可以看官方文档。
1.4 模拟运用场景
1.4.1 操作系统判断
判断当前操作系统,根据平台选择不同的编译选项。
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
message("On Windows")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
message("On Linux")
else()
message("Other system")
endif()
endif()
1.4.2 模块启用控制
动态启用或禁用某些功能模块,通过变量开关来实现。
option(ENABLE_LOGS "Enable logging" ON)
if(ENABLE_LOGS)
add_definitions(-DENABLE_LOGS) # 添加全局的 log 输出宏
elseif(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(WARNING "Logging is disabled") # Debug 模式不应该禁止log输出
endif()
1.4.3 外部依赖(或者叫外部文件)检查
检查第三方库文件是否存在。
set(LIB_PATH "/usr/lib/libz.so")
if(EXISTS ${LIB_PATH} AND NOT IS_DIRECTORY ${LIB_PATH})
message(STATUS "Library found: ${LIB_PATH}")
else()
message(FATAL_ERROR "Library not found")
endif()
message(FATAL_ERROR "Library not found")
这里将会使得 CMake 构建失败,属于自定义错误。
2. 循环 foreach()
和 while()
2.1 foreach()
的三种用法
2.1.1 基于范围:foreach(<loop_var> RANGE <stop>)
foreach(i RANGE 5)
message(STATUS "Current value: ${i}")
endforeach()
2.1.2 基于范围,指定起点和步长:foreach(<loop_var> RANGE <start> <stop> [<step>])
如果不指定 step
,默认为1。
foreach(i RANGE 1 10 2)
message(STATUS "Current step: ${i}")
endforeach()
2.1.3 基于列表或值的迭代:foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]])
set(FRUITS Apple Banana Cherry)
list(APPEND COLORS Blue Red Orange Pink)
foreach(fruit IN LISTS FRUITS)
message(STATUS "Current fruit: ${fruit}")
endforeach()
foreach(fruit IN LISTS FRUITS COLORS)
message(STATUS "Current fruit: ${fruit}")
endforeach()
2.2 while()
的用法
其实有点废话:
while(<condition>)
<commands>
endwhile()
condition
部分与 if
是一样的,对着用就好。
2.3 break()
和 continue()
不介绍了,可以用在 foreach()
和 while()
中。
要是看不懂怎么用,基本上就告别开发了。。。
3. 消息输出 message()
我们也经常用到了,它最常用的方式就是输出内容到命令行。
message([<mode>] "message text" ...)
接下来介绍一下各个 mode
:
- FATAL_ERROR:CMake 错误,停止处理和生成。
- SEND_ERROR:CMake 错误,继续处理,但跳过生成。
- WARNING:CMake 警告,继续处理。
- STATUS:项目用户可能感兴趣的主要信息。这些信息应该简洁,不超过一行,但仍然具有信息量。
上面这几个就是常用的,其他的我是没用过,你们想用看官方文档吧。
4. 其他常用函数
还有两个比较重要的函数:file()
和 string()
,主要作用如下:
file()
:创建文件夹、读取/写入文件、遍历目录等。string()
:处理字符串(替换、拼接和分割等)。
由于它们的作用比较泛且杂,我打算在后面用到再说。
现在完全介绍这两个函数并不能很好的起到学习 CMake 的作用,因为脱离了实践!
二、构建案例:跨平台多线程项目
我们通过一个实例来熟悉 CMake 在跨平台方面的使用。
项目结构:
ThreadSimple
├── CMakeLists.txt
├── main.c
├── thread_wrapper.c # 线程封装实现
└── thread_wrapper.h # 线程封装头文件
thread_wrapper.h 内容:
#ifndef THREAD_WRAPPER_H
#define THREAD_WRAPPER_H
// 跨平台县城宏定义
#ifdef _WIN32
#include <windows.h>
typedef HANDLE thread_t; // Windows 线程类型
typedef DWORD thread_return_t; // Windows 线程函数返回类型
typedef LPVOID thread_param_t; // Windows 线程参数类型
#else
#include <pthread.h>
typedef pthread_t thread_t; // POSIX 线程类型
typedef void *thread_return_t; // POSIX 线程函数返回类型
typedef void *thread_param_t; // POSIX 线程参数类型
#endif
/// 通用线程函数指针
typedef thread_return_t (*thread_func_t)(thread_param_t arg);
/// 创建线程
int create_thread(thread_t *thread, thread_func_t func, void *arg);
/// 等待线程执行完成
void join_thread(thread_t thread);
#endif //! THREAD_WRAPPER_H
thread_wrapper.c 内容:
#include "thread_wrapper.h"
int create_thread(thread_t *thread, thread_func_t func, void *arg)
{
#ifdef _WIN32
*thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)func, (LPVOID)arg, 0, NULL);
return *thread == NULL ? -1 : 0;
#else
return pthread_create(thread, NULL, func, arg);
#endif
}
void join_thread(thread_t thread)
{
#ifdef _WIN32
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
#else
pthread_join(thread, NULL);
#endif
}
main.c 内容:
#include <stdio.h>
#include "thread_wrapper.h"
#define THREAD_COUNT 5 // 线程数量
/// 线程函数
thread_return_t thread_function(thread_param_t arg)
{
int thread_num = *((int *)arg);
printf("Thread %d is running\n", thread_num);
// 模拟线程任务
for (int i = 0; i < 5; i++) {
printf("Thread %d: %d\n", thread_num, i);
}
return 0;
}
int main()
{
thread_t threads[THREAD_COUNT]; // 线程句柄
int thread_args[THREAD_COUNT]; // 线程参数
// 创建线程
for (int i = 0; i < THREAD_COUNT; i++) {
thread_args[i] = i + 1;
if (create_thread(&threads[i], thread_function, &thread_args[i]) != 0) {
printf("Error creating thread %d\n", i + 1);
return -1;
}
}
// 等待线程完成
for (int i = 0; i < THREAD_COUNT; i++) {
join_thread(threads[i]);
}
printf("All threads finished.\n");
return 0;
}
从源码上看,我们需要在合适的平台上添加 pthread
库作为依赖。
直接上 CMakeLists.txt 代码:
cmake_minimum_required(VERSION 3.21)
project(ThreadSimple LANGUAGES C) # 指定项目名称和语言
set(CMAKE_C_STANDARD 99) # 指定C标准为C99
add_executable(${PROJECT_NAME})
target_sources(${PROJECT_NAME} PRIVATE main.c thread_wrapper.c)
# 只有在非Windows平台才链接pthread库
if(NOT WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE pthread)
endif()
WIN32
变量只有在 Windows 系统下才存在。
project()
中使用了 LANGUAGES C
,也就意味着只用到了C语言,CMake 不会去查找以及使用C++编译器。
大家也发现,我并没有在 add_executable()
函数内添加源文件,而是使用了 target_sources()
函数,target_sources()
的用法和第一章讲的 target_link_libraries
非常相似,可以借鉴。
Linux 执行结果:
Windows 执行结果:
【CMake 教程】常用函数与构建案例解析(三) 结束,希望大家提提意见,欢迎指正!