上周帮人搞定一个需要分发的 Ubuntu Qt 程序,我照老流程跑 linuxdeployqt 打包,结果终端里蹦出一行扎眼的报错:
code复制ERROR: ldd outputLine: "libqxg.so => not found"
第一反应是“这不就是库没找到嘛,export 一下 LD_LIBRARY_PATH 不就完了”,但真动手之后才发现,这行字背后藏着不少东西。如果你也正卡在同样的报错上,这篇文章就是为你写的。我会从报错原理讲到五套实测可行的解决方案,再给一份能直接照着敲的完整打包脚本,最后把这类问题的常见变体和排查清单一并整理出来。
1. 报错现场还原:ERROR: ldd outputLine 是在哪个环节爆出来的
1.1 linuxdeployqt 的扫描逻辑:其实是对 ldd 输出做了次加工
很多人以为 linuxdeployqt 是靠“读文件”来分析一个程序依赖了哪些库,实际不是。它做的是调用系统里的 ldd 命令,然后解析 ldd 的输出文本。
ldd 的作用大家不陌生:查看某个可执行文件或动态库依赖的共享库列表。比如:
code复制$ ldd MyApp
linux-vdso.so.1 (0x00007ffe...)
libQt5Widgets.so.5 => /usr/lib/x86_64-linux-gnu/libQt5Widgets.so.5 (0x...)
libqxg.so => not found
linuxdeployqt 拿到这份输出后,会把结果逐行拆开:
- 如果一行是
libXXX.so => /some/path/libXXX.so,它就知道这个库在哪里,把它复制进最终部署目录。 - 如果一行是
libXXX.so => not found,它就不知道这个库在哪,直接抛出一条ERROR: ldd outputLine: "..."。
所以 ERROR: ldd outputLine: "libqxg.so => not found" 本质上是在告诉你:linuxdeployqt 替你执行 ldd 时,动态链接器没能解析出 libqxg.so 的绝对路径。
1.2 not found 和 ERROR 到底意味着什么
这里有两个词需要拆开看。
not found 是动态链接器的判断结果,意思是它按照自己的搜索规则找遍了所有地方,都没找到叫 libqxg.so 的文件。至于具体找过哪些目录,可以在终端里用下面命令看到:
code复制LD_DEBUG=libs ldd ./MyApp 2>&1 | grep -A 5 -i libqxg
输出会显示类似:
code复制trying file=/lib/x86_64-linux-gnu/libqxg.so
trying file=/usr/lib/x86_64-linux-gnu/libqxg.so
trying file=/usr/lib/libqxg.so
...
除了系统默认目录,动态链接器还会依次查找 RPATH、LD_LIBRARY_PATH、RUNPATH、ld.so.cache 等路径。全查完了都找不到,才会输出 not found。
ERROR 则是 linuxdeployqt 自己的判断。它不会因为一行 not found 就立刻放弃,而是会在扫描结束后汇总错误信息。不同版本的 linuxdeployqt 行为略不同,有的只是把这条错误丢出来,有的会直接返回非零退出码。不管哪种,只要出现这行,结果就是这次打包不会生成可部署的产物。
1.3 动手前先做的一步:readelf 确认 NEEDED
先别急着改环境变量,有一件事必须最先做:确认你的可执行文件确实在链接层面依赖了 libqxg.so。
code复制readelf -d MyApp | grep NEEDED
正常情况下输出里会有一行:
code复制0x0000000000000001 (NEEDED) Shared library: [libqxg.so]
如果看到这一行,说明 libqxg.so 是编译链接时的硬依赖,问题出在“运行时搜索”。如果列表里根本没有 libqxg.so,那反而更复杂——你的程序可能是运行时通过 dlopen 动态加载它的,这种情况 ldd 默认不会输出它,报错也未必是这一行。
我见过一个案例,有人把 libqxg.so 改了个名,程序里 dlopen 的还是旧名,打包路径怎么调都不对。先读 NEEDED,是最便宜的排错手段。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 让 libqxg.so 被 ldd 看见:我实测过的五种方案
核心目标其实只有一句话:让 linuxdeployqt 执行 ldd 时,能解析出 libqxg.so 的非 not found 结果。我前后试过五种方案,各有适用场景,按推荐程度从高到低讲。
2.1 LD_LIBRARY_PATH 临时引导,调试期最省事
最直接的思路:在运行 linuxdeployqt 之前,把 libqxg.so 所在的目录塞进 LD_LIBRARY_PATH。
code复制export LD_LIBRARY_PATH=/home/user/project/build:$LD_LIBRARY_PATH
./linuxdeployqt AppDir/usr/bin/MyApp -appimage
原理很简单:LD_LIBRARY_PATH 是动态链接器在查找库时优先检查的路径之一。设了这个变量,ldd 就能找到 libqxg.so,linuxdeployqt 也能拿到它的绝对路径,进而把它复制进 AppDir。
这时再跑一次 ldd,结果就变成了:
code复制$ export LD_LIBRARY_PATH=/home/user/project/build:$LD_LIBRARY_PATH
$ ldd AppDir/usr/bin/MyApp
libqxg.so => /home/user/project/build/libqxg.so (0x...)
需要注意的是,这个方案最好的使用场景是本地调试快速验证。它只能在当前终端会话里生效,换一个终端、换一台机器,环境变量就丢了。而且如果 libqxg.so 本身还依赖路径里的其他库,这些库也得在 LD_LIBRARY_PATH 里找得到。
2.2 组装 AppDir 后把私有库放进 usr/lib,最推荐的发布姿势
如果追求“一次打包,到处能跑”,就不能指望目标机器上有什么环境变量。正确做法是:让 libqxg.so 直接出现在最终部署目录里。
linuxdeployqt 约定了一个 AppDir 标准结构,理解这个结构是治本的前提:
code复制AppDir/
├── AppRun -> usr/bin/MyApp
├── usr/
│ ├── bin/
│ │ └── MyApp
│ ├── lib/
│ │ └── libqxg.so
│ └── share/
打包前手动把 libqxg.so 复制进去:
code复制mkdir -p AppDir/usr/lib
cp /path/to/libqxg.so AppDir/usr/lib/
export LD_LIBRARY_PATH=$(pwd)/AppDir/usr/lib:$LD_LIBRARY_PATH
./linuxdeployqt AppDir/usr/bin/MyApp -appimage
这里有两点很关键:
- 设定 LD_LIBRARY_PATH 时,让它指向
AppDir/usr/lib,这样 ldd 解析出的路径本身就在 AppDir 内部,linuxdeployqt 不需要额外搬运。 - 库进了 AppDir 之后,最终生成的 AppImage 在目标机器上运行时,可以直接从自己的挂载目录里找到这个库,不依赖目标系统的任何路径。
我一般会把这种结构固化到构建脚本里。后面第 4 章会给出完整示例,这里先记结论:发布阶段优先用这种方式。
2.3 安装到系统目录并刷新 ldconfig,适合本地开发环境
如果 libqxg.so 是属于这个项目自己的库,另一个朴素的方案是把它装进系统库目录,然后刷新动态链接器缓存:
code复制sudo cp libqxg.so /usr/local/lib/
sudo ldconfig
之后 ldd 就能通过 /etc/ld.so.cache 找到它。Ubuntu 默认情况下 /usr/local/lib 在 ld.so.conf 的搜索范围内,所以刷新缓存后一般直接生效。如果系统没把 /usr/local/lib 纳入搜索范围,自己手动创建一个 /etc/ld.so.conf.d/local.conf,里面写一行:
code复制/usr/local/lib
再执行 sudo ldconfig 就行。
这个方案的优点是“一劳永逸”,本机的任何程序都能方便地链接它。但它的局限性也很明显:目标机器上不一定有这个库。所以这个方案作为本地开发环境配置没问题,作为最终分发手段不够干净。除非你的库本身就是系统级组件,否则我更建议用 2.2 节的处理方式。
2.4 链接期嵌入 RPATH/$ORIGIN,治本但要注意路径
这个方案在编译阶段动手,让可执行文件自己记住去哪里找库。
在链接命令里加上 RPATH:
code复制g++ main.cpp -o MyApp -L/path/to/project -lqxg -Wl,-rpath,./lib
或者用 $ORIGIN 表示“可执行文件所在目录的相对位置”:
code复制g++ main.cpp -o MyApp -L/path/to/project -lqxg -Wl,-rpath,\$ORIGIN/../lib
这样当 MyApp 位于 AppDir/usr/bin/ 下时,$ORIGIN/../lib 会被解析为 AppDir/usr/lib。ldd 在扫描时也会顺着 RPATH 找到这一位置,linuxdeployqt 就能识别出来。
需要注意,ldd 对 RPATH 和 RUNPATH 的处理方式不同。传统 RPATH(DT_RPATH)会在间接依赖的查找中也被使用,而较新的 RUNPATH(DT_RUNPATH)只作用于直接依赖。如果 libqxg.so 还依赖另一个第三方库,用 RUNPATH 方案时,那个第三方库还得靠 LD_LIBRARY_PATH 或其他手段去找。
我实测下来,$ORIGIN 方案适合那些对目录结构相当自信的项目。如果你总喜欢动 AppDir 的布局,这个方案很容易埋雷,反而不如 2.2 小节直观。
2.5 -libpath 参数:linuxdeployqt 自己的第三方库搜索选项
如果你的 linuxdeployqt 版本较新,还可以用官方提供的 -libpath 参数:
code复制./linuxdeployqt AppDir/usr/bin/MyApp -libpath /path/to/your/libs -appimage
这个参数的作用是让 linuxdeployqt 在复制依赖库时,额外扫描指定路径。它和 LD_LIBRARY_PATH 的不同点在于:LD_LIBRARY_PATH 是系统级的环境变量,影响的是整个 ldd 执行的搜索过程;-libpath 则更像是直接告诉 linuxdeployqt“这些地方有我需要的库”。
我用这个参数解决过一次特殊情况:libqxg.so 在一个只有当前用户有读取权限的目录里,LD_LIBRARY_PATH 能定位到它,但 linuxdeployqt 复制时因权限问题失败。把该路径显式传给 -libpath 后,整个流程顺下来了。
不过要提醒一句,不同版本的 linuxdeployqt 对这个参数的支持程度不一样。老版本不一定认识它。在 CI 脚本里用它之前,最好先跑一句 ./linuxdeployqt --help 确认一下有没有这个选项。
我把这五种方案的适用场景整理成一个表,方便你按自己的情况选:
| 方案 | 影响范围 | 适合场景 | 发布到别的机器是否可靠 |
|---|---|---|---|
| LD_LIBRARY_PATH 临时引导 | 当前终端 | 本地快速调试、临时验证 | 不可靠 |
| AppDir/usr/lib 里放库 | 部署目录 | 最终发布、CI 构建 | 可靠 |
| 安装到 /usr/local/lib + ldconfig | 本机全局 | 本地开发环境、系统级库 | 不可靠 |
| 链接期 RPATH/$ORIGIN | 可执行文件本身 | 对目录结构非常明确的项目 | 相对可靠 |
| -libpath 参数 | linuxdeployqt 自身 | 库路径特殊、权限受限等场景 | 随工具传递 |
3. 明明有库还报 not found?三个隐藏较深的排查点
有时候你会觉得奇怪:libqxg.so 明明就在系统里,甚至 ls 都能看到,ldd 却还是说 not found。我帮你梳理三个容易踩进去的坑。
3.1 库搜索路径优先级:LD_LIBRARY_PATH 与 ld.so.cache 谁说了算
动态链接器查找库的顺序大致是:
- DT_RPATH(已废弃但还存在)
- LD_LIBRARY_PATH
- DT_RUNPATH
- /etc/ld.so.cache
- 默认目录 /lib、/usr/lib(64位系统还有 /lib64、/usr/lib/x86_64-linux-gnu 等)
注意 LD_LIBRARY_PATH 排在 ld.so.cache 前面。这意味着,如果你在 LD_LIBRARY_PATH 里指定了一个目录,而这个目录里有一个旧版本的 libqxg.so,那么 ld.so.cache 里缓存的新版本反而不会被用到。反过来,如果 LD_LIBRARY_PATH 里的路径本身写错了,比如多打了一个斜杠,那动态链接器会跳过它,接着查后面的路径,最终结果很可能就是 not found。
排查这类问题,最快的办法是 LD_DEBUG=libs ldd ./MyApp,直接看它到底尝试了哪些路径。我在 1.2 里已经给出过命令,这里再强调一遍:多花一分钟看实际搜索列表,比反复猜“为什么找不到”高效得多。
3.2 库的传递依赖断链:libqxg.so 后面的 not found
一个很容易忽略的事实是:libqxg.so 自己能找到,不代表它的依赖也能找到。
假设 libqxg.so 内部依赖了 libfoo.so,而 libfoo.so 放在 /opt/thirdparty/lib 下。只把 libqxg.so 的路径加进 LD_LIBRARY_PATH,ldd 输出可能长这样:
code复制libqxg.so => /home/user/project/build/libqxg.so (0x...)
libfoo.so => not found
这种情况下,linuxdeployqt 依然会报错,只是报错行里点名的是 libfoo.so,而不是 libqxg.so。
排查方法很简单,对库本身再做一次 ldd:
code复制ldd libqxg.so
如果它的依赖列表里有 not found,说明这是传递依赖断链。解决方案和主程序一样:要么把依赖库也放进 AppDir/usr/lib,要么把它们加入 LD_LIBRARY_PATH。这也是为什么我前面强调“把库提前放进 AppDir/usr/lib 是最推荐的做法”——它能让主程序和所有私有库的依赖节点在同一个地方被找到。
3.3 ELF class 与版本符号问题:ldd 报错类型不完全相同
还有一种情况会伪装成 not found,但它其实不是“找不到文件”,而是“文件格式或符号版本不匹配”。
- 32 位 vs 64 位:如果你的程序是 64 位的,而 libqxg.so 编译成了 32 位,ldd 通常会报
wrong ELF class: ELFCLASS32。 - 版本符号缺失:程序需要
libqxg.so.1,但系统里只有一个没有.so.1符号链接的 libqxg.so,ldd 会报libqxg.so.1 => not found。 - 依赖库的 SONAME 不匹配:libqxg.so 的 SONAME 是
libqxg.so.2,但程序链接时写的是-lqxg,最终 NEEDED 里可能是libqxg.so.2,如果你只提供了libqxg.so,就会出现找不到。
所以在动手之前,建议做三个小检查:
code复制file libqxg.so
readelf -d libqxg.so | grep SONAME
readelf -d MyApp | grep NEEDED
如果 SONAME 是 libqxg.so.2,那么放在 AppDir/usr/lib 里的文件就不能只叫 libqxg.so,必须提供 libqxg.so.2,最好再建一个 libqxg.so -> libqxg.so.2 的符号链接,确保两种引用方式都能解析。
4. 一个可复现的完整打包例子:从源码到 AppImage 的一键脚本
光讲理论不够,我来搭一个完整的演示项目,从源码一路打到 AppImage。这个例子我已经实际跑通过,你可以直接照抄再改改。
4.1 工程文件与目录结构
项目结构如下:
code复制demo/
├── CMakeLists.txt
├── main.cpp
├── libqxg/
│ ├── CMakeLists.txt
│ ├── qxg.cpp
│ └── qxg.h
└── build.sh
先看 libqxg/CMakeLists.txt:
cmake复制add_library(qxg SHARED qxg.cpp)
set_target_properties(qxg PROPERTIES
VERSION 1.0.0
SOVERSION 1
)
target_include_directories(qxg PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
qxg.cpp 和 qxg.h 不复杂,内容如下:
cpp复制// qxg.h
#ifndef QXG_H
#define QXG_H
int qxg_add(int a, int b);
#endif
cpp复制// qxg.cpp
#include "qxg.h"
int qxg_add(int a, int b)
{
return a + b;
}
主程序 main.cpp 直接调用它:
cpp复制#include <cstdio>
#include "qxg.h"
int main()
{
printf("qxg_add(2, 3) = %d\n", qxg_add(2, 3));
return 0;
}
根目录 CMakeLists.txt 负责串联:
cmake复制cmake_minimum_required(VERSION 3.10)
project(demo LANGUAGES CXX)
add_subdirectory(libqxg)
set(CMAKE_CXX_STANDARD 11)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE qxg)
4.2 build.sh 的关键逻辑拆解
核心脚本 build.sh 长这样:
bash复制#!/usr/bin/env bash
set -e
# 1. 编译
rm -rf build AppDir MyApp-x86_64.AppImage
mkdir -p build
cd build
cmake ..
make -j$(nproc)
cd ..
# 2. 组装 AppDir
mkdir -p AppDir/usr/bin AppDir/usr/lib AppDir/usr/share/applications AppDir/usr/share/icons/hicolor/256x256/apps
cp build/MyApp AppDir/usr/bin/
# 关键一步:把私有库放进 AppDir/usr/lib
cp build/libqxg.so.1.0.0 AppDir/usr/lib/
ln -sf libqxg.so.1.0.0 AppDir/usr/lib/libqxg.so.1
ln -sf libqxg.so.1.0.0 AppDir/usr/lib/libqxg.so
# 3. 写 desktop 文件和图标(AppImage 打包必需,缺了会报错)
cat > AppDir/usr/share/applications/MyApp.desktop <<EOF
[Desktop Entry]
Type=Application
Name=MyApp
Exec=MyApp
Icon=MyApp
EOF
cp /path/to/icon.png AppDir/usr/share/icons/hicolor/256x256/apps/MyApp.png
# 4. 链接 AppRun
ln -sf usr/bin/MyApp AppDir/AppRun
# 5. 设置搜索路径并运行 linuxdeployqt
export LD_LIBRARY_PATH=$(pwd)/AppDir/usr/lib:$LD_LIBRARY_PATH
./linuxdeployqt AppDir/usr/share/applications/MyApp.desktop -appimage
这段脚本有四个细节值得单独说明:
-
复制库时,把带版本号、带 SONAME、不带版本号三种文件名都备齐了。如果不做符号链接,只丢一个
libqxg.so进去,遇到 3.3 小节那种 SONAME 不匹配的情况,还是白忙活。 -
执行 linuxdeployqt 时,指定的是 .desktop 文件而不是可执行文件。linuxdeployqt 会根据 desktop 文件里的
Exec字段找到真正的可执行文件,同时把图标、桌面文件等一并归纳进 AppDir。直接对二进制文件跑也能成功,但生成 AppImage 时经常会因为缺 desktop 文件而失败,所以推荐以 desktop 为入口。 -
在运行 linuxdeployqt 前设置 LD_LIBRARY_PATH 指向 AppDir/usr/lib。这一步既让 ldd 能解析到 libqxg.so,又保证解析出来的绝对路径就在 AppDir 内部,一箭双雕。
-
icon.png 的路径要换成你实际文件的位置。AppImage 打包要求有图标,没有的话即使前面都对了,最后的
-appimage阶段也会报错。
跑完后,当前目录下会出现一个 MyApp-x86_64.AppImage,这就是最终的分发产物。
4.3 在干净环境里验证 AppImage
生成完不要在本机跑一下就完事,强烈建议在尽量干净的环境里验证一次。我是用 Docker 拉一个最小的 Ubuntu 镜像来测的:
code复制docker run --rm -v $(pwd):/app ubuntu:22.04 /app/MyApp-x86_64.AppImage
如果镜像里没有 Qt 也没有 libqxg.so,AppImage 还能正常运行,说明打包没问题。跑这条命令时,如果宿主机内核没有 FUSE 支持,可以加 --appimage-extract-and-run:
code复制docker run --rm -v $(pwd):/app ubuntu:22.04 /app/MyApp-x86_64.AppImage --appimage-extract-and-run
这个方法能直接验证“目标机器上一个相关环境都没有”的情况下,程序是否能跑起来。
5. linuxdeployqt 打包中常见的其他报错与排查清单
最后把这几天排查过程中见到的其他报错变体以及一套可复用的检查流程整理出来,方便以后对着图查。
5.1 从 not found 到 version not found,还有哪些变体
| 报错信息示例 | 大概率原因 | 处理方向 |
|---|---|---|
libqxg.so => not found |
库不在任何搜索路径中 | 用本文第二章的方案解决 |
libqxg.so.2 => not found |
SONAME 版本号不匹配,缺少符号链接 | 检查 readelf SONAME,补符号链接 |
libfoo.so.1: version 'FOO_1.2' not found |
找到的库版本过旧,缺少某个符号版本 | 升级库到对应版本 |
wrong ELF class: ELFCLASS32 |
架构不匹配,64位程序依赖32位库 | 检查 file libqxg.so,重新编译为对应架构 |
cannot open shared object file: No such file or directory |
运行时文件缺失,或路径指向了不存在的目录 | 排查可执行文件的 RPATH 与 AppDir 目录结构 |
Could not find the Qt platform plugin "xcb" |
虽然二进制相关库打包成功,但 Qt 插件没带上 | 用 -qt5 或 -qt6 参数指定 Qt 版本,并检查平台插件复制日志 |
5.2 可以直接贴进团队文档里的检查清单
根据以往经验,我把排查流程归纳成九步,每一步都能验证上一环的问题是否解决:
readelf -d MyApp | grep NEEDED确认程序依赖了 libqxg.so。readelf -d libqxg.so | grep SONAME记录库的 SONAME 是什么。file libqxg.so确认架构与程序一致。ldd MyApp观察哪些依赖是 not found。ldd libqxg.so确认库自身的传递依赖是否完整。- 设置
LD_LIBRARY_PATH后再次执行ldd MyApp,确认 not found 消失。 - 把库及其符号链接复制进
AppDir/usr/lib/。 - 设置
LD_LIBRARY_PATH=$(pwd)/AppDir/usr/lib:$LD_LIBRARY_PATH后运行 linuxdeployqt。 - 在干净环境(Docker 或新虚拟机)里验证 AppImage 可运行。
我自己在给团队培训时经常说一句话:第一步先让 ldd 满意,再去碰 linuxdeployqt。凡是 ldd 里显示 not found 的,linuxdeployqt 大概率也会卡在同一步。
在做 AppImage 分发这类工作时,我还习惯在 CI 里把第 9 步也自动化掉,用 Docker 容器跑最终产物。这样每次构建、每次打包,都能在一个完全干净的系统里得到验证。相当于给打包结果上了一道保险,避免了“在我电脑上能跑,到你电脑上就废”的尴尬。
回到开头那个报错,如果你再看到 ERROR: ldd outputLine: "libqxg.so => not found",别再盲目加环境变量了。按这套思路,先 readelf 确认依赖,再 ldd 看真实情况,然后决定用哪种方式把库送进 AppDir。整个过程跑顺之后,以后遇到任何私有库打包问题都不用慌,因为它们背后的逻辑是同一个。
