dupeGuru 构建辅助模块 hscommon.build 全解析:文件操作、打包与多平台分发的核心工具链
dupeGuru 构建辅助模块 hscommon.build 全解析:文件操作、打包与多平台分发的核心工具链
dupeGuru(一个开源重复文件查找工具)的构建体系由一层位于 hscommon/ 下的辅助模块支撑,其中 hscommon/build.py 是整个构建与打包流程的“公共工具箱”。本篇技术指南以该模块为绝对主体,逐函数讲解其职责、实现细节与实际调用链,并结合仓库根目录的 build.py、package.py 与 Makefile 说明它如何在日常构建、Debian/Arch/macOS/Windows 打包中发挥作用。读完本文,你将掌握这套构建辅助 API 的设计思路,并能独立梳理 dupeGuru 从源码编译到出包分发的完整流水线。
背景说明:开发文档 build.rst 通过 Sphinx 的
automodule指令自动生成该模块的 API 文档,其正文内容即hscommon.build的全部公开成员。本文在此基础上深入源码,将每个函数与真实调用场景一一对应,作为对该文档的完整展开。
1. 模块定位:HS 应用构建流程的“函数集合”
hscommon/build.py 的模块 docstring 明确写道:"This module is a collection of function to help in HS apps build process."(该模块是用于辅助 HS 应用构建流程的函数集合)。它不实现任何具体的构建工具(如 setuptools、PyInstaller、NSIS),而是提供构建脚本中反复出现的通用操作:
- 执行 shell 命令并打印;
- 移动/复制文件或整个目录(支持通配符批量操作);
- 模板文本替换(基于
{变量}占位符); - 读取包版本号、修改
PYTHONPATH; - 按规则拷贝 Python 包(剔除测试、文档等冗余数据);
- 读取并渲染变更日志(为 Debian changelog 服务);
- macOS DMG 构建与签名辅助、打包命令行参数解析;
- Qt 资源文件(pyrcc5 输出)的编码修复。
从调用关系看,它被四个入口使用,形成清晰的分工:
| 调用方 | 使用的函数 | 用途 |
|---|---|---|
| build.py(仓库根目录) | add_to_pythonpath、print_and_do、fix_qt_resource_file |
日常编译、本地化、帮助文档、Qt 资源生成 |
| package.py | copy_packages、build_debian_changelog、get_module_version、filereplace、copy、copy_all、setup_package_argparser、print_and_do |
生成 Debian/Arch 源码包、macOS/Windows 安装包 |
| hscommon/sphinxgen.py | read_changelog_file、filereplace |
生成多语言帮助文档 |
| pkg/debian/build_pe_modules.py | move_all |
Debian 源码包中移动编译产物 .so |
2. 命令执行与文件操作:构建脚本的基本原语
2.1 打印并执行 shell 命令:print_and_do
def print_and_do(cmd: str) -> int:
"""Prints ``cmd`` and executes it in the shell."""
print(cmd)
p = Popen(cmd, shell=True)
return p.wait()
函数签名与行为见 hscommon/build.py:先打印命令本身,再通过 subprocess.Popen(..., shell=True) 执行,返回进程退出码。构建脚本用它在 build_normal() 中调用 pyrcc5 把 qt/dg.qrc 编译为 qt/dg_rc.py(见 build.py),也用于打包阶段执行 git archive、xz、hdiutil create 等外部工具。因为返回值是退出码,调用方可以据此判断成败——例如 macOS 的 codesign 签名失败时(返回非 0)会直接中止打包(见 hscommon/build.py)。
2.2 带保护的移动与复制:move / copy
def _perform(src, dst, action, actionname) -> None:
if not op.lexists(src):
print("Copying %s failed: it doesn't exist." % src)
return
if op.lexists(dst):
if op.isdir(dst):
shutil.rmtree(dst)
else:
os.remove(dst)
print("{} {} --> {}".format(actionname, src, dst))
action(src, dst)
底层辅助 _perform(见 hscommon/build.py)做了三件事:源路径不存在时打印错误并安全返回;目标已存在时先递归删除目录或删除文件;最后打印 Moving src --> dst 之类的日志再执行动作。这保证重复构建不会因目标残留而失败。
move(src, dst):基于os.rename;copy(src, dst):基于copy_file_or_folder(见 hscommon/build.py)——目标是目录则shutil.copytree(..., symlinks=True)(保留符号链接),是文件则shutil.copy。
copy 在 package.py 中被大量使用,例如把 qt/pe/modules/block.c 复制为 Debian 包里的 block_qt.c,把 pkg/debian 下的 control、rules、compat 等文件复制进打包目录。
2.3 通配符批量操作:move_all / copy_all
def _perform_on_all(pattern, dst, action) -> None:
# pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst,
# no folder structure from src is kept.
filenames = glob.glob(pattern)
for fn in filenames:
destpath = op.join(dst, op.basename(fn))
action(fn, destpath)
见 hscommon/build.py:move_all(pattern, dst) 与 copy_all(pattern, dst) 接受 glob 模式(如 "folder/foo*"),把匹配到的所有文件平铺移动到目标目录(不保留源目录层级,文件名取 basename)。典型场景是 Debian 源码包构建:copy_all(op.join("core", "pe", "modules", "*.*"), op.join(destpath, "modules")) 把所有 C 模块源文件复制进 modules/(package.py);而 pkg/debian/build_pe_modules.py 在编译完成后用 move_all("_block_qt*", ...)、move_all("_cache*", ...)、move_all("_block*", ...) 把生成的扩展模块分发回 src/qt/pe 与 src/core/pe 对应目录。
2.4 模板占位符替换:filereplace
def filereplace(filename, outfilename=None, **kwargs) -> None:
"""Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`."""
if outfilename is None:
outfilename = filename
fp = open(filename, encoding="utf-8")
contents = fp.read()
fp.close()
# We can't use str.format() because in some files, there might be {} characters that mess with it.
for key, item in kwargs.items():
contents = contents.replace(f"{{{key}}}", item)
fp = open(outfilename, "wt", encoding="utf-8")
fp.write(contents)
fp.close()
见 hscommon/build.py。它读取文件,把所有 {key} 占位符替换为 kwargs 中对应值后写出。注释解释了为何不用 str.format():部分文件本身含有 {} 字符会破坏格式化。该函数承担了“配置文件模板化”的职责,例如:
- package.py 用
debopts(来自pkg/debian/dupeguru.json)填充control、Makefile、dupeguru.desktop模板,生成 Debian 包所需的最终文件; - pkg/arch/dupeguru.desktop 同理在 Arch 打包时被填充;
- hscommon/sphinxgen.py 用它把
help/conf.tmpl中的{language}替换成目标语言后生成 Sphinx 配置文件。
pkg/debian/dupeguru.json 与 pkg/arch/dupeguru.json 就是这些模板的变量来源,字段(如 pkgname、版本占位符)直接决定了最终 .desktop 文件与 control 文件的内容。
3. 包管理与运行环境:构建脚本的“胶水”
3.1 读取包版本:get_module_version
def get_module_version(modulename: str) -> str:
mod = importlib.import_module(modulename)
return mod.__version__
见 hscommon/build.py:动态导入指定模块并返回其 __version__。打包脚本用它在运行时读取 core/init.py 中的版本号,进而决定 Debian 包版本 app_version~distribution(package.py)、Windows 版本信息文件 win_version_info.txt 和源码包名 dupeguru-src-<version>.tar(package.py)。
3.2 修改运行环境:add_to_pythonpath
def add_to_pythonpath(path) -> None:
"""Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``."""
abspath = op.abspath(path)
pythonpath = os.environ.get("PYTHONPATH", "")
pathsep = ";" if ISWINDOWS else ":"
pythonpath = pathsep.join([abspath, pythonpath]) if pythonpath else abspath
os.environ["PYTHONPATH"] = pythonpath
sys.path.insert(1, abspath)
见 hscommon/build.py。它同时把路径写入环境变量 PYTHONPATH 和进程内 sys.path,并用 hscommon.plat.ISWINDOWS 决定分隔符(Windows 用 ;,其余用 :)。build.py 在 build_normal() 开头调用 add_to_pythonpath("."),确保后续导入 hscommon、core、qt 等本地包不受解释器搜索路径影响。
3.3 纯净拷贝 Python 包:copy_packages
def copy_packages(packages_names, dest, create_links=False, extra_ignores=None) -> None:
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
Copy will happen without tests, testdata, mercurial data or C extension module source.
"""
if ISWINDOWS:
create_links = False
if not extra_ignores:
extra_ignores = []
ignore = shutil.ignore_patterns(".hg*", "tests", "testdata", "modules", "docs", "locale", *extra_ignores)
...
见 hscommon/build.py。这是打包阶段的核心函数,注释解释了动机:"This is a method to hack around those freakingly tricky data inclusion/exclusion rules in setuptools. We copy the packages without data in a build folder and then build the plugin from there."——setuptools 的数据包含/排除规则“相当诡异”,所以先把包不带数据地复制到构建目录,再从这里构建插件,是唯一可靠的方式。
关键行为:
- 默认忽略
.hg*(Mercurial 数据)、tests、testdata、modules(C 扩展源码)、docs、locale; extra_ignores可追加规则:Debian 源码包传<a href="https://link.gitcode.com/i/86d9fcbd686ca0f1d6adbccc21eb466b" target="_blank">"*.so"]以剔除编译产物(因为.so会在构建机上重新编译),Arch 二进制包则保留它们([package.py);create_links=True时用os.symlink建立符号链接(Windows 上强制关闭),便于快速迭代;- 包名既可以是磁盘路径,也可以是可导入的模块名(通过
__import__定位__file__)。
package.py 在 copy_files_to_package() 中调用它打包 hscommon、core、qt、send2trash 等包,随后把 build/help 与 build/locale 一并拷入并 compileall.compile_dir(destpath) 预编译字节码。
4. 打包参数与平台产物生成
4.1 统一打包命令行参数:setup_package_argparser
def setup_package_argparser(parser: ArgumentParser):
parser.add_argument("--sign", dest="sign_identity",
help="Sign app under specified identity before packaging (OS X only)")
parser.add_argument("--nosign", action="store_true", dest="nosign",
help="Don't sign the packaged app (OS X only)")
parser.add_argument("--src-pkg", action="store_true", dest="src_pkg",
help="Build a tar.gz of the current source.")
parser.add_argument("--arch-pkg", action="store_true", dest="arch_pkg",
help="Force Arch Linux packaging type, regardless of distro name.")
见 hscommon/build.py。它向 ArgumentParser 注册四个打包选项,package.py 直接复用:
--sign <identity>:macOS 下以指定开发者身份签名后再打包;--nosign:macOS 下跳过签名;--src-pkg:只构建当前源码的 tar 包;--arch-pkg:强制按 Arch Linux 方式打包(无视发行版检测结果)。
主流程在 package.py 中按平台分发:win32 → Windows 打包;darwin → macOS 打包;否则用 distro.id() 识别发行版(或 --arch-pkg 强制走 Arch 分支)。
4.2 macOS:签名与 DMG 构建
package_cocoa_app_in_dmg(app_path, destfolder, args)(见 hscommon/build.py):先把签名推迟到打包阶段(因为运行 app 会修改内容、破坏签名),按Developer ID Application: <identity>执行codesign --force --deep --sign,失败则打印 ERROR 并中止;若既未给--sign也未给--nosign,同样报错要求二选一。build_dmg(app_path, destfolder)(见 hscommon/build.py):从 app 的Contents/Info.plist读取CFBundleName与CFBundleVersion,在临时目录中创建卷目录、cp -R拷贝 app、建立/Applications符号链接,最终用hdiutil create -format UDBZ -nocrossdev -srcdir生成名为{name}_osx_{version}.dmg的安装镜像。注释特别说明UDBZ(bzip 压缩)替代了旧的UDZO(zip 压缩),因为后者压缩率低得多。
实际打包时 package.py 用 PyInstaller 生成 dupeguru.app(带 --osx-bundle-identifier=com.hardcoded-software.dupeguru,捆绑 locale 与 help),再由上述函数完成签名与 DMG 封装。
4.3 Windows:版本信息与 NSIS 安装器
Windows 分支(package.py)从 core 版本号解析出 MAJOR.MINOR.PATCH 与架构位数(x64/x86),用 str.format 填充 win_version_info.temp 生成 win_version_info.txt,交给 PyInstaller(--windowed、图标 images/dgse_logo.ico、--add-data 捆绑 build/locale 与 build/help),最后调用 NSIS 的 makensis.exe 执行 setup.nsi 生成安装程序。注意该分支依赖系统装有 Windows SDK 的 UCRT DLL 目录(--paths 参数)。
5. 变更日志解析与 Debian changelog 生成
5.1 读取 YAML 风格 changelog:read_changelog_file
re_changelog_header = re.compile(r"=== ([\d.b]*) \(([\d\-]*)\)")
def read_changelog_file(filename) -> List[Dict[str, Any]]:
...
for version, date_str, description in iter_by_three(iter(splitted)):
date = datetime.strptime(date_str, "%Y-%m-%d").date()
d = {"date": date, "date_str": date_str, "version": version, "description": description.strip()}
result.append(d)
return result
见 hscommon/build.py。仓库的 help/changelog 采用如下格式:
=== 4.3.1 (2022-07-08)
* Fix issue where cache db exceptions could prevent files being hashed (#1015)
* Add extra guard for non-zero length files without digests to prevent false duplicates
* Update Italian translations
正则 === ([\d.b]*) \(([\d\-]*)\) 每三行切出一个条目(版本、日期、描述),返回结构化的 dict 列表。该函数同时服务于 hscommon/sphinxgen.py(渲染帮助文档中的 changelog 页面)与 Debian 打包。
5.2 渲染 Debian changelog:build_debian_changelog
ENTRY_MODEL = (
"{pkg} ({version}) {distribution}; urgency=low\n\n{changes}\n "
"-- Virgil Dupras <hsoft@hardcoded.net> {date}\n\n"
)
CHANGE_MODEL = " * {description}\n"
见 hscommon/build.py。函数把 read_changelog_file 的结果渲染成 Debian 标准 changelog:
- 描述文本按
*分隔为条目列表(desc2list),每个条目套CHANGE_MODEL; - 日期格式化为
%a, %d %b %Y 00:00:00 +0000; - 可选参数:
from_version只保留从某版本往后的日志;fix_version把最新一条的版本改写为指定值(用于“打包出错急需修复版本号”的场景); distribution默认"precise",写入每条目的 urgency 行。
package.py 在 Debian 打包时调用它:from_version="2.9.2" 截取 2.9.2 之后的变更,distribution 取自打包目标(当前脚本为 "unstable",见 package.py),生成的 changelog 写入 debian/changelog,最终由 dpkg-buildpackage -F -us -uc 产出 .deb(package.py)。
6. Qt 资源文件编码修复:fix_qt_resource_file
def fix_qt_resource_file(path) -> None:
# pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date
# containing accented characters. If it does, the encoding is wrong and it prevents the file
# from being correctly frozen by cx_freeze. To work around that, we open the file, strip all
# comments, and save.
with open(path, "rb") as fp:
contents = fp.read()
lines = contents.split(b"\n")
lines = [line for line in lines if not line.startswith(b"#")]
with open(path, "wb") as fp:
fp.write(b"\n".join(lines))
见 hscommon/build.py。针对 Windows 上非英文 locale 下 pyrcc5 生成的源文件头部日期注释可能含重音字符、导致编码损坏、进而无法被 cx_Freeze 冻结的问题,函数按二进制读取文件、剔除所有 # 开头的注释行后原样写回。build.py 在 build_normal() 中执行 pyrcc5 qt/dg.qrc > qt/dg_rc.py 后立即调用它修复生成文件,保证后续打包链路不受影响。
7. 从函数到流水线:一次完整构建的调用链
把上述函数串起来,可以还原 dupeGuru 的完整构建/打包流程(对应 README.md 的指引):
- 环境准备:Python 3.7+ 与 PyQt5;Linux 下需
python3-pyqt5、pyqt5-dev-tools(提供pyrcc5,可用which pyrcc5检查)、python3-venv、python3-dev、build-essential;打包额外需要python3-setuptools与debhelper。 - 日常构建:
make && make run或手动流程python3 -m venv --system-site-packages ./env && source ./env/bin/activate && pip install -r requirements.txt && python build.py && python run.py(README.md)。build.py内部依次调用add_to_pythonpath(".")、编译 PE C 扩展(build_pe_modules复用setup.py build_ext --inplace)、编译本地化.mo、pyrcc5生成并修复dg_rc.py、生成帮助文档。Makefile 的i18n/modules/mergepot/normpo目标也都会间接落到build.py的对应分支。 - 打包:安装
requirements-extra.txt后执行python build.py --clean && python package.py(README.md)。package.py通过setup_package_argparser解析--sign/--nosign/--src-pkg/--arch-pkg,按平台调用copy_packages组装源/二进制目录、filereplace填充模板、build_debian_changelog生成 changelog,最终产出.deb(Debian/Ubuntu)、Arch 包或 Windows/macOS 安装镜像。
值得注意的工程细节:--clean 会先删除整个 build/ 目录(build.py);打包前若缺少 build/locale 或 build/help,check_loc_doc() 会提示“先运行 build.py --loc / --doc”并退出(package.py)。这些“防御式”设计与 hscommon.build 中 _perform 的目标清理逻辑一脉相承,保证了构建脚本可重复执行。
8. 小结
hscommon.build 的价值不在于“发明”构建工具,而在于把 dupeGuru 各平台构建/打包中反复出现的非业务操作收敛成一组小而稳的函数:文件级操作(move/copy/move_all/copy_all)、文本模板化(filereplace)、环境注入(add_to_pythonpath/get_module_version)、包筛选拷贝(copy_packages)、changelog 解析渲染(read_changelog_file/build_debian_changelog)以及平台专项支持(签名/DMG、打包参数解析、Qt 资源修复)。理解它们,就相当于掌握了 dupeGuru 构建体系的“底层 API”,也为阅读 build.py、package.py、Makefile 以及 pkg/debian/build_pe_modules.py 提供了完整的上下文。