第44例:平台检测——CMAKE_SYSTEM_NAME 判断 Windows/Linux/macOS

跨平台开发,说白了就是「一套代码,到处编译」。但不同操作系统之间,API 不同、库路径不同、编译器行为也不同。CMake 怎么知道当前在哪个平台上?靠的就是 CMAKE_SYSTEM_NAME 这个内置变量。

我个人习惯在项目一开始就做平台检测,而不是等到编译报错了再回头改。你想想看,如果代码里写了 Windows 特有的 Sleep(),拿到 Linux 上编译,那错误信息能让你排查半天。提前判断平台,把条件分支写好,后面就省心了。

CMAKE_SYSTEM_NAME 是什么?

CMake 在配置阶段会自动检测当前操作系统,并把结果存到 CMAKE_SYSTEM_NAME 里。它的值是一个字符串,常见的有:

操作系统 CMAKE_SYSTEM_NAME 值
Windows Windows
Linux Linux
macOS Darwin
Android Android
iOS iOS
FreeBSD FreeBSD

注意 macOS 的值是 Darwin,不是 macOS 也不是 OSX。我记得刚用 CMake 时,第一反应写了个 if(CMAKE_SYSTEM_NAME STREQUAL "macOS"),结果死活不生效。后来查文档才发现是 Darwin——嗯,这个坑我替你们踩过了。

基本用法:if 判断平台

最直接的方式就是用 if() 命令做字符串比较。来看一个典型例子:

cmake_minimum_required(VERSION 3.10)
project(PlatformCheck)

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "当前平台:Windows")
    # Windows 特有的设置
    set(PLATFORM_LIBS ws2_32)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "当前平台:Linux")
    # Linux 特有的设置
    set(PLATFORM_LIBS pthread)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "当前平台:macOS")
    # macOS 特有的设置
    set(PLATFORM_LIBS "")
else()
    message(WARNING "未知平台:${CMAKE_SYSTEM_NAME}")
endif()

message(STATUS "平台相关库:${PLATFORM_LIBS}")

这段代码会在配置阶段打印出当前平台,并设置对应的链接库。我在项目中经常用这种模式来区分 pthread(Linux)、ws2_32(Windows)和 CoreFoundation(macOS)等系统库。

更简洁的写法:使用平台简写变量

CMake 还提供了一些简写变量,不用每次都写字符串比较:

  • WIN32:在 Windows 上为 TRUE(包括 64 位)
  • UNIX:在 Linux、macOS、BSD 等类 Unix 系统上为 TRUE
  • APPLE:在 macOS、iOS 等 Apple 系统上为 TRUE

举个例子:

if(WIN32)
    message("这是 Windows 系统")
elseif(APPLE)
    message("这是 Apple 系统")
elseif(UNIX)
    message("这是类 Unix 系统(非 Apple)")
endif()

我个人更推荐用 CMAKE_SYSTEM_NAME 做精确判断,因为 UNIXAPPLE 的范围比较宽,有时候会误判。比如你想专门给 Linux 写逻辑,用 if(UNIX) 会把 macOS 也带进来,这时候就得再加一层判断。

实战:根据平台设置编译选项

跨平台项目里,不同平台的编译器选项往往不一样。比如 Windows 上常用 /W4 开启警告,Linux 和 macOS 上用 -Wall -Wextra。下面是一个完整的例子:

cmake_minimum_required(VERSION 3.10)
project(CrossPlatformDemo CXX)

# 根据平台设置编译选项
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    # MSVC 风格
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
    # 定义 Windows 平台宏
    add_definitions(-DPLATFORM_WINDOWS)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    # GCC/Clang 风格
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread")
    add_definitions(-DPLATFORM_LINUX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -stdlib=libc++")
    add_definitions(-DPLATFORM_MACOS)
endif()

# 添加源文件
add_executable(myapp main.cpp)

# 平台相关的链接库
target_link_libraries(myapp PRIVATE ${PLATFORM_LIBS})

这里我用了 add_definitions() 来定义预处理宏,这样在 C++ 代码里就可以用 #ifdef PLATFORM_WINDOWS 来写平台相关的代码了。不过要注意,add_definitions() 会影响整个项目,如果只想对某个目标生效,建议用 target_compile_definitions()

避坑指南:交叉编译时的 CMAKE_SYSTEM_NAME

我曾经在做一个嵌入式项目时,用 CMake 交叉编译 ARM Linux 程序。在工具链文件里手动设置了 set(CMAKE_SYSTEM_NAME Linux),结果 CMake 检测到宿主机是 Windows,就自动把 CMAKE_SYSTEM_NAME 改回了 Windows

为什么会这样?因为交叉编译时,你必须显式地在工具链文件里设置 CMAKE_SYSTEM_NAME,并且要放在 project() 命令之前。否则 CMake 会认为你在为本机编译,覆盖掉你的设置。

正确的做法:

# 工具链文件 toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 然后在 CMakeLists.txt 中
cmake_minimum_required(VERSION 3.10)
# 这里 project() 会读取上面设置的 CMAKE_SYSTEM_NAME
project(CrossProject)

# 此时 CMAKE_SYSTEM_NAME 就是 Linux,而不是宿主机的 Windows

平台检测的完整知识结构

下面这张图总结了平台检测的核心逻辑和常用变量:

CMake 平台检测知识结构 CMAKE_SYSTEM_NAME Windows WIN32 = TRUE MSVC / MinGW 编译器 链接 ws2_32.lib Linux UNIX = TRUE GCC / Clang 编译器 链接 pthread / rt Darwin (macOS) APPLE = TRUE Clang 编译器 链接 CoreFoundation 核心原则:先检测,后编译,避免平台假设 交叉编译时需在工具链文件中显式设置 CMAKE_SYSTEM_NAME

常用平台检测模式总结

根据我多年的项目经验,下面几种模式最常用:

  • 精确匹配if(CMAKE_SYSTEM_NAME STREQUAL "Linux") —— 适合需要精确区分平台的场景
  • 范围匹配if(WIN32)if(UNIX) —— 适合大类区分,比如 Windows vs 非 Windows
  • 组合判断if(UNIX AND NOT APPLE) —— 适合「Linux 但不包括 macOS」的场景
  • 交叉编译检测if(CMAKE_CROSSCOMPILING) —— 配合 CMAKE_SYSTEM_NAME 使用
小技巧: 在 CMakeLists.txt 开头加一行 message(STATUS "CMAKE_SYSTEM_NAME = ${CMAKE_SYSTEM_NAME}"),配置时就能看到当前平台,方便调试。
注意: 不要在 project() 命令之前依赖 CMAKE_SYSTEM_NAME 做复杂逻辑。因为 project() 会触发编译器检测,可能会修改这个变量。如果你需要在 project() 之前判断平台,建议用 CMAKE_HOST_SYSTEM_NAME(宿主机的系统名)。

好了,这一例的内容就这些。平台检测是跨平台 CMake 的基石,掌握了 CMAKE_SYSTEM_NAME,后面再处理平台相关的库路径、编译器选项、预处理宏,就都顺理成章了。


公众号:蓝海资料掘金营,微信 deep3321