3、指定交叉编译器:CMAKE_C_COMPILER与CMAKE_CXX_COMPILER的配置
好,咱们接着往下走。上一章我们把工具链文件的基本骨架搭好了,也知道了怎么让 CMake 加载它。但说实话,那个文件还是个空壳子——最核心的东西还没填:编译器路径。
你想想看,交叉编译和本地编译最大的区别在哪?就是编译器不一样。本地编译你用 gcc,交叉编译你得用 arm-linux-gnueabihf-gcc。CMake 怎么知道用哪个?就是靠两个变量:CMAKE_C_COMPILER 和 CMAKE_CXX_COMPILER。
核心概念:CMAKE_C_COMPILER 指定 C 编译器,CMAKE_CXX_COMPILER 指定 C++ 编译器。这两个变量必须在 project() 命令之前设置,否则 CMake 会先用默认的本地编译器探测一遍,后面再改就晚了。
3.1 直接指定编译器路径
最直接的方式,就是在工具链文件里把路径写死。我个人习惯用 set() 命令加 CACHE PATH 的方式,这样用户还能在命令行覆盖。
# 方式一:直接写死路径
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)
# 方式二:带 CACHE 变量(我推荐这个)
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc CACHE PATH "C compiler")
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++ CACHE PATH "C++ compiler")
这里有个坑,我曾经踩过:路径里不能有空格。Windows 上交叉编译时,如果编译器装在 C:\Program Files\ 下面,CMake 会解析出错。解决办法是用 file(TO_CMAKE_PATH) 转换一下,或者干脆装到无空格的路径下。
3.2 查找交叉编译器路径
实际项目中,编译器路径往往不是固定的。不同的开发板 SDK,安装位置五花八门。我一般用两种方式来找:
方式一:环境变量 + find_program
让用户设置一个环境变量,比如 CROSS_COMPILER_PATH,然后在工具链文件里用 find_program 去搜。
# 先尝试从环境变量获取路径
if(DEFINED ENV{CROSS_COMPILER_PATH})
set(TOOLCHAIN_PATH "$ENV{CROSS_COMPILER_PATH}")
else()
# 默认路径,给个提示
set(TOOLCHAIN_PATH "/opt/toolchains/arm-cortex_a9-linux-gnueabihf/bin")
message(STATUS "CROSS_COMPILER_PATH not set, using default: ${TOOLCHAIN_PATH}")
endif()
# 查找编译器
find_program(CMAKE_C_COMPILER
NAMES arm-linux-gnueabihf-gcc
PATHS ${TOOLCHAIN_PATH}
NO_DEFAULT_PATH
)
find_program(CMAKE_CXX_COMPILER
NAMES arm-linux-gnueabihf-g++
PATHS ${TOOLCHAIN_PATH}
NO_DEFAULT_PATH
)
# 没找到就报错
if(NOT CMAKE_C_COMPILER)
message(FATAL_ERROR "C compiler not found in ${TOOLCHAIN_PATH}")
endif()
注意那个 NO_DEFAULT_PATH,加上它之后,find_program 就不会去系统默认路径里搜了。为什么要加?因为系统路径里可能也有个 gcc,但那不是交叉编译器,搜到了反而坏事。
方式二:从 SDK 包中自动提取
有些 SDK 会把编译器放在固定的相对路径下。比如 TI 的 Processor SDK,编译器在 linux-devkit/sysroots/x86_64-arago-linux/usr/bin/arm-linux-gnueabihf-。我写过这样的逻辑:
# 假设用户设置了 SDK_ROOT
if(NOT SDK_ROOT)
message(FATAL_ERROR "Please set SDK_ROOT to your SDK installation path")
endif()
# 构造编译器路径
set(TOOLCHAIN_BIN_DIR "${SDK_ROOT}/linux-devkit/sysroots/x86_64-arago-linux/usr/bin")
set(CMAKE_C_COMPILER "${TOOLCHAIN_BIN_DIR}/arm-linux-gnueabihf-gcc")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_BIN_DIR}/arm-linux-gnueabihf-g++")
这种方式的好处是,用户只需要设置一个 SDK_ROOT,剩下的路径自动拼出来。但前提是你得熟悉 SDK 的目录结构。
3.3 验证编译器是否可用
路径配好了,不代表编译器就能用。我见过太多人配完路径就以为万事大吉,结果 cmake 一跑就报错。所以,验证这一步不能省。
CMake 在 project() 阶段会自动做一次编译器检测。它会尝试用你指定的编译器编译一个简单的测试程序,如果编译失败,CMake 会直接报错退出。但有时候错误信息不够直观,我习惯在工具链文件里加一段主动检测:
# 主动检测编译器是否可用
include(CheckCSourceCompiles)
include(CheckCXXSourceCompiles)
# 写一个简单的测试程序
set(CMAKE_REQUIRED_FLAGS "-Wall -Wextra")
check_c_source_compiles("
#include <stdio.h>
int main() {
printf(\"Hello from cross compiler\\n\");
return 0;
}
" C_COMPILER_WORKS)
if(NOT C_COMPILER_WORKS)
message(FATAL_ERROR "C compiler check failed: ${CMAKE_C_COMPILER}")
endif()
# C++ 同理
check_cxx_source_compiles("
#include <iostream>
int main() {
std::cout << \"Hello from cross C++ compiler\" << std::endl;
return 0;
}
" CXX_COMPILER_WORKS)
小技巧:如果检测失败,可以用 execute_process 直接运行编译器,看看它输出的错误信息。比如:
execute_process(COMMAND ${CMAKE_C_COMPILER} --version
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE error)
if(NOT result EQUAL 0)
message(FATAL_ERROR "Compiler not executable: ${error}")
endif()
3.4 常见问题与避坑
| 问题 | 现象 | 解决办法 |
|---|---|---|
| 编译器路径写错 | CMake 报 "CMAKE_C_COMPILER not set" | 用 file(TO_CMAKE_PATH) 标准化路径,或者用 find_program 自动搜索 |
| 编译器版本不匹配 | 编译通过但链接时报错,比如找不到 crt0.o | 检查工具链的 sysroot 是否设置正确,后面会讲 CMAKE_SYSROOT |
| 权限问题 | 编译器文件没有执行权限 | chmod +x 给编译器加上执行权限 |
| 32位 vs 64位 | 64位系统上运行32位交叉编译器,报 "No such file or directory" | 安装 32 位兼容库:sudo apt install lib32z1 lib32ncurses5 |
我曾经遇到过一个特别隐蔽的问题:编译器路径里包含软链接,而软链接指向的目录在 cmake 运行时被卸载了。结果 cmake 检测时能通过(因为软链接还在),但实际编译时却报错。所以,我建议在工具链文件里用 get_filename_component(REAL_PATH ... REALPATH) 把路径解析成真实路径再使用。
3.5 本章知识体系
下面这张图,把编译器配置的整个流程串起来了。你可以对照着看,每一步都做了什么。
说白了,配置交叉编译器就这五步。但每一步都有细节,尤其是路径查找和验证环节,最容易出问题。我个人建议,在工具链文件里把验证逻辑写完整,宁可多花几分钟检测,也不要等编译到一半才发现编译器不对。