30. 8.2 常见平台交叉编译实战

引言:施工队长的”海外工程图鉴”

在上一节(8.1)中,我们的 CMake “施工队长”拿到了《交叉编译通用护照》——学会了工具链文件(Toolchain File)的基本结构和关键变量。但理论签证只是第一步,真正到了异国工地,面对不同的”当地法规、气候土壤和建筑材料”,队长还得有具体的施工手册。

这一节,我们把镜头对准六大热门”海外工地”:AndroidiOS/macOS嵌入式 LinuxWindows (MinGW) 以及 WebAssembly。每个平台我们都会给出可直接落地的工具链文件模板和 CMake 调用方式,让你的代码真正做到”一次编写,到处编译”。

Android NDK:在移动端工地搭脚手架

Android 是交叉编译最常见的目的地之一。Google 提供的 NDK(Native Development Kit)已经内置了一个官方工具链文件,但在现代 CMake 项目中,我们更推荐显式控制 ABI 和 API Level。

工具链文件模板(android-ndk.cmake)

以下是一个精简而实用的自定义工具链文件,假设 NDK 路径通过环境变量 ANDROID_NDK 传入:

# android-ndk.cmake
set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_SYSTEM_VERSION 21)          # API Level 21 (Android 5.0)
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a) # 目标 ABI: arm64-v8a, armeabi-v7a, x86, x86_64
set(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK})
set(CMAKE_ANDROID_STL_TYPE c++_shared) # 推荐使用 libc++_shared

# 让 CMake 自动推导编译器路径
set(CMAKE_C_COMPILER   ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang)
set(CMAKE_CXX_COMPILER ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++)

# 查找根路径设置
set(CMAKE_FIND_ROOT_PATH ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

配置与构建命令

# 配置阶段
cmake -B build/android-arm64 
      -DCMAKE_TOOLCHAIN_FILE=cmake/android-ndk.cmake 
      -DCMAKE_BUILD_TYPE=Release 
      -DANDROID_NDK=/path/to/ndk/25.2.9519653

# 构建阶段
cmake --build build/android-arm64 --parallel

ABI 选择速查表

选择合适的 ABI 就像选择合适的地基类型,直接决定你的 App 能运行在哪些设备上:

  • arm64-v8a现代 Android 设备首选(2016年后主流),64位 ARM,性能最佳。
  • armeabi-v7a:32位 ARM,兼容老旧设备,但 Google Play 已要求必须提供 64 位版本。
  • x86_64 / x86:主要用于 Android 模拟器,真机极少使用。

iOS/macOS:Xcode 工地的特殊安全帽

iOS 是一个”封闭式工地”:你必须使用 Apple 的 Xcode 工具链,还要区分真机(iOS Device)和模拟器(iOS Simulator)两套完全不同的 SDK。

真机工具链文件(ios-device.cmake)

# ios-device.cmake
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET 14.0)  # 最低支持 iOS 14.0

# 对于真机,指定 ARCHS 和 SYSROOT
set(CMAKE_OSX_ARCHITECTURES arm64)
execute_process(
    COMMAND xcrun --sdk iphoneos --show-sdk-path
    OUTPUT_VARIABLE CMAKE_OSX_SYSROOT
    OUTPUT_STRIP_TRAILING_WHITESPACE
)

set(CMAKE_C_COMPILER   xcrun --sdk iphoneos clang)
set(CMAKE_CXX_COMPILER xcrun --sdk iphoneos clang++)
set(CMAKE_AR xcrun --sdk iphoneos ar)

# iOS 不允许编译可执行文件直接运行(沙盒限制),通常只编译静态库/动态库/Framework
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

模拟器工具链文件(ios-simulator.cmake)

模拟器运行在 x86_64 或 arm64(Apple Silicon Mac)上,SDK 路径截然不同:

# ios-simulator.cmake
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET 14.0)
set(CMAKE_OSX_ARCHITECTURES x86_64;arm64) # 支持 Intel 和 Apple Silicon Mac

execute_process(
    COMMAND xcrun --sdk iphonesimulator --show-sdk-path
    OUTPUT_VARIABLE CMAKE_OSX_SYSROOT
    OUTPUT_STRIP_TRAILING_WHITESPACE
)

set(CMAKE_C_COMPILER   xcrun --sdk iphonesimulator clang)
set(CMAKE_CXX_COMPILER xcrun --sdk iphonesimulator clang++)

构建技巧

  1. 使用 -G Xcode 生成器可以大幅简化多架构管理,CMake 会自动处理 SDK 切换。
  2. 如果要在真机调试,需要在 Xcode 中处理代码签名(Code Signing),CMake 本身不管理证书。
  3. CMAKE_TRY_COMPILE_TARGET_TYPE 设置为 STATIC_LIBRARY 是因为 iOS 真机环境下 CMake 的测试编译(try_compile)无法链接可执行文件。

Universal Binary:一栋楼盖两套地基

Apple 从 Intel 迁移到 Apple Silicon(arm64)的过程中,诞生了一种”双面建筑”需求:同一个可执行文件或库,内部同时包含 x86_64 和 arm64 两套机器码,运行时由操作系统自动选择。这就是 Universal Binary(通用二进制),俗称 FAT 文件。

CMake 原生支持

在 macOS 上构建 Universal Binary 非常简单,甚至不需要交叉编译工具链,直接在 Apple Silicon Mac 上执行:

cmake -B build/universal -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
cmake --build build/universal

注意事项

  • 所有依赖库(如 OpenSSL、libpng)也必须提供 Universal Binary 版本,否则链接阶段会报架构不匹配错误。
  • 如果依赖库只有单架构版本,你需要分别编译 x86_64 和 arm64 版本,再用 lipo -create 合并。
  • 对于 Intel Mac 用户,要构建 arm64 代码仍需交叉编译(因为旧 Intel Mac 不能原生运行 arm64 指令)。

嵌入式 Linux:ARM、RISC-V 与 MIPS 的极简工地

嵌入式开发是交叉编译最古老也最硬核的领域。这里的”外地”不仅是不同架构,往往连标准库(libc)都是裁剪过的。

ARM 交叉编译工具链文件(arm-linux.cmake)

以主流的 ARM64(aarch64)交叉编译器为例,通常从 Linaro 或发行版仓库获取:

# arm-linux.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

# 指定交叉编译器前缀
set(CROSS_PREFIX aarch64-linux-gnu)

set(CMAKE_C_COMPILER   ${CROSS_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${CROSS_PREFIX}-g++)
set(CMAKE_AR           ${CROSS_PREFIX}-ar)
set(CMAKE_RANLIB       ${CROSS_PREFIX}-ranlib)
set(CMAKE_STRIP        ${CROSS_PREFIX}-strip)

# 关键:sysroot 指向目标设备的根文件系统(包含 lib/ 和 usr/include/)
set(CMAKE_SYSROOT /path/to/target/rootfs)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

# 若目标设备没有完整的开发环境,需关闭某些编译特性检测
set(CMAKE_C_COMPILER_WORKS 1)
set(CMAKE_CXX_COMPILER_WORKS 1)

Staging 前缀:别把家具搬进别人家客厅

嵌入式开发中,make install 默认会把文件装到 CMAKE_SYSROOT 里,这可能污染你精心制作的目标根文件系统。正确的做法是设置 Staging Prefix

cmake -B build/arm64 
      -DCMAKE_TOOLCHAIN_FILE=cmake/arm-linux.cmake 
      -DCMAKE_INSTALL_PREFIX=/path/to/staging 
      -DCMAKE_BUILD_TYPE=Release

这样编译产物会先安装到 Staging 目录,经过测试后再通过 rsync/scp 部署到真实设备。

RISC-V 与 MIPS

对于 RISC-V(riscv64-linux-gnu-gcc)和 MIPS(mipsel-linux-gnu-gcc),工具链文件的结构与 ARM 几乎完全一致,只需修改 CMAKE_SYSTEM_PROCESSOR 和编译器前缀即可。但需特别注意:

  • RISC-V:确认是否启用原子指令扩展(A Extension)和浮点单元(F/D Extension),这通常通过编译器标志 -march=rv64gc 控制。
  • MIPS:大小端(Big/Little Endian)选择至关重要,工具链名称中的 mipsel(小端)和 mips(大端)不可混用。

MinGW-w64 与 MSYS2:在 Linux 上盖 Windows 的楼

有时你的主力开发机是 Linux,但需要生成 Windows 可执行文件(.exe)。MinGW-w64 工具链让 Linux 机器变成了”Windows 预制构件厂”。

工具链文件(mingw-w64.cmake)

# mingw-w64.cmake
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

set(CROSS_PREFIX x86_64-w64-mingw32)

set(CMAKE_C_COMPILER   ${CROSS_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${CROSS_PREFIX}-g++)
set(CMAKE_RC_COMPILER  ${CROSS_PREFIX}-windres) # Windows 资源编译器
set(CMAKE_AR           ${CROSS_PREFIX}-ar)
set(CMAKE_RANLIB       ${CROSS_PREFIX}-ranlib)

set(CMAKE_FIND_ROOT_PATH /usr/${CROSS_PREFIX})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

MSYS2 环境注意事项

如果你在 Windows 本地使用 MSYS2 进行类似 Linux 的开发体验,通常不需要交叉编译工具链文件——因为 MSYS2 的 MinGW-w64 shell 本身就是原生环境。此时只需直接运行:

cmake -B build -G "MinGW Makefiles"
cmake --build build

只有在 Linux → Windows 的真·交叉编译场景下,才需要上面的工具链文件。

WebAssembly:把大楼搬到浏览器里

WebAssembly(Wasm)是最特殊的”工地”:你的代码最终运行在浏览器或 Node.js 的沙盒中,没有传统意义上的操作系统,甚至文件系统都是虚拟的。

使用 Emscripten 的推荐方式

Emscripten 官方提供了 emcmake 包装脚本,它会自动注入正确的工具链参数,这是最简单可靠的方式:

# 激活 Emscripten 环境后
emcmake cmake -B build/wasm -DCMAKE_BUILD_TYPE=Release
cmake --build build/wasm

手动工具链文件(wasm.toolchain.cmake)

如果你需要更精细的控制(比如在 CI 中避免激活 emsdk 环境),可以这样写:

# wasm.toolchain.cmake
set(CMAKE_SYSTEM_NAME Emscripten)
set(CMAKE_SYSTEM_VERSION 1)

set(EMSDK_ROOT /path/to/emsdk/upstream/emscripten)

set(CMAKE_C_COMPILER   ${EMSDK_ROOT}/emcc)
set(CMAKE_CXX_COMPILER ${EMSDK_ROOT}/em++)
set(CMAKE_AR           ${EMSDK_ROOT}/emar)
set(CMAKE_RANLIB       ${EMSDK_ROOT}/emranlib)

# Wasm 没有常规意义上的动态链接库,默认生成 .js + .wasm 组合
set(CMAKE_EXECUTABLE_SUFFIX ".js")

# 查找模式限制(Emscripten 自带 sysroot)
set(CMAKE_FIND_ROOT_PATH ${EMSDK_ROOT}/system)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

CMakeLists.txt 中的特殊适配

WebAssembly 有许多”施工限制”,需要在 CMakeLists.txt 中做条件判断:

if(EMSCRIPTEN)
    # 开启 Embind 或 WebIDL 绑定以支持 JS/C++ 互操作
    target_link_options(my_target PRIVATE 
        "-sEXPORT_ES6=1"
        "-sMODULARIZE=1"
        "-sENVIRONMENT=web"
    )
    
    # 禁用某些不支持的多线程或文件系统特性(视需求而定)
    # target_compile_options(my_target PRIVATE "-sUSE_PTHREADS=0")
endif()

平台实战速查表

最后,把本节的核心参数整理成一张”海外施工速查卡”,方便你随时翻阅:

目标平台 SYSTEM_NAME 关键变量/命令 典型生成器
Android NDK Android CMAKE_ANDROID_ARCH_ABI, ANDROID_NDK Ninja
iOS 真机 iOS CMAKE_OSX_SYSROOT (iphoneos) Xcode
iOS 模拟器 iOS CMAKE_OSX_SYSROOT (iphonesimulator) Xcode
macOS Universal Darwin CMAKE_OSX_ARCHITECTURES=”x86_64;arm64″ Xcode / Ninja
嵌入式 ARM Linux CMAKE_SYSROOT, CMAKE_STAGING_PREFIX Unix Makefiles
MinGW-w64 Windows CMAKE_RC_COMPILER (windres) Unix Makefiles / Ninja
WebAssembly Emscripten emcmake, CMAKE_EXECUTABLE_SUFFIX=”.js” Unix Makefiles

小结:因地制宜,但不改图纸核心

交叉编译的”海外工地”千差万别:Android 要区分 ABI,iOS 要区分模拟器与真机,嵌入式 Linux 要操心 sysroot,Wasm 则完全是一套新的运行规则。但万变不离其宗的是 CMake 的核心工作流:

  1. 写一份干净的工具链文件,把”当地气候”(编译器、sysroot、查找规则)封装起来。
  2. 在 CMakeLists.txt 中避免硬编码绝对路径,善用 CMAKE_SYSTEM_NAME 的条件判断做平台适配。
  3. 保持源码目录与构建目录分离(Out-of-source),为每个目标平台创建独立的 build/<platform> 目录。

掌握了这六大平台的实战要领,你的 CMake “施工队”就真正拥有了全球承建能力。下一节(8.3),我们将把目光投向编译器的高级特性与链接优化,看看如何让这座”海外大楼”不仅盖得起来,还要盖得又快又稳。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……