Mac OS部署QT QML应用之踩坑篇

说到大名鼎鼎的QT,想必熟悉C/C++的各位一定不会陌生。虽说QT号称跨平台,但是只有真正开发过跨平台软件的人才知道这这句话的水有多深。本文特此记录一次让人抓狂的找bug篇。

好不容易将软件的功能写好并且测试通过,兴致勃勃的准备部署到各个平台。结果发现程序在开发环境能运行,但打包后却直接闪退。以下是寻找问题的过程:

动态连接库问题

一开始以为是动态库有问题,查找一圈后发现并不是这个原因。
otool -L app app: @rpath/QtQuick.framework/Versions/A/QtQuick (compatibility version 6.0.0, current version 6.5.3) @rpath/QtSql.framework/Versions/A/QtSql (compatibility version 6.0.0, current version 6.5.3)

缺少平台插件

QML应用依赖libqcocoa.dylib平台插件,具体位置在
app/Contents/PlugIns/platforms/libqcocoa.dylib
我的软件里已经有这个插件,所以说不是这个问题

macdeployqt版本问题

最开始还真中招了,装了多个版本QT(5和6)的系统不知道为什么,默认用的是brew安装的QT(我这里的QT5)的macdeployqt。 奇怪的是macdeployqt居然没报错。不过幸好检查了一下版本,不然之后又要云里雾里,不知所云了。

lldb和console日志迷思

使用lldb app/Contents/MacOS/app直接打开Bundle里的可执行文件,发现app可以完美运行,没有任何问题。但是如果直接双击.app打开之后直接就被系统给毙了,百思不得其解。

关键:通过console.app查看软件崩溃日志,找到错误信息:[0x140807830] Channel could not return listener port. libxpc.dylib 但是没找到有用信息。

QML 模块未找到

根据以下报错信息,以为是缺失app/Contents/PlugIns/qml目录(后来事实证明是GPT在框我)

qrc:/main.qml:2:1: module “QtQuick.Controls” is not installed
qrc:/main.qml:3:1: module “QtQuick.Controls.Material” is not installed
qrc:/main.qml: module “QtQml.WorkerScript” is not installed

尝试手动复制QT安装目录qml文件下的所有文件到应用程序的app/Contents/PlugIns/qml目录,但是还是不行,推测还是因为库的动态连接问题。

到这一步一直以为是qml文件路径和qml动态库的问题,以为是部署程序找不到qml文件和动态库,遂尝试以下办法:

  • 加上qmldir参数指定qml文件目录 👉 闪退
  • 手动复制所有动态库(dylib)和依赖(qml,framworks)文件 👉 闪退
  • 使用官方文档介绍的install_name_tool重新链接动态库到软件本身目录,otool输出可知链接成功而且framworks也齐全(原因未知),但是依旧闪退。

otool -l app/Contents/MacOS/app | grep -A2 LC_RPATH cmd LC_RPATH cmdsize 48 path @executable_path/…/Frameworks (offset 12)

QtQuick.framework
QtQml.framework
QtQuickControls2.framework
QtQuickTemplates2.framework
QtQmlWorkerScript.framework

  • 项目使用qrc管理qml文件 ,遂怀疑是QML 被 CMake embed 到 qrc,导致没有找到qml文件然后整个项目换用qt_add_qml_module。话费大量时间修改项目结构适配qt_add_qml_module之后,发现结果还是不行,应用依旧打不开。不过幸好考虑到有可能不行而提前备份项目,不然再改回去又是一个折磨。

问题进展到这一步,被逼的没办法,实在找不到原因,已经开始考虑使用非Framework 版本的qt了,也就是静态编译版本。但是考虑到做这件事的精力和时间成本,不到万不得已不考虑。

最后孤注一掷,觉得还是要看系统日志,遂使用log show --predicate ‘process == “app”’ --last 1m查看软件崩溃日志,再次找到Channel could not return listener port. libxpc.dylib,遂觉得可以以 libxpc.dylib为突破口查找原因。

最后经过又好一番查找 终于找到罪魁祸首 !!

那就是坑爹苹果的坑爹机制:Finder 双击沙箱和 codesign 约束。导致必须给应用内的framwork也必须签名,否则直接在打开的瞬间毙掉软件。

以下是来自网络的关于这两个机制的详细介绍:

从 macOS 10.15(Catalina)开始,系统对应用程序引入了 Hardened RuntimeGatekeeper 保护: 未经签名或签名不完整的二进制文件无法被 Finder 直接打开。

如果一个 app 内含有 嵌入式 Framework 或插件,macOS 要求这些 Framework、动态库也必须 签名。未签名或签名不完整的 Framework 会触发 libxpc / listener port 错误或直接闪退。Qt 6 默认 macOS 构建是 framework 形式:这些 framework 内部也包含动态库和插件。

Finder 运行时会验证 主程序签名 + 内部 framework + 插件签名。如果任何一部分未签名或签名不匹配,应用会被 macOS 阻止启动。

所以,知道了原因,解决问题就变的简单了,直接执行以下命令:

解决方案
codesign --force --deep --sign - app
  • --force
    覆盖原有签名,如果你之前签过或部分签名不完整,会重新签。

  • --deep
    递归签名,会签名 app 内所有 Framework、插件、动态库。这个参数对 Qt 应用很重要,因为 Qt 的 framework 和插件里有很多 dylib,若未签名会导致启动直接闪退。

  • --sign -
    使用 临时自签名(ad-hoc signing),不需要 Apple Developer 证书。

终于,在签名之后,软件不再闪退并且运行正常。

实在没想到是签名导致的问题。我一直以为QT的macddeploy自动打包工具是包括签名的,官方文档对这件事也是只字未提。

事情的最后,只想轻轻的,微笑着对QT和苹果说一声:我丢雷楼某啊(手动张学友
来自一个被折磨几个小时的苦难人的温柔的问候。

✍️ 作者:𝓜.𝓦𝓱𝓲𝓽𝓮

📄 共享协议: CC 4.0协议

🔗 原文链接: https://www.alloworld.me/archives/qt-qml-deploy-issue

评论