打包 Python 程序的痛点,每个开发者都深有体会。PyInstaller 年久失修、Nuitka 体积庞大——而 PyApp 用 Rust 的方式重新定义了这场“打包革命”。
一、PyApp 到底是什么?
PyApp 是由 Ofek Lev(同时也是 Hatch 的作者)开发的一个创新工具。与 PyInstaller 这类传统打包工具不同,PyApp 本身不是一个 Python 库,而是一个用 Rust 语言编写的程序。
它的工作原理非常独特:PyApp 是一个“运行时自举”(runtime bootstrap)的包装器——你将 Python 项目信息嵌入到 PyApp 的源码中,然后编译出一个独立的可执行文件。当最终用户双击这个可执行文件时,它会自动完成以下操作:
在首次运行时,自动下载(或解压内嵌的)Python 解释器
创建一个独立的虚拟环境
安装你的项目及其所有依赖
执行你的应用程序入口点
最妙的是:最终用户无需预先安装 Python 环境。这就是 PyApp 的核心魅力。
工作流程图
二、为什么 PyApp 与众不同?
在 PyApp 出现之前,Python 打包领域主要由 PyInstaller 统治。但 PyInstaller 有着不可忽视的缺点:使用起来相当棘手,往往需要大量试错才能生成一个可用的分发包。
Nuitka 是另一个较新的选择,它将 Python 程序编译为可分发的二进制文件,但生成的产物体积可能非常庞大,且编译耗时极长。
PyApp 则另辟蹊径,它的核心设计理念是“运行时自举”,带来了几个关键优势:
轻量化:不需要把整个 Python 解释器和所有依赖全部塞进一个超大二进制文件中
灵活性:支持从 PyPI 动态拉取依赖,也支持完全离线内嵌
可配置性:通过丰富的环境变量控制运行时行为
自更新能力:内置 self update 命令,让应用能够自我升级
三、实战准备:安装与环境配置
前置条件
开始使用 PyApp 之前,你需要准备好以下内容:
1. 安装 Rust 工具链
PyApp 需要 Rust 编译器来构建。访问 rustup.rs 安装 Rust:
# Linux/macOScurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh# Windows(下载并运行 rustup-init.exe)# 或使用 Windows Package Managerwinget install Rustlang.Rustup
2. 克隆 PyApp 源码
git clone https://github.com/ofek/pyappcd pyapp
3. 准备好你的 Python 项目
你可以将 Python 项目打包成 Wheel(.whl)格式,也可以直接使用托管在 PyPI 上的包。如果没有 Wheel 文件,可以用以下命令生成:
# 安装 build 工具pip install build# 在项目根目录执行python -m build
生成的 .whl 文件将位于 dist/ 目录下。
四、从零开始:构建你的第一个 PyApp 应用
让我们通过一个完整的示例来演示 PyApp 的打包过程。假设我们要打包一个名为“greeter”的简单命令行工具。
第 1 步:创建 Python 项目
# greeter/cli.pyimport sysimport click@click.command()@click.option("--name", "-n", default="World", help="你要问候的人")@click.option("--count", "-c", default=1, help="问候的次数")def main(name, count): """一个简单的问候程序""" for i in range(count): click.echo(f"Hello, {name}! (第 {i + 1} 次)")if __name__ == "__main__": sys.exit(main())
项目结构:
greeter/├── greeter/│ ├── __init__.py│ └── cli.py├── pyproject.toml└── README.md
pyproject.toml 配置:
[project]name = "greeter"version = "0.1.0"description = "一个简单的问候工具"dependencies = [ "click>=8.0.0",][project.scripts]greeter = "greeter.cli:main"[build-system]requires = ["setuptools>=61.0"]build-backend = "setuptools.build_meta"
第 2 步:生成 Wheel 包
cd greeterpip install buildpython -m build
生成的文件:
dist/├── greeter-0.1.0-py3-none-any.whl└── greeter-0.1.0.tar.gz
第 3 步:配置环境变量并编译
# 进入 PyApp 源码目录cd /path/to/pyapp# 设置环境变量export PYAPP_PROJECT_NAME="greeter"export PYAPP_PROJECT_VERSION="0.1.0"export PYAPP_PROJECT_PATH="/path/to/greeter/dist/greeter-0.1.0-py3-none-any.whl"export PYAPP_EXEC_SPEC="greeter.cli:main"export PYAPP_DISTRIBUTION_EMBED="1" # 内嵌 Python 发行版,实现离线运行# 使用 Rust 的 Cargo 进行编译cargo build --release
首次编译大约需要几分钟,因为 PyApp 有 363 个 Rust 依赖需要下载和编译。后续编译会快很多。
第 4 步:获取可执行文件
编译完成后,可执行文件位于:
# Linux/macOStarget/release/pyapp# Windowstarget/release/pyapp.exe
你可以将其重命名为任何你喜欢的名称:
mv target/release/pyapp ./greeterchmod +x ./greeter# 测试运行./greeter --name "Python爱好者" --count 3
输出:
Hello, Python爱好者! (第 1 次)Hello, Python爱好者! (第 2 次)Hello, Python爱好者! (第 3 次)
五、进阶玩法:PyApp 的更多代码示例
5.1 从 PyPI 直接打包(无需本地 Wheel)
如果你的项目已经发布到 PyPI,可以省略 PYAPP_PROJECT_PATH:
export PYAPP_PROJECT_NAME="black" # 使用 PyPI 上的 black 格式化工具export PYAPP_PROJECT_VERSION="24.1.0"export PYAPP_EXEC_MODULE="black" # 相当于 python -m blackcargo build --release
5.2 自定义执行脚本
PyApp 允许你提供一个任意的 Python 脚本作为入口点:
# 创建一个入口脚本cat > entry.py << 'EOF'#!/usr/bin/env pythonimport sysprint("=== 欢迎使用 PyApp 打包的应用 ===")print(f"Python 版本: {sys.version}")print(f"命令行参数: {sys.argv[1:]}")print("=== 应用启动成功 ===")# 在这里执行你的实际应用逻辑def main(): # 你的应用代码 passif __name__ == "__main__": main()EOF# 配置环境变量export PYAPP_PROJECT_NAME="myapp"export PYAPP_PROJECT_VERSION="1.0.0"export PYAPP_PROJECT_PATH="./dist/myapp-1.0.0-py3-none-any.whl"export PYAPP_EXEC_SCRIPT="./entry.py"cargo build --release
5.3 使用依赖文件管理复杂依赖
对于依赖复杂的项目,可以使用依赖文件:
# 创建 requirements.txtcat > requirements.txt << 'EOF'requests>=2.28.0click>=8.1.0rich>=13.0.0pyyaml>=6.0EOFexport PYAPP_PROJECT_NAME="complex-app"export PYAPP_PROJECT_VERSION="1.0.0"export PYAPP_PROJECT_DEPENDENCY_FILE="./requirements.txt"export PYAPP_EXEC_MODULE="myapp"
5.4 指定 Python 版本
通过 PYAPP_PYTHON_VERSION 环境变量可以指定运行时使用的 Python 版本:
# 使用 Python 3.11export PYAPP_PYTHON_VERSION="3.11"# 其他配置...export PYAPP_PROJECT_NAME="myapp"export PYAPP_PROJECT_VERSION="1.0.0"
PyApp 默认使用 indygreg/python-build-standalone 项目提供的可重定位 Python 发行版。
5.5 自更新功能
PyApp 打包的应用内置了 self 子命令组:
# 检查当前版本./myapp self --help# 恢复到初始状态(清除缓存的环境)./myapp self restore# 更新到最新版本(如果项目在 PyPI 上有新版本)./myapp self update
你还可以在应用中集成这些功能:
# 在你的 CLI 工具中添加版本检查import subprocessimport sysdef check_for_updates(): """检查是否有更新""" try: result = subprocess.run( [sys.argv[0], "self", "update", "--dry-run"], capture_output=True, text=True ) if "new version available" in result.stdout: print("发现新版本,请运行 `myapp self update` 更新") except Exception: pass
六、PyApp 与其他工具的对比
为了帮你更好地理解 PyApp 的定位,这里将其与其他主流 Python 打包工具进行对比:
各工具特点速览
PyInstaller:最成熟的工具,但打包速度慢、体积大、常有兼容性问题,需要大量试错
Nuitka:真正的编译,但体积极大、编译耗时,不适合快速迭代
Shiv/PEX:生成 zipapp 格式,但需要目标系统安装 Python
PyApp:Rust 包装器 + 运行时自举,最终用户无需 Python,轻量且支持自更新
选择 PyApp 的典型场景
命令行工具分发:你想让用户直接下载一个可执行文件就能用,无需折腾 Python 环境
内部工具部署:企业内部使用的脚本工具,需要跨平台支持
需要自更新能力:应用需要能够自动检查并升级到新版本
离线环境部署:通过设置 PYAPP_DISTRIBUTION_EMBED=1 内嵌 Python,实现完全离线运行
七、总结
PyApp 代表了一种全新的 Python 应用打包思路。它不试图把所有东西都塞进一个庞大的二进制文件中,而是巧妙地利用“运行时自举”机制,让 Python 解释器和依赖在用户首次运行时优雅地“现身”。
它的优势在于:
轻量级:最终可执行文件相对较小
用户友好:最终用户无需安装 Python
灵活可配置:丰富的环境变量满足各种场景
自更新能力:内置管理命令,支持应用自我升级
跨平台:支持 Windows、macOS、Linux
当然,PyApp 也有学习曲线——你需要了解 Rust 的编译流程、掌握一系列环境变量的含义。但一旦配置好,后续的打包流程将变得异常顺畅。如果你正在寻找一个现代、高效的 Python 打包方案,PyApp 绝对值得一试。