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,因为它最精确。WIN32和UNIX虽然方便,但有些交叉编译场景下会不准。
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()
CMAKE_CXX_COMPILER_ID检测到的是AppleClang,而不是Clang。AppleClang和标准Clang版本号不同,有些特性支持也不一样。所以判断时最好把AppleClang也考虑进去。
25.3 系统库查找:find_package的跨平台之道
跨平台最头疼的就是找系统库。Windows上的库路径和Linux完全不同。我一般用find_package配合hints和paths来解决。
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()
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
dlopen加载动态库的模块,结果移植到Windows上发现根本没有dlopen。后来用LoadLibrary和GetProcAddress重写了一遍。所以跨平台时,尽量用跨平台的库,比如Boost.DLL或者Qt的插件机制。
25.5 知识体系总览
下面这张图总结了跨平台编译的核心知识点,你可以对照着检查自己的项目。
25.6 实战建议
说了这么多,最后给几个实战建议:
- 先判断平台,再设置选项。别上来就写
target_compile_options,先看看自己在哪。 - 用CMake的跨平台函数。比如
file(TO_CMAKE_PATH)可以自动转换路径格式。 - 测试要覆盖全平台。我一般会在CI里配置Windows、Linux、macOS三个平台的构建任务。
- 别偷懒用硬编码。路径、库名、编译器选项,都尽量用CMake变量来管理。
cmake/文件夹,里面放各个平台的配置文件,比如platform_windows.cmake、platform_linux.cmake。然后在主CMakeLists.txt里用include引入。这样代码清晰,也好维护。
跨平台编译说难不难,说简单也不简单。关键是要把平台差异抽象出来,用CMake的统一接口去处理。你想想看,如果每个平台都写一套构建脚本,那维护成本得多高?
好了,这一章就到这里。记住:跨平台不是把代码复制三份,而是用一份代码适配三个平台。