GammaRay在mac上的编译及分发

最近在网上瞎逛时,发现了一个调试Qt应用程序的好东西:GammaRay。
按照官方介绍:
GammaRay is a software introspection tool for Qt applications developed by KDAB. Leveraging the QObject introspection mechanism it allows you to observe and manipulate your application at runtime. This works both locally on your workstation and remotely on an embedded target.
使用GammaRay可以在运行时直接查看Qt应用内的对象树,修改对象属性,查看信号槽连接和发射情况,查看界面布局等等。
对于我来说,GammaRay最大的作用有两个:

  • 简单直观的查看信号和事件的触发时机
  • 运行时动态设置对象属性和界面样式

对于团队新人来说,还可以借助这个工具,快速了解应用的架构以及界面布局,加快入门速度。

这么好的工具,当然要在团队内推广开来。但目前GammaRay并不直接提供编译好的二进制文件,只开源了源代码。
让每个人自己下载源码编译一次,显然是个很蠢的做法。于是大家达成共识,编译一份团队特供版的GammaRay。而我就来负责mac版本的编译。

编译

GammaRay的源码可以从 https://github.com/KDAB/GammaRay.git 上获取。稳定起见,选择最近release版本(v2.11.3)对应的tag。
GammaRay本身使用CMake进行工程管理,在*nix系统上编译很方便。按照INSTALL.md中的步骤,添加Qt库所在路径,执行cmake && cmake install即可完成编译。
此处需要注意:

  1. 建议直接使用编译应用程序的Qt库编译GammaRay
    工程会编译GammaRay本体和Probe。编译Probe需要使用和被调试的Qt应用程序一致的Qt动态库,GammaRay加载Probe之后才能正常工作。
    编译参数中可以看到,GammaRay支持单Launcher加载多Probe的方式。但没有具体的文档说明。在我们的项目中,目前统一使用一个Qt库,没有类似的需求。这里就没有深入研究。

  2. 使用cmake install生成的二进制产物
    mac上存在rpath的概念。mac上的动态库、可执行文件本身也会记录要其依赖的其他动态库的路径。
    默认情况下,cmake build的产物,记录了依赖动态库在本地的绝对路径。cmake install时,则会更新这些内容,变为包含rpath的相对路径。
    如果直接使用cmake build的产物,最终生成的可执行程序,在分发给其他电脑之后,会因为依赖的动态库无法加载,导致程序无法运行。

分发

执行完cmake install之后,默认会在/Applications/下生成一个名为GammaRay.app的应用程序。但直接运行时大概率会无法启动,或者无法正常工作。因为此时GammaRay.app内部没有包含依赖的Qt库,启动时会尝试加载系统目录下的Qt库。
Qt官方提供了macdelpoyqt来解决这个问题,将应用依赖的Qt库复制到app内,并会修改相关动态库的rpath等。但如果只是执行

1
${QTDIR}/bin/macdelpoyqt /Applications/GammaRay.app

GammaRay.app可以正常启动,依然无法正常工作。为什么呢?

一顿尝试之后发现,macdelpoyqt只能检查应用程序的主程序直接或者间接显式依赖的Qt动态库。
举个例子,主程序显式依赖了QtWidget和三方动态库X,三方动态库X显式依赖了QtOpenglWidget,运行时会动态加载Qt3DRender。
在这种场景下,macdelpoyqt只能分析出应用程序需要QtWidget和QtOpenglWidget,以及它俩依赖的其他Qt组件,并不能分析出对Qt3DRender的依赖。运行时,就会因为缺少Qt3DRender导致的错误。
回到GammaRay这边,install完成之后,Probe会作为插件被复制到GammaRay.app/Contents/Plugins/gammaray目录下,在GammaRay运行时动态加载。macdelpoyqt只会分析GammaRay.app/Contents/MacOS下几个可执行文件的依赖,丢掉了对Probe的依赖分析。

针对这个问题,可以利用macdelpoyqt的-executable选项,将需要分析依赖的动态库文件也一并传入。
因为Probe下的动态库个数比较多,手写容易出错,我就写了个python脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from os import walk
from os.path import join
from subprocess import check_call

APP_PATH = '/Applications/GammaRay.app' # GammaRay.app路径
QT_DIR = '/Users/yoooooo/Qt/5.9.9/clang_64' # Qt库路径

executable_file_list = []
for parent, _, files in walk(join(APP_PATH, 'Contents/MacOS')):
for file in files:
executable_file_list.append(join(parent, file))

for parent, _, files in walk(join(APP_PATH, 'Contents/PlugIns/gammaray')):
for file in files:
if file.endswith('.so') or file.endswith('.dylib'):
executable_file_list.append(join(parent, file))

check_call([
join(QT_DIR, 'bin/macdeployqt'),
APP_PATH,
'-verbose=3',
'-dmg', # 生成dmg文件
*['-executable=' + executable_file for executable_file in executable_file_list]
],
cwd='/Applications', # 最终生成的dmg文件所在目录
)

其他

可以看到脚本中调用macdeployqt时,除了增加-executable选项之外,还增加了-dmg选项,最后直接生成了dmg文件。
期间因为偷懒,直接生成zip包进行分发。结果因为app中存在软链接文件,而标准格式的zip是不支持软链接的。导致解压zip得到的app无法正常工作,排查了很久才意识到,踩中这个老坑了。

此外,如果编译GammaRay的macOS版本比较新时,建议在GammaRay根目录下的CMakeList.txt中增加

1
2
# 此处设置最低支持的macOS版本为10.13
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X deployment version")

确保编译出来的app可以在低版本macOS上运行。