ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

aarch64下Qt 5.14.2静态交叉编译完整实战手册

aarch64下Qt 5.14.2静态交叉编译完整实战手册 1. 为什么你大概率需要这份手册做嵌入式Linux开发的同行应该都明白目标板子跑的是aarch64架构开发机却是x86_64的PC这种组合下给板子准备Qt运行环境就绕不开交叉编译。如果你只是做动态库版本把编译好的so扔到板子上用系统自带的依赖碰运气那倒还省事。可一旦遇到设备需要离线部署、系统环境精简、或者客户要求单文件直接跑的场景静态编译就成了唯一靠谱的出路。我这次折腾Qt 5.14.2的aarch64静态交叉编译前后踩了三天坑查遍各种资料才理清头绪。市面上的教程不少但要么针对老版本Qt要么零零散散只讲其中一步很少有从零到一、把工具链准备到最终程序跑上板子全流程讲透的。所以我把整个搭建过程、每一个坑、每一条命令完整记录下来整理成这份手册。这篇内容适合三类人一是要给aarch64板卡移植Qt应用、但没跑通过交叉编译的嵌入式开发二是需要在离线环境下交付GUI程序的工程师三是想搞懂Qt交叉编译原理、不想只会复制粘贴命令的初学者。读完这份手册你能独立完成一套可复用的aarch64静态Qt环境并且明白每一步在做什么、为什么这么做。先声明一点整个方案我基于常见的aarch64目标板比如树莓派、瑞芯微、全志系列这样的Linux环境使用的宿主机是x86_64的Ubuntu 20.04/22.04系统。如果你的板子用的是glibc版本特别老的话后面关于sysroot的章节一定不能跳过。2. 为什么选择静态交叉编译而非动态方案2.1 静态编译能解决什么问题交叉编译本身不新鲜但静态交叉编译在使用场景上有它的独到之处。先说说我为什么坚持要上静态而不是用更省事的动态库方案。最直接的原因就是部署成本。动态方案做出来产物是一堆so加一个可执行文件你得把这些东西一起打包、一起分发、还要考虑目标板上的Qt依赖是否齐全。遇到系统版本不一致的板子比如开发时用glibc 2.31现场设备却是glibc 2.28运气好能跑运气不好直接报版本符号找不到这种诡异错误。而静态编译的可执行程序依赖都在自己身上拷过去就能跑glibc版本差异只要不太离谱基本不影响运行。第二个原因在于稳定性。动态方案下Qt插件目录、qml模块路径、字体库、平台插件全部依赖运行时环境环境变量稍微配不对程序起来就是黑屏或者段错误。静态编译把所有插件全部链接进可执行文件省去了一堆QTDIR、QT_QPA_PLATFORM_PLUGIN_PATH这类运行时配置程序行为更可控。这一点在交付给现场、由不熟悉Qt的人去部署的场景下优势特别明显。第三个原因和板子资源有关。很多aarch64嵌入式板卡Flash空间只有几百MB甚至更小动态装一套完整的Qt运行库加上必要的插件和依赖占用轻松超过100MB。而静态编译出来的单个可执行文件即便把Qt全部打进去通常也就是20到30MB对存储空间的压力小得多。2.2 静态方案需要付出的代价静态编译并不是银弹付出的代价也必须说清楚。最痛的一点是体积。Debug版本随便编译一下就是上百MBRelease版本压缩后25MB左右跟动态方案的几MB比起来确实大很多。但考虑到嵌入式设备上省掉的运行库体积整体算下来往往还是静态更划算。第二个代价是编译时间。全套Qt静态编译在8核心16线程的机器上大概需要40分钟到1小时动态编译差不多能快一半。如果第一次配置错了返工重编的时间成本更高。第三个代价是license和合规问题。如果你的Qt是商业授权静态链接时需要留意L/GPL条款的要求如果是开源项目LGPLv3在静态链接场景下有提供relink条件的义务。我这里用的是Qt 5.14.2开源版如果只是内部使用和分发自己的应用一般是没问题的但如果要商业化闭源交付建议先和公司法务确认清楚。把这些代价想清楚再回去看自己的需求是选择静态还是动态答案就很清楚了。如果你的应用要和系统里其他Qt程序共享运行环境、频繁升级动态更合适如果像我一样是整机设备交付、软件环境完全自控静态是更好的选择。3. 交叉编译环境的完整搭建3.1 宿主机系统与基础工具准备开始在环境搭建之前先检查一下宿主机的系统版本和架构。我用的是Ubuntu 22.04在x86_64环境下工作。交叉编译最怕的就是工具链和系统环境不匹配所以第一步先把基础的编译工具链装齐。sudo apt update sudo apt install -y build-essential g gcc make cmake ninja-build \ mesa-common-dev libgl1-mesa-dev libxkbcommon-dev \ libfontconfig1-dev libdbus-1-dev libgles2-mesa-dev \ libssl-dev libicu-dev libasound2-dev gperf bison flex \ libclang-dev python3这里每个包都有自己的用途。build-essential提供宿主机自编译的基础工具mesa和libgl相关的是为了后面处理OpenGL相关依赖libfontconfig是Qt字体渲染的依赖libssl和libicu是Qt网络和国际化模块需要的gperf、bison、flex是Qt编译过程中生成代码的工具。需要注意的是宿主机上安装的这些开发库严格来说只服务于Qt源码的配置检测阶段真正链接时用的是后面为aarch64准备的sysroot。但有些检测项在宿主机上没有对应库会直接报错所以索性都装上省得中途卡住。3.2 交叉编译工具链选择工具链选择是整个交叉编译里最核心的决策点。我对比过三种方案Linaro GCC、ARM官方GNU工具链、以及Buildroot生成的工具链。我最后选的是ARM官方GNU-A工具链gcc版本10.3下载的时候选aarch64-linux-gnu的x86_64 Linux版本就行。wget https://developer.arm.com/-/media/Files/downloads/gnu/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz sudo mkdir -p /opt/toolchains sudo tar -xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz -C /opt/toolchains解压后把工具链的bin目录加进PATHexport PATH/opt/toolchains/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH验证是否正常aarch64-none-linux-gnu-gcc -v能看到gcc version 10.3.0的输出就说明工具链没问题。为什么选这个而不选Linaro的主要原因是ARM官方工具链的glibc版本比较新搭配较新Ubuntu宿主机时兼容性更好不太会出现头文件不匹配的问题。Linaro的维护活跃度这几年明显下降而且它的工具链版本和老内核的板子配起来虽然没问题但匹配新系统时容易出现奇怪的链接错误。3.3 Sysroot的准备与配置sysroot是交叉编译里容易被忽视、却极其关键的一环。简单来说sysroot就是目标板系统根文件系统的一个拷贝包含板子上Linux系统的头文件、标准库、以及各种底层依赖库让交叉编译工具链在编译时能正确引用目标系统的API和库文件。这里最容易踩坑Qt的很多依赖是宿主机上没有的必须先准备好aarch64版本的依赖库放到sysroot里然后再编译Qt。要不然就算交叉编译工具链没问题配置阶段会报各种头文件找不到的错误。我准备sysroot的方式是直接从目标板子上拷贝。如果你的板子已经跑了一个完整的Linux系统可以直接在板子上执行rsync -avx / 宿主机IP:/opt/sysroot/aarch64/如果板子还没有系统或者不太方便拷贝可以在宿主机上用debootstrap做一个sudo debootstrap --archarm64 --foreign bookworm /opt/sysroot/aarch64注意debootstrap这种纯手工方式做出来的sysroot一般只有基础的glibc后面编译Qt时大概率还是缺各种依赖库一样得自己手动往sysroot里补充aarch64的库。所以我个人最推荐的方式是在目标板上先装好需要的系统库然后再整体rsync出来。我用树莓派4Baarch64Debian系统做实验时先在板子上安装这些依赖库sudo apt install -y libfontconfig1-dev libdbus-1-dev libfreetype6-dev \ libxkbcommon-dev libxcb1-dev libxcb-keysyms1-dev libxcb-image0-dev \ libxcb-shm0-dev libxcb-util1-dev libxcb-xinerama0-dev \ libxcb-xkb-dev libxkbcommon-x11-dev libgl1-mesa-dev libgles2-mesa-dev \ libssl-dev libicu-dev libasound2-dev装完后rsync整个根文件系统到宿主机。用真实板子的sysroot能最大程度保证编译环境和运行环境的一致性后面出问题的概率小很多。这里还有个小细节需要注意rsync拷回来的sysroot里的动态库很多都是符号链接。比如libc.so.6可能指向libc-2.31.so。做交叉编译时一定要保留这些符号链接拷贝时记得加-l参数否则之后工具链链接时会因为找不到真名而报错。3.4 全局环境变量设置所有环境都准备完毕后建议把环境变量写到一个文件里每次新开终端source一下就行。# /opt/sysroot/env.sh export CROSS_COMPILEaarch64-none-linux-gnu- export PATH/opt/toolchains/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH export SYSROOT/opt/sysroot/aarch64 export CC${CROSS_COMPILE}gcc export CXX${CROSS_COMPILE}g export AR${CROSS_COMPILE}ar export RANLIB${CROSS_COMPILE}ranlib export LD${CROSS_COMPILE}ld export STRIP${CROSS_COMPILE}strip export CFLAGS--sysroot$SYSROOT export CXXFLAGS--sysroot$SYSROOT export LDFLAGS--sysroot$SYSROOT这些变量在后面编译各种第三方依赖库时经常用到提前配好能省下大量的重复劳动。4. Qt 5.14.2源码获取与静态编译配置4.1 源码下载与目录结构规划Qt源码下载很简单直接到官方仓库或者镜像拉取。我用的是清华镜像速度快得多。mkdir -p ~/qt-build cd ~/qt-build wget https://mirrors.tuna.tsinghua.edu.cn/qt/archive/qt/5.14/5.14.2/single/qt-everywhere-src-5.14.2.tar.xz tar -xf qt-everywhere-src-5.14.2.tar.xz解压后可以看到完整的目录结构qtbase是Qt的核心模块qtserialport、qtcharts这些是附加模块。虽然qt-everywhere-src里包含全套模块但嵌入式场景下没必要全部编译只挑需要的模块编译可以大幅缩短编译时间。Qt 5.14.2是Qt 5.15之前比较稳定、兼容性较好的版本aarch64支持已经比较成熟很多嵌入式产品公司至今仍把它作为首选版本。它的configure参数体系非常完善静态编译的支持度也好。4.2 configure参数逐项解析进入源码目录开始配置。configure参数是整个Qt交叉编译中最重要的一个参数错了轻则编译出来的库功能不全重则直接编译失败。我最终使用的configure命令如下然后逐项解释cd qt-everywhere-src-5.14.2 mkdir -p build-static cd build-static ../configure -prefix /opt/qt5.14.2-static-aarch64 \ -static \ -release \ -opensource \ -confirm-license \ -xplatform linux-aarch64-gnu-g \ -nomake examples \ -nomake tests \ -no-compile-examples \ -skip qtwebengine \ -skip qt3d \ -skip qtcanvas3d \ -skip qtpurchasing \ -skip qtvirtualkeyboard \ -skip qtscript \ -skip qtspeech \ -skip qtdoc \ -skip qtandroidextras \ -skip qtmacextras \ -skip qtwinextras \ -skip qtquickcontrols \ -skip qtquickcontrols2 \ -skip qtmultimedia \ -no-opengl \ -no-glib \ -no-icu \ -no-dbus \ -no-feature-dbus \ -qt-libpng \ -qt-libjpeg \ -qt-zlib \ -qt-freetype \ -qt-harfbuzz \ -qt-pcre \ -no-xcb \ -no-eglfs \ -linuxfb \ -no-cups \ -no-iconv \ -no-evdev \ -no-gif \ -no-feature-printdialog \ -v一个一个参数说-prefix指定安装路径后续make install会把头文件、库文件装到这里。-static核心中的核心告诉Qt要生成静态库而不是动态库。-release编译release版本体积更小、性能更好。-opensource和-confirm-license自动接受开源协议。-xplatform linux-aarch64-gnu-g告诉Qt使用哪个交叉平台配置。Qt自带这个平台文件在qtbase/mkspecs/devices/目录下可以找到linux-aarch64-gnu-g这个mkspec它是专门针对aarch64 Linux场景的。-nomake和-skip干掉不需要的模块和构建目标缩短编译时间。接下来说明一些关键的feature开关-no-opengl这个要重点解释。很多嵌入式板子没有完整的OpenGL驱动支持即便有静态编译时链接OpenGL也是一堆麻烦事。如果不需要GPU加速渲染直接关掉OpenGL可以省掉大量依赖问题。需要注意如果你的程序依赖Qt Quick 2D渲染或者需要GPU加速那就不能关反而要去配置eglfs和对应的GPU驱动。-no-icuICU负责Unicode和文本布局体积巨大。如果目标平台不需要复杂的国际化文本处理关闭后编译时间和体积都能减少很多。如果要做网页渲染或者复杂排版就需要打开。-no-dbusD-Bus是Linux桌面环境的进程间通信纯嵌入式场景基本用不上关掉。-qt-libpng/-qt-jibjpeg/-qt-zlib等一堆-qt开头的参数让Qt使用自己携带的第三方库源码而不是去系统sysroot里找。这一步可以极大减少对sysroot内容的要求打包时也不用纠结库版本兼容性。-linuxfb启用Linux Framebuffer平台插件。在无窗口系统的嵌入式板子上这是最基础的GUI渲染方式直接往/dev/fb0写像素。如果你的板子只有LCD屏没有桌面环境这个参数确保有平台插件可用。-no-xcb和-no-eglfs既然走了framebuffer路线这两类显示后端都不需要关掉减少依赖。这里我用了-no-eglfs如果你要接HDMI屏且GPU支持KMS/DRM可以把-no-eglfs去掉改成-eglfs然后在运行时通过QT_QPA_PLATFORMeglfs来指定平台插件。选择linuxfb还是eglfs取决于你的显示方案和硬件能力这一点在后续运行阶段还需要根据实际效果调整。4.3 静态编译的系统依赖处理细节上面的configure参数已经用了大量-qt开头参数让Qt自带第三方库但有些系统库是绕不开的必须让Qt使用sysroot里提供的版本比如libfontconfig、libfreetype、libssl。如果你的程序用到字体渲染、网络请求这些功能这些库是必需的。这里有个容易犯的错误为了让fontconfig正常工作要把-fontconfig保留不要加-no-fontconfig但在编译之前需要确认sysroot里有aarch64版本的fontconfig头文件和库文件。如果没有Qt的configure会检测不到fontconfig然后回退到使用内部的freetype字体渲染效果会差一些。检的方法很简单ls $SYSROOT/usr/include/fontconfig/fontconfig.h ls $SYSROOT/usr/lib/aarch64-linux-gnu/libfontconfig.so如果文件不存在回到第3.3节在板子上装库后重新同步sysroot。这一步保证Qt发现的fontconfig是我们准备的目标板系统版本而不是宿主机x86_64的。4.4 开始编译与常见编译错误现场configure通过之后就开始编译。编译时间取决于机器性能我用了8核心16线程的机器大概40分钟。make -j$(nproc)整个编译过程比较漫长期间可能出现几个比较典型的错误第一个常见错误是“cannot find -ltsan”。这是工具链sysroot里缺少libtsan导致。检查一下sysroot/usr/lib/aarch64-linux-gnu下有没有libtsan相关的库没有的话从板子上拷过来或者直接apt安装libtsan0。另一个是“GL/gl.h: No such file or directory”。就算加了-no-opengl有些模块仍然会去检查OpenGL头文件。解决办法是在sysroot里装上libgl1-mesa-dev的aarch64版本或者检查configure时是否真的排除干净了qt3d等涉及GL的模块。还有一个容易遇到的“error: ‘__GLIBC_PREREQ’ is not defined”。这个问题通常是sysroot和工具链版本不匹配导致的比如老的工具链配新的glibc sysroot。我用的是gcc 10.3和配套的glibc整体匹配度比较好基本没遇到。编译结束后安装make install安装完成后检查一下安装目录ls /opt/qt5.14.2-static-aarch64/lib/你会看到大量.a结尾的静态库文件这就是我们想要的静态Qt库。4.5 交叉编译Qt应用工程的qmake配置Qt库编译安装完成后给具体的Qt应用程序配置交叉编译环境就是最后一公里。这里常见的问题是qmake找不到合适的交叉工具链或者编译时依然调用了宿主机的gcc。在项目工程目录下先确保Qt的bin目录在PATH里export PATH/opt/qt5.14.2-static-aarch64/bin:$PATH用qmake生成Makefile并确认工具链cd ~/projects/myapp /opt/qt5.14.2-static-aarch64/bin/qmakeqmake会根据安装时记录的mkspec自动选择交叉编译器。检查Makefile里是否出现aarch64-none-linux-gnu-g如果没有那就是qmake没有按照预期工作。比较稳妥的方式是创建一个qmake的交叉编译配置文件放在设备mkspecs下面。这里我直接用一个简单的办法在项目根目录建一个名叫“qmake-aarch64.conf”的文件内容如下QMAKE_CC aarch64-none-linux-gnu-gcc QMAKE_CXX aarch64-none-linux-gnu-g QMAKE_LINK aarch64-none-linux-gnu-g QMAKE_AR aarch64-none-linux-gnu-ar cqs QMAKE_STRIP aarch64-none-linux-gnu-strip然后在项目目录执行/opt/qt5.14.2-static-aarch64/bin/qmake -spec /opt/qt5.14.2-static-aarch64/mkspecs/linux-aarch64-gnu-g qmake-aarch64.conf再执行make就能看到用交叉编译器在编译。编出来的可执行文件用file命令查看file myapp输出里有“ELF 64-bit LSB executable, ARM aarch64”就说明交叉编译成功。5. 一个完整可运行的交叉编译工程示例5.1 示例程序代码理论部分说完用一个完整的示例程序把整个流程串起来。目标很简单一个Qt Widgets窗口程序界面上有一个按钮点击后弹出一个对话框。这个例子虽然小但覆盖了静态交叉编译的主要节点。先在项目目录新建main.cpp#include QApplication #include QPushButton #include QMessageBox int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button(Click Me); QObject::connect(button, QPushButton::clicked, []() { QMessageBox::information(nullptr, Info, Hello from aarch64!); }); button.resize(320, 240); button.show(); return app.exec(); }再新建工程文件myapp.proQT core gui widgets TARGET myapp TEMPLATE app CONFIG c17 SOURCES main.cpp这个例子虽然简单但已经用到了Qt Core、Gui、Widgets三个最核心的模块。如果这三个模块的静态链接都成功其他模块基本也问题不大了。5.2 完整编译与运行验证按第4.5节的方法执行qmake并makecd ~/projects/myapp export PATH/opt/qt5.14.2-static-aarch64/bin:$PATH /opt/qt5.14.2-static-aarch64/bin/qmake make -j$(nproc)make完成后会生成一个静态链接的myapp可执行文件。用file命令确认架构和链接方式file myapp应该看到“ELF 64-bit LSB executable, ARM aarch64, ... statically linked”。复制到目标板直接执行./myapp -platform linuxfb如果前面配置正确屏幕上会显示一个带按钮的窗口点击按钮弹出提示框。整个过程不需要设置QTDIR也不需要拷贝任何Qt库这就是静态编译的价值所在。5.3 平台插件选择说明理论上如果configure用了linuxfb运行时需要用-platform linuxfb参数指定平台插件。不过我实测下来如果只有linuxfb一个插件Qt有时会默认选择它就启动不需要额外指定。如果你的系统还有eglfs插件直接运行时不指定平台参数可能报“could not find a platform plugin”这时用-platform eglfs或者-platform linuxfb指定一个就行。静态编译的程序选择平台插件有一点需要注意平台插件是“链接”进程序里的所以运行时用-platform参数指定的是编译进去的插件之一。configure里编译了哪些平台插件运行时就能用哪些。6. 现场移植经验与问题避坑指南6.1 sysroot缺失依赖导致配置失败这是我最开始最容易犯的错。configure阶段报各种头文件缺失后来才发现sysroot里缺了大量库。如果你是从板子上rsync的sysroot先用这个命令检查依赖for f in /opt/sysroot/aarch64/usr/lib/aarch64-linux-gnu/*.so; do echo $f aarch64-none-linux-gnu-readelf -d $f 2/dev/null | grep NEEDED done这样可以一览无余地看到sysroot里的库依赖关系。如果有些库没在sysroot里在板子上补装再重新同步即可。6.2 静态链接时的strip策略静态编译的可执行文件里包含大量的符号信息体积膨胀得很厉害。我build完release版本大约35MB经过strip操作能降到约17MBaarch64-none-linux-gnu-strip --strip-unneeded myapp这里有个取舍。strip之后如果程序在板子上崩溃gdb回溯堆栈时看不到符号名排查问题困难很多。所以我一般保留一个没strip的版本用于调试真正部署时再strip。6.3 字体与中文显示问题静态编译的程序在板子上运行最常见的坑之一就是中文乱码或者字体发虚。原因是系统字体没有随程序一起部署。最简单的解决办法在程序代码里直接指定字体文件用绝对路径加载#include QFontDatabase // 程序启动时加载外部字体 QFontDatabase::addApplicationFont(/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc); QFont font(Noto Sans CJK SC); QApplication::setFont(font);前提是目标板上有这个字体文件。如果板子是精简系统可能连字体文件都没有那就需要把字体文件一起打包进去并在代码里指定相对路径或固定路径。6.4 Qt插件静态链接的初始化顺序静态编译Qt时还有一个容易出问题的地方插件和模块的初始化顺序。Qt使用Q_IMPORT_PLUGIN宏在编译期显式导入插件。如果程序运行时报“no such plugin”错误尽管插件已经链接进程序了通常是因为缺少Q_IMPORT_PLUGIN声明。在main函数里加上#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)如果还需要其他插件比如字体、图片格式的插件可以继续往这里加对应的Q_IMPORT_PLUGIN。不过如果configure和qmake的处理都正常qmake会自动生成plugin import文件一般不需要手动添加。仅在程序报找不到插件时才需要检查这一步。6.5 版本选择建议Qt 5.14.2还是更高版本最后聊一下版本选择的经验。Qt 5.14.2是我验证过的、在aarch64静态交叉编译场景下最稳的版本。如果你问为什么不用5.15 LTS或者6.x说说我的看法。5.15 LTS虽然维护周期长但源码包不太容易直接下载需要走在线安装器自动化脚本集成起来比较麻烦。Qt 6.x的构建系统换成了CMake配置逻辑变化很大aarch64静态编译的坑比5.14.2多不少除非你的新项目从设计上就需要Qt 6否则没必要在那个上面折腾。Qt 5.14.2恰好处于一个很微妙的位置模块足够完整QML/Qt Quick已经能正常使用我对它的交叉编译configure行为非常熟悉网上能找到的资料也足够多遇到问题排查起来方便很多。对于生产环境的嵌入式项目稳定性优先的同事我的建议是先上5.14.2等Qt 6的交叉编译生态成熟一些再考虑升级。7. 我还有几句实在话要说这套环境搭起来之后它就成了我手上所有aarch64板卡Qt应用开发的标准底座。每次新起一个项目直接用它来编译省掉了重复搭建环境的时间。如果哪天我需要编译其他模块比如Qt Charts只需要回到源码目录单独qmake和make对应模块再make install就能增量加上去不需要重新编译整个Qt。另外补充一个很少被提到的小经验在编译Qt之前建议先用一条简单的交叉编译命令验证工具链和sysroot能不能配合echo int main(){return 0;} | aarch64-none-linux-gnu-gcc --sysroot$SYSROOT -x c - -o /tmp/test file /tmp/test如果这一步都不能生成aarch64可执行文件那问题就在工具链或者sysroot不要浪费时间去跑Qt的configure。很多次configure阶段报的莫名其妙的错误根源就是这一步没做好。交叉编译说到底是环境工程环境搭对了后面让Qt工作就是顺水推舟的事。希望这份手册能帮你少走几段弯路。如果你在实操中遇到我没写到的坑欢迎去社区讨论这类问题很多时候一个环境细节就能卡住好几天。
返回列表