 Qt 交叉編譯實(shí)戰(zhàn):從工具鏈到部署)
1. 為什么值得折騰 aarch64 靜態(tài) Qt1.1 這個(gè)手冊(cè)解決的是什么問題如果你手上有塊 ARM 開發(fā)板——比如樹莓派、Orange Pi、瑞芯微或者鯤鵬平臺(tái)的服務(wù)器——想在上面跑一個(gè) Qt 界面程序大概率會(huì)遇到一個(gè)很現(xiàn)實(shí)的問題板子上的系統(tǒng)版本五花八門glibc 版本、libstdc 版本、各種依賴庫(kù)的版本全都不一樣。你在一臺(tái)機(jī)器上編譯好的動(dòng)態(tài)鏈接程序拷到另一臺(tái)機(jī)器上直接報(bào)version GLIBC_2.29 not found或者cannot mix incompatible Qt library這種讓人頭大的錯(cuò)誤。靜態(tài)交叉編譯就是來解決這個(gè)問題的。它的核心思路是在 x86_64 的開發(fā)機(jī)上用一套針對(duì) aarch64 架構(gòu)的交叉編譯工具鏈把 Qt 庫(kù)和你的應(yīng)用程序全部編譯成靜態(tài)庫(kù)最終鏈接出一個(gè)幾乎不依賴目標(biāo)系統(tǒng)任何動(dòng)態(tài)庫(kù)的可執(zhí)行文件。這個(gè)文件拷到板子上只要內(nèi)核架構(gòu)對(duì)得上直接就能跑。我這次選的是Qt 5.14.2這個(gè)版本。為什么不是 5.15 或者 6.x原因很實(shí)際5.14.2 是 LTS 版本穩(wěn)定性經(jīng)過大量項(xiàng)目驗(yàn)證而且它的構(gòu)建系統(tǒng)還是傳統(tǒng)的 qmake configure 腳本交叉編譯的坑相對(duì)少。5.15 之后有些模塊的構(gòu)建邏輯變了6.x 更是全面轉(zhuǎn)向 CMake交叉編譯的復(fù)雜度直接上一個(gè)臺(tái)階。對(duì)于第一次做靜態(tài)交叉編譯的人來說5.14.2 是最友好的起點(diǎn)。1.2 靜態(tài)編譯和動(dòng)態(tài)編譯的本質(zhì)區(qū)別很多人對(duì)靜態(tài)的理解停留在文件大一點(diǎn)這個(gè)層面其實(shí)遠(yuǎn)不止如此。動(dòng)態(tài)鏈接的程序在運(yùn)行時(shí)需要加載器去找到對(duì)應(yīng)的.so文件這些文件可能來自系統(tǒng)路徑、可能來自你設(shè)置的LD_LIBRARY_PATH一旦找不到或者版本對(duì)不上程序直接起不來。而靜態(tài)鏈接是把所有用到的代碼在編譯期就焊死進(jìn)可執(zhí)行文件里運(yùn)行時(shí)不需要再去找任何外部庫(kù)。這帶來的好處很直接部署極簡(jiǎn)一個(gè)文件拷過去就能跑不用管目標(biāo)機(jī)的庫(kù)版本環(huán)境隔離不會(huì)因?yàn)槟繕?biāo)機(jī)升級(jí)了某個(gè)庫(kù)導(dǎo)致你的程序崩潰啟動(dòng)略快省去了動(dòng)態(tài)鏈接器的符號(hào)解析過程代價(jià)也很明顯體積膨脹一個(gè)簡(jiǎn)單的 Qt 窗口程序靜態(tài)編譯后可能 15MB 起步動(dòng)態(tài)的可能只有幾百 KB許可證問題Qt 的 LGPL 協(xié)議對(duì)靜態(tài)鏈接有額外要求商業(yè)項(xiàng)目需要購(gòu)買商業(yè)許可或者開源你的代碼編譯時(shí)間長(zhǎng)Qt 本身幾百萬行代碼全量靜態(tài)編譯在普通機(jī)器上要跑一兩個(gè)小時(shí)提示如果你的項(xiàng)目是內(nèi)部工具、不對(duì)外分發(fā)靜態(tài)鏈接的許可證風(fēng)險(xiǎn)基本可以忽略。但如果是商業(yè)產(chǎn)品務(wù)必先確認(rèn) Qt 的授權(quán)條款。1.3 適用人群和前置知識(shí)這份手冊(cè)面向的是有一定 Linux 基礎(chǔ)、了解基本編譯流程的開發(fā)者。你不需要是 Qt 專家但至少得知道configure、make、make install這三板斧是干什么的。如果你之前做過任何形式的交叉編譯——哪怕只是編譯過一個(gè) hello world——那上手會(huì)更快。需要提前說明的是整個(gè)過程涉及大量的命令行操作和參數(shù)配置我會(huì)盡量把每個(gè)參數(shù)為什么這么寫講清楚而不是讓你無腦復(fù)制粘貼。因?yàn)榻徊婢幾g這件事環(huán)境差異太大了照抄別人的配置大概率會(huì)在某個(gè)環(huán)節(jié)卡住只有理解了原理才能自己排查。2. 工具鏈選型與環(huán)境準(zhǔn)備2.1 交叉編譯工具鏈的選擇邏輯aarch64 的交叉編譯工具鏈有好幾個(gè)來源常見的有工具鏈來源特點(diǎn)適用場(chǎng)景Linaro GCC官方維護(hù)版本更新快通用 ARM 開發(fā)ARM 官方 GNU ToolchainARM 自家發(fā)布針對(duì)性強(qiáng)Cortex-A 系列Bootlin Toolchains預(yù)編譯好開箱即用快速驗(yàn)證發(fā)行版自帶如 gcc-aarch64-linux-gnu安裝方便版本較舊Ubuntu/Debian 開發(fā)機(jī)我個(gè)人的選擇是ARM 官方發(fā)布的 GNU Toolchain具體版本用 10.3-2021.07 這一版。原因有三第一它是 ARM 自己維護(hù)的對(duì) aarch64 架構(gòu)的支持最到位第二這個(gè)版本對(duì)應(yīng)的 glibc 是 2.33兼容性夠用又不會(huì)太新第三它是預(yù)編譯好的 x86_64 可執(zhí)行文件解壓就能用不需要自己從頭編譯工具鏈。如果你用的是 Ubuntu也可以直接apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu省事但版本可能偏舊。對(duì)于 Qt 5.14.2 來說GCC 7 以上就夠用了所以發(fā)行版自帶的也能湊合。2.2 開發(fā)機(jī)環(huán)境的具體配置我的開發(fā)機(jī)是 Ubuntu 20.04 LTSx86_64 架構(gòu)配置是 8 核 16GB 內(nèi)存。這個(gè)配置編譯 Qt 大概需要 40 分鐘到 1 小時(shí)。如果你只有 4 核時(shí)間可能要翻倍而且內(nèi)存不足的話鏈接階段容易 OOM。先裝一批基礎(chǔ)依賴這些是編譯 Qt 本身需要的sudo apt update sudo apt install -y build-essential perl python3 git \ libgl1-mesa-dev libglu1-mesa-dev \ libxkbcommon-dev libxkbcommon-x11-dev \ libfontconfig1-dev libfreetype6-dev \ libssl-dev libdbus-1-dev \ libinput-dev libts-dev \ flex bison gperf這里有幾個(gè)包值得單獨(dú)說一下。libxkbcommon-dev是處理鍵盤映射的沒有它 Qt 的 GUI 模塊編譯會(huì)失敗。libfontconfig1-dev和libfreetype6-dev是字體渲染的基礎(chǔ)靜態(tài)編譯時(shí)字體會(huì)被嵌進(jìn)去。libinput-dev和libts-dev是觸摸屏輸入相關(guān)的如果你的板子有觸摸屏就需要。2.3 工具鏈的下載與驗(yàn)證從 ARM 官網(wǎng)下載工具鏈壓縮包解壓到/opt目錄下cd /opt sudo tar -xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz sudo mv gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu aarch64-toolchain然后把工具鏈的 bin 目錄加到 PATH 里。我習(xí)慣在~/.bashrc里加一行export PATH/opt/aarch64-toolchain/bin:$PATH驗(yàn)證工具鏈?zhǔn)欠窨捎胊arch64-none-linux-gnu-gcc --version aarch64-none-linux-gnu-g --version正常的話會(huì)輸出 GCC 10.3.1 的版本信息。如果提示 command not found檢查一下 PATH 有沒有生效或者工具鏈的目錄結(jié)構(gòu)是不是和我說的一致。注意工具鏈的路徑里不要有空格和中文否則 configure 腳本解析路徑時(shí)會(huì)出問題。這個(gè)坑我踩過排查了半天才發(fā)現(xiàn)是路徑里的空格導(dǎo)致的。2.4 目標(biāo)系統(tǒng) sysroot 的處理sysroot 是交叉編譯里一個(gè)核心概念簡(jiǎn)單說就是目標(biāo)系統(tǒng)的根文件系統(tǒng)的一個(gè)副本。交叉編譯時(shí)編譯器需要從 sysroot 里找目標(biāo)系統(tǒng)的頭文件和庫(kù)文件。理想情況下你應(yīng)該從目標(biāo)板子上把/usr/include、/usr/lib、/lib這些目錄拷貝出來做成一個(gè) sysroot。但實(shí)際操作中很多人包括我會(huì)直接用工具鏈自帶的 sysroot因?yàn)?ARM 官方工具鏈里已經(jīng)包含了一套完整的 glibc 和基礎(chǔ)庫(kù)。工具鏈自帶的 sysroot 在aarch64-toolchain/aarch64-none-linux-gnu/libc這個(gè)路徑下。用--sysroot參數(shù)指定它就行。這樣做的好處是簡(jiǎn)單壞處是如果你的目標(biāo)系統(tǒng)有特殊的庫(kù)比如某個(gè)廠商定制的硬件加速庫(kù)就需要手動(dòng)往 sysroot 里補(bǔ)。3. Qt 源碼的獲取與配置3.1 源碼下載與目錄規(guī)劃Qt 5.14.2 的源碼包可以從 Qt 官方下載站獲取文件名是qt-everywhere-src-5.14.2.tar.xz大概 500MB 左右。下載完之后解壓到一個(gè)工作目錄mkdir -p ~/qt-build cd ~/qt-build tar -xf qt-everywhere-src-5.14.2.tar.xz解壓后會(huì)得到一個(gè)qt-everywhere-src-5.14.2目錄。我建議再建一個(gè)獨(dú)立的構(gòu)建目錄不要在源碼目錄里直接編譯這樣源碼保持干凈出問題了可以刪掉構(gòu)建目錄重來mkdir -p ~/qt-build/build-aarch64 cd ~/qt-build/build-aarch643.2 configure 參數(shù)的逐項(xiàng)拆解configure 是整個(gè)編譯過程最關(guān)鍵的一步參數(shù)寫錯(cuò)了后面全白搭。我先把完整的命令列出來然后逐項(xiàng)解釋../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 qtwebkit \ -skip qtdeclarative \ -xplatform linux-aarch64-gnu-g \ -sysroot /opt/aarch64-toolchain/aarch64-none-linux-gnu/libc下面逐項(xiàng)說明-prefix指定安裝路徑。這個(gè)路徑是目標(biāo)系統(tǒng)上的路徑不是開發(fā)機(jī)上的。編譯完成后make install會(huì)把文件裝到開發(fā)機(jī)的這個(gè)路徑下方便你后續(xù)引用。-static是核心參數(shù)告訴 Qt 構(gòu)建靜態(tài)庫(kù)而不是動(dòng)態(tài)庫(kù)。這個(gè)參數(shù)一開所有的模塊都會(huì)以.a文件的形式產(chǎn)出。-nomake examples -nomake tests跳過示例和測(cè)試代碼的編譯。Qt 自帶的示例非常多編譯它們會(huì)浪費(fèi)大量時(shí)間而且對(duì)最終產(chǎn)物沒有影響。-no-opengl禁用 OpenGL 支持。如果你的板子沒有 GPU 或者不需要硬件加速加上這個(gè)能省不少事。Qt 的 OpenGL 模塊依賴比較復(fù)雜靜態(tài)編譯時(shí)容易出問題。-no-xcb禁用 X11 后端。嵌入式設(shè)備通常不跑 X11用 framebuffer 或者 EGLFS 就夠了。-linuxfb啟用 Linux framebuffer 后端。這是最基礎(chǔ)的顯示方式直接往/dev/fb0寫數(shù)據(jù)幾乎所有 Linux 板子都支持。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype這幾個(gè)參數(shù)讓 Qt 使用自帶的第三方庫(kù)版本而不是系統(tǒng)上的。靜態(tài)編譯時(shí)強(qiáng)烈建議這么做否則鏈接階段會(huì)因?yàn)檎也坏较到y(tǒng)庫(kù)的靜態(tài)版本而失敗。-skip qtwebengine -skip qtwebkit -skip qtdeclarative跳過這幾個(gè)重量級(jí)模塊。qtwebengine 就是 Chromium編譯它需要幾十 GB 磁盤和幾個(gè)小時(shí)而且靜態(tài)編譯基本不可行。qtdeclarative 是 QML 相關(guān)的如果你只用 Widgets 就不需要。3.3 mkspec 文件的定制-xplatform linux-aarch64-gnu-g這個(gè)參數(shù)指定了使用哪個(gè) mkspec。Qt 源碼里自帶了很多 mkspec但針對(duì) aarch64 的靜態(tài)編譯最好自己定制一個(gè)。在qt-everywhere-src-5.14.2/qtbase/mkspecs/目錄下復(fù)制linux-aarch64-gnu-g這個(gè)目錄改名為linux-aarch64-gnu-g-static然后修改里面的qmake.confQMAKE_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_OBJCOPY aarch64-none-linux-gnu-objcopy QMAKE_STRIP aarch64-none-linux-gnu-strip QMAKE_CFLAGS -marcharmv8-a -mtunecortex-a53 QMAKE_CXXFLAGS -marcharmv8-a -mtunecortex-a53-marcharmv8-a指定了 ARMv8-A 架構(gòu)這是 aarch64 的基礎(chǔ)指令集。-mtunecortex-a53是針對(duì) Cortex-A53 核心做優(yōu)化如果你的板子是 A72 或者 A76可以改成對(duì)應(yīng)的。這個(gè)優(yōu)化主要影響指令調(diào)度對(duì)性能有輕微影響但不會(huì)影響兼容性。4. 編譯過程與關(guān)鍵環(huán)節(jié)4.1 啟動(dòng)編譯與并行度設(shè)置configure 跑完之后會(huì)輸出一個(gè)摘要告訴你哪些模塊會(huì)被編譯、哪些被跳過。仔細(xì)檢查這個(gè)摘要確認(rèn)Static build是 yesCross compilation是 yes。然后開始編譯make -j$(nproc) 21 | tee build.log-j$(nproc)是用滿所有 CPU 核心。我 8 核的機(jī)器大概跑了 50 分鐘。tee build.log是把輸出同時(shí)寫到文件和終端方便出錯(cuò)了回頭看。編譯過程中最常見的錯(cuò)誤是內(nèi)存不足。Qt 的某些模塊比如 qtbase 的 qmake 部分鏈接時(shí)很吃內(nèi)存如果機(jī)器只有 8GB 內(nèi)存可能會(huì)在鏈接階段被 OOM Killer 干掉。解決辦法是減少并行度比如make -j4或者臨時(shí)加 swap。4.2 編譯過程中的典型報(bào)錯(cuò)與處理報(bào)錯(cuò)一fatal error: bits/libc-header-start.h: No such file or directory這個(gè)通常是 sysroot 路徑不對(duì)或者工具鏈的 sysroot 里缺少某些頭文件。檢查--sysroot參數(shù)指向的路徑下有沒有usr/include目錄。報(bào)錯(cuò)二cannot find -lstdc靜態(tài)鏈接時(shí)需要libstdc.a但有些工具鏈默認(rèn)只帶了.so版本。檢查工具鏈的lib目錄下有沒有l(wèi)ibstdc.a。如果沒有需要從工具鏈的lib64或者lib/gcc子目錄里找。報(bào)錯(cuò)三error: numeric_limits is not a member of std這是 C 標(biāo)準(zhǔn)庫(kù)頭文件包含順序的問題通常出現(xiàn)在 GCC 版本和 Qt 源碼不完全匹配時(shí)。解決辦法是在qmake.conf里加上QMAKE_CXXFLAGS -include limits。報(bào)錯(cuò)四undefined reference to pthread_create靜態(tài)鏈接 pthread 時(shí)需要顯式指定-lpthread。在qmake.conf里加上QMAKE_LIBS -lpthread -ldl -lrt。4.3 安裝與產(chǎn)物驗(yàn)證編譯成功后執(zhí)行安裝make install安裝完成后檢查/opt/qt-5.14.2-aarch64-static目錄應(yīng)該能看到lib、include、bin等子目錄。lib目錄下應(yīng)該全是.a文件如果看到.so文件說明靜態(tài)編譯沒生效。驗(yàn)證一下 qmake 是否可用/opt/qt-5.14.2-aarch64-static/bin/qmake -v正常的話會(huì)輸出QMake version 3.1和Using Qt version 5.14.2。5. 應(yīng)用程序的交叉編譯實(shí)戰(zhàn)5.1 一個(gè)最小 Qt 程序的編譯寫一個(gè)最簡(jiǎn)單的 Qt Widgets 程序來驗(yàn)證整個(gè)工具鏈// main.cpp #include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello aarch64 static Qt!); label.show(); return app.exec(); }對(duì)應(yīng)的.pro文件QT widgets TARGET hello-qt SOURCES main.cpp用交叉編譯版的 qmake 生成 Makefile/opt/qt-5.14.2-aarch64-static/bin/qmake hello-qt.pro make -j$(nproc)編譯完成后用file命令檢查產(chǎn)物file hello-qt應(yīng)該輸出類似ELF 64-bit LSB executable, ARM aarch64, statically linked的信息。如果顯示dynamically linked說明靜態(tài)鏈接沒生效需要檢查.pro文件里有沒有加CONFIG static。5.2 靜態(tài)鏈接的依賴檢查用ldd檢查一下aarch64-none-linux-gnu-ldd hello-qt靜態(tài)鏈接的程序應(yīng)該輸出not a dynamic executable。如果列出了一些.so文件說明還有動(dòng)態(tài)依賴。有時(shí)候即使加了-static某些庫(kù)還是會(huì)以動(dòng)態(tài)方式鏈接。這時(shí)候需要檢查 Qt 的mkspecs目錄下的qconfig.pri確認(rèn)QT_CONFIG里沒有shared選項(xiàng)。5.3 部署到目標(biāo)板把編譯好的可執(zhí)行文件拷到板子上直接運(yùn)行scp hello-qt root192.168.1.100:/root/ ssh root192.168.1.100 ./hello-qt如果板子上有 framebuffer 設(shè)備/dev/fb0程序應(yīng)該能正常顯示。如果沒有顯示檢查一下QT_QPA_PLATFORM環(huán)境變量export QT_QPA_PLATFORMlinuxfb ./hello-qt6. 常見問題排查與避坑經(jīng)驗(yàn)6.1 運(yùn)行時(shí)問題速查表現(xiàn)象可能原因解決方法could not find the Qt platform plugin linuxfb平臺(tái)插件沒編譯進(jìn)去確認(rèn) configure 時(shí)加了-linuxfbcannot mix incompatible Qt library鏈接了系統(tǒng)上的動(dòng)態(tài) Qt檢查.pro里有沒有CONFIG static程序啟動(dòng)后無顯示framebuffer 設(shè)備不對(duì)設(shè)置QT_QPA_PLATFORMlinuxfb:fb/dev/fb1字體顯示為方塊字體沒嵌入configure 時(shí)加-qt-freetype運(yùn)行時(shí)設(shè)置QT_QPA_FONTDIR觸摸屏無響應(yīng)輸入設(shè)備沒配置設(shè)置QT_QPA_GENERIC_PLUGINStslib或evdevtouch6.2 幾個(gè)我踩過的坑坑一-static和-static-libgcc的區(qū)別-static是鏈接所有庫(kù)的靜態(tài)版本-static-libgcc只是靜態(tài)鏈接 GCC 的運(yùn)行時(shí)庫(kù)。Qt 的 configure 里-static會(huì)同時(shí)影響 Qt 庫(kù)和最終程序的鏈接方式。但有時(shí)候即使加了-staticlibgcc 還是動(dòng)態(tài)鏈接的需要在qmake.conf里額外加QMAKE_LFLAGS -static-libgcc -static-libstdc??佣ysroot 里的庫(kù)版本沖突工具鏈自帶的 sysroot 里的 glibc 版本可能和目標(biāo)板子上的不一致。如果目標(biāo)板子的 glibc 比 sysroot 里的舊程序跑起來會(huì)報(bào)version GLIBC_2.xx not found。解決辦法是從目標(biāo)板子上拷貝/lib和/usr/lib到 sysroot 里覆蓋工具鏈自帶的版本??尤幾g時(shí)間過長(zhǎng)導(dǎo)致的中斷Qt 全量編譯動(dòng)輒一小時(shí)以上如果中途因?yàn)榫W(wǎng)絡(luò)問題或者誤操作中斷了重新make會(huì)從斷點(diǎn)繼續(xù)但有時(shí)候中間產(chǎn)物會(huì)損壞。這時(shí)候需要make clean然后重新來。我建議在make的時(shí)候用nohup或者screen防止 SSH 斷線導(dǎo)致編譯中斷。坑四磁盤空間不足Qt 源碼解壓后大概 2GB編譯過程中會(huì)產(chǎn)生大量中間文件最終占用可能到 15-20GB。加上工具鏈和 sysroot建議預(yù)留至少 30GB 磁盤空間。6.3 性能優(yōu)化的小技巧靜態(tài)編譯出來的程序體積大啟動(dòng)時(shí)加載到內(nèi)存的時(shí)間也長(zhǎng)。如果對(duì)啟動(dòng)速度有要求可以在鏈接時(shí)加-Wl,--gc-sections來移除未使用的代碼段配合編譯時(shí)的-ffunction-sections -fdata-sections使用。這個(gè)優(yōu)化通常能減少 10%-20% 的體積。另外strip命令可以移除符號(hào)表進(jìn)一步減小體積aarch64-none-linux-gnu-strip hello-qt但 strip 之后就沒法用 gdb 調(diào)試了建議保留一份未 strip 的版本用于調(diào)試。7. 后續(xù)擴(kuò)展與個(gè)人體會(huì)這套流程跑通之后你可以把它固化成一個(gè)腳本下次換 Qt 版本或者換工具鏈時(shí)只需要改幾個(gè)變量。我自己的做法是把 configure 參數(shù)、mkspec 定制、編譯命令全部寫到一個(gè)build.sh里配合 Docker 使用這樣換一臺(tái)開發(fā)機(jī)也能快速?gòu)?fù)現(xiàn)環(huán)境。另外如果你需要用到 Qt 的某個(gè)特定模塊——比如 Qt SerialPort、Qt SQL——記得在 configure 時(shí)不要-skip它們并且確認(rèn)對(duì)應(yīng)的依賴庫(kù)在 sysroot 里存在。Qt 的模塊依賴關(guān)系比較復(fù)雜缺一個(gè)庫(kù)可能導(dǎo)致整個(gè)模塊編譯失敗。最后說一個(gè)實(shí)際體會(huì)靜態(tài)交叉編譯這件事第一次做的時(shí)候會(huì)覺得處處是坑但一旦跑通一次后面就是重復(fù)勞動(dòng)。真正花時(shí)間的不是編譯本身而是排查各種環(huán)境問題。所以我的建議是第一次做的時(shí)候把每一步的命令和輸出都記錄下來形成自己的文檔下次遇到類似問題直接查自己的記錄比在網(wǎng)上搜半天效率高得多。