最近帮人迁移一个老 Qt 项目,开发机换到 macOS 26 之后,qmake 一切正常,代码编译也顺利,结果一进链接阶段直接甩了个红脸:ld: framework 'AGL' not found
这句话本身很好懂,就是链接器找不到 AGL 这个 framework。但真正让人头疼的是网上答案五花八门,有让你重装 Qt 的、有让你换 Xcode 的、还有让你去系统目录里“补一个”framework 的——大部分都不解决问题,甚至会把事情搞得更糟。这篇文章把我从报错原理到三种可用解决方式的完整排查过程整理出来,给正在被 AGL 卡住、尤其是在新版 macOS 上维护 Qt 5 老项目的朋友一份能直接抄的作业。
这篇文章适合谁看?两类人:第一类是 Qt 5.15 及更早版本项目在升级 macOS / Xcode 后链接失败的开发者;第二类是想在新系统上从源码编译 Qt 5.15,结果 configure 或 make 阶段绕不开 AGL 的人。如果你只是 Qt 6 项目突然报这个错,大概率是某个第三方库带了历史包袱,后面有一节专门讲这个场景。
1. 把报错拆开看:ld、framework、AGL 到底各是什么
1.1 链接器在找什么
macOS 上链接这一布由ld(实际是 clang 调用内部的 ld64)完成。它干两件事:一是把你目标文件里的未定义符号,从各种库和框架里解析出来;二是把依赖的 dylib/framework 记录进最终 Mach-O 文件的 load commands 里。
framework是 Apple 特有的一种目录打包形式,比如AGL.framework本质上就是一个特定目录结构:
AGL.framework/ AGL -> 实际动态库 Headers/ -> 头文件 Resources/ -> 资源链接器通过-framework AGL这个参数去找它。搜索路径主要包括:
- 系统 SDK 内的
/System/Library/Frameworks - 命令行里通过
-F显式指定的额外路径 - 环境变量
LD_FRAMEWORK_PATH指定的路径
报错framework 'AGL' not found的含义很直白:链接器走完上面所有搜索路径,都没找到AGL.framework。对老项目来说,通常不是路径配错,而是新系统 SDK 把这个 framework 整个删掉了。
1.2 AGL 是什么,为什么会牵连到 Qt
AGL 全称 Apple Graphics Library,是上世纪九十年代末到本世纪初苹果提供的 OpenGL 上下文管理 C 接口。那时候还没有NSOpenGLView,做 Mac 版 OpenGL 窗口基本都得和 AGL 打交道。后来 AppKit 提供了 NSOpenGLView,再后来又有 CGL,最后 Metal 彻底取代了 OpenGL,AGL 也就成了纯粹的“化石”。
时间线是这样的:
| 时间 | 事件 |
|---|---|
| 2000s 之前 | AGL 是 mac 上 OpenGL 上下文的标准入口 |
| 2001 年起 | NSOpenGLView 等接口逐渐替代 AGL |
| macOS 10.14 | Apple 宣布 OpenGL 全线废弃,AGL 同时标记 deprecate |
| 近几个大版本 | AGL.framework 逐步从 SDK 和系统中移除 |
| macOS 26 | 新版 SDK 里已经找不到 AGL.framework |
再说到 Qt 这边。Qt 4 时代,QGLWidget在 mac 上的实现直接调用 AGL,所以 Qt 4 项目链接-framework AGL是理所当然。进入 Qt 5 之后,Qt 内部其实早就切到了 NSOpenGL 路径,不会再真正调用 AGL 函数,但 qmake 的 mkspec 配置里把 AGL 的链接参数原封不动保留了下来。
具体位置在 Qt 5.15 源码的qtbase/mkspecs/common/mac.conf,里面有一行:
QMAKE_LIBS_OPENGL = -framework AGL -framework OpenGL只要你的.pro文件写了QT += opengl,qmake 就会把-framework AGL -framework OpenGL都塞进链接命令。AGL 那边链接器找不到,于是就是你现在看到的这行报错。这是 Qt 5 项目遇到这个问题最常见的原因。
1.3 先冷静判断:这个错影响多大
说实话,这类报错本质上是个“纸老虎”。Qt 5 在 mac 上早就不会实际调用 AGL API 了,链接 AGL 纯属历史惯性。它不像缺 OpenGL 那样会导致图形功能失效,大多数情况下只是链接器在较劲。搞清楚这一点,你就不会慌着去重装 Qt 或改 Xcode 版本——直接按下面几章的思路,十分钟内基本能解决。
2. 动手定位:AGL 的引用到底是从哪进来的
在动手改方案之前,先花几分钟定位源头。因为不同来源对应的解法完全不同,定位错了,后面所有操作都是在浪费时间。
2.1 看最终链接命令
qmake 项目最容易定位。用 verbose 模式重新跑一次编译,把最终的链接命令打出来:
make clean make V=1 2>&1 | grep -i agl | head -20V=1会让 make 打印完整命令行。如果输出里直接出现了-framework AGL,那基本可以断定是 qmake 配置带上来的。再看一眼完整链接命令,确认它是来自你.pro里的LIBS,还是来自 qmake 的 mkspec 默认值,后面处理方式会不一样。
2.2 检查 .pro 文件和 mkspec
直接在项目里搜一下:
grep -Rni "agl" --include="*.pro" --include="*.pri" .同时打开.pro看有没有这句:
QT += opengl有这一行,并且你项目里没有用到QGLWidget这类老接口,那基本就是元凶。因为QT += opengl会激活上面说的QMAKE_LIBS_OPENGL,把 AGL 带进来。
如果.pro里写的是:
LIBS += -framework AGL那更直接,就是你或者某些年头的模板自己写进去的死依赖。
2.3 检查第三方静态库和动态库
如果.pro里没有 AGL,QT += opengl也没有,那就要怀疑第三方库了。静态库用 nm 查未定义符号:
nm -u libOldGameEngine.a | grep -i agl动态库用 otool 看依赖:
otool -L libOldRender.dylib | grep -i agl还有一个很容易漏的地方:如果你用的是预编译的 Qt 5 动态库(比如官方装的 5.15.2),可以顺手看看 QtOpenGL 这个模块有没有链接 AGL:
otool -L $QTDIR/lib/QtOpenGL.framework/QtOpenGL | grep -i agl大部分官方预编译包在较新的 SDK 上构建时已经去掉了 AGL,但如果你的 Qt 是很老的构建版本,这也会是源头。注意:如果最终可执行文件链接成功后otool -L还能看到 AGL 依赖,那说明不光链接期有问题,运行时也可能出问题——这个放到第 4 节专门讲。
2.4 判断结果对照表
| 定位结果 | 代表原因 | 优先解法 |
|---|---|---|
链接命令里有-framework AGL,且.pro有QT += opengl | qmake 的 QMAKE_LIBS_OPENGL 带出来的 | 删掉QT += opengl,或覆盖变量 |
.pro里有LIBS += -framework AGL | 项目模板/历史代码显式引用 | 直接删掉这一行 |
静态库 nm 看到_aglXxx | 第三方静态库依赖 AGL | 链接垫片 framework 或升级库 |
| 动态库 otool 看到 AGL | 第三方 dylib 依赖 AGL | 替换库,或运行时重定向 |
| Qt 源码编译 configure/make 失败 | Qt 5 自带 mkspec 引用 | 改 mac.conf 配置 |
3. 对症下药:三套能直接落地的解决方案
3.1 方案一:从项目层面摘掉 AGL(最快)
如果你的项目只是普通 Qt Widgets/QML 应用,没有用到 2000 年那会儿的QGLWidget,那么QT += opengl完全是个不必要的依赖。现代 OpenGL 相关类,比如QOpenGLWindow、QOpenGLWidget,都归在gui模块里,不需要额外加 opengl 模块。
做法很简单。先在.pro里确认业务代码没有老接口:
grep -Rni "QGLWidget" .没用到就直接把这一行删掉:
# 删掉下面这行 QT += opengl如果某些代码非得用QOpenGLFunctions(它也属于 gui),你甚至不需要这个模块。删掉之后重新:
make clean qmake make链接命令里就不再有-framework AGL,问题消失。
如果你暂时不想动.pro,或者有些公共 pri 文件绕不开,可以直接在.pro末尾强制覆盖 qmake 的默认变量:
QMAKE_LIBS_OPENGL = -framework OpenGL这样等于告诉 qmake:别按 mkspec 默认值来,OpenGL 只需要链接 OpenGL.framework。这个写法比一行行LIBS -= -framework AGL更干净,因为它是从源头把值替换掉。
这个方案的优点是零侵入、不改环境、不引入任何运行时风险。缺点是只解决“依赖来自 Qt 自身配置”的情况,如果第三方库也引用了 AGL,光做这步不够。
3.2 方案二:能升级就升级到 Qt 6
如果你本来就在评估 Qt 版本升级,那这正好是个强力的推动理由。Qt 6 从一开始就彻底移除了 AGL 相关的链接配置,新的 mkspec 里QMAKE_LIBS_OPENGL不再包含 AGL,也不会出现这行报错。
不过要提醒一句:Qt 5 → Qt 6 不是简单换个 Qt 安装包就完事。主要差异包括:
- Qt 5 的
QGLWidget类在 Qt 6 中彻底移除,真正还在用的老代码需要迁移到QOpenGLWidget - OpenGL 相关类被整理进
QtOpenGL/QtOpenGLWidgets模块,.pro 写法要相应调整 - QML 模块结构也变了,比如
QtQuick.Controls拆成QtQuick.Controls+QtQuick.Controls.Basic等
如果你的项目已经有迁移计划,我建议直接借这次报错顺势升级。如果项目非常大、短期没法迁,那就用方案一或方案三把眼前这关过了。
这里还有个折中思路: Qt 5.15 是 Qt 5 商业/社区版的长期支持版本,如果你手头项目必须留在 Qt 5,也不想魔改系统,那就用下一个方案,给 AGL 做一个“影子”framework,让链接器有东西可找。
3.3 方案三:自制一个最小的 AGL 兼容 framework(垫片方案)
这是所有方案里“下限最低”但适用范围最广的一个:既然链接器只是在找AGL.framework,那我就自己做一个空的、只带符号的 framework 放在项目目录里,链接时用-F指过去。
为什么这可行?因为 Qt 5 项目里 AGL 的函数在运行时根本不会被调用,只是链接期需要解析一些历史符号。所以垫片里的函数全部可以做成空实现,返回一个“错误值”就够了。
先创建一个 C 源文件agl_stub.c:
#include <stddef.h> typedef void *AGLContext; typedef void *AGLPixelFormat; typedef int AGLBoolean; typedef int AGLenum; AGLPixelFormat aglChoosePixelFormat(const void *attrs, void *devices, int ndevs) { (void)attrs; (void)devices; (void)ndevs; return NULL; } AGLBoolean aglDestroyPixelFormat(AGLPixelFormat pf) { (void)pf; return 1; } AGLBoolean aglDescribePixelFormat(AGLPixelFormat pf, int attr, int *val) { (void)pf; (void)attr; if (val) *val = 0; return 1; } AGLContext aglCreateContext(AGLPixelFormat pf, AGLContext share) { (void)pf; (void)share; return NULL; } AGLBoolean aglDestroyContext(AGLContext ctx) { (void)ctx; return 1; } AGLBoolean aglCopyContext(AGLContext src, AGLContext dst, int mask) { (void)src; (void)dst; (void)mask; return 1; } AGLBoolean aglUpdateContext(AGLContext ctx) { (void)ctx; return 1; } AGLBoolean aglSetCurrentContext(AGLContext ctx) { (void)ctx; return 1; } AGLContext aglGetCurrentContext(void) { return NULL; } AGLBoolean aglSetDrawable(AGLContext ctx, void *draw) { (void)ctx; (void)draw; return 1; } AGLBoolean aglSetOffScreen(AGLContext ctx, int w, int h, int rowbytes, void *buf) { (void)ctx; (void)w; (void)h; (void)rowbytes; (void)buf; return 1; } AGLBoolean aglSetFullScreen(AGLContext ctx, int w, int h, int rate, int recovery) { (void)ctx; (void)w; (void)h; (void)rate; (void)recovery; return 1; } AGLBoolean aglSwapBuffers(AGLContext ctx) { (void)ctx; return 1; } AGLBoolean aglSetInteger(AGLContext ctx, int pname, const int *params) { (void)ctx; (void)pname; (void)params; return 1; } AGLBoolean aglGetInteger(AGLContext ctx, int pname, int *params) { (void)ctx; (void)pname; if (params) *params = 0; return 1; } AGLenum aglGetError(void) { return 0; } void aglResetError(void) { } AGLBoolean aglGetVersion(int *major, int *minor) { if (major) *major = 2; if (minor) *minor = 0; return 1; } AGLBoolean aglGetDrawable(AGLContext ctx, void **draw) { (void)ctx; if (draw) *draw = NULL; return 1; }这个列表覆盖了 AGL 最常用的一批入口。编译成动态库并打包成标准 framework:
mkdir -p /tmp/AGLCompat/AGL.framework/Versions/A/Headers clang -dynamiclib -fPIC \ -Wl,-install_name,@rpath/AGL.framework/Versions/A/AGL \ -o /tmp/AGLCompat/AGL.framework/Versions/A/AGL \ agl_stub.c ln -sfn Versions/A/AGL /tmp/AGLCompat/AGL.framework/AGL ln -sfn Versions/A/Headers /tmp/AGLCompat/AGL.framework/Headers然后在.pro里把搜索路径指过去:
# 如果项目和第三方库的 AGL 引用都已经去掉,根本不需要这两行 # 如果还有未解决的 AGL 符号,加这两行让链接器找到垫片 QMAKE_LFLAGS += -F /tmp/AGLCompat注意顺序问题:-F /tmp/AGLCompat必须出现在-framework AGL之前(qmake 里 QMAKE_LFLAGS 通常会放在前面,一般没问题)。如果链接器仍然报找不到,就显式写全:
LIBS += -F/tmp/AGLCompat -framework AGL重新 qmake、make。链接阶段如果又冒出新的 undefined symbol,比如_aglSomeFunc,就按同样的模式在agl_stub.c里补一个空实现,重编一遍垫片即可。我实测下来,绝大多数 Qt 5 项目用上面这一批符号就够了。
这个方案的注意事项有三条:
- 垫片只保证链接期和启动期不出错,不保证真正调用 AGL 的函数有正确行为。好在 Qt 5 界的老代码实际调用 AGL 的概率极低。
- 如果你最终要把程序发到别的机器,
@rpath的路径需要打包时一并布置,或者直接把 install_name 改成绝对路径(例如/tmp/AGLCompat/AGL.framework/Versions/A/AGL)只在本地验证用。 - 千万不要一时上头把
AGL.framework塞进/System/Library/Frameworks。SIP 会拦,而且系统升级又会把它抹掉,纯属给自己挖坑。
3.4 补充方案:从源码编译 Qt 5.15 时的处理
很多人在 macOS 26 上从源码编 Qt 5.15.2,为的就是给老项目保一套可用的 Qt 环境。这种情况下同样会遇到 AGL,而且报错出现得更早——可能在configure阶段,也可能在make阶段。
先确认源码里的相关配置:
grep -R "AGL" qtbase/mkspecs/common/mac.conf找到QMAKE_LIBS_OPENGL那一行,直接改成:
QMAKE_LIBS_OPENGL = -framework OpenGL也就是把-framework AGL从默认链接参数里剔除。之后再跑 configure 和 make。
如果 configure 的检测步骤仍然报 AGL,说明某个 configure 测试用的链接命令里带了 AGL,可以把测试环境变量指到垫片目录:
export LDFLAGS="-F /tmp/AGLCompat" ./configure -prefix /usr/local/qt-5.15.2 -opensource -confirm-license ...这里有个经验之谈:Qt 5.15 源码包在较新 SDK 上编译本身就有不少兼容性小坑,AGL 只是第一道坎。后面可能还有-Werror报错、deprecated API 告警被当成错误等。建议编译时加上-platform macx-clang QMAKE_CXXFLAGS+="-Wno-deprecated-declarations"这类参数,能少踩很多坑。
4. 常见问题与踩坑实录
4.1 链接成功了,但程序打开秒退,dyld 报 AGL 找不到
这种情况通常不是链接阶段报错,而是运行阶段的 load 命令问题。你或者第三方库在链接时把对 AGL.framework 的依赖写进了可执行文件的 load commands,系统启动程序时 dyld 去做依赖加载,发现 AGL 不存在,于是拒绝启动。
用 otool 先确认:
otool -L build/MyApp.app/Contents/MacOS/MyApp | grep -i agl如果能看到 AGL 依赖,最简单的处理是把这条依赖改指到垫片库:
APP=build/MyApp.app/Contents/MacOS/MyApp install_name_tool -change \ /System/Library/Frameworks/AGL.framework/Versions/A/AGL \ @rpath/AGL.framework/Versions/A/AGL \ "$APP" # Apple Silicon 下要重新签名,否则跑不起来 codesign --force -s - "$APP"改完之后再otool -L验证。对多数场景,这个操作能救急。但要注意:每次都手动改二进制不优雅,正确做法还是在链接阶段就用垫片重生出干净的 load commands。
4.2 我没写 QT += opengl,为什么还是报 AGL
八成是第三方库的锅。用前面第 2 节的nm -u和otool -L把范围缩小到具体哪些库带了_aglXxx符号或 AGL 依赖。解决方案并没有更高级的:能升级这个库就升级,不能升级就把垫片 framework 链接进去,或者用install_name_tool -change把库的 AGL 依赖重定向。
4.3 Debug 构建正常,Release 构建报错
Qt 的 Debug 和 Release 可能链接了不同的 Qt 库,也会因为宏定义差异走不同的代码分支。碰到这种一半好一半坏的情况,先不要怀疑玄学,把两个构建的最终链接命令分别拉出来对比:
make debug V=1 2>&1 | grep -i agl make release V=1 2>&1 | grep -i agl多数时候你会发现只是 Release 分支或 Release 用的静态库才带 AGL,定位到具体那条库,参照第 3.3 节处理。
4.4 Qt 6 项目也报 AGL
正常情况下 Qt 6 自己绝不会链接 AGL。如果你的 Qt 6 项目还报这个错,那几乎可以断定是某个陈年第三方库或者你自己某段代码里的LIBS带了-framework AGL。别慌,按第 2 节的排查顺序走一遍即可,处理方式与 Qt 5 相同。
4.5 用 Qt 5.15.2 官方安装包,但还是报 AGL
我遇到过一种情况:.pro里完全没写 opengl,第三方库也没有,但就是报 AGL。最后发现是某个公共.pri文件被历史提交加了一行LIBS += -framework AGL,当时在旧系统上能链过就一直留着。所以排查时别只盯着主.pro,把项目里所有.pri都搜一遍:
grep -Rni "framework AGL" --include="*.pri" --include="*.pro" .这个坑特别隐蔽,因为.pri文件通常藏在子目录里,IDE 里也不容易一眼看到。
5. 写在最后的几点体会
这个 AGL 问题我前前后后处理过三轮,最早的印象是 Qt 4 时代,那时候 AGL 是真被调用的,不能随便去掉。到了 Qt 5 时代,它已经只剩一个“符号名”的意义。所以处理它的核心原则就一句话:能删就删,删不干净就用垫片接住,千万不要去改系统。
我个人实际操作中最省事的路径,永远是把QT += opengl删掉,或者把QMAKE_LIBS_OPENGL覆盖成 OpenGL-only。这条路径既不碰第三方库,也不引入垫片,干净利落。只有遇到无法升级的静态库、且确认它确实需要 AGL 符号时,我才会把垫片方案拿出来。
还有一个后续预防建议:以后拿到老项目,第一时间跑一遍otool -L和grep -R "framework"看看有没有这类“僵尸依赖”。新版系统每年都在清理历史包袱,今天删 AGL,明天可能就是别的什么 framework。趁早把项目里的历史依赖清干净,比每次升级系统后临时救火要舒服得多。