ARTICLE DETAIL

资讯详情

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

Qt 5.14.2 aarch64 静态交叉编译实战:从工具链到部署全流程

Qt 5.14.2 aarch64 静态交叉编译实战:从工具链到部署全流程 1. 为什么值得折腾 Qt 5.14.2 的 aarch64 静态交叉编译如果你手上有 Orange Pi CM5、树莓派这类 aarch64 开发板又打算把 Qt 程序直接烧进系统镜像里跑那你迟早会撞上“静态交叉编译”这堵墙。动态编译出来的 Qt 程序部署到板子上要么缺库、要么版本对不上cannot mix incompatible qt library这类报错能让人抓狂。静态编译把 Qt 库直接塞进可执行文件拷过去就能跑省掉一整套运行时依赖这在嵌入式量产场景里几乎是刚需。Qt 5.14.2 是个很微妙的版本。它比 5.12 系列新又不像 5.15 那样对编译器和依赖要求那么苛刻在 CentOS 7.9 aarch64 或者 Ubuntu 交叉环境里都相对好伺候。但“相对好伺候”不等于“好伺候”静态编译 Qt 本身就是个连环坑工具链要选对、configure 参数一个不能错、serialport 这类模块动不动就unknown module、静态链接还牵扯到-static和-static-libstdc的取舍。我前后在几块板子上折腾过好几轮踩的坑足够写一本小册子。这篇手册就是把这些经验摊开讲。从工具链选型、源码准备、configure 参数逐条拆解到编译、安装、验证、排错全流程走一遍。目标读者是已经会基本 Linux 操作、懂一点交叉编译概念、但没完整做过 Qt 静态交叉编译的嵌入式开发者。看完你应该能自己从零搭出一套可用的 aarch64 静态 Qt 工具链并且知道每个参数为什么这么写出了问题往哪儿查。2. 整体方案设计与关键选型思路2.1 静态编译 vs 动态编译到底该选哪个先把这件事说清楚不然后面全是白费功夫。动态编译的 Qt 程序运行时依赖一堆.so部署时你得把libQt5Core.so.5、libQt5Gui.so.5、平台插件platforms/libqlinuxfb.so等等全部拷到板子上还得保证LD_LIBRARY_PATH对、版本一致。板子上的系统 Qt 版本和你编译用的版本一旦对不上就是那句经典的cannot mix incompatible qt library (5.15.3) with this library (5.15.2)。静态编译则是把用到的 Qt 代码全部编进最终可执行文件。好处很直接单文件部署拷过去chmod x就能跑不依赖目标机的 Qt 环境。代价是体积大一个带 GUI 的程序轻松几十 MB、编译时间长我这边 16 核机器全量编译接近 40 分钟、而且一旦某个模块没编进去运行时才发现就晚了。我的建议是面向量产的嵌入式设备、系统镜像定制、或者目标机 Qt 环境不可控的场景优先静态编译。如果是开发调试阶段、板子上已经有完整 Qt 运行时动态编译更省事。这篇手册聚焦静态方案。2.2 工具链选型为什么是 gcc-arm 而不是别的aarch64 的交叉工具链选择其实不少常见的有 Linaro 的gcc-arm-*系列、Bootlin 的 toolchain、以及各家芯片厂自带的 SDK 工具链。热词里提到的env工具链、unity工具链、autosar工具链、etas工具链大多是特定行业或特定芯片的配套工具链通用性不强。我最终选的是Linaro 的gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu理由有三条。第一它基于 glibc和主流 aarch64 发行版Ubuntu、Debian、CentOS的运行时兼容性最好第二GCC 10.3 对 C17 支持完整Qt 5.14.2 的源码用它能干净编过第三这个版本足够稳定社区资料多出问题好查。注意不要用aarch64-none-elf这类裸机工具链那是给没有操作系统的场景用的编 Qt 会缺一堆系统头文件。一定要选带linux-gnu后缀的。工具链的sysroot也很关键。Linaro 工具链自带一个精简 sysroot但 Qt 编译需要目标系统的完整头文件和库。稳妥做法是从目标板子上把/usr/include、/usr/lib、/lib打包下来或者用和目标系统同版本的发行版 rootfs 作为 sysroot。我这边用的是和目标板同版本的 Ubuntu 20.04 aarch64 rootfs。2.3 源码版本与目录规划Qt 5.14.2 的源码包是qt-everywhere-src-5.14.2.tar.xz注意是everywhere版本包含了所有模块。热词里有人问qt-everywhere-src-5.15.10 交叉编译思路完全一样只是版本号不同。下载建议走国内镜像速度差好几倍。目录规划我习惯这样~/qt-build/ ├── toolchain/ # 交叉工具链解压目录 ├── sysroot/ # 目标系统 rootfs ├── src/ # Qt 源码 │ └── qt-everywhere-src-5.14.2/ ├── build/ # 编译输出目录out-of-source └── install/ # 安装目录一定要用 out-of-source 编译也就是在build/目录里执行 configure源码目录保持干净。Qt 的 configure 会生成大量中间文件污染源码目录后想重新配置会很痛苦删都删不干净。3. 环境准备与工具链配置实操3.1 工具链安装与验证先把工具链解压到~/qt-build/toolchain/然后配置环境变量。我习惯写一个env.sh每次开新终端 source 一下export TOOLCHAIN_ROOT~/qt-build/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu export SYSROOT~/qt-build/sysroot export PATH$TOOLCHAIN_ROOT/bin:$PATH export CROSS_COMPILEaarch64-none-linux-gnu- export CC${CROSS_COMPILE}gcc export CXX${CROSS_COMPILE}g验证工具链能不能用aarch64-none-linux-gnu-gcc -v能打印出 GCC 版本信息就对了。再编个 hello world 验证一下echo int main(){return 0;} test.c aarch64-none-linux-gnu-gcc test.c -o test file testfile输出里应该看到ELF 64-bit LSB executable, ARM aarch64。如果显示的是 x86-64说明 PATH 没配对编出来的是本机程序。3.2 sysroot 的准备与常见坑sysroot 是交叉编译里最容易出问题的地方。Qt 编译过程中会去 sysroot 里找libGL.so、libX11.so、libfontconfig.so这些依赖。如果 sysroot 不完整configure 阶段就会报各种cannot find -lXXX。我的做法是从目标板子上直接 rsync 一份完整 rootfsrsync -avz --numeric-ids roottarget:/usr/include/ $SYSROOT/usr/include/ rsync -avz --numeric-ids roottarget:/usr/lib/ $SYSROOT/usr/lib/ rsync -avz --numeric-ids roottarget:/lib/ $SYSROOT/lib/注意rsync 的时候一定要加--numeric-ids否则 uid/gid 映射会出问题。另外/usr/lib里可能有指向绝对路径的符号链接跨机器拷贝后可能失效需要检查一下。如果板子不方便联网也可以用debootstrap在本地搭一个同版本的 aarch64 rootfs或者直接下载发行版的 aarch64 minimal rootfs 压缩包。关键是目标系统的 glibc 版本不能低于工具链的 glibc 版本否则编出来的程序在板子上跑不起来报GLIBC_2.XX not found。3.3 依赖库的交叉编译Qt 的 GUI 模块依赖一堆第三方库。如果 sysroot 里已经有这些库的 aarch64 版本直接指过去就行。如果没有就得自己交叉编译。常见的几个zlibQtCore 的压缩支持libpng / libjpeg图片格式支持freetype字体渲染GUI 必需fontconfig字体配置libinput / tslib触摸屏输入以 freetype 为例交叉编译的套路是./configure --hostaarch64-none-linux-gnu \ --prefix$SYSROOT/usr \ --enable-static --disable-shared make -j$(nproc) make install--host指定交叉编译器前缀--prefix指到 sysroot 里这样 Qt configure 时能自动找到。静态编译 Qt 时依赖库也尽量编成静态的否则最终链接还是会引入.so依赖静态编译的意义就打折了。热词里提到的boost库交叉编译也是同样的思路不过 Qt 本身不依赖 boost除非你的业务代码用了。交叉编译chrony那是系统工具和 Qt 无关这里不展开。4. configure 参数逐条拆解与编译实战4.1 configure 参数为什么这么写这是整个流程的核心。Qt 5.14.2 的 configure 参数有几百个但真正关键的就那么十几个。我先把完整命令贴出来再逐条解释../src/qt-everywhere-src-5.14.2/configure \ -prefix /opt/qt-5.14.2-aarch64-static \ -opensource -confirm-license \ -release \ -static \ -nomake examples -nomake tests \ -no-opengl \ -no-xcb \ -no-eglfs \ -linuxfb \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype \ -qt-pcre \ -no-iconv \ -skip qtwebengine \ -skip qtdeclarative \ -sysroot $SYSROOT \ -platform linux-g \ -xplatform linux-aarch64-gnu-g \ -device-option CROSS_COMPILEaarch64-none-linux-gnu- \ -v逐条说-prefix是安装路径注意这是目标机上的路径不是编译机的。程序运行时如果动态加载插件会去这个路径找。静态编译下影响较小但保持一致没坏处。-static是核心生成静态库。-release关掉调试符号减小体积。-opensource -confirm-license是自动接受开源协议不加会卡在交互式确认。-nomake examples -nomake tests强烈建议加上。examples 和 tests 编译量巨大而且静态编译下很多 example 会链接失败白白浪费时间。-no-opengl -no-xcb -no-eglfs -linuxfb这组是平台后端选择。嵌入式板子通常没有 X11用linuxfb直接写 framebuffer 最省事。如果你的板子有 GPU 且要用 EGLFS那就把-no-eglfs换成-eglfs但需要额外的 GPU 驱动库支持。-no-opengl是因为很多嵌入式场景不需要 OpenGL关掉能省不少编译时间和体积。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre这组是让 Qt 用自带的第三方库源码编译而不是去 sysroot 找系统库。静态编译强烈建议这么干因为系统库的静态版本往往没装而且版本可能不匹配。-qt-pcre尤其重要Qt 的正则表达式依赖它。-no-iconv关掉字符集转换嵌入式场景一般用不上关掉省依赖。-skip qtwebengine -skip qtdeclarative跳过这两个大块。qtwebengine 基于 Chromium交叉编译极其痛苦静态编译基本不可能qtdeclarative 是 QML 运行时如果你只用 Widgets 就不需要。如果你要用 QML那 qtdeclarative 不能跳但编译量和依赖会大幅增加要有心理准备。-sysroot指向前面准备的 sysroot。-platform linux-g是编译机host的平台-xplatform linux-aarch64-gnu-g是目标机target的平台。这两个不能搞反。-device-option CROSS_COMPILE...把交叉编译器前缀传给 qmake 的 mkspec。4.2 mkspec 的定制linux-aarch64-gnu-g这个 mkspec 在 Qt 5.14.2 里不一定存在需要自己建。在qtbase/mkspecs/下复制一份linux-aarch64-gnu-g目录如果没有就从linux-arm-gnueabi-g改编辑qmake.confMAKEFILE_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-none-linux-gnu-gcc QMAKE_CXX aarch64-none-linux-gnu-g QMAKE_LINK aarch64-none-linux-gnu-g QMAKE_LINK_SHLIB aarch64-none-linux-gnu-g QMAKE_AR aarch64-none-linux-gnu-ar cqs QMAKE_OBJCOPY aarch64-none-linux-gnu-objcopy QMAKE_NM aarch64-none-linux-gnu-nm -P QMAKE_STRIP aarch64-none-linux-gnu-strip load(qt_config)QT_QPA_DEFAULT_PLATFORM linuxfb这行决定了默认的平台插件和 configure 里的-linuxfb呼应。4.3 编译过程与时间预估configure 跑完会打印一份配置摘要一定要仔细看。重点确认这几项Build type: releaseBuild options: -staticQPA backends里 linuxfb 是 yesQt modules里你需要的模块都是 yes确认无误后开始编译make -j$(nproc) 21 | tee build.logtee是为了留日志出问题好回溯。16 核机器全量编译大概 30-40 分钟8 核大概 1 小时以上。编译过程中如果报错先看build.log里第一个 error后面的错误往往是连锁反应。编译完成后安装make install安装到-prefix指定的目录。静态编译下这个目录主要是给后续开发用的里面有lib/libQt5Core.a这些静态库和头文件。5. 常见问题排查与避坑经验5.1 unknown module in qt:serialport 怎么解决这是热词里出现频率最高的报错:-1: error: unknown module(s) in qt: serialport。原因很简单QtSerialPort 模块没被编译进去。Qt 5.14.2 的 serialport 在qtserialport子模块里默认是编的但如果你 configure 时加了-skip qtserialport或者编译过程中 serialport 因为依赖问题失败了就会缺。排查步骤检查 configure 摘要里Qt SerialPort是不是 yes检查install/lib/下有没有libQt5SerialPort.a如果缺重新 configure确保没有 skip并且 sysroot 里有libudev的开发文件serialport 依赖 udev如果 sysroot 里没有 udev可以-no-libudev关掉 udev 支持serialport 仍能编只是设备热插拔检测功能受限。5.2 静态链接时的 -static 与 -static-libstdc 取舍静态编译 Qt 后你的业务程序链接时也要加-static。但这里有个坑全静态链接 glibc 会有问题尤其是涉及getaddrinfo、dlopen这些函数时静态 glibc 的行为和动态不一样可能出诡异 bug。我的做法是Qt 库静态链接但 glibc 和 libstdc 动态链接。也就是链接时用-static-libgcc -static-libstdc而不是全-static。这样 Qt 的代码进了可执行文件但 C 运行时还是用系统的兼容性最好。代价是目标机得有对应版本的 glibc不过这个要求通常都能满足。5.3 编译过程中的典型报错速查报错信息原因解决cannot find -lGLsysroot 缺 libGL装-no-opengl或补 sysrootGLIBC_2.XX not found工具链 glibc 高于目标机换低版本工具链或升级目标机unknown module(s) in qt: serialportserialport 未编译检查 configure 摘要补 udevcannot mix incompatible qt library动态库版本冲突静态编译或统一版本undefined reference to dlopen全静态链接 glibc改用-static-libstdcqmake: command not foundPATH 未配置source env.shProject ERROR: Unknown module(s) in QT: quickqtdeclarative 被 skip去掉-skip qtdeclarative5.4 几个只有踩过才知道的细节第一configure 的-v一定要加。它会打印详细的检测过程哪个库没找到、哪个特性被禁用一目了然。不加-v的话 configure 静默跳过很多东西你以为编进去了其实没有。第二编译前先make confclean。如果你改过 configure 参数重新编一定要先清理。Qt 的增量编译对 configure 变更支持不好残留的中间文件会导致莫名其妙的链接错误。第三注意-qt-zlib和系统 zlib 的冲突。如果 sysroot 里有 zlib 且你的业务代码也链接了系统 zlib可能出现符号重复定义。统一用-qt-zlib最省心。第四静态编译的插件加载。Qt 的静态插件比如 platform 插件、imageformat 插件需要在代码里用Q_IMPORT_PLUGIN宏显式导入否则运行时找不到。这是静态编译和动态编译最大的行为差异很多人栽在这里。比如要用 linuxfb#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)图片格式插件同理Q_IMPORT_PLUGIN(QJpegPlugin)之类。第五qmake 的路径。安装完成后install/bin/qmake是交叉编译版的 qmake用它来生成 Makefile 才能编出 aarch64 程序。别用系统自带的 qmake那是 x86 的。6. 验证与部署编出来到底能不能跑6.1 用 file 和 readelf 验证架构编出一个测试程序后先验证架构file myapp # 应输出: ELF 64-bit LSB executable, ARM aarch64, ... readelf -d myapp | grep NEEDEDreadelf -d看动态依赖。如果静态编译成功NEEDED里应该只有libc.so.6、libstdc.so.6、libm.so.6这几个基础库没有libQt5*.so。如果看到libQt5Core.so.5说明静态链接没生效检查链接参数。6.2 部署到板子上的注意事项拷到板子上跑之前确认几件事板子的 glibc 版本不低于工具链的如果用 linuxfb确认/dev/fb0存在且有权限触摸屏的话确认/dev/input/eventX权限字体文件要放到板子上或者用 Qt 内置字体跑的时候如果报This application failed to start because no Qt platform plugin could be initialized说明 platform 插件没静态导入回去加Q_IMPORT_PLUGIN。6.3 体积优化静态编译的程序动辄几十 MB量产时可能需要裁剪。几个手段-no-opengl -no-xcb已经砍掉一大块-skip qtwebengine -skip qtdeclarative再砍一大块编译后aarch64-none-linux-gnu-strip myapp去掉符号表能减 30% 以上configure 时加-no-feature-XXX关掉不需要的特性我实测一个纯 Widgets 的串口工具静态编译 strip 后大概 12MB可以接受。7. 我个人的几点实操体会折腾 Qt 静态交叉编译这件事最大的感受是configure 参数决定了 80% 的成败。前面花半小时把参数理清楚比后面花三小时排查链接错误划算得多。我现在的习惯是每次 configure 都把完整命令和摘要存一份下次换版本直接改版本号复用。另一个体会是sysroot 一定要干净且完整。我早期图省事sysroot 里混了 x86 的头文件结果编出来的东西架构混乱报错信息还特别误导。后来严格用目标板 rsync 下来的 rootfs问题少了一大半。还有一点别怕重编。Qt 编译虽然慢但配置错了硬扛着改最后往往要推倒重来。发现 configure 摘要不对果断make confclean重来比在错误的基础上打补丁快。最后分享一个小技巧如果你只是想在板子上快速验证 Qt 程序又不想折腾静态编译可以先用动态编译 把 Qt 库打包进一个目录用LD_LIBRARY_PATH指过去跑通逻辑确认业务代码没问题后再切静态编译做最终交付。这样能把“业务调试”和“编译环境折腾”两件事解耦效率高很多。
返回列表