起因
需求: 加密 AetherAgent 项目代码
背景: Nuitka本地编译OOM,PyArmor有文件对象大小、函数数量etc限制,网上看到有项目jmpy,但项目年久失修..
启发:Python打包EXE防破解实战:PyInstaller+Nuitka双保险配置指南
(项目开发全程由AI完成,我仅提供完善思路及优化需求)
概述
PySack 是一个 Python 源码保护工具链,它将 Nuitka 的编译加密能力与 PyInstaller 的打包分发能力串联为一条完整流水线。开发者执行一条命令,即可将纯 Python 项目转换为加密的独立可执行文件。
仓库地址: github.com/JiuZero/PySack
安装方式:
1 | pip install --extra-index-url https://test.pypi.org/simple/ pysack |

一、核心架构:两阶段流水线
PySack 的工作流分为两个阶段,由 start_full_workflow() 函数编排:
1 | def start_full_workflow(input_file_path, output_file_path=None, ...): |
阶段一:Nuitka 编译加密
start_encrypt() 执行以下子步骤:
1. 文件拷贝与过滤
将源码拷贝到输出目录,排除 dist/、.git/、venv/、__pycache__/ 等目录:
1 | def copy_files(src_path, dst_path): |
2. 智能文件筛选
filter_cannot_encrypted_py() 负责过滤掉不适合加密的文件:
- 跳过
__init__.py、__main__.py等双下划线文件 - 扫描文件内容,跳过包含
if __name__ == "__main__"的入口文件 - 支持
force_encrypt_files白名单,强制加密特定文件
1 | def filter_cannot_encrypted_py(files, except_main_file, force_encrypt_files=None): |
3. 逐文件 Nuitka 编译
对每个 .py 文件调用 python -m nuitka --module,生成对应的 .pyd(Windows)或 .so(Linux/macOS)二进制文件:
1 | def encrypt_py(py_files): |
4. 平台标签清理
Nuitka 生成的 .pyd 文件名包含平台标签(如 module.cp314-win_amd64.pyd),需重命名为 module.pyd 才能正常导入:
1 | def rename_excrypted_file(output_file_path): |

阶段二:PyInstaller 打包
start_pyinstaller_pack() 将加密后的 .pyd 文件与运行时依赖打包为独立目录:
1. 输出名称自动推导
可执行文件输出名称默认取自入口文件名(如 aether.py → aether.exe),无需手动指定 -n 参数:
1 | if dist_name is None: |
2. 入口文件自动检测
若未指定 -m 参数,PySack 会自动在加密目录中查找常见入口文件名:
1 | if main_entry is None: |
3. 构建 PyInstaller 命令
1 | cmd = [sys.executable, "-m", "PyInstaller", "--onedir", "--name", dist_name] |
关键设计决策:
- 使用
sys.executable -m PyInstaller而非直接调用pyinstaller命令,确保 Nuitka 和 PyInstaller 使用同一 Python 解释器,避免.pyd文件的 DLL 版本冲突 - 不显式添加
--add-binary和--add-data:PyInstaller 的 modulegraph 分析可通过入口文件的 import 链自动发现已编译的.pyd/.so文件和__init__.py。显式添加这些标志会使命令行在 Windows 上超过 8191 字符限制,导致 PyInstaller 静默失败
4. 清理构建产物
编译完成后清理 Nuitka 生成的 .build/ 目录和 PyInstaller 的 dist/、build/ 目录,只保留最终输出。
二、自动依赖检测系统
这是 PySack 最具技术价值的设计。Nuitka 编译后会在每个模块旁生成 .pyi 存根文件,其中包含该模块的 import 语句。PySack 利用这些文件逆向推导第三方依赖关系。
检测流程
1 | def auto_detect_dependencies(encrypted_dir): |

过滤策略详解
| 过滤层 | 依据 | 行为 |
|---|---|---|
| 标准库顶层 | sys.stdlib_module_names |
跳过(如 os、sys) |
| 标准库子模块 | 顶层在 stdlib 中,但完整名不在 | 保留为 hidden-import(如 xml.dom、collections.abc) |
| 内部模块 | .pyi/.pyd 文件路径 | 保留为 hidden-import(编译后为二进制,modulegraph 无法解析) |
| 内部包顶层 | __init__.py 目录 |
跳过(PyInstaller 可自动发现) |
| 内部包子模块 | 顶层在 internal_packages 中 | 保留为 hidden-import(如 conf.i18n、core.executor) |
| 私有模块 | 以 _ 开头 |
跳过 |
两类关键边界情况:
标准库子模块(如
xml.dom):sys.stdlib_module_names包含"xml"但不包含"xml.dom"。若仅检查module_name in stdlib_modules,xml.dom会漏过过滤,导致 fallthrough 到all_third_party.add("xml")(添加顶层而非子模块)。正确做法是检查top_module是否在 stdlib 中,再根据module_name == top_module区分顶层和子模块。内部包子模块(如
conf.i18n):项目内部模块被 Nuitka 编译为.pyd/.so后,PyInstaller 的 modulegraph 无法解析二进制文件中的 import。因此内部子模块必须显式添加为--hidden-import,否则运行时抛出ModuleNotFoundError。
传递依赖补全
许多第三方库的依赖在 .pyi 中不可见(因为它们是在 C 扩展或运行时内部导入的)。PySack 内置了一份传递依赖映射表:
1 | TRANSITIVE_DEPS = { |
当检测到 fastapi 时,自动补充 python_multipart;检测到 uvicorn 时,补充 httptools 和 watchfiles。补全前会通过 importlib.util.find_spec() 验证模块实际存在,避免引入不存在的依赖。
精确导入模式
v0.2.15 起,所有检测到的模块以精确子模块路径作为 --hidden-import 添加,不再使用 --collect-submodules:
1 | # 保留完整子模块路径 |
好处:PyInstaller 只导入实际用到的子模块,而非整个包。例如项目只用了 sqlalchemy.orm,不会将 sqlalchemy.sql、sqlalchemy.engine 等未使用的子模块打包进来,显著减小输出体积。
三、CLI 与配置系统
命令行接口
PySack 提供四种子命令:
| 命令 | 功能 | 典型用法 |
|---|---|---|
encrypt |
仅加密(默认) | pysack encrypt -i project/ -o output/ |
pack |
仅打包 | pysack pack -i encrypted_dir/ -m main.py |
full |
加密 + 打包 | pysack full -i project/ |
build |
从配置文件执行 | pysack build(自动读取 .pysack.cfg) |
full 和 pack 子命令的 -n 参数为可选,默认输出名称取自入口文件名。-m 参数同样可选,PySack 会自动检测常见入口文件名。
配置文件模式
对于重复构建场景,PySack 支持 .pysack.cfg 配置文件:
1 | [pysack] |
load_config() 函数使用 configparser 解析配置,无缝映射到 start_full_workflow() 的参数:
1 | def load_config(config_path): |
四、使用场景
场景一:一键保护整个项目
1 | # -n 可选,默认输出名称为入口文件名 |

场景二:两步分离(加密 + 打包独立执行)
1 | # 先加密 |
场景三:CI/CD 自动化构建
1 | # 在项目目录中放置 .pysack.cfg 配置文件 |
五、技术要点总结
| 特性 | 实现方式 | 关键代码 |
|---|---|---|
| 编译加密 | Nuitka --module 模式 |
subprocess.run(["python", "-m", "nuitka", "--module", file]) |
| 依赖检测 | .pyi 文件逆向解析 + 正则匹配 import | re.match(r"^(?:from\s+(\S+)\s+import|\s*import\s+(\S+))", line) |
| 标准库子模块 | 检查 top_module in stdlib_modules 而非 module_name in stdlib_modules |
if top_module in stdlib_modules and module_name != top_module: all_third_party.add(module_name) |
| 内部子模块 | 遍历 .pyi/.pyd 文件路径 + __init__.py 目录构建 internal 集合 |
internal_packages.add(rel.replace(os.sep, ".")) + 子模块保留为 hidden-import |
| 精确导入 | 保留完整子模块路径,替代 --collect-submodules |
使用 sqlalchemy.orm 而非 sqlalchemy 作为 --hidden-import |
| 传递依赖 | 预定义映射表 + importlib.util.find_spec() 验证 |
TRANSITIVE_DEPS 字典 + find_spec(dep) |
| 平台兼容 | 重命名 .pyd/.so 文件,去除平台标签 | re.sub(r"(.*)\..*\.(.*)", r"\1.\2", file) |
| 命令行长度 | 避免冗余 --add-binary/--add-data,依赖 modulegraph 自动发现 |
利用 PyInstaller 的 import 链分析,替代显式文件添加 |
| 输出命名 | 默认使用入口文件名作为输出名称 | os.path.splitext(os.path.basename(main_entry))[0] |
| 配置管理 | configparser 解析 .pysack.cfg | configparser.ConfigParser().read(config_path) |
六、结语
PySack 解决了一个现实痛点:Python 项目最高效且免费的源码保护。
Nuitka 提供了编译级别的加密强度,PyInstaller 提供了分发级别的打包便利,而 PySack 填补了二者之间的集成空白——尤其是自动依赖检测和传递依赖补全,消除了手动维护 --hidden-import 列表的繁琐工作。
在实际开发中,PySack 经过多次迭代解决了一系列工程问题:标准库子模块(xml.dom)的正确过滤、内部包子模块(conf.i18n)的 hidden-import 保留、Windows 命令行长度限制的规避、跨平台 .pyd/.so 文件处理等。这些细节确保了从源码到可执行文件的自动化流水线稳定可靠。
对于追求源码安全同时又不想放弃 Python 开发效率的团队,PySack 提供了一条简洁的自动化路径。
说些什么吧!