JZX轻语:简

[Qt/PyQt] PyQt5全层次构建与调试

发表于2024年07月28日

#Qt #PyQt #构建 #调试

在调试PyQt程序中,对于一些更底层所诱发的问题,有时候PyCharm无法捕捉到此类问题,通常需要在C/C++层面的Qtsip进行调试。 而对于RiverBank的PyQt5,由于其并没有提供Debug版本的库,因此无法通过windbg等调试器对更底层的Qtsip进行调试。本文以此为出发点,以Qt 5.15.2 64位为例,介绍如何手动构建PyQt5的各个层次,以及如何在VSCode中使用WinDbg进行调试。

这是一个耗时巨大的过程…

环境

层次概述

PyQt5sip为基础封装QtC++库。总的来说,PyQt5的层次大概如下:

+------------------------------------------+
|            Python Interfaces             |
+-------------------^----------------------+
                    |
+-------------------^----------------------+
|               PyQt5(pyd)                 |  # Step 5
+-------------------^----------------------+
                    |
+-------------------^----------------------+
|                 sip                      |  # Step 4
+---------^-------------------------^------+
          |                         |
+---------^--------+                |
|         Qt       | # Step2, 3     | 
+------------------+                |
                                    | 
                          +---------^--------+
                          |      CPython     |  # Step 1
                          +------------------+

1. 安装带符号文件的Python

在官网上下载Python的安装包,并勾选Download debugging symbolsDownload debug binaries,如图1所示:

Python安装截图

图1:Python安装截图

为了能定位到具体的源码,可以在相同的下载页面下载Python的源码,如图2所示。将下载下来的源码压缩包解压到某个目录下,以便后续调试。

Python源码下载地址

图2:Python源码下载地址

当然,也可以选择自己构建Python,构建方法可参见官方教程,这里不再赘述。

2. 下载Qt

随后我们需要安装Qt,为方便构建,在Qt官网上下载无需编译的在线安装程序(Qt目前仅提供5.15.2的安装包,5.15.3及以上版本需要自行构建),安装时在组件上务必选择Qt的源码(Sources),如图3所示。

Qt安装截图

图3:Qt安装截图

此时不断下一步即可,安装完成后,在Qt安装目录下,可以找到Qt的源码(<Qt安装路径>\5.15.2\Src),以及Qt的构建工具qmake(<Qt安装路径>\5.15.2\msvc_2019_64\bin)。

3. 绑定所下载Qt的dll至PyQt5

3.1 首先下载PyQt 5.15.2的wheel包(注意是64位,一般命名为PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl),并解压至某个目录(为便于后续描述,这里假设为A)。

3.2 进去上述目录A,输入命令python -m venv buildenv构建虚拟环境,并激活虚拟环境.\buildenv\Scripts\activate

3.3 使用pip install PyQt-builder安装PyQt-builder

3.4 由于pyqt-bundleQt 5.15.2的支持有点问题,需要修改buildvenv\Lib\site-packages\pyqtbuild\bundle中的abstract_package.py文件,按图4修改get_target_qt_dir方法:

修改abstract_package.py文件

图4:修改abstract_package.py文件

3.5 通过以下命令绑定QtdllPyQt5

pyqt-bundle --verbose --qt-dir <你的Qt安装路径>\5.15.2\msvc2019_64 .\PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl

3.6 同理,下载PyQtWebEngine 5.15.2的wheel包,并解压至相同目录A。并通过以下命令绑定:

pyqt-bundle --verbose --qt-dir <你的Qt安装路径>\5.15.2\msvc2019_64 .\PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl

3.7 安装上述绑定后的wheel包,需要注意的是,由于我的Python版本是3.11,直接安装会报错(如图5),因为PyQt的版本5.15.2有点旧了,元信息上没有加上后续的py版本,需要在两个whl包名后面加上cp310cp311就好啦:

pip安装报错截图

图5:pip安装报错截图

# 重命名whl包,加上cp310,cp311
mv PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310.cp311-none-win_amd64.whl
mv PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310.cp311-none-win_amd64.whl
# pip安装
pip install .\PyQt5-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl
pip install .\PyQtWebEngine-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl

4. 构建sip

官方的PyQt5-sip并不提供pdb符号文件,如果需要通过Attach调试C++层级的源码时,仅用官方的sip二进制文件是无法调试里面的内容的。

首先下载官方的源码分发包(Source Distribution,并解压到本地的一个目录上,用编辑器打开setup.py,新增以下代码:

# 下面两行是新增的部分
ext_compile_args = []
ext_link_args = []

if sys.platform == 'win32':
    module_src.append('bool.cpp')
    # 以下两行是新增的部分
    ext_compile_args.append('/Z7')
    ext_link_args.append('/DEBUG')

# 以下一行是修改的部分
module = Extension('PyQt5.sip', module_src, extra_link_args=ext_link_args, extra_compile_args=ext_compile_args)

PyQt5-sip 12.13.0为例,最终代码文件为:

# ... 省略官方注释

import glob
import sys

from setuptools import Extension, setup

# Build the extension module.
module_src = sorted(glob.glob('*.c'))

ext_compile_args = []
ext_link_args = []

if sys.platform == 'win32':
    module_src.append('bool.cpp')
    ext_compile_args.append('/Z7')
    ext_link_args.append('/DEBUG')

module = Extension('PyQt5.sip', module_src, extra_link_args=ext_link_args, extra_compile_args=ext_compile_args)

# Do the setup.
setup(
        name='PyQt5_sip',
        version='12.13.0',
        license='SIP',
        python_requires='>=3.7',
        ext_modules=[module]
     )

打开终端(命令提示符 or Powershell),构建sip

python setup.py build

在目录的build\lib.xxxxx\PyQt5上,找到pyd文件和pdb文件,将其替换到Python安装目录的Lib\site-packages\PyQt5上(原来的sippyd文件可按需备份一下)。

5. 构建PyQt5

至此,我们已经可以调试QtC++层,CPython层,以及sip层,但我们还是会发现有些地方是没办法找到符号文件的(如图6),这是因为PyQt5pyd文件并没有符号文件

Qt相关的pyd没有符号文件

图6:Qt相关的pyd没有符号文件

其实,第3步只是将我们的Qtdll绑定到PyQt5pyd文件上,并没有真正构建PyQt5pyd文件,这些pyd文件是RiverBank构建的。如果我们想看看sip内部对Qt具体做了什么封装,还需要手动构建PyQt5pyd文件,并输出调试符号。

首先去PyPI下载PyQt5 5.15.2的源码分发包(注意是Source Distribution),然后解压到本地某个目录上(同样,为了方便描述,下文将该目录描述为B),用编辑器打开project.py,找到方法PyQt.update,在该方法的末尾,新增三行以使得构建时生成符号文件

# ...其他内容

class PyQt(PyQtProject):
    # ...其他内容
    def update(self, tool):
        # ...其他内容
        # !!! 在build方法的末尾新增以下三行 !!!
        for binding in self.bindings.values():
            binding.extra_compile_args.extend(['/Z7'])
            binding.extra_link_args.extend(['/DEBUG'])

在目录B上,打开Developer PowerShell for VS 2022,构建PyQt5:

sip-build --verbose --tracing --disable QtNfc

在这里,我们使用--verbose以输出更多信息,--tracing以在运行程序时可输出更多的调试信息,--disable QtNfc以禁用QtNfc模块,因为QtNfc模块会存在导致构建失败的问题

我们之所以不用--debug选项,而选择直接编辑project.py文件,是因为--debug选项需要Pythondebug版本(如下图所示),这样会导致一连串的后续问题:python_d.exe的运行需要一系列的debug版本的dll,而这些dll往往都找不到。相关报错如图7所示。

sip-build的--debug选项需要Python的debug版本

图7:sip-build--debug选项需要Pythondebug版本

注:如果出现图8所示的解码错误,这往往是我们的代码页并非utf-8导致的。 构建PyQt5时的解码错误

图8:构建PyQt5时的解码错误

我目前的做法是直接修改sip-build的源码…根据输出找到报错的地方,使用编辑器打开,找到方法Project.read_command_pipe,在with subprocess.Popen语句内,修改如图9: 修复编码错误

图9:修复编码错误

在这里,如果使用utf-8无法解码子进程输出的字节串,就再尝试使用gb2312解码一遍(如果还是不行就使用替换字符),具体用哪个编码取决于自己系统的配置。

构建完成后,在build目录内每个模块都会生成对应pydpdb文件,将这些文件直接复制到PythonLib\site-packages\PyQt5目录下,重启Python即可。

6. 在VSCode中调试

此时大功告成,我们可以愉快地调试任一层次的源码了!

以VSCode为例,我们将上述各个层次的源码目录统统放在一个workspace上,新增Launch配置,编写如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(Windows) Attach",
            "type": "cppvsdbg",
            "request": "attach",
            "processId": "${command:pickProcess}",
            // 下面这行是新增的部分, 用于指定pdb符号文件的路径, 注意修改为自己的路径
            "symbolSearchPath": "C:\\Qt\\5.15.2\\msvc2019_64\\bin;E:\\PyQt5_sip-12.13.0\\build\\lib.win-amd64-cpython-311\\PyQt5",
            "sourceFileMap": {
                // Qt源码路径映射, 因为符号文件是下载下来的, 并非自己编译的, 所以需要构建映射
                "c:\\Users\\qt\\work\\qt\\": "C:\\Qt\\5.15.2\\Src",
                // Python源码路径映射, 理由同上
                "D:\\a\\1\\s\\python\\": "C:\\Users\\Jeza\\Downloads\\Python-3.11.7\\Python",
		"D:\\a\\1\\s\\Objects\\": "C:\\Users\\Jeza\\Downloads\\Python-3.11.7\\Objects",
		"D:\\a\\1\\s\\Include\\": "C:\\Users\\Jeza\\Downloads\\Python-3.11.7\\Include",
            }
        }
    ]
}

此时再Attach到一个Python进程上,就可以愉快地调试了!如图10所示:

最终效果

图10:最终效果

上一篇

RustDesk自建服务器札记[简易版]

下一篇

ST表的Python实现模板(支持泛型类型检查)