ARTICLE DETAIL

资讯详情

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

aarch64 上 Qt 5.14.2 静态交叉编译实战指南

aarch64 上 Qt 5.14.2 静态交叉编译实战指南 1. 为什么要在 aarch64 上折腾 Qt 5.14.2 静态编译如果你手上有 Orange Pi CM5、树莓派这类 aarch64 开发板又恰好要做一套需要独立分发的 Qt 界面程序那你大概率绕不开静态交叉编译这条路。动态编译出来的程序扔到板子上第一件事就是报error while loading shared libraries: libQt5Core.so.5然后你开始往板子上拷各种.so拷到最后自己都记不清哪个版本对应哪个。静态编译就是一次性把 Qt 库全部塞进可执行文件里拷一个文件过去就能跑省心。但 Qt 5.14.2 这个版本有点特殊。它是 Qt 5.14 系列的最后一个补丁版本官方对 aarch64 的预编译包支持并不完整尤其是静态库基本没有现成的。你要么用发行版仓库里的动态包要么自己从源码编。而自己编就涉及到交叉编译工具链的选择、configure 参数的取舍、依赖库的处理每一步都有坑。这篇手册面向的是这样一类人你有一台 x86_64 的 Linux 开发机Ubuntu 或 CentOS 都行有一块 aarch64 的目标板你想从零把 Qt 5.14.2 的静态库编出来并且能跑通一个最简单的窗口程序。我不假设你有交叉编译经验但假设你会用基本的 Linux 命令和 make。先说清楚一个概念免得后面混淆。交叉编译指的是在 A 架构的机器上编译出能在 B 架构上运行的程序。这里 A 是 x86_64B 是 aarch64。静态编译指的是把依赖库的代码直接链接进最终的可执行文件而不是运行时动态加载。两者叠加就是在 x86_64 上编出一个自带 Qt 库的 aarch64 可执行文件。为什么不用 Qt 5.15因为 5.15 的开源版本在依赖和模块拆分上改动较大而且部分子模块的许可协议有变化对于需要长期维护的项目来说5.14.2 反而更稳。至于 Qt 6它对交叉编译的 CMake 配置要求更高工具链文件写起来更啰嗦不适合作为第一次尝试的目标。提示静态编译 Qt 会显著增大可执行文件体积。一个带 Widgets 的最小程序静态链接后大概 15 到 25 MB。如果你的板子存储紧张这一点要提前考虑。2. 工具链选型为什么是 gcc-arm 而不是别的2.1 aarch64 交叉工具链的几种来源市面上能编 aarch64 的交叉工具链主要有这么几类Linaro 发布的gcc-arm-*-aarch64-none-linux-gnu、Ubuntu 仓库里的gcc-aarch64-linux-gnu、以及各种厂商 SDK 里自带的工具链。我实测下来最省事的是 Linaro 的gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu原因有三点。第一它的 glibc 版本相对保守编出来的程序在旧一点的板子系统上也能跑。Ubuntu 仓库里的gcc-aarch64-linux-gnu往往跟着宿主机的 glibc 走版本偏新扔到板子上容易报GLIBC_2.xx not found。第二Linaro 的工具链自带完整的 sysroot头文件和库都在里面configure 的时候指定--sysroot就行不用额外去板子上拷根文件系统。第三它的命名规范统一aarch64-none-linux-gnu-gcc这种前缀在 configure 里写起来不容易出错。工具链的下载和解压很简单解压到一个固定路径比如/opt/toolchain/然后把bin目录加到PATH里。验证方式是执行aarch64-none-linux-gnu-gcc -v能看到版本信息就说明装好了。2.2 工具链前缀与 configure 的对应关系这里有个容易搞混的点工具链前缀aarch64-none-linux-gnu-和 Qt configure 里的-xplatform参数不是一回事。-xplatform指定的是 Qt 源码里mkspecs目录下的平台配置文件比如linux-aarch64-gnu-g。你需要确保这个 mkspec 文件里的QMAKE_CC、QMAKE_CXX等变量指向你的工具链。Qt 5.14.2 源码里自带linux-aarch64-gnu-g这个 mkspec但它的默认配置可能和你的工具链不完全匹配。我的做法是复制一份出来改比如复制成linux-aarch64-custom-g然后修改qmake.conf里的编译器路径和 sysroot。这样既不动原始文件又能精确控制。# 复制 mkspec cp -r qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-aarch64-gnu-g \ qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-aarch64-custom-g # 编辑 qmake.conf关键几行如下 # QMAKE_CC /opt/toolchain/bin/aarch64-none-linux-gnu-gcc # QMAKE_CXX /opt/toolchain/bin/aarch64-none-linux-gnu-g # QMAKE_LINK /opt/toolchain/bin/aarch64-none-linux-gnu-g # QMAKE_AR /opt/toolchain/bin/aarch64-none-linux-gnu-ar cqs # QMAKE_STRIP /opt/toolchain/bin/aarch64-none-linux-gnu-stripsysroot 的指定有两种方式一种是在qmake.conf里加QMAKE_CFLAGS --sysroot/path/to/sysroot另一种是在 configure 命令行里通过-sysroot传。我倾向于后者因为改起来方便不用动 mkspec 文件。2.3 为什么不用 Buildroot 或 Yocto 自带的工具链有人会问板子厂商提供的 SDK 里不是有工具链吗直接用那个不就行了。理论上可以但实际用起来有两个问题。一是厂商工具链的版本往往偏老gcc 可能还是 7.x 甚至 6.x编 Qt 5.14.2 的时候会遇到 C14 特性支持不全的问题。二是厂商工具链的 sysroot 里库的版本和你的目标系统未必一致编出来的 Qt 可能在板子上跑不起来。Linaro 的工具链相对中立兼容性更好。3. 依赖库的准备静态编译绕不开的前置工作3.1 哪些依赖必须提前编好Qt 本身依赖一堆第三方库动态编译的时候这些库可以运行时加载静态编译就必须在链接阶段全部准备好。核心的几个是zlib、libpng、libjpeg-turbo、freetype、harfbuzz、libffi、glib如果要用 GLib 事件循环、dbus如果要用 D-Bus。其中glib和dbus是可选但常用的如果你只做纯 Widgets 程序可以关掉。这些库的静态版本Linaro 工具链的 sysroot 里不一定都有。我的做法是单独建一个deps目录把每个库交叉编译成静态库安装到deps/下然后在 configure 的时候通过-I和-L指向这个目录。以 zlib 为例交叉编译的流程是CCaarch64-none-linux-gnu-gcc \ ./configure --prefix/home/user/deps --static make -j$(nproc) make installlibpng 和 libjpeg-turbo 类似但要注意 libpng 依赖 zlib所以 configure 的时候要加CPPFLAGS-I/home/user/deps/include和LDFLAGS-L/home/user/deps/lib。freetype 依赖 libpng 和 zlibharfbuzz 依赖 freetype 和 glib依赖链要按顺序编。3.2 依赖库的版本选择与踩坑记录这里有个我踩过的坑freetype 的版本不要选太新的。2.11 之后的版本对 harfbuzz 的依赖方式有变化静态链接的时候容易出现符号重复定义。我最后用的是 freetype 2.10.4 配 harfbuzz 2.6.8这个组合在 Qt 5.14.2 上验证过没问题。另一个坑是 glib。glib 的静态编译比较麻烦因为它内部用了很多gmodule和gthread的东西静态链接的时候需要显式链接-lpthread -ldl。如果你不需要 GLib 事件循环configure 的时候加-no-glib直接关掉能省很多事。Qt 默认会用 GLib 做事件循环但关掉之后用自带的QEventDispatcherUNIX也能跑只是某些和系统集成相关的功能会受限。注意静态编译时所有依赖库都必须是静态库.a不能混用动态库.so。如果某个依赖只有动态版本要么自己编静态版要么在 configure 里关掉对应的 Qt 模块。3.3 用 pkg-config 管理依赖路径依赖多了之后configure 命令行会变得很长。用pkg-config可以简化这个过程。把每个依赖库的.pc文件放到deps/lib/pkgconfig/下然后设置PKG_CONFIG_PATH指向这个目录configure 的时候 Qt 会自动通过 pkg-config 找到依赖。但要注意交叉编译时 pkg-config 默认会去找宿主机的库必须设置PKG_CONFIG_SYSROOT_DIR和PKG_CONFIG_LIBDIR来限制搜索范围。我通常写一个环境脚本把这些变量都设好每次编译前 source 一下。export PKG_CONFIG_PATH/home/user/deps/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR/home/user/deps export PKG_CONFIG_LIBDIR/home/user/deps/lib/pkgconfig4. configure 参数详解每一个开关背后的取舍4.1 静态编译的核心开关Qt 的 configure 脚本参数非常多但静态交叉编译真正关键的就那么几个。-static开启静态编译-release关掉调试信息除非你要调试 Qt 内部-opensource确认开源许可-confirm-license跳过交互确认。-prefix指定安装路径这个路径是目标板上的路径不是宿主机的路径通常设成/opt/qt5.14.2-static。-xplatform linux-aarch64-custom-g指定我们前面改好的 mkspec。-sysroot指向工具链的 sysroot。-no-opengl关掉 OpenGL因为大多数 aarch64 板子的 GPU 驱动对 Qt 的 OpenGL 支持并不好关掉能避免一堆链接错误。如果你确实需要 OpenGL改成-opengl es2并确保板子有对应的 EGL 库。-nomake examples -nomake tests跳过示例和测试能省大量编译时间。-skip可以跳过整个模块比如-skip qtwebengine这个模块编译极其耗时且依赖复杂静态编译基本用不上。4.2 模块裁剪哪些该留哪些该砍Qt 5.14.2 的模块很多全编一遍在普通开发机上可能要两三个小时。静态编译时模块裁剪尤其重要因为每多一个模块最终可执行文件的体积就大一圈。我的建议是只保留qtbase如果确实需要图表再加qtcharts需要串口再加qtserialport。这里要提一个热词里出现的问题unknown module(s) in qt: serialport。这个报错通常是因为qtserialport模块没有编译或者没有安装。静态编译时如果你在.pro里写了QT serialport但 configure 时没有包含这个模块就会报这个错。解决办法是在 configure 时加上-qt-serialport注意不是-skip确保模块被编进去。qtbase内部也有细粒度的裁剪。-no-feature-*可以关掉特定特性比如-no-feature-cups关掉打印支持-no-feature-printdialog关掉打印对话框。这些在嵌入式场景下基本用不到关掉能减小体积。4.3 一个完整的 configure 命令示例把上面的取舍综合起来我的 configure 命令大概长这样./configure \ -prefix /opt/qt5.14.2-static \ -opensource -confirm-license \ -release -static \ -xplatform linux-aarch64-custom-g \ -sysroot /opt/toolchain/aarch64-none-linux-gnu/libc \ -no-opengl \ -no-glib \ -no-dbus \ -no-cups \ -no-feature-printdialog \ -no-feature-printer \ -nomake examples -nomake tests \ -skip qtwebengine -skip qtwebkit \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz \ -I /home/user/deps/include \ -L /home/user/deps/lib注意-qt-zlib这些参数的意思是使用 Qt 自带的 zlib 源码而不是用系统的。Qt 源码里自带了一些第三方库的副本用自带的能避免版本兼容问题。但如果你已经自己编了静态版也可以用-system-zlib指向自己的。我倾向于用 Qt 自带的省事。-no-glib和-no-dbus是我个人的选择因为我的目标板不需要这些。如果你需要去掉这两个参数但要确保 glib 和 dbus 的静态库已经准备好。5. 编译与安装从 make 到 make install 的完整链路5.1 编译过程中的资源控制configure 跑完之后会生成 Makefile接下来就是make。这里有个实际问题Qt 的编译非常吃内存尤其是qtbase里的qmake和moc这些工具。如果你的开发机内存小于 8GB建议用make -j2而不是make -j$(nproc)否则容易触发 OOM。编译时间方面在一台 8 核 16GB 的机器上只编qtbase大概 40 分钟到 1 小时。如果加上qtcharts和qtserialport再多 15 分钟左右。qtwebengine就别想了静态编译它可能需要一整天而且成功率不高。编译过程中如果报错最常见的原因是头文件找不到或者符号未定义。头文件找不到通常是-I路径没设对符号未定义通常是某个依赖库没链接上。这时候要看具体的错误信息定位到是哪个模块的问题然后检查对应的依赖。5.2 安装路径的处理与 stripmake install会把编译好的库和头文件安装到-prefix指定的路径。但注意这个路径是目标板上的路径在宿主机上它只是一个普通目录。安装完成后你会在/opt/qt5.14.2-static下看到lib、include、plugins等目录。静态库的.a文件通常比较大可以用aarch64-none-linux-gnu-strip去掉调试符号能减小不少体积。但 strip 之后如果出问题就不好调试了所以建议保留一份未 strip 的备份。# 对静态库进行 strip find /opt/qt5.14.2-static/lib -name *.a -exec \ aarch64-none-linux-gnu-strip --strip-debug {} \;plugins 目录在静态编译时比较特殊。Qt 的插件机制在静态链接下需要显式导入不能像动态库那样自动加载。比如图片格式插件、平台插件都需要在代码里用Q_IMPORT_PLUGIN宏导入。这一点后面在写测试程序的时候会详细说。5.3 验证编译结果检查库文件和 mkspec安装完成后先别急着写程序做几个基本检查。第一确认libQt5Core.a等静态库存在。第二确认mkspecs目录下有我们自定义的linux-aarch64-custom-g。第三用aarch64-none-linux-gnu-objdump检查一下.a文件的架构确保是 aarch64 而不是 x86_64。aarch64-none-linux-gnu-objdump -f /opt/qt5.14.2-static/lib/libQt5Core.a | head -5输出里应该能看到architecture: aarch64。如果是i386:x86-64说明工具链用错了得回去检查 mkspec 里的编译器路径。6. 用 qmake 跑通第一个静态程序6.1 项目文件与 qmake 配置写一个最简单的 Widgets 程序来验证。.pro文件里关键的是QT widgets和CONFIG static。但光这样还不够静态链接时需要显式指定 Qt 的库路径和平台插件。QT widgets CONFIG static release TARGET hello-qt-static SOURCES main.cpp # 指定 Qt 安装路径 QMAKE_INCDIR_QT /opt/qt5.14.2-static/include QMAKE_LIBDIR_QT /opt/qt5.14.2-static/lib # 静态链接时需要导入平台插件 QTPLUGIN qlinuxfb qminimalQTPLUGIN这一行很关键。静态编译的 Qt 程序如果没有导入平台插件运行时会报This application failed to start because no Qt platform plugin could be initialized。qlinuxfb是给没有 X11 的板子用的qminimal是最小化平台插件通常作为后备。6.2 平台插件的静态导入方式除了在.pro里写QTPLUGIN还可以在main.cpp里用宏导入。两种方式选一种就行我倾向于在代码里导入因为更直观。#include QApplication #include QLabel #include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbPlatformPlugin) Q_IMPORT_PLUGIN(QMinimalPlatformPlugin) int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello Qt Static aarch64); label.show(); return app.exec(); }注意Q_IMPORT_PLUGIN里的类名要和插件源码里的类名一致。QLinuxFbPlatformPlugin是qlinuxfb插件的类名QMinimalPlatformPlugin是qminimal的类名。如果写错了编译时会报找不到符号。6.3 交叉编译与部署验证用交叉编译版的 qmake 来生成 Makefile/opt/qt5.14.2-static/bin/qmake hello-qt-static.pro make编出来的可执行文件用file命令检查一下应该是ELF 64-bit LSB executable, ARM aarch64。然后把这个文件拷到板子上直接运行。如果板子有显示屏应该能看到一个显示 Hello Qt Static aarch64 的窗口。如果运行时报错常见的有这么几种。一是no Qt platform plugin could be initialized说明插件没导入成功检查Q_IMPORT_PLUGIN的类名和QTPLUGIN的配置。二是GLIBC_2.xx not found说明工具链的 glibc 版本比板子上的新需要换更旧版本的工具链。三是段错误通常是某个依赖库的静态版本和 Qt 的编译选项不匹配需要逐个排查。提示在板子上运行前先确认板子的系统架构是 aarch64 而不是 armv7l。用uname -m查看输出aarch64才对。如果是armv7l那这套工具链编出来的程序跑不了。7. 静态编译中那些文档不会告诉你的坑7.1 符号冲突与重复定义静态链接最容易遇到的问题是符号重复定义。比如你同时链接了 Qt 自带的 zlib 和系统里的 zlib链接器就会报multiple definition of inflate。解决办法是确保只链接一份。用-qt-zlib的时候Qt 会把 zlib 的符号编进libQt5Core.a你就不要再额外链接-lz了。另一个常见的符号冲突是libpng和libjpeg的版本问题。如果 Qt 自带的版本和你自己编的版本不一致链接时可能报undefined reference to png_set_longjmp_fn之类的错误。这时候要么统一用 Qt 自带的要么统一用自己编的不要混用。7.2 插件加载失败的排查思路静态程序的插件加载失败排查起来比动态程序麻烦因为没有.so文件可以检查。我的排查步骤是这样的先用Q_IMPORT_PLUGIN确保插件被编进可执行文件然后用QT_DEBUG_PLUGINS1环境变量运行程序看 Qt 打印的插件加载日志。日志里会显示它尝试加载了哪些插件、为什么失败。如果日志显示插件被找到但初始化失败通常是插件依赖的某个库没链接上。比如qlinuxfb插件依赖libQt5Gui.a里的某些符号如果链接顺序不对就会初始化失败。这时候要调整.pro里的LIBS顺序把-lQt5Gui放在-lQt5Core前面。7.3 体积优化与裁剪的边界静态编译出来的程序体积大这是没办法的事。但可以通过一些手段控制。第一用-release而不是-debug能省一半以上。第二strip 掉调试符号。第三在 configure 时关掉不需要的特性比如-no-feature-cups、-no-feature-printdialog。第四用-Os而不是-O2优化体积但可能会牺牲一点性能。不过要注意裁剪过度会导致某些功能不可用。比如关掉-no-feature-printdialog之后代码里如果用了QPrintDialog就会编译失败。所以裁剪之前要确认你的程序确实不用这些功能。8. 从静态库到最终交付一些工程化的建议8.1 把工具链和依赖固化到脚本里交叉编译的环境配置很繁琐每次手动敲命令容易出错。我的做法是写一个env.sh把工具链路径、依赖路径、pkg-config 变量都设好每次编译前 source 一下。再写一个build.sh把 configure、make、make install 串起来这样换一台机器也能快速复现。#!/bin/bash # env.sh export TOOLCHAIN/opt/toolchain export DEPS/home/user/deps export QT_INSTALL/opt/qt5.14.2-static export PATH$TOOLCHAIN/bin:$PATH export PKG_CONFIG_PATH$DEPS/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR$DEPS export PKG_CONFIG_LIBDIR$DEPS/lib/pkgconfig8.2 版本管理与可复现性Qt 5.14.2 的源码包要保留好依赖库的源码包也要保留。最好把每个库的版本号、configure 参数、编译命令都记录在一个文档里。这样半年后需要重新编译时不用从头回忆。我见过太多人编完就把源码删了结果要改个参数重新编的时候发现连当时用的工具链版本都记不清了。如果团队协作建议把工具链和依赖库打包成一个压缩包放在内部文件服务器上新人入职直接解压就能用。这比让每个人自己从头编一遍要高效得多。8.3 什么时候该放弃静态编译静态编译不是万能的。如果你的程序需要频繁更新 Qt 库或者需要动态加载插件那静态编译反而麻烦。另外如果目标板子的存储空间足够动态编译加打包.so也是可行的方案只是部署时多一步拷贝。我的判断标准是如果程序需要独立分发、目标环境不可控、或者对启动速度有要求那就静态编译。如果程序只在内部使用、环境统一、需要灵活更新那动态编译更合适。没有绝对的好坏只有适不适合。最后分享一个我在实际项目中总结的小技巧静态编译 Qt 的时候先把qtbase单独编一遍确认能跑通最小程序再去编其他模块。这样如果出问题排查范围小很多。我见过有人一上来就全模块编译结果报错了几百行根本不知道从哪查起。分步验证是交叉编译这种复杂任务里最省时间的做法。
返回列表