十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Qt 5.14.2 AArch64静态交叉编译实战:从零构建嵌入式环境

Qt 5.14.2 AArch64静态交叉编译实战:从零构建嵌入式环境 1. 为什么偏偏是Qt 5.14.2 aarch64 静态编译先说结论这个组合在2024年依然是工业级嵌入式设备里最常见的一套“保守稳定型”技术栈。Qt 5.14.2是The Qt Company在Qt 5分支里最后一个带有官方离线安装包的版本5.15之后官方就转向在线安装模式很多内网开发环境根本没法用。而且5.14.2对嵌入式Linux、ARM平台的支持非常成熟bug修复得也比较到位社区遇到的坑基本都有人踩过、填过了遇到问题搜索答案非常容易。aarch64也就是ARMv8的64位指令集现在几乎所有新出的工业主板、边缘计算盒子、医疗终端、自助设备都用的是这个架构。交叉编译的意思是我们在x86的PC上编译出aarch64能跑的二进制而不是在目标板上现场编译。这样做的原因很现实目标板性能通常有限尤其是只有2GB内存、四核A53的板子你让它在上面跑Qt的完整编译一次configure加make可能要三四个小时折腾几次就崩溃了。静态编译就更好理解了就是让Qt库直接打进你的可执行文件里。产物是一个巨大的、自包含的ELF文件扔到目标板上只要内核和glibc版本兼容就能跑不用管目标板上有没有装Qt运行库。这对工控、军工、医疗这类“交付之后不能随便动系统”的项目来说几乎是刚需。但静态交叉编译这套东西官方文档说得轻描淡写实际自己搭起来坑非常多。我这次是从零开始在一台干净的Ubuntu 20.04上把交叉工具链、依赖库、Qt源码全部手工编译一遍最后产出一个aarch64静态Qt环境并且用qmake和CMake两种方式各编译了一个测试程序验证通过。整个过程记录成文里面每一步都是我实际操作后确认过的参数也是跑通的不是那种网上抄来抄去的老教程。这套流程踩完坑之后你会对整个Qt构建系统、Linux下动态库与静态库的链接机制都有更深的理解以后想换Qt版本、换工具链都心里有数。2. 环境准备与工具链搭建2.1 主机环境要求强烈建议用一台干净的Ubuntu 20.04 x86_64虚拟机或者独立机器。为什么是20.04而不是22.04因为我实际测试下来20.04自带的gcc-9和binutils版本跟Qt 5.14.2的构建脚本兼容性最好坑最少。22.04其实也能做但系统自带的cmake、gcc版本较新偶尔会跟Qt老版本源码里的编译检测脚本发生一些莫名其妙的冲突。硬件方面没什么特殊要求4核CPU、8GB内存、30GB磁盘足够了。整个编译过程最耗时间的是Qt源码的configure和make我的机器是8核Qt完整编译大约25分钟如果只有4核预算40到50分钟比较合理耐心等就好。建议主机环境做这样几个准备sudo apt update sudo apt install -y build-essential cmake ninja-build python3 wget tar unzip gperf bison flex texinfo这里gperf、bison、flex是Qt构建时生成某些代码文件需要的网上很多教程没提等configure报错了才到处找。提前装好一劳永逸。2.2 安装aarch64交叉编译工具链Ubuntu 20.04的软件源里直接有aarch64的gcc交叉编译器版本是gcc-9搭配Qt 5.14.2很合适。安装命令sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu装完之后两个编译器在系统里的名字分别是aarch64-linux-gnu-gcc和aarch64-linux-gnu-g文件在/usr/bin下可以直接用。还有几个配套工具建议一起装sudo apt install -y pkg-config-aarch64-linux-gnu libtool automake autoconf其实Qt的cross compile并不强制用到pkg-configJDK但后面编译第三方库的时候configure脚本经常需要通过pkg-config去找依赖库组建好交叉环境的pkg-config比较省事。验证工具链是否正常aarch64-linux-gnu-gcc --version aarch64-linux-gnu-g --version如果能正常输出版本信息说明工具链安装没问题。再写一个最简单的hello.c测试一下能不能编译并链接出aarch64的ELFecho int main(){return 0;} test.c aarch64-linux-gnu-gcc test.c -o test file testfile命令输出里应该能看到ELF 64-bit LSB executable, ARM aarch64字样这就说明交叉工具链工作正常了。2.3 规划目录结构与下载源码实际操作时目录结构一定要提前规划好否则后面路径一复杂configure脚本的各种相对路径、绝对路径会让你崩溃。我的目录结构如下~/qt-aarch64/ ├── src/ # 所有源码包解压后的目录 ├── build/ # 编译过程中的临时目录 ├── sysroot/ # 最终生成的aarch64根文件系统含库和Qt │ ├── usr/ │ └── qt5.14.2/ └── downloads/ # 原始源码压缩包sysroot这个目录的命名你可能听说过在交叉编译里sysroot就是一个“模拟的目标板根目录”。我们把所有编译好的aarch64库放到这个sysroot里软件编译时指定--sysroot参数工具链就会自动从这个目录下找头文件和库就像在目标板自己的根目录下编译一样。接下来的重点是准备依赖库源码。这里必须注意的是不能用主机系统上apt装的库那些是x86编译的架构不对。我们需要拿到这些库的源码用aarch64工具链重新编译一遍。需要的依赖库清单和版本如下zlib-1.2.13 libpng-1.6.37 libjpeg-turbo-2.1.5.1 sqlite-autoconf-3390400 openssl-1.1.1w libffi-3.4.4 glib-2.56.4可选取决于你用到的Qt模块 libxkbcommon-1.0.0可选这里面zlib、libpng、libjpeg-turbo是Qt的imageformats插件和核心库依赖。sqlite是Qt SQL模块的默认驱动。openssl是Qt Network模块做HTTPS要用的。libffi和glib是某些平台插件比如xcb间接依赖的但如果你的目标板只是跑LinuxFBlinuxfb插件或者EGLFS不跑X11的xcb插件glib可以不要。我的经验是能砍掉的依赖尽量砍静态链接时每多一个库就多一层麻烦。3. 第三方依赖库的静态交叉编译3.1 环境变量统一配置编译多个库之前先把环境变量统一设定好后面每个库的configure命令都要引用这些变量。我把这些变量写到了~/qt-aarch64/env.sh里每次编译新库之前source一下export CROSS_COMPILEaarch64-linux-gnu export AR${CROSS_COMPILE}-ar export AS${CROSS_COMPILE}-as export LD${CROSS_COMPILE}-ld export RANLIB${CROSS_COMPILE}-ranlib export CC${CROSS_COMPILE}-gcc export CXX${CROSS_COMPILE}-g export SYSROOT$HOME/qt-aarch64/sysroot export PREFIX$SYSROOT/usr export PKG_CONFIG_PATH$PREFIX/lib/pkgconfig export PKG_CONFIG_LIBDIR$PKG_CONFIG_PATH export CFLAGS--sysroot$SYSROOT -I$PREFIX/include export CXXFLAGS$CFLAGS export CPPFLAGS$CFLAGS export LDFLAGS--sysroot$SYSROOT -L$PREFIX/lib这里有个容易踩坑的点PKG_CONFIG_PATH和PKG_CONFIG_LIBDIR是一对容易混淆的变量。PKG_CONFIG_LIBDIR是pkg-config查找.pc文件的主目录PKG_CONFIG_PATH是追加的搜索路径。在交叉编译时最好把PKG_CONFIG_LIBDIR指向sysroot下的pkgconfig目录同时确保不会去读主机系统的/usr/lib/x86_64-linux-gnu/pkgconfig否则pkg-config会把主机上的库路径传给编译器导致链接时架构不匹配的报错。3.2 zlib编译zlib是基础中的基础编译很简单但要注意configure脚本是它自己写的一个shell脚本不是GNU autotools的标准configure重要参数格式不太一样cd ~/qt-aarch64/src/zlib-1.2.13 make clean CC${CROSS_COMPILE}-gcc \ AR${CROSS_COMPILE}-ar \ RANLIB${CROSS_COMPILE}-ranlib \ ./configure --prefix$PREFIX --static make -j$(nproc) make install这里--static参数会强制生成静态库libz.a同时不会去尝试编译动态库。实测下来zlib的configure脚本对交叉编译的兼容性不错只要把CC、AR、RANLIB都显式传进去基本一把过。编译完检查一下file $PREFIX/lib/libz.a输出应该是current ar archive。再用nm看一眼里面的符号是不是aarch64架构aarch64-linux-gnu-nm $PREFIX/lib/libz.a | head看汇编指令对应的符号如果出现__aarch64相关的符号就对了。3.3 libpng与libjpeg-turbo编译libpng和libjpeg-turbo都是标准autotools工程configure参数几乎一样。libpng需要注意它依赖zlib所以要让它的configure找到我们的静态zlib。两种方式一种是通过环境变量CFLAGS和LDFLAGS指定一种是通过前面的pkg-config设置让libpng的检测脚本自动找到zlib.pc。我实际倾向于两种都做双保险。libpng编译cd ~/qt-aarch64/src/libpng-1.6.37 source ~/qt-aarch64/env.sh ./configure --hostaarch64-linux-gnu --prefix$PREFIX --disable-shared --enable-static make -j$(nproc) make install这里的关键是--host参数autotools工程用--host指定目标平台会做三件事检查交叉编译器存在、启用跨平台编译模式、编译出的可执行测试程序不会在主机上乱跑因为根本跑不了。如果你的系统里没有--host指定的工具链前缀configure会报错所以必须确保aarch64-linux-gnu-gcc已经在PATH里。libjpeg-turbo的编译方式略有不同它用的是cmake。cmake工程在交叉编译时就需要一个toolchain文件。我在~/qt-aarch64/toolchain-aarch64.cmake里放了这个文件set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g) set(CMAKE_AR aarch64-linux-gnu-ar) set(CMAKE_RANLIB aarch64-linux-gnu-ranlib) set(CMAKE_FIND_ROOT_PATH $ENV{HOME}/qt-aarch64/sysroot) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)这个文件的核心逻辑就是告诉cmake你用这套编译器和这套sysroot来模拟一个aarch64环境。CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER的意思是在找可执行程序时可以去主机系统找因为编译过程中可能需要host端工具但找库和头文件时只能在sysroot里找避免误用到主机上的x86库。cmake编译libjpeg-turbocd ~/qt-aarch64/build mkdir build-libjpeg cd build-libjpeg cmake ~/qt-aarch64/src/libjpeg-turbo-2.1.5.1 \ -DCMAKE_TOOLCHAIN_FILE~/qt-aarch64/toolchain-aarch64.cmake \ -DCMAKE_INSTALL_PREFIX$PREFIX \ -DENABLE_SHAREDOFF -DENABLE_STATICON make -j$(nproc) make installENABLE_SHAREDOFF跟--disable-shared是一个作用。libjpeg-turbo这个库默认会同时编出静态库和一堆命令行工具静态库是我们要的命令行工具无所谓能编过就算成功。3.4 SQLite与OpenSSL编译SQLite用autotools方式编译但有一个意外情况需要注意SQLite的configure脚本如果检测到当前机器是64位会默认启用某些与位置无关的代码选项这不会影响交叉编译但会拖慢编译速度。实测下来直接编就行参数如下cd ~/qt-aarch64/src/sqlite-autoconf-3390400 source ~/qt-aarch64/env.sh ./configure --hostaarch64-linux-gnu --prefix$PREFIX --disable-shared --enable-static make -j$(nproc) make installOpenSSL的编译比较特殊它既不是autotools也不是cmake而是自己的Configure脚本。交叉编译OpenSSL 1.1.1w的关键是指定linux-aarch64平台和no-shared选项cd ~/qt-aarch64/src/openssl-1.1.1w source ~/qt-aarch64/env.sh perl Configure linux-aarch64 \ --prefix$PREFIX \ --cross-compile-prefix${CROSS_COMPILE}- \ no-shared \ no-tests make -j$(nproc) make install注意--cross-compile-prefix后面直接跟工具链前缀不带结尾的-时要小心OpenSSL脚本会自己补上-。所以我上面写的是aarch64-linux-gnu-带了横杠。如果写成aarch64-linux-gnuOpenSSL会去找aarch64-linux-gnugcc这当然找不到直接报错。这个小细节我当年踩过一次折腾了半小时。OpenSSL编译时间比较久大约5到10分钟取决于机器性能。如果configure时报错说缺少Perl模块执行sudo cpan IPC::Cmd装上就行但一般Ubuntu自带的perl已经够了。到这里Qt静态编译需要的基础依赖库基本准备完毕。这些库编译好了后面Qt的configure才不心慌。4. Qt 5.14.2源码的配置与编译4.1 获取Qt源码Qt 5.14.2的源码包可以到Qt官方下载页抽取对应版本或者用国内镜像站。文件名为qt-everywhere-src-5.14.2.tar.xz大约500MB。下载后解压到~/qt-aarch64/src/下cd ~/qt-aarch64/downloads wget https://download.qt.io/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 -C ~/qt-aarch64/src/解压后目录就是~/qt-aarch64/src/qt-everywhere-src-5.14.2。我习惯把这个目录改名为qt5.14.2-src方便后续操作不改也行路径别弄错就好。4.2 configure参数解析Qt的configure是整个过程中最关键的环节参数决定了编译什么模块、用什么工具链、以什么方式链接。我在实际操作中最终使用的configure命令是这样的cd ~/qt-aarch64/src/qt5.14.2-src source ~/qt-aarch64/env.sh ./configure \ -prefix /opt/qt5.14.2 \ -sysroot $SYSROOT \ -extprefix $HOME/qt-aarch64/sysroot/qt5.14.2 \ -xplatform linux-aarch64-gnu-g \ -opensource -confirm-license \ -release \ -static \ -no-shared \ -no-feature-xcb \ -no-xcb-xlib \ -no-opengl \ -no-gtk \ -platform-linux \ -linuxfb \ -eglfs \ -no-feature-vulkan \ -qt-zlib \ -qt-libpng \ -qt-libjpeg \ -qt-sqlite \ -openssl-linked \ -I$PREFIX/include \ -L$PREFIX/lib \ -no-compile-examples \ -nomake examples \ -nomake tests \ -skip qtwebengine \ -skip qtwebview \ -skip qt3d \ -skip qtcanvas3d \ -skip qtscript \ -skip qtcharts \ -skip qtdatavis3d \ -skip qtgamepad \ -skip qtlocation \ -skip qtlottie \ -skip qtmqtt \ -skip qtnetworkauth \ -skip qtopcua \ -skip qtquick3d \ -skip qtremoteobjects \ -skip qtscxml \ -skip qtsensors \ -skip qtserialbus \ -skip qtspeech \ -skip qttranslations \ -skip qtvirtualkeyboard \ -skip qtwebglplugin \ -skip qtxmlpatterns这一大串参数看着吓人其实每一条都有明确的含义。我给你逐一拆开讲-prefix /opt/qt5.14.2是指定目标板上Qt的安装路径。所谓“目标板上的路径”意思是我们编译出来的Qt在部署到目标板那一刻会被放到目标板的/opt/qt5.14.2目录下。Qt编译过程中生成的一些路径引用会以这个为基准。为了不写死在编译机里配合后面的-extprefix。-sysroot $SYSROOT指定我们前面做的sysroot目录。configure会到这个目录下去找各种依赖库的头文件和库文件。-extprefix $HOME/qt-aarch64/sysroot/qt5.14.2指定Qt实际安装到我们编译机的什么位置。也就是说Qt自身会被安装到sysroot里的qt5.14.2子目录。这样在编译依赖Qt的应用时Qt的库会在sysroot路径下被发现同时在部署的时候需要把sysroot/qt5.14.2整个目录同步到目标板的/opt/qt5.14.2。这个-prefix和-extprefix的组合理解起来有点绕我打一个比方-prefix是“将来住的地方的门牌号”-extprefix是“现在存放家具的仓库地址”。编译阶段所有文件放到仓库部署时整体搬过去。-xplatform linux-aarch64-gnu-g告诉Qt使用linux-aarch64-gnu-g这个mkspec。在Qt源码的qtbase/mkspecs目录下有个同名文件夹里面定义了交叉编译器前缀、编译选项和链接选项。Qt从5.14开始已经为aarch64提供了几个现成spec其中最常用的就是linux-aarch64-gnu-g它默认使用aarch64-linux-gnu-g作为C编译器。-static -no-shared这两个是核心中的核心。-static让Qt编译静态库.a文件-no-shared明确告诉构建系统不要生成任何.so文件。有些教程只写-static不加-no-shared会导致Qt编译时产出部分.so库虽然不影响静态应用编译但产物不干净部署时容易混淆。我建议两个都写。-no-feature-xcb和-no-xcb-xlib是砍掉xcb平台插件。这一条要根据你的目标板决定。如果目标板是纯嵌入式跑linuxfb或eglfs那xcb必须砍掉。因为xcb依赖一堆X11库而且不少库很难静态编译是静态编译路上最大的坑之一。如果你的程序必须跑在X server上比如目标板装了完整桌面那就不能砍还要编译libxcb-util等一堆依赖库复杂度直接上一个台阶。-no-opengl砍掉OpenGL支持。这个也要看情况。如果目标板有GPU并且你想用Qt Quick 2或QML渲染那需要完整的OpenGL/EGL支持还要指定EGL的库路径非常麻烦。如果只是跑Widgets程序或者纯CPU绘制砍掉OpenGL能大幅降低编译难度和最终体积。-linuxfb启用linuxfb平台插件也就是直接写/dev/fb0帧缓冲设备的显示方式。这是最简单、最不依赖外部库的平台插件适合大多数嵌入式场景。-qt-zlib -qt-libpng -qt-libjpeg -qt-sqlite这四个参数比较微妙。它们的含义是“用Qt源码包内置的第三方库”。Qt源码里其实自带zlib、libpng、libjpeg、sqlite的副本如果你指定-qt-前缀Qt会用内置的版本如果指定-system-前缀Qt会用sysroot里你编译好的系统库。那问题来了既然我们自己编译了这些库为什么要用内置的一个原因是内置库版本和Qt 5.14.2验证过的版本一致兼容性更有保障另一个原因是内置库的编译会直接被Qt的构建系统接管少一次自己编译出问题的机会。但是注意如果你用了-openssl-linkedOpenSSL必须用系统库没有-qt-openssl这个选项。所以我们的组合是能用内置的尽量内置OpenSSL用系统编译好的静态库。-openssl-linked指示Qt以链接方式使用OpenSSL库而不是运行时动态加载。Qt还有一个-openssl-runtime选项它允许运行时通过dlopen加载libssl.so但这对静态链接场景没意义静态链接必须用-openssl-linked。-skip系列的参数是去掉一堆你用不到的Qt模块。这是整个configure中最重要的优化步骤。Qt 5.14.2完整模块数量相当庞大如果都编译光make时长就奔着两小时去了还会引入各种额外的依赖。我一个一个跳过的模块里特别提醒这几个qtwebengine是Chromium内核编译极其吃时间和硬盘而且静态编译成aarch64后体积巨大没问题就用不到qtvirtualkeyboard是触摸屏虚拟键盘依赖Enginio和Wayland交叉静态编译几乎不可行qtquick3d依赖一堆3D渲染库必须砍。-no-compile-examples -nomake examples -nomake tests是让Qt只编译库不编译例子和测试程序。反正最终我们只要库和工具。4.3 configure时常见的小插曲如果你严格按照上面的参数configure大概率能顺利通过但我也经历过几次异常列出来供你参考报错Could not find qmake spec linux-aarch64-gnu-g说明Qt源码里没有这个spec你的Qt版本不是完整的qt-everywhere发行版可能只下载了qtbase。重新下载全量源码包即可。报错The OpenSSL library is missing or unversionedOpenSSL没有正确编译或路径不对。执行ls $SYSROOT/usr/lib/libssl.a看看静态库是否真的生成。如果路径不对检查-I和-L参数是否指向了sysroot/usr/include和sysroot/usr/lib。报错Could not find libdl in the system这个一般是工具链的libc配置问题Ubuntu的aarch64交叉工具链缺了某些symlink或者multilib支持。执行sudo apt install libc6-dev-arm64-cross修复。4.4 make与make installconfigure通过后编译就相对机械了但时间仍然很可观make -j$(nproc) 21 | tee build.log make install 21 | tee install.logmake -j$(nproc)会按照CPU核数并行编译。我在8核机器上完整编译约25分钟4核约40到50分钟。期间如果某个编译单元报错build.log里会有详细的错误输出可以直接搜error:关键词定位。整个编译过程中最耗时的模块是qtbase和qtdeclarative这两个大概占了编译时间的一半多。qtimageformats和qtnetworkauth之类的模块相对较快。如果编译过程中报错先判断是代码编译错误还是链接错误链接错误大概率是某个依赖库没有静态编译好或路径不对代码编译错误则可能是编译器版本兼容问题。make install结束后检查一下产物ls $HOME/qt-aarch64/sysroot/qt5.14.2/lib/看到一堆.a文件比如libQt5Core.a、libQt5Gui.a、libQt5Widgets.a、libQt5Network.a等就是成功了一半。再确认没有.so文件出现在这个目录里如果有说明configure的-no-shared没有生效回去找原因。4.5 sysroot的最终状态整个sysroot目录在编译完Qt后应该是下面这个结构~/qt-aarch64/sysroot/ ├── usr/ │ ├── include/ # zlib/libpng/libjpeg/sqlite/openssl头文件 │ └── lib/ # 各依赖库的静态库(.a) └── qt5.14.2/ ├── bin/ ├── include/ ├── lib/ ├── mkspecs/ └── plugins/在部署时把整个sysroot目录同步到目标板的根目录下。需要注意usr/下的库都是存放在/usr/lib下的qt5.14.2/对应目标板的/opt/qt5.14.2。如果你的目标板不想把依赖库装到/usr/lib而是放到自己的/opt/libs那在编译依赖库时就要把--prefix改成/opt/libs然后把-I和-L同步改掉。我是为了简单统一放在了/usr下。5. 应用工程接入与验证5.1 用qmake构建一个静态测试程序Qt环境编译好后最直接的验证方式是编译一个最简单的Widgets程序。在~/qt-aarch64/testapp/下建一个最小工程// main.cpp #include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello Qt aarch64 static); label.show(); return app.exec(); }工程文件# testapp.pro QT widgets SOURCES main.cpp TARGET testapp用我们刚编译的qmake来构建export PATH$HOME/qt-aarch64/sysroot/qt5.14.2/bin:$PATH cd ~/qt-aarch64/testapp qmake testapp.pro make等编译完成后查看产物file testapp输出应该是testapp: ELF 64-bit LSB executable, ARM aarch64, dynamically linked, with debug info, not stripped注意虽然展示的是dynamically linked但这只说明它链接了libc等系统库并不代表链接了Qt动态库。真正的判断方式是检查它是否依赖Qt的.so文件aarch64-linux-gnu-readelf -d testapp | grep -i needed正常情况下应该看不到任何libQt5开头的库。然后看看文件大小ls -lh testapp一个带几行文字的QWidget程序静态编译后大小通常在10MB到15MB之间这是正常的Qt5 Widgets核心库就这么大。将这个二进制直接拷贝到目标板上执行需要确认两点目标板的Linux内核版本和glibc版本不能比编译机的工具链要求的版本低目标板上要有/dev/fb0帧缓冲设备。执行时加-platform linuxfb./testapp -platform linuxfb如果显示器上出现一个窗口里面写着Hello Qt aarch64 static说明整套静态编译环境完全跑通了。5.2 用CMake接入静态Qt现在越来越多的项目用CMake而不是qmake了。Qt 5.14.2对CMake的支持虽然没有Qt 6那么原生但也足够好用。在一个CMake工程里接入我们编译的静态Qt关键是设置CMAKE_PREFIX_PATH# CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(testapp_cmake) set(CMAKE_PREFIX_PATH $ENV{HOME}/qt-aarch64/sysroot/qt5.14.2) find_package(Qt5 COMPONENTS Widgets REQUIRED) add_executable(testapp_cmake main.cpp) target_link_libraries(testapp_cmake PRIVATE Qt5::Widgets)编译方法cd ~/qt-aarch64/build/testapp_cmake cmake ~/qt-aarch64/testapp_cmake_src \ -DCMAKE_TOOLCHAIN_FILE~/qt-aarch64/toolchain-aarch64.cmake \ -DCMAKE_PREFIX_PATH$HOME/qt-aarch64/sysroot/qt5.14.2 \ -DCMAKE_BUILD_TYPERelease make这里的关键是CMAKE_TOOLCHAIN_FILE和CMAKE_PREFIX_PATH不能冲突。toolchain文件指定编译器CMAKE_PREFIX_PATH指定Qt的位置。cmake的find_package(Qt5)会去CMAKE_PREFIX_PATH指向的lib/cmake目录下查找Qt5Config.cmake。如果这一步没设置cmake会去系统默认路径找Qt找到的肯定是你的主机版Qt编译时就会报头文件找不到或者架构不匹配。这里再提一个我实际遇到的坑如果toolchain文件里设置了CMAKE_FIND_ROOT_PATH为sysroot路径那么find_package找到的也是sysroot下的Qt这是OK的。但如果CMAKE_FIND_ROOT_PATH设置不全比如只设置了/usr/aarch64-linux-gnu而没有设置sysroot根目录那么Qt的cmake配置文件里的绝对路径可能找不到会报Qt5 not found。保险起见CMAKE_PREFIX_PATH和toolchain文件里的CMAKE_FIND_ROOT_PATH都指向同一个sysroot根目录就不会有这个问题。5.3 部署时的路径约定与运行环境静态编译最大的好处就是部署随意但也不是完全任意。有几个检查点要记住目标板的/lib或/usr/lib下必须有目标板自带的glibc和libstdc动态库。虽然静态编译让Qt库不用部署了但C和C的标准库通常还是以动态链接的方式存在。我用的工具链是gcc-9的aarch64版本它默认链接libstdc.so.6。这是正常的因为glibc和libstdc包含NSS、locale、异常处理等机制全静态链接会有兼容性问题几乎没有项目会去全静态。如果你想连libstdc都静态链接进去可以在Qt的mkspecs/qmake.conf里修改链接选项加上-static-libgcc -static-libstdc。这两个参数会把libgcc和libstdc静态链接进产物最终产物大小会再增加2MB左右。实测在大多数glibc版本比较老的目标板上这样做确实能提升二进制兼容性。但要注意libstdc全静态后如果目标板上缺少locale数据或者NSS模块运行时可能报奇怪的错误所以非必要不建议全静态。目标板上需要存在基本的系统库比如libm.so.6、libdl.so.2、libpthread.so.0、libc.so.6。这些是Linux系统的base库几乎所有发行版都有不用额外装。Qt程序要显示界面目标板必须有图形环境。linuxfb插件要求内核有帧缓冲驱动并在系统启动时激活帧缓冲比如启动参数里写vga...或者加载对应的DRM驱动。如果目标板走的是eglfs路线那要确保OpenGL ES的驱动库如libMali.so、libmali-utgard.so已经安装好并且EGL/GLES的头文件和库在sysroot里已经配好。这也是为什么我前面-no-opengl的取舍很重要没有GPU驱动es平台插件会直接崩。6. 常见问题与排查实录6.1 OpenSSL相关的“暗坑”静态编译Qt最常遇到的就是OpenSSL相关的链接错误。典型报错之一error: undefined reference to SSL_library_init这个问题分两种情况。第一种是你根本没编译OpenSSLQt的configure却通过了这是因为configure阶段Qt可能退而选择了-no-openssl也就是完全不启用加密支持这种情况下Qt的Network模块能用HTTP但HTTPS会失败。第二种是你用了-openssl-linked但链接时找不到静态库。我的排查顺序是先看configure日志里关于openssl的检测结果是yes、no还是linked。如果是linked检查$SYSROOT/usr/lib/libssl.a和libcrypto.a是否存在用file确认它们的架构是aarch64。有时候你交叉编译了OpenSSL但因为configure时--prefix写的是/usr库被装到了sysroot的/usr/lib这个路径在LDFLAGS里如果漏掉了-L$PREFIX/libQt就找不到。另一个需要特别注意的是下载OpenSSL时务必选择1.1.1系列的版本。不要选OpenSSL 3.0及之后的版本。Qt 5.14.2发布时OpenSSL 3.0还没出来Qt源码里检测的OpenSSL 1.1 API在3.0里有不少废弃和移除链接时会报一堆undefined reference。而且很多老系统的加密算法策略不一致部署到目标板也会出问题。在这个问题上我奉行的原则是工具链版本、Qt版本、依赖库版本全部对齐到2019-2020年的水平反而最省事。6.2 链接时报错“cannot find -lGL”或者“cannot find -lEGL”这个问题的根源是你启用了-opengl但sysroot里没有GL/EGL库。两种解决办法第一目标板有GPU而且你确实需要OpenGL那我建议你不要用-no-opengl而是要在sysroot里装好Mali或其他厂商的OpenGL ES开发库。具体做法是把厂商提供的libGLESv2.so、libEGL.so及其头文件放到sysroot的usr/lib和usr/include里然后Qt configure时加上-eglfs。第二目标板没GPU那老老实实加-no-opengl并确保不用任何依赖OpenGL的Qt模块。我见过很多人死磕在GL上结果发现自己只需要显示一个二维码或者几行监控数据根本用不到GPU。嵌入式开发里“够用”比“全能”重要砍掉不必要的模块是静态编译的第一原则。6.3 Qt 5.14.2自带的mkspecs有问题linux-aarch64-gnu-g这个mkspecs文件在实际编译过程中有可能出现一个问题它默认不定义QT_QPA_DEFAULT_PLATFORM。这意味着应用在没有指定-platform参数时Qt不知道用什么平台插件会尝试加载一堆插件然后全部失败最后报“could not find a Qt platform plugin”退出。解决办法是在mkspecs的qmake.conf里加一行QT_QPA_DEFAULT_PLATFORM linuxfb然后重新make install Qt。或者在部署时写一个环境变量export QT_QPA_PLATFORMlinuxfb ./testapp两种方式都行我更推荐第一种因为集成到系统服务systemd时不用每次写环境变量。6.4 静态链接时遇到“undefined reference to glXGetProcAddressARB”这个报错一般出现在X11和OpenGL共存的时候。我已经把xcb和OpenGL都砍了照理不该遇到但有些Qt模块比如qtwayland内部还是会引用一些X11符号。如果你确实遇到了有一种粗暴的解决办法在链接选项里加上-lXext -lX11 -lGL -lEGL前提是这些库在sysroot里存在。如果没有那就要排查是哪个模块引入的依赖然后在configure里把这个模块也-skip掉。6.5 程序在目标板上提示“Failed to load platform plugin linuxfb”这个问题的标准原因是Qt的插件库没有部署到正确位置或者插件目录路径不对。静态编译时platforms插件是一个libqlinuxfb.a静态库它被链接进可执行文件理论上不需要单独部署。但如果Qt在运行时仍然去动态查找插件可能的原因是程序在编译时引用了Q_IMPORT_PLUGIN相关的机制。解决方法是在main.cpp里显式导入插件#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)这样编译器会把linuxfb插件强制链接进可执行文件不至于运行时找不到。实际上Qt的static build文档里提到过使用静态Qt时对于你需要的每个平台插件、图片格式插件都需要通过Q_IMPORT_PLUGIN显式导入。这个点太容易忽略了官方文档藏在Qt for Embedded Linux页面角落里很容易错过。用CMake时Qt的cmake模块会自动生成qmake_import_plugins相关的链接规则所以用CMake构建时只要目标平台选对了插件导入一般是自动处理的。但用纯qmake或手写链接命令时必须手动导入。6.6 图片格式插件缺失我编译的Qt没有启用所有图片格式插件只默认支持PNG和JPEG。如果你的程序需要加载GIF、SVG、WebP等格式要在configure时添加-qt-imageformats在qtimageformats模块里或者在Q_IMPORT_PLUGIN部分导入对应插件。一般嵌入式程序用PNG就够了SVG如果有反而增加不少体积。7. 进一步优化与实际经验7.1 给Qt减减肥静态编译出来的程序体积确实大。一个Hello级别的QWidget程序动辄10MB以上而实际业务程序可能几十MB。有几个可操作的方向可以压缩确认只链接你需要的Qt模块。如果程序不用网络QT core gui widgets即可不要加network。动态库时代多链接一个模块只是运行时占用一点内存静态库时代多链接一个模块会让最终二进制直接膨胀几MB到十几MB而且这是永久性的。用strip去掉符号表aarch64-linux-gnu-strip --strip-unneeded testapp一个未strip的Hello程序可能12MBstrip后能到8MB左右。对发布版本来说是必做的一步。编译时用-optimize-size选项。Qt configure支持-optimize-size编译器会优先优化体积而不是性能。嵌入式设备对性能没那么敏感这个参数能再压掉一些空间。实测能让最终库和可执行程序再小5%到10%。如果你的程序只用QWidget和QLabel这些基础控件可以考虑在Qt configure时关闭qml和quick模块-skip qtdeclarative -skip qtquickcontrols2等。Qt QML相关的模块静态编译下来体积非常可观有时会让库目录增加几百MB。一个纯Widgets程序完全用不到它们。7.2 目标板运行时的环境变量即使静态编译目标板上仍然有一些环境变量需要注意export QT_QPA_PLATFORMlinuxfb export QT_QPA_FB_BLIT1 export QT_QPA_FB_NODIRTY0QT_QPA_FB_BLIT1启用帧缓冲的blit加速如果你的显示控制器支持的话QT_QPA_FB_NODIRTY控制局部刷新在性能较弱的设备上如果把NODIRTY设为1有些程序会出现刷新残留的残影。这几个变量具体效果跟内核帧缓冲驱动实现有关建议在目标板上都测一遍。字体方面静态Qt默认可能没有freetype支持。如果你没有显式编译freetypeQt只能用内置的字体引擎渲染基本字体。中文显示需要部署一个中文字体文件比如思源黑体的ttc放到目标板的/usr/share/fonts下然后通过QT_QPA_FONTDIR指定export QT_QPA_FONTDIR/usr/share/fonts不要省这一步我见过不少人在目标板上跑Qt程序显示中文全是方块的排查半天结果是字体没部署。7.3 静态交叉编译的适用场景与边界说句实话静态编译不是银弹它适用场景局限在嵌入式Linux和工业设备。如果你的目标板是普通的ARM Linux发行版比如Ubuntu for ARM、Debian ARM完全可以直接在目标板上安装官方仓库的Qt运行库用动态链接的方式开发部署简单升级方便而且开发和调试效率高得多。但如果你遇到我前面说的那种项目——系统镜像定制好之后不能动、交付后现场没有网络、要确保程序在任意一台同架构板子上拷过去就能跑、或者板子存储空间小到装不下完整的Qt运行库——那静态编译就是唯一解。静态编译的另一个代价是升级麻烦。Qt静态库一旦编译好如果某个依赖库出了安全漏洞比如OpenSSL你几乎要重新编译整条工具链才能修复。所以在选择静态编译方案前一定想清楚项目的维护周期和升级策略。7.4 这套方案的扩展方向这次搭好的环境可以继续扩展。如果你之后需要支持更多的Qt模块比如qtcharts、qtdata*只需回到configure步骤去掉对应的-skip参数再次执行configure并make install。因为前面编译的静态库基础都还在第二次编译会快很多。另外如果你后面做的项目用CMake比较多可以把~/qt-aarch64/toolchain-aarch64.cmake和Qt的CMake配置封装成一个模板项目新项目直接拷贝CMakeLists.txt改改名字就开干。8. 总结与最后的实操心得写了这么多唯一想跟你重点强调的其实是那句老话交叉编译之路没有捷径但每一步都可以留下标记。我个人的经验是把整个过程拆成三层每层单独验证能省掉后面大量的排查时间。第一层是工具链验证交叉编译器能编一个helloworld并在目标板上运行这层过了才谈其他。第二层是依赖库验证zlib、openssl这些库要能交叉编译成功并且用file和readelf确认架构和链接路径。第三层才是Qt本身而且这一层最省心的地方就在于它自带完整的检测机制configure会帮你检查环境是否满足要求只要按照报错一项项补齐很少遇到真正的死结。最后再分享一个小技巧在configure执行前一定记得把sysroot里所有库的头文件、静态库文件路径都统一整理好。如果哪一步编译不过去先不要急着改参数先去看生成的config.log或者build.log里的实际报错内容。日志告诉你的是真正原因网上搜到的解决办法不一定适配你的版本组合。这套环境搭好之后后续再编译其他aarch64项目都会轻松很多。工具链、sysroot、Qt静态库都齐了本质上就是一个可复用的嵌入式开发基础设施。我第一次搭的时候花了整整两天主要时间都耗在OpenSSL和xcb这两个坑上现在整理好文档新环境照着走一遍基本半天到一天就能搞定。如果你在搭建过程中遇到别的坑欢迎在评论区或相应技术社区里交流。很多问题往往不是孤例可能别人已经遇到并解决了交流一下能省不少事。
返回列表