27、跨平台构建:Windows/Linux/macOS差异处理、编译器宏检测、平台特定源文件、路径兼容性

跨平台构建,说白了就是让你的代码在Windows、Linux、macOS上都能一次编译通过。我刚开始接触CMake时,觉得这玩意儿不就是写几个命令吗?直到我在一个项目里同时要支持MSVC、GCC和Clang,才发现坑有多深。

嗯,这一章我们就来聊聊怎么优雅地处理这些差异。我个人习惯是把平台相关的逻辑集中管理,而不是散落在各个CMakeLists.txt里。

27.1 编译器宏检测:让代码自己认路

为什么需要编译器宏?因为不同编译器对C++标准的支持程度不同,有的特性在MSVC上能用,在GCC上就得换种写法。我曾在项目中遇到过MSVC的__declspec(dllexport)和GCC的__attribute__((visibility("default")))之争,最后靠宏统一解决了。

CMake提供了target_compile_definitions来传递宏,但更常见的做法是在源码里直接用编译器预定义宏做判断:

#if defined(_WIN32) || defined(_WIN64)
    #define PLATFORM_WINDOWS 1
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
    #if TARGET_OS_MAC
        #define PLATFORM_MACOS 1
    #endif
#elif defined(__linux__)
    #define PLATFORM_LINUX 1
#endif

你想想看,这样写的好处是什么?代码自己就能识别当前运行在哪个平台上,不需要手动改任何配置。我曾经在一个遗留项目里看到有人用#ifdef WIN32,结果在64位Windows上死活编译不过——因为WIN32在64位系统上不一定被定义,得用_WIN32才保险。

小技巧: 在CMake里可以用CMAKE_SYSTEM_NAME变量判断平台,比如if(WIN32)if(APPLE)if(UNIX)。但注意UNIX在macOS上也是真,所以判断macOS时要先判断APPLE

27.2 平台特定源文件:按需编译

有些源文件只针对特定平台,比如Windows下的win_main.cpp、Linux下的linux_main.cpp。如果一股脑全编译,肯定报错。CMake里可以用条件判断来筛选:

if(WIN32)
    target_sources(myapp PRIVATE src/win_main.cpp src/win_utils.cpp)
elseif(APPLE)
    target_sources(myapp PRIVATE src/mac_main.mm src/mac_utils.mm)
elseif(UNIX)
    target_sources(myapp PRIVATE src/linux_main.cpp src/linux_utils.cpp)
endif()

我个人习惯把平台相关的源文件放在src/platform/目录下,按平台分文件夹:

src/platform/
├── win/
│   ├── win_main.cpp
│   └── win_utils.cpp
├── mac/
│   ├── mac_main.mm
│   └── mac_utils.mm
└── linux/
    ├── linux_main.cpp
    └── linux_utils.cpp

然后在CMake里用GLOB或者直接写路径。注意,macOS的Objective-C++文件后缀是.mm,CMake会自动识别并调用合适的编译器。

避坑指南: 我曾经在Windows上用target_sources添加了linux_main.cpp,结果MSVC尝试编译它,报了一堆POSIX相关的错误。后来我加了EXCLUDE_FROM_ALL属性才解决。其实更好的做法是根本不要添加不相关的源文件。

27.3 路径兼容性:正斜杠与反斜杠的战争

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

我建议在CMake里统一用正斜杠:

set(MY_PATH "src/subdir/file.cpp")  # 正斜杠,跨平台通用

CMake在Windows上会把正斜杠自动转换成反斜杠(如果需要的话)。但如果你用file(READ ...)读取文件路径,或者用add_custom_command执行外部命令,就得小心了。

举个例子,在add_custom_command里调用外部工具时,Windows上可能需要反斜杠:

if(WIN32)
    set(TOOL_PATH "C:\\tools\\mytool.exe")
else()
    set(TOOL_PATH "/usr/local/bin/mytool")
endif()

嗯,这里要注意:CMake的file(TO_CMAKE_PATH)file(TO_NATIVE_PATH)可以帮助你做转换。我个人习惯在需要原生路径的地方显式调用file(TO_NATIVE_PATH),避免手写反斜杠。

27.4 跨平台构建的常见陷阱

除了上面这些,还有几个坑我不得不提:

  • 动态库后缀名: Windows是.dll,Linux是.so,macOS是.dylib。CMake的add_library会自动处理,但如果你手动拼接文件名,记得用CMAKE_SHARED_LIBRARY_SUFFIX变量。
  • 可执行文件后缀: Windows是.exe,其他平台没有后缀。用CMAKE_EXECUTABLE_SUFFIX获取。
  • 换行符差异: Windows用\r\n,Linux用\n。CMake的file(WRITE ...)默认使用当前平台格式,但如果你要生成跨平台脚本,可以用FILE_ENCODING控制。
核心原则: 尽量让CMake替你处理平台差异,而不是在源码里写一堆#ifdef。CMake的变量和生成器表达式(比如$<PLATFORM_ID>)已经覆盖了大部分场景。

27.5 知识体系图:跨平台构建的核心逻辑

下面这张图展示了跨平台构建的整体流程和关键决策点:

跨平台构建核心逻辑 CMakeLists.txt 平台检测 (CMAKE_SYSTEM_NAME) Windows Linux macOS 编译器宏检测 平台源文件筛选 路径兼容处理 编译器宏检测 平台源文件筛选 路径兼容处理 编译器宏检测 平台源文件筛选 路径兼容处理

从图中可以看出,CMakeLists.txt是入口,通过CMAKE_SYSTEM_NAME检测平台后,分别进入Windows、Linux、macOS三个分支。每个分支都要处理编译器宏、源文件筛选和路径兼容性。嗯,这个结构我在实际项目中用了好几年,基本没出过问题。

27.6 实战建议:从零开始搭建跨平台项目

如果你现在要开始一个新项目,我建议按这个步骤来:

  1. 先写一个最小的CMakeLists.txt,只包含project()add_executable(),确保在三个平台上都能编译通过。
  2. 逐步添加平台相关代码,每加一个文件就在对应平台上测试一次。不要一次性加太多,否则出问题很难定位。
  3. 用CI/CD自动测试,比如GitHub Actions可以同时跑Windows、Linux、macOS三个环境。我个人的项目都是这么配置的,省心很多。
  4. 注意第三方库的跨平台性,有些库在Windows上需要额外配置,比如OpenGL的glewglfw。用CMake的find_package可以统一管理。
个人经验: 我曾在macOS上遇到一个坑——std::filesystem在Xcode 12之前的版本不支持。后来我在CMake里加了target_compile_features(myapp PUBLIC cxx_std_17)才解决。所以,记得检查编译器版本。

好了,跨平台构建的核心内容就这些。记住,CMake本身就是为了解决跨平台问题而生的,你只需要把规则写清楚,剩下的交给它。嗯,下一节我们会聊聊更高级的生成器表达式,但那是后话了。


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