25、CMake跨平台编译:平台判断、编译器检测、系统库查找、跨平台注意事项

跨平台编译,说白了就是让同一份CMake代码,能在Windows、Linux、macOS上都能跑通。我刚开始接触跨平台时,觉得不就是改改路径吗?结果被坑得挺惨。今天咱们就把这块掰开揉碎,讲清楚。

25.1 平台判断:你得知道自己在哪

CMake提供了几个内置变量,用来判断当前是什么操作系统。我个人习惯用CMAKE_SYSTEM_NAME,它最直接。

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    message(STATUS "当前是Windows平台")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    message(STATUS "当前是Linux平台")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    message(STATUS "当前是macOS平台")
else()
    message(WARNING "未知平台: ${CMAKE_SYSTEM_NAME}")
endif()

嗯,这里要注意:CMAKE_SYSTEM_NAME在macOS上返回的是Darwin,不是macOS。我第一次写的时候直接写了STREQUAL "macOS",结果死活不匹配。后来查文档才发现这个坑。

除了CMAKE_SYSTEM_NAME,还有几个常用的:

变量名 说明 示例值
WIN32 Windows平台时为true TRUE / FALSE
UNIX Unix-like平台时为true(包括Linux、macOS) TRUE / FALSE
APPLE Apple平台时为true(macOS、iOS等) TRUE / FALSE
CMAKE_SYSTEM_PROCESSOR CPU架构 x86_64 / ARM64
小技巧:判断平台时,优先用CMAKE_SYSTEM_NAME,因为它最精确。WIN32UNIX虽然方便,但有些交叉编译场景下会不准。

25.2 编译器检测:别让编译器类型坑了你

不同编译器对C++标准的支持程度不一样。我记得有一次项目在GCC上编译通过,换到MSVC就报了一堆错。从那以后,我养成了检测编译器的习惯。

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    message(STATUS "编译器: GCC ${CMAKE_CXX_COMPILER_VERSION}")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    message(STATUS "编译器: Clang ${CMAKE_CXX_COMPILER_VERSION}")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    message(STATUS "编译器: MSVC ${CMAKE_MSVC_VERSION}")
else()
    message(WARNING "未知编译器: ${CMAKE_CXX_COMPILER_ID}")
endif()

你想想看,如果代码里用了GCC的扩展语法,在MSVC上肯定编译不过。所以检测编译器后,可以针对性地设置编译选项:

if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options(my_target PRIVATE -Wall -Wextra -Wpedantic)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    target_compile_options(my_target PRIVATE /W4 /EHsc)
endif()
避坑指南:我曾经在macOS上使用CMAKE_CXX_COMPILER_ID检测到的是AppleClang,而不是Clang。AppleClang和标准Clang版本号不同,有些特性支持也不一样。所以判断时最好把AppleClang也考虑进去。

25.3 系统库查找:find_package的跨平台之道

跨平台最头疼的就是找系统库。Windows上的库路径和Linux完全不同。我一般用find_package配合hintspaths来解决。

find_package(OpenSSL REQUIRED)
if(OpenSSL_FOUND)
    target_link_libraries(my_target PRIVATE OpenSSL::SSL OpenSSL::Crypto)
endif()

但有些库没有提供CMake配置文件,这时候就得用FindXXX.cmake模块。我个人习惯自己写一个通用的查找脚本:

# 自定义查找libcurl
find_library(CURL_LIBRARY
    NAMES curl libcurl
    HINTS
        /usr/lib
        /usr/local/lib
        /opt/homebrew/lib  # macOS Homebrew路径
        "C:/Program Files/curl/lib"  # Windows路径
)
find_path(CURL_INCLUDE_DIR
    NAMES curl/curl.h
    HINTS
        /usr/include
        /usr/local/include
        /opt/homebrew/include
        "C:/Program Files/curl/include"
)

if(CURL_LIBRARY AND CURL_INCLUDE_DIR)
    add_library(CURL::CURL UNKNOWN IMPORTED)
    set_target_properties(CURL::CURL PROPERTIES
        IMPORTED_LOCATION "${CURL_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIR}"
    )
endif()
核心要点:跨平台查找系统库时,一定要把各个平台的典型路径都列出来。别只写Linux的路径,Windows和macOS的也要覆盖到。

25.4 跨平台注意事项:那些年我踩过的坑

跨平台编译的坑太多了,我挑几个最常见的说说。

25.4.1 路径分隔符

Windows用反斜杠\,Linux和macOS用正斜杠/。CMake会自动处理,但如果你在代码里硬编码路径,那就麻烦了。

# 错误写法
set(MY_PATH "C:\\Users\\me\\project")

# 正确写法
set(MY_PATH "C:/Users/me/project")  # CMake会自动转换

25.4.2 动态库后缀名

Windows上是.dll,Linux上是.so,macOS上是.dylib。CMake的find_library会自动处理,但如果你手动拼接,就会出问题。

# 错误写法
set(MY_LIB "mylib.so")

# 正确写法
find_library(MY_LIB mylib)  # CMake自动加后缀

25.4.3 编译器特有的宏

有些编译器定义了特有的宏,比如MSVC的_MSC_VER,GCC的__GNUC__。在代码里用这些宏做条件编译时,要小心。

#ifdef _MSC_VER
    // Windows特有的代码
    #pragma warning(disable: 4996)
#elif defined(__GNUC__)
    // GCC/Clang特有的代码
    #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
我曾经:在Linux上写了一个用dlopen加载动态库的模块,结果移植到Windows上发现根本没有dlopen。后来用LoadLibraryGetProcAddress重写了一遍。所以跨平台时,尽量用跨平台的库,比如Boost.DLL或者Qt的插件机制。

25.5 知识体系总览

下面这张图总结了跨平台编译的核心知识点,你可以对照着检查自己的项目。

CMake跨平台编译 平台判断 CMAKE_SYSTEM_NAME WIN32 / UNIX 编译器检测 CMAKE_CXX_COMPILER_ID 编译选项差异化 系统库查找 find_package find_library 跨平台注意事项 路径分隔符差异 动态库后缀名不同 编译器特有宏

25.6 实战建议

说了这么多,最后给几个实战建议:

  • 先判断平台,再设置选项。别上来就写target_compile_options,先看看自己在哪。
  • 用CMake的跨平台函数。比如file(TO_CMAKE_PATH)可以自动转换路径格式。
  • 测试要覆盖全平台。我一般会在CI里配置Windows、Linux、macOS三个平台的构建任务。
  • 别偷懒用硬编码。路径、库名、编译器选项,都尽量用CMake变量来管理。
个人习惯:我会在项目根目录放一个cmake/文件夹,里面放各个平台的配置文件,比如platform_windows.cmakeplatform_linux.cmake。然后在主CMakeLists.txt里用include引入。这样代码清晰,也好维护。

跨平台编译说难不难,说简单也不简单。关键是要把平台差异抽象出来,用CMake的统一接口去处理。你想想看,如果每个平台都写一套构建脚本,那维护成本得多高?

好了,这一章就到这里。记住:跨平台不是把代码复制三份,而是用一份代码适配三个平台。

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