简介:一套 Qt 5.9.9 的 Linux 静态编译成品库,支持图形界面,面向需要免安装、易分发的 Linux 桌面应用或嵌入式 Qt 项目的开发与运维人员。编译环境为 CentOS 7.6 x64、GCC 4.8.5、glibc 2.17,由此库编译的图形程序经 ldd 检查不依赖 Qt 动态库,可有效规避系统 Qt 版本不一致带来的兼容问题。资源共 2000 个文件,压缩包为 gz 格式,约 197.4MB,核心内容为 3746 个头文件与 172 个 a 静态库,同时包含 287 个 qm 翻译资源、183 个 pri 与 144 个 prf 工程配置、181 个 cmake 构建脚本等,目录结构完整,并附有 qmake、moc、uic、rcc 等工具链,可直接替换或挂接既有 Qt 工程,免去从源码自行编译的漫长过程。已有 568 人学习下载。结合 configure 参数(-confirm-license -opensource -static -qt-xcb 等)可按需复现相同产物,配合静态配置与构建脚本,尤其适合离线安装、容器镜像或 CI/CD 交付等对依赖有严格要求的场景,中高级开发者可快速获得零 Qt 依赖的图形程序构建环境。
1. Linux 下 Qt 静态编译的现成答案:gcc 4.8.5 与 libc 2.17 能省下什么
做 Linux 下 Qt 静态编译的人都知道,configure 一行命令背后是三四小时的 CPU 满载、依赖报错和反复调整,那种玄学谁碰谁知道。qt-5.9.9-gcc485-libc217-static-qt-xcb.tar.gz 这个包,把最费时的编译环节提前完成了:Qt 5.9.9 是 5.9 系列最后一个 LTS 补丁版,gcc485 表示面向 GCC 4.8.5 的 C++ ABI,libc217 表示按 glibc 2.17 链接,qt-xcb 表示 xcb 平台插件已经编进包里。这套组合基本对应 CentOS 7 这一代的 x86_64 服务器,目标机 glibc 不低于 2.17,拷过去就能用它编出不依赖 Qt 动态库的程序。适合谁:需要在没装 Qt 的目标机上跑 GUI 程序、被缺 .so 文件搞到头大、又不想自己从源码编 Qt 的开发者。
2. 静态 Qt 的选型与原理:动态链接差在哪,qt-xcb 为什么避不开
先想清楚一个问题:同样一个 Qt 程序,动态链接和静态链接的部署体验差在哪里?只有把这个问题想透,你才知道这个包值不值得要。
2.1 动态链接部署的三个日常痛点
动态链接的 Qt 程序部署起来是很烦人的。最常见的翻车场景是:把编译好的程序拷到一台干净的服务器,双击运行,控制台直接给你一句 error while loading shared libraries: libQt5Widgets.so.5: cannot open shared object file,然后就没有然后了。你只能回头把整个 Qt 运行库目录(可能两三百 MB)一起拷过去,配好 LD_LIBRARY_PATH 环境变量,再战。
第二个痛点是版本冲突。目标机上如果已经有一个老项目在跑,它带了旧版本的 libQt5Core.so.5,而你新程序需要新版接口,那么加载时要么报 symbol 版本不对,要么在运行时因为 ABI 不匹配莫名崩溃。这种"明明拷了库还是挂"的问题最花时间,日志里只有一段段无头绪的崩溃栈,连 core dump 都骗你走弯路。
第三个痛点是升级连锁反应。你给程序打了 Qt 安全补丁,就得把运行库目录重新拷贝到每台机器,漏一台就埋雷。静态链接把这些都压缩成一件最简单的事:程序编出来是什么样,跑到哪里都是什么样,目标机上不需要有 Qt,甚至不需要知道 Qt 是什么。这就是静态包的核心价值。
2.2 文件名拆解:每个字段都是一个约定
拿到 qt-5.9.9-gcc485-libc217-static-qt-xcb.tar.gz,我一向的习惯是先拆文件名,因为它已经把跟谁编的、在什么系统上跑全写在里面了。
qt-5.9.9 是 Qt 版本。5.9 是 Qt 5 的 LTS 分支,5.9.9 是这条分支的最后一个补丁版,功能早就冻结,只修 bug,稳定性反而比 5.12、5.15 的新分支更适合"编完就不动"的交付场景。注意 5.9 的生态停留在 C++11 时代,Widgets 应用完全够用,但 QML 生态偏弱。
gcc485 指的是编译 Qt 库时用的 GCC 版本 4.8.5。这个数字比看起来更重要,它决定 C++ ABI。GCC 4.8.5 用的是老式 std::string ABI(_GLIBCXX_USE_CXX11_ABI 未定义或为 0),如果你用 GCC 5.1 以上的编译器默认值(new ABI)编自己的代码,链接时可能不报错,但运行时 Qt 接口返回的 std::string 在你这边解释错位,轻则字符串乱掉,重则直接崩。后面我会讲怎么在 pro 文件里加编译宏把这个对齐。
libc217 是 glibc 2.17。注意 glibc 很难真正全静态,后面避坑章会细说,这里只需要记住目标机的 glibc 版本不能低于 2.17,否则运行时报 GLIBC_x.x not found。
qt-xcb 是平台插件。Qt 在 X11 下的图形输出默认走 xcb 协议,libqxcb 是 Qt 和 X server 之间的翻译层。这个插件如果没编进去,程序在任何有 X11 的机器上都会报 "could not load the Qt platform plugin xcb",窗口根本出不来。静态包里带 qt-xcb,说明编包的人踩过这个坑,帮你也踩完了。
2.3 用静态包之前,先回答三个问题
静态不是银弹,拿到包先对着下面三个问题过一遍,再决定用不用。
目标机能装基础系统库吗?如果连 libxcb、libX11、fontconfig 都不存在且不允许安装,静态 Qt 帮不了你太多,你需要的是全静态方案,把 X11 和字体库也编进去。这个包解决的是"目标机有 X11 基础库但没有 Qt"的场景,不是"目标机除了内核什么都没有"的场景。
程序会不会大量依赖动态加载的插件?Qt 的 imageformats、sqldrivers 这类插件在静态链接下需要在代码里显式导入,用 Q_IMPORT_PLUGIN 宏,或者依赖 qmake 的自动插件导入机制,样板代码比动态版多。如果插件体系复杂,静态化的收益会打折。
有没有考虑许可证?Qt 5.9.9 在 GPL v3 与 LGPL v3 双许可下发布。LGPL 允许闭源商用,但静态链接时你有额外义务——必须让使用者具备重新链接的能力。不打算遵守这条的话,要么买商业授权,要么劝你换个包。
3. 部署与冒烟验证:从 tar 包到可用工具链的四步
拿到包别急着编业务程序,先把工具链本身验证一遍。我的经验是:静态包经常因为路径、版本匹配的问题白折腾两个小时,而这四步十分钟就能把坑排掉。
3.1 解压前先查 glibc 和系统库
头一件事是确认你手上的机器和包匹配。三条命令:
# 看目标机的 glibc 版本,必须 >= 2.17 ldd --version | head -n 1 # 看 X11 相关动态库在不在,静态 Qt 也绕不开它们 ldconfig -p | grep -E 'libxcb|libX11|libfontconfig|libxkbcommon' # 看当前 g++ 的 C++ ABI 设置,0 是老 ABI,1 是新 ABI g++ -dM -E -x c++ /dev/null | grep _GLIBCXX_USE_CXX11_ABI第二条命令的输出应该至少出现 libxcb.so.1、libX11.so.6、libfontconfig.so.1 这几个名字。如果连这些都没有,先装系统包,否则程序编出来在本地都起不来。第三条命令结果如果是 _GLIBCXX_USE_CXX11_ABI 1,后面编源码时就得手动对齐,这个在避坑章展开。
3.2 解压到固定前缀并建软链
mkdir -p /opt/qt-static tar -xzf qt-5.9.9-gcc485-libc217-static-qt-xcb.tar.gz -C /opt/qt-static ls /opt/qt-static # 如果解压出来是带版本目录,建一个 current 软链,方便以后切版本 ln -s /opt/qt-static/qt-5.9.9 /opt/qt-static/current解压后先看一眼目录里有没有 qt.conf 文件(通常在 bin/ 下)。Qt 的可执行程序在启动时会读 qt.conf,用它决定去哪个目录找库和插件。有这个文件说明包支持路径迁移,你把整个目录搬到别的地方也能用;没有的话说明编译时写死了前缀,目录移动后 qmake -query 的结果会不对,尽量别挪。
3.3 环境变量:只设 PATH 和 QTDIR
export QTDIR=/opt/qt-static/current export PATH=$QTDIR/bin:$PATH export PKG_CONFIG_PATH=$QTDIR/lib/pkgconfig:$PKG_CONFIG_PATH这一步最容易被忽略的是先后顺序。PATH 里如果把系统自带的 /usr/bin 放在前面,which qmake 找到的可能是系统里的老 Qt,后面整个构建都会用到错误版本的 qmake,你怎么查都查不出来。
提示:静态包这里恰恰不需要 LD_LIBRARY_PATH。程序不依赖 Qt 动态库,设了反而可能让程序去加载一个不匹配的 libQt5Core.so.5,掩盖静态链接的效果。这三行可以写进 ~/.bashrc 持久化。
3.4 用 qmake -query 验证编译配置
qmake -query # 单独查某个字段也行 qmake -query QT_INSTALL_LIBSqmake -query 会列出 Qt 为你准备的编译环境信息,重点核对三个字段。QT_INSTALL_PREFIX 应该指向解压目录(或者 qt.conf 指定的路径);QT_INSTALL_LIBS 目录下应该躺着一堆 .a 结尾的文件:libQt5Core.a、libQt5Widgets.a、libQt5Gui.a。如果你看到的全是 .so.5,说明这个包根本不是静态版,或者你 PATH 里混进了动态版 Qt,后面编译必然白费。QT_INSTALL_PLUGINS 也要记一下,静态 Qt 的插件经常直接编进库或放在 plugins 目录下,需要时按这个路径找。
3.5 空窗口冒烟测试
环境没问题,就先编一个最小程序验证链路。新建一个目录,放两个文件。
main.cpp:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("static qt ready"); label.resize(200, 100); label.show(); return app.exec(); }smoke.pro:
QT += core gui widgets TEMPLATE = app TARGET = smoke CONFIG += release CONFIG -= debug SOURCES += main.cpp编译并运行:
cd smoke qmake && make -j$(nproc) ./smoke这里有一个容错技巧:如果你在命令行环境(没有显示器或者 ssh 进去)测试,可以把 QT_QPA_PLATFORM=offscreen 临时指定为离屏渲染,验证程序能不能正常启动:
QT_QPA_PLATFORM=offscreen ./smoke能正常启动不报错,说明 Qt 库、插件、链接配置全部就绪。这一步通过之前不要碰业务代码,否则出了错你分不清是业务的问题还是环境的问题。
4. 静态链接实战:pro 文件配置、openssl 依赖与 ldd 验收
冒烟测试过了,工具链没问题,接下来进入正式的工程配置。这里有几个参数是静态 Qt 特有的,动态 Qt 工程里根本不会遇到。
4.1 pro 文件里的静态化配置
一个常规 Widgets 工程的 pro 文件在静态包下长这样:
QT += core gui widgets TEMPLATE = app TARGET = demo CONFIG += release c++11 CONFIG -= debug DESTDIR = $$PWD/bin QMAKE_CXXFLAGS += -D_GLIBCXX_USE_CXX11_ABI=0 QMAKE_LFLAGS += -static-libgcc -static-libstdc++ SOURCES += main.cpp \ window.cpp HEADERS += window.h说几个关键点。
不需要 CONFIG += static。很多人照着老教程往 pro 里加 static,但 qmake 会从 Qt 库本身的形态判断:当它发现 libQt5Core.a 这堆静态库存在,会自动把静态链接需要的路径和依赖库列表加进去,你再写 CONFIG += static 反而可能干扰插件导入逻辑。
QMAKE_CXXFLAGS += -D_GLIBCXX_USE_CXX11_ABI=0 是为了兼容 gcc485 包的旧 ABI。如果你的本机编译器是 GCC 8/9/10,默认是 new ABI,而 Qt 库是 4.8.5 的老 ABI 编的,这两者在传 std::string 的时候会互相错位。加上这个宏让编译器回到老 ABI,问题直接消失。代价是这套代码的其他依赖库也得保持老 ABI,否则另一处错位。
QMAKE_LFLAGS += -static-libgcc -static-libstdc++ 的作用是把 gcc 运行库也静态链进来,这样目标机上不需要 libstdc++.so.6。注意它不碰 glibc,glibc 永远动态,避坑章会说。
4.2 静态链接的库顺序和 openssl
静态链接对库的顺序极其敏感:GNU ld 处理 .a 归档文件是扫描式的,被依赖的库必须出现在依赖者的后面,否则直接报 undefined reference。qmake 在静态 Qt 下生成的链接命令已经处理好了 Qt 自身的顺序(Widgets 在 Gui 前,Gui 在 Core 前),容易翻车的是你手动加的第三方库。
最典型的例子是 QtNetwork。QT += network 之后,Qt 静态库里含有对 OpenSSL 符号的引用(SSL_read、SSL_new 这些),但它们不会跟着 Qt 自动进入链接。你不显式链 openssl,链接器就会报 undefined reference to SSL_read。处理方式:
LIBS += -lssl -lcrypto如果库之间有循环依赖,用 --start-group 包住一组库强制全部解析:
QMAKE_LFLAGS += -Wl,--start-group -lssl -lcrypto -lz -Wl,--end-group另外,Qt 5.9 的 QtNetwork 对 OpenSSL 1.1 的支持是后来补丁里加的,很多静态包是按 1.0.2 的 API 编的,所以静态链接 openssl 时的版本匹配要单独查。这个包如果内部带了 openssl 静态库,通常在 lib/ 下能看到 libssl.a 和 libcrypto.a,优先用它们而不是系统库。
4.3 用 ldd 验收:什么是真正的静态成功
编译完成别急着拷走,先做验收。两条命令:
file bin/demo ldd bin/demofile 的输出应该是 ELF 64-bit LSB executable。ldd 的输出才是关键:理想的输出里不应该出现任何 libQt5 开头的 .so 文件。允许出现的是 libc.so.6、libstdc++.so.6(如果你没加 -static-libstdc++)、libm.so.6、libX11.so.6、libxcb.so.1、libfontconfig.so.1 这些系统库。
只要出现 libQt5Core.so.5 或者 libQt5Widgets.so.5,就说明链接的不是静态库。最常见的原因是 which qmake 指向了系统里的动态版 Qt,clean 之后重新 qmake 再 make。第二个原因是 Makefile 里的路径变量被手工改过,指向了别处。
注意:静态和动态切换后必须全量重建。删掉整个构建目录,重新从 qmake 开始,别用增量编译,否则你看到的还是上一次的动态链接产物。
4.4 拷贝到目标机的检查单
把 bin/demo 拷到目标机后,按顺序执行下面这张表:
| 步骤 | 命令 | 期望结果 |
|---|---|---|
| 1 | ldd demo | 没有 libQt5 开头的行;不出现 not found |
| 2 | fc-list :lang=zh 2>/dev/null | 有中文字体,否则程序中文显示方块 |
| 3 | ./demo | 窗口能弹出,无 "platform plugin xcb" 报错 |
| 4 | QT_QPA_PLATFORM=offscreen ./demo | 无显示环境下能正常退出码 0 |
这张表我每次部署都走一遍。第 3 步失败大概率是 libxcb 系列缺失,第 2 步失败是字体问题,这两条直接对应避坑章的前两个大坑。
5. 避坑排查:xcb 加载失败、豆腐块字体与 LGPL 合规的四个典型翻车点
静态 Qt 的坑很有规律。我在几个项目里反复踩过,按出现频率排个序讲。
5.1 现象一:平台插件 xcb 反复加载失败
程序启动直接吐两行经典错误:
qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found. This application failed to start because no Qt platform plugin could be initialized.原因:libqxcb 已经静态编进程序了,但它运行时要依赖系统的 libxcb.so.1、libX11.so.6、libxkbcommon.so,目标机缺任何一个,插件初始化就失败。还有种情况是环境变量 QT_QPA_PLATFORM 被人设成了 offscreen 或 minimal,Qt 会故意绕过 xcb,窗口自然不出来。
解决:先 echo $QT_QPA_PLATFORM 排除环境变量问题,再在目标机执行 ldd demo | grep not found 看缺哪个,最后用发行版包管理器补齐:libxcb、libxcb-xkb、libxkbcommon、libX11 这几个包名对应装齐。没有包管理器的情况下,从编译机把这些 .so 拷过去放在 /usr/local/lib 并执行 ldconfig,也能临时应急。记住 xcb 不算 Qt 的锅,它是 X11 的底层协议库,静态 Qt 静不了 X11。
5.2 现象二:目标机上中文全是方块
英文和数字正常,中文全部渲染成一个个空心的 □,QT_QPA_PLATFORM 换回 xcb 也一样。
原因:静态 Qt 不携带字体。Qt 通过 fontconfig 枚举系统字体,目标机没有中文字体,fontconfig 返回的字体列表里没有中文族,fallback 失败就画方块。这个跟 Qt 版本、静态动态都无关,但静态部署最容易遇到,因为目标机往往是精简系统,什么都不装。
解决:三选一。一是在目标机安装中文字体包(wqy-zenhei 这类),安装后执行 fc-cache -f 刷新缓存。二是在代码里把字体文件跟程序一起发布,启动时用 QFontDatabase::addApplicationFont("/opt/demo/fonts/NotoSansCJK-Regular.ttc") 注册,再设置 QApplication::setFont。三是更暴力的做法,在程序 main 函数里 qputenv("QT_QPA_FONTDIR", "/opt/demo/fonts"),直接把 Qt 的字体搜索路径指过去。第一种最省事,第二种最可控,我一般用第二种,字体直接打进部署目录,目标机什么样无所谓。
5.3 现象三:LGPL 合规问题被法务找上门
这不是运行时报错,但比任何报错都贵。Qt 5.9.9 以 LGPL v3 发布,静态链接意味着 Qt 库的代码被编译进了你的可执行文件,LGPL 要求必须给最终用户重新链接的能力——你要么提供程序的目标文件(.o),要么提供完整源码,要么提供足以让用户重新构建的材料和说明。
原因:老团队的惯性认知是"开源协议只是写着玩的"。内部工具跑几十年没人管,一旦对外分发商业软件,这条就是硬红线。常见翻车是只发一个可执行文件,别的什么都不给。
解决:最简单的合规做法是在发布包的 relink/ 目录里放上链接用的 .o 文件、链接命令行(make 记录的完整命令)、以及这份静态 Qt 包的位置说明。用户拿着这些材料用同一份 Qt 库重新链一次,就能得到自己的版本。涉及商业授权能不能直接用,问法务,别问搜索引擎。
提示:这里不是让你交出 Qt 源码,而是提供"用同一库重构"的能力,这是 LGPL 和 GPL 的分水岭。
5.4 现象四:目标机报 GLIBC_2.18 not found
程序拷到老机器上,运行时报:
./demo: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by ./demo)原因:这个包是面向 glibc 2.17 编的,目标机比 2.17 还老(比如 glibc 2.12 的系统)。glibc 用符号版本机制,程序里编译期引用的符号版本在目标机的 libc 里没有,动态链接器直接拒绝启动。很多人以为静态包是全静态,实际上 glibc 出于 NSS、DNS、locale 等机制无法完整静态链接,或者说全静态 glibc 会让程序在很多网络和本地化场景下行为异常。所以你加的 -static-libgcc -static-libstdc++ 只解决了 libstdc++ 和 libgcc,解决不了 libc。
解决:先确认目标机版本,ldd --version。目标机 glibc 必须是 2.17 或更高,包才能跑。如果目标机更老,只能换一个用 glibc 2.12 编的静态包,或者用 musl 交叉编译绕开 glibc,但那要重新配置整个编译链,成本远高于换包。部署前把这条写进检查清单第一条,能省很多白跑的辛苦。
5.5 现象五:链接时 undefined reference 一串串
链接阶段报一长串 undefined reference,但是 QT 模块看着都加了。
原因:静态链接的依赖传递问题。Qt 的 libQt5Network.a 需要 zlib、openssl;libQt5Gui.a 需要 libpng、libjpeg 等。qmake 会按编译期的配置把这些依赖维持住,但如果包编译时某些依赖用的是系统库而你本机版本和包的有出入,或者你自己往 LIBS 里加的库顺序不对,就会出现符号找不到。注意看第一个报错的符号是哪个领域——libpng 的开头是 png_,openssl 是 SSL_ 或者 EVP_,zlib 是 z_。看到哪个就补哪个库。
解决:按符号前缀定位依赖并补 LIBS。例如补 openssl:
LIBS += -L$$QTDIR/lib -lssl -lcrypto -ldl -lpthread如果循环依赖,用 4.2 节说的 -Wl,--start-group 包住。还有一个容易忽略的点:静态包的 .a 如果没带 debug 符号,调试时看不到 Qt 内部的调用栈,只能看到自己的代码栈,这是常态,不算异常,别花时间找符号表。
6. 收尾技巧:strip 瘦身与一个改不掉的验证习惯
静态链接的程序体积不小,一个 Widgets hello world 静态编出来通常在 20MB 到 40MB 之间,模块开得多还能涨一倍。发布前记得瘦身,一条命令能砍掉 30% 到 40%。
在 pro 文件里加:
QMAKE_LFLAGS_RELEASE += -s或者编译完成后手动执行:
strip bin/demostrip 会去掉 ELF 里的符号表、调试信息、重定位信息,不影响运行,但对排查 crash 时的 backtrace 有影响。调试阶段别 strip,发布版本再 strip,这是我自己的固定顺序。另外,strip 之后顺手用 size bin/demo 看一眼段大小,如果 .text 段明显不对劲(比如只有几 KB),多半是链接出错,没把 Qt 库真正静态进去,重复 4.3 节的验收流程。
体积优化的另一个方向是砍模块。静态 Qt 在 configure 阶段可以用 -no-*** 关闭用不到的模块,但那是编包的人决定的事。拿到现成包,你能控制的是 pro 文件里 QT += 的粒度:只用 Widgets 就别加 network、sql,链接器按需从 .a 提取目标文件,少一个模块就少一带依赖链。
最后说一个我改不掉的验证习惯。每次拿到新的静态 Qt 包,我强制自己做三连:第一步空窗口冒烟测试,确认 qmake 和链接链路通;第二步 ldd 验证输出里一个 libQt5 都不能有;第三步把产物拷到 glibc 版本最低的一台目标机跑一遍 xcb 和字体。这三步跑完,我才允许自己写业务代码。上次偷懒直接编业务程序,花了三个小时才发现是包路径迁移导致 qmake 链接了动态库,从那以后每一步都不跳。你拿到这个包,也先走一遍这三连,再开始干活,希望帮到你。
本文还有配套的精品资源,点击获取