1. 为什么值得折腾Qt5.14.2的aarch64静态交叉编译
如果你手上有Orange Pi、树莓派这类aarch64开发板,又想把Qt程序直接丢上去跑,不依赖板子上的Qt运行库,那静态交叉编译几乎是绕不开的一条路。我第一次在Orange Pi CM5上部署Qt程序的时候,图省事直接在板子上装了Qt5的运行时,结果程序拷过去各种库版本对不上,cannot mix incompatible qt library这种报错反复出现,折腾了一整天才跑起来。后来换成静态编译,一个二进制文件拷过去就能跑,那种清爽感真的回不去了。
Qt5.14.2这个版本在嵌入式圈子里用得特别多,一方面它是LTS版本,稳定性经过大量项目验证,另一方面它对aarch64的支持已经相当成熟,配合qt-everywhere-src-5.14.2源码包可以完整地做静态构建。所谓静态交叉编译,说白了就是在你的x86_64开发机上,用aarch64的交叉编译工具链,把Qt的所有依赖(包括Qt自身的库)全部编译成静态库,最终链接出一个不依赖任何动态库的可执行文件。这个文件丢到板子上,只要内核和C库兼容,直接就能运行。
这套流程适合谁呢?如果你在做嵌入式Qt开发,板子存储空间有限、不想在板子上装一堆Qt运行库,或者你希望发布软件时只给用户一个文件,那静态交叉编译就是你的菜。当然,这个过程坑不少,从工具链选择到configure参数,从依赖库编译到最终的链接报错,每一步都可能卡住你。我踩过的坑包括但不限于:unknown module(s) in qt: serialport、链接时找不到libstdc++.a、板子上跑起来字体乱码等等。这篇手册就是把这些经验整理出来,让你少走弯路。
2. 环境准备与工具链选型
2.1 开发机环境搭建
开发机我建议用Ubuntu 20.04或者22.04的x86_64版本,别用CentOS 7.9,虽然CentOS 7.9也能做,但它的glibc版本太老,编译出来的东西在较新的板子上反而可能出问题。我实测下来Ubuntu 20.04最稳,gcc版本是9.x,配合aarch64的工具链兼容性最好。
先装一堆基础依赖,这些是编译Qt源码必须的:
sudo apt update sudo apt install -y build-essential libgl1-mesa-dev libglu1-mesa-dev \ libxkbcommon-dev libxkbcommon-x11-dev libfontconfig1-dev \ libfreetype6-dev libpng-dev libjpeg-dev libssl-dev \ python3 perl git wget flex bison gperf \ libx11-dev libxext-dev libxrender-dev libxi-dev \ libxcb1-dev libxcb-keysyms1-dev libxcb-image0-dev \ libxcb-shm0-dev libxcb-icccm4-dev libxcb-sync-dev \ libxcb-xfixes0-dev libxcb-shape0-dev libxcb-randr0-dev \ libxcb-render-util0-dev libxcb-xinerama0-dev \ libxcb-xkb-dev libxcb-glx0-dev这些包看着多,但每一个都有用。比如libxcb-*系列是Qt的xcb平台插件依赖的,libfontconfig1-dev和libfreetype6-dev是字体渲染的基础,少了它们configure阶段就会报错。我建议一次性装完,别等到报错了再一个个补,那样很浪费时间。
2.2 aarch64交叉编译工具链选择
工具链的选择直接决定了后面能不能顺利编译。市面上常见的aarch64工具链有几种:Linaro的GCC、ARM官方的GNU Toolchain、以及各大板子厂商提供的工具链。我推荐用Linaro的gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu,这个版本经过大量项目验证,对C++17的支持也够用。
下载解压后放到/opt目录下:
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz -C /opt/ export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH验证一下工具链是否可用:
aarch64-linux-gnu-gcc -v如果能看到gcc version 7.5.0,说明工具链没问题。这里有个细节要注意:工具链的sysroot里包含了目标板的C库和头文件,Qt编译时会用到这些。Linaro的工具链sysroot在aarch64-linux-gnu/libc目录下,configure的时候需要指定-sysroot参数。
提示:不要用Ubuntu自带的
gcc-aarch64-linux-gnu包,那个工具链的sysroot不完整,编译Qt时会出现找不到crt1.o之类的错误。我试过一次,折腾了半天最后还是换回Linaro的。
2.3 目录规划与源码准备
在开始之前,先把目录结构规划好,后面会清晰很多:
mkdir -p ~/qt-aarch64/{src,build,install} cd ~/qt-aarch64/src wget https://download.qt.io/archive/qt/5.14/5.14.2/single/qt-everywhere-src-5.14.2.tar.xz tar -xvf qt-everywhere-src-5.14.2.tar.xz源码包大概有500多MB,解压后接近2GB。如果你下载速度慢,可以用国内镜像,比如清华的mirrors.tuna.tsinghua.edu.cn/qt/,速度会快很多。解压完成后,源码目录是qt-everywhere-src-5.14.2,我们所有的编译操作都在这个目录之外进行,保持源码干净。
3. 核心配置与编译参数拆解
3.1 configure参数逐项解析
Qt的configure脚本参数非常多,但做静态交叉编译,核心参数就那么十几个。我先把完整的configure命令列出来,然后逐项解释为什么这么设:
cd ~/qt-aarch64/build ../src/qt-everywhere-src-5.14.2/configure \ -prefix /home/yourname/qt-aarch64/install \ -opensource -confirm-license \ -release -static -optimize-size \ -xplatform linux-aarch64-gnu-g++ \ -sysroot /opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/aarch64-linux-gnu/libc \ -nomake examples -nomake tests \ -skip qtwebengine -skip qtwebview \ -no-opengl -no-openssl \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre \ -no-feature-accessibility \ -no-feature-cups -no-feature-printdialog \ -no-feature-sql-sqlite \ -no-icu -no-glib \ -no-dbus -no-xcb -no-wayland \ -no-compile-examples \ -v-prefix指定安装路径,这个路径是开发机上的路径,不是板子上的。-static是核心,告诉Qt编译静态库。-optimize-size会让编译器优先优化体积,对于嵌入式场景很实用,编译出来的库会小不少。
-xplatform linux-aarch64-gnu-g++指定使用哪个平台配置文件。Qt源码里qtbase/mkspecs/目录下有很多平台配置,但默认没有aarch64的,我们需要自己创建一个。这个后面会详细说。
-sysroot指向工具链的sysroot,Qt编译时会从这里找目标板的C库和头文件。-nomake examples -nomake tests跳过示例和测试,能省大量编译时间。-skip qtwebengine跳过WebEngine模块,这个模块依赖Chromium,编译极其耗时且容易出错,嵌入式场景基本用不上。
-no-opengl和-no-openssl是因为板子上通常没有GPU和OpenSSL,如果你确实需要OpenSSL,可以自己交叉编译一个再链接进来。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre表示使用Qt自带的这些第三方库,而不是系统库,这样能避免依赖问题。
-no-feature-*系列是裁剪功能,accessibility、cups、printdialog这些在嵌入式设备上基本用不到,裁掉能减小体积。-no-icu -no-glib也是同理,ICU是国际化库,体积很大,如果你的程序不需要多语言支持,直接关掉。
3.2 创建aarch64的mkspec配置文件
Qt默认没有aarch64的mkspec,我们需要在源码目录里创建一个:
cd ~/qt-aarch64/src/qt-everywhere-src-5.14.2/qtbase/mkspecs sudo cp -r linux-arm-hisilicon-hix5hd2-g++ linux-aarch64-gnu-g++ cd linux-aarch64-gnu-g++然后编辑qmake.conf,内容改成:
MAKEFILE_GENERATOR = UNIX CONFIG += incremental QMAKE_INCREMENTAL_STYLE = sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g++-unix.conf) QT_QPA_DEFAULT_PLATFORM = linuxfb QMAKE_CC = aarch64-linux-gnu-gcc QMAKE_CXX = aarch64-linux-gnu-g++ QMAKE_LINK = aarch64-linux-gnu-g++ QMAKE_LINK_SHLIB = aarch64-linux-gnu-g++ QMAKE_AR = aarch64-linux-gnu-ar cqs QMAKE_OBJCOPY = aarch64-linux-gnu-objcopy QMAKE_NM = aarch64-linux-gnu-nm -P QMAKE_STRIP = aarch64-linux-gnu-strip load(qt_config)这里QT_QPA_DEFAULT_PLATFORM = linuxfb指定默认的平台插件是linuxfb,也就是直接写framebuffer,不依赖X11或Wayland。如果你的板子跑的是X11,可以改成xcb,但嵌入式场景大多数用linuxfb或者eglfs。
注意:
QMAKE_CC这些变量里的工具链前缀必须和你的实际工具链名字一致。如果你用的是aarch64-none-linux-gnu-前缀,这里也要相应改掉。我见过有人直接复制网上的配置,结果工具链名字对不上,configure阶段就报command not found。
3.3 静态编译的依赖处理策略
静态编译最麻烦的地方在于依赖库的处理。Qt本身依赖zlib、libpng、libjpeg、freetype、pcre这些第三方库,如果系统里没有aarch64版本的静态库,就必须用Qt自带的。这就是为什么configure里要加-qt-zlib -qt-libpng这些参数。
但有些库Qt不自带,比如OpenSSL、DBus、GLib,这些就需要你自己交叉编译。我的建议是能不用就不用,嵌入式场景下这些库往往不是必需的。如果确实需要OpenSSL,可以这样交叉编译:
cd ~/qt-aarch64/src wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz tar -xvf openssl-1.1.1w.tar.gz cd openssl-1.1.1w ./Configure linux-aarch64 shared no-asm \ --prefix=/home/yourname/qt-aarch64/install \ --cross-compile-prefix=aarch64-linux-gnu- make -j$(nproc) make install编译完成后,在Qt的configure里加上-openssl-linked -I /home/yourname/qt-aarch64/install/include -L /home/yourname/qt-aarch64/install/lib即可。不过要注意,静态链接OpenSSL时,最终的可执行文件会大不少,而且OpenSSL的许可证和Qt的许可证兼容性需要你自己确认。
4. 编译过程与关键环节实操
4.1 执行configure与常见报错处理
configure命令执行后,会输出一大堆检测信息。如果一切顺利,最后会看到Qt is now configured for building的提示。但实际情况往往没那么顺利,我整理了几个最常见的报错和解决方法:
报错一:ERROR: Cannot find libxcb
这是因为缺少xcb相关的开发库。虽然我们最终不用xcb,但configure阶段还是会检测。解决方法就是装齐前面列出的那些libxcb-*包。如果装完了还报错,检查一下是不是pkg-config找不到,可以用export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig指定路径。
报错二:ERROR: Feature 'fontconfig' was enabled, but the pre-condition 'libs.fontconfig' failed
缺少fontconfig的开发库,装libfontconfig1-dev即可。如果装完了还报错,可能是sysroot里没有fontconfig,这时候可以加-no-fontconfig关掉它,改用Qt自带的freetype渲染字体。
报错三:Project ERROR: Unknown module(s) in QT: serialport
这个报错通常出现在你编译完Qt后,编译自己的项目时。原因是configure时没有启用serialport模块。Qt5.14.2的serialport模块默认是自动检测的,如果检测不到就会跳过。你可以在configure时显式加上-qt-serialport强制启用。如果还是不行,检查一下qtbase/src/plugins/目录下有没有serialport相关的源码。
报错四:cannot mix incompatible qt library (5.15.3) with this library (5.15.2)
这个报错说明你的程序链接了不同版本的Qt库。静态编译时一般不会遇到,但如果你之前装过系统Qt,qmake可能会找到系统Qt的库。解决方法是确保PATH里你的交叉编译qmake在最前面,或者直接用完整路径调用qmake。
4.2 编译与安装
configure成功后,就可以开始编译了:
make -j$(nproc) 2>&1 | tee build.log编译时间取决于你的机器性能,一般8核机器大概需要1到2小时。-j$(nproc)表示用所有CPU核心并行编译,能快不少。tee build.log把编译日志保存下来,方便出错时排查。
编译过程中如果报错,先看日志最后几行,通常是某个模块编译失败。常见的错误包括:头文件找不到、链接时符号未定义、内存不足等。内存不足的话,可以减少并行编译的job数,比如make -j4。
编译完成后安装:
make install安装完成后,~/qt-aarch64/install目录下就是完整的Qt静态库和头文件。检查一下lib目录,应该能看到libQt5Core.a、libQt5Gui.a、libQt5Widgets.a这些静态库文件。
4.3 验证交叉编译工具链
安装完成后,用交叉编译的qmake验证一下:
~/qt-aarch64/install/bin/qmake -query输出里应该能看到QT_INSTALL_PREFIX指向你的安装路径,QMAKE_SPEC是linux-aarch64-gnu-g++。然后写一个最简单的Qt程序测试:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello aarch64"); label.show(); return app.exec(); }用交叉编译的qmake生成Makefile并编译:
~/qt-aarch64/install/bin/qmake test.pro make file testfile命令的输出应该显示ELF 64-bit LSB executable, ARM aarch64,说明编译成功。把这个文件拷到板子上,加上-platform linuxfb参数运行:
./test -platform linuxfb如果板子上有屏幕,应该能看到一个显示"Hello aarch64"的窗口。
5. 常见问题与排查技巧实录
5.1 编译阶段问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
Cannot find -lstdc++ | 工具链的静态库路径没配好 | 在qmake.conf里加QMAKE_LFLAGS += -static-libstdc++,或检查sysroot路径 |
undefined reference to 'pthread_create' | 链接时缺少pthread库 | 在pro文件里加LIBS += -lpthread |
error: 'numeric_limits' is not a member of 'std' | 缺少#include <limits> | 在源码里补上头文件,这是gcc版本差异导致的 |
Unknown module(s) in QT: serialport | serialport模块没编译 | configure时加-qt-serialport,或单独编译该模块 |
qrc: file not found | 资源文件路径不对 | 检查pro文件里的RESOURCES路径,用相对路径 |
relocation R_AARCH64_ADR_PREL_PG_HI21 | 静态链接时的重定位问题 | 加-fPIC重新编译,或在链接时加-Wl,-z,notext |
| 程序在板子上跑起来字体乱码 | 没有嵌入字体 | 用QFontDatabase::addApplicationFont加载字体文件,或编译时加-qt-freetype |
5.2 静态链接的坑与避坑技巧
静态链接最大的坑是符号冲突和重复定义。比如你的程序同时链接了Qt的静态库和系统里的某个静态库,两者都定义了同一个符号,链接器就会报错。我遇到过一次,程序里用了libpng,Qt也自带了libpng,结果链接时符号冲突。解决方法是确保只用一方的库,要么全用Qt自带的,要么全用系统的。
另一个坑是静态库的链接顺序。GNU链接器对静态库的顺序很敏感,被依赖的库要放在后面。比如libQt5Widgets.a依赖libQt5Gui.a,libQt5Gui.a依赖libQt5Core.a,链接顺序就应该是-lQt5Widgets -lQt5Gui -lQt5Core。顺序反了就会报undefined reference。用qmake的话,它会自动处理这个顺序,但如果你手动写Makefile,就要特别注意。
还有一个经验:静态编译时尽量加-Wl,--gc-sections,这个选项会让链接器丢弃未使用的代码段,能显著减小最终可执行文件的体积。配合编译时的-ffunction-sections -fdata-sections一起用,效果更好。我实测过一个简单的Qt程序,不加这些选项是15MB,加了之后降到8MB左右。
5.3 板子上的部署与运行调试
程序拷到板子上后,如果运行报错,可以用ldd检查动态依赖:
ldd ./test静态编译的程序应该显示not a dynamic executable。如果显示了一堆依赖,说明静态编译没成功,检查一下是不是链接了动态库。
如果程序运行起来但界面显示不正常,比如花屏、颜色不对,通常是framebuffer的像素格式问题。可以在程序启动时指定:
./test -platform linuxfb:fb=/dev/fb0:size=1024x600:offset=0x0如果板子支持OpenGL ES,可以用eglfs平台插件,性能会好很多:
./test -platform eglfs但eglfs需要板子的GPU驱动支持,不是所有板子都能用。Orange Pi CM5的GPU驱动在官方镜像里是有的,但需要确认/dev/dri/card0设备节点存在。
提示:静态编译的程序在板子上运行时,如果报
QFontDatabase: Cannot find font directory,说明字体路径不对。可以在程序里用QFontDatabase::addApplicationFont("/path/to/font.ttf")手动加载字体,或者把字体文件放到/usr/share/fonts/目录下。
6. 进阶优化与模块裁剪
6.1 按需裁剪Qt模块减小体积
静态编译出来的程序体积往往比较大,因为Qt把很多用不到的模块也链接进来了。可以通过configure时的-skip参数裁剪掉不需要的模块。比如你的程序只用Widgets,那可以跳过qtdeclarative、qtquickcontrols、qtmultimedia这些:
-skip qtdeclarative -skip qtquickcontrols -skip qtquickcontrols2 \ -skip qtmultimedia -skip qtwebsockets -skip qtsensors \ -skip qtlocation -skip qtconnectivity -skip qtwayland裁剪之后,编译时间会大幅缩短,最终的可执行文件也能小不少。我做过对比,完整编译的Qt静态库大概有500MB,裁剪后能降到200MB左右。当然,具体裁剪哪些模块要根据你的项目需求来定,别裁掉了正在用的模块。
6.2 编译选项优化与体积控制
除了裁剪模块,编译选项也能影响体积。-optimize-size会让gcc用-Os而不是-O2,优先优化体积。如果对性能要求不高,这个选项很实用。另外可以在qmake.conf里加:
QMAKE_CFLAGS_RELEASE += -ffunction-sections -fdata-sections QMAKE_CXXFLAGS_RELEASE += -ffunction-sections -fdata-sections QMAKE_LFLAGS_RELEASE += -Wl,--gc-sections这些选项配合使用,能让链接器丢弃未使用的函数和数据,进一步减小体积。还有一个技巧是编译时加-fno-exceptions和-fno-rtti,但这要求你的代码不使用异常和RTTI,Qt本身是支持的,但如果你用了第三方库,可能就不行了。
6.3 静态编译与动态编译的取舍
静态编译虽然部署方便,但也不是没有缺点。首先是体积大,一个简单的Qt程序静态编译后可能十几MB,动态编译只有几百KB。其次是内存占用,静态链接的程序在运行时会把整个库加载到内存,而动态链接是多个程序共享同一份库。最后是更新麻烦,如果Qt出了安全补丁,静态编译的程序需要重新编译整个程序,而动态链接只需要更新库文件。
所以我的建议是:如果你的板子存储空间充足,而且上面跑多个Qt程序,那动态编译更合适。如果板子空间紧张,或者你只想发布一个独立的可执行文件,那静态编译是更好的选择。我自己的项目里,Orange Pi CM5的eMMC有32GB,空间不是问题,但我还是选了静态编译,因为部署简单,一个文件拷过去就能跑,不用管板子上的Qt版本。
7. 我踩过的那些坑与个人经验
最后分享几个我在实际项目中踩过的坑,希望能帮你省点时间。
第一个坑是工具链的sysroot路径。Linaro的工具链解压后,sysroot在aarch64-linux-gnu/libc目录下,但有些版本的路径是aarch64-linux-gnu/sysroot。我一开始没注意,configure时-sysroot指错了路径,结果编译时找不到stdio.h。后来用find /opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu -name "stdio.h"找了一下,才发现路径不对。
第二个坑是Qt的版本和qmake的版本不匹配。我开发机上装了系统Qt5.15.3,交叉编译的是5.14.2,结果用交叉编译的qmake生成Makefile时,qmake却调用了系统Qt的库。原因是PATH里系统Qt的qmake在前面。解决方法是用完整路径调用交叉编译的qmake,或者在PATH里把交叉编译的qmake放到最前面。
第三个坑是静态链接时的-lpthread。Qt的静态库依赖pthread,但qmake生成的Makefile里有时候不会自动加上-lpthread,导致链接时报undefined reference to 'pthread_create'。解决方法是在pro文件里手动加LIBS += -lpthread,或者在qmake.conf里加QMAKE_LIBS += -lpthread。
第四个坑是板子上的字体。静态编译的程序在板子上跑起来后,界面上的文字全是方块。原因是板子的文件系统里没有字体文件,而Qt默认会去/usr/share/fonts/找。解决方法是在程序里用QFontDatabase::addApplicationFont加载一个字体文件,或者把字体文件打包到程序资源里。我一般用后者,把字体文件放到qrc里,程序启动时加载,这样就不依赖板子的文件系统了。
第五个坑是编译时的内存不足。Qt的某些模块编译时非常吃内存,比如qtdeclarative,单个编译单元可能占用2GB以上内存。如果你的开发机内存小于8GB,建议减少并行编译的job数,用make -j2甚至make -j1。我一开始用make -j8,结果编译到一半被OOM Killer杀掉了,日志里只看到Killed,排查了半天才发现是内存不够。
这套流程走下来,从零到跑通大概需要半天到一天的时间,主要时间花在编译上。但一旦编译成功,后面就轻松了,程序改完直接交叉编译,拷到板子上就能跑。希望这篇手册能帮你顺利搞定Qt5.14.2的aarch64静态交叉编译,少踩几个坑。