将 Python 应用程序打包为二进制文件有许多中方法,一些常用的库如下方表格中所示:
库 | 跨平台支持 | 资源文件支持 | 可配置性 | 活跃度 |
---|---|---|---|---|
PyOxidizer | Linux、Windows、macOS | 良好支持 | 高度可配置 | 活跃开发中 |
PyInstaller | Linux、Windows、macOS | 支持 | 中等可配置 | 活跃 |
Nuitka | Linux、Windows、macOS | 支持 | 一些可配置选项 | 活跃 |
Py2Exe | Windows | 支持 | 有限的可配置选项 | 不太活跃 |
Py2App | macOS | 支持 | 有限的可配置选项 | 相对不活跃 |
Shiv | Linux、Windows、macOS | 支持 | 可配置 | 相对较新 |
Briefcase | Linux、Windows、macOS、iPhone/iPad、Android、Web | 支持 | 中等可配置 | 活跃 |
在 M1 Mac 上尝试使用 PyInstaller 和 Nuitka 打包 Python 应用程序,但是结果并不好:
- PyInstaller 能够成功打包,但是二进制文件响应很慢,似乎大部分时间都花在了加载相关代码上;
- Nuitka 在 M1 Mac 上编译失败。
最后尝试使用 PyOxidizer 进行打包,虽然文件体积稍大,但是二进制文件在使用上没有其他问题。接下来简单介绍下 PyOxidizer 的一些基础概念,并使用例子展示如何使用 PyOxidizer 将一个 Python 应用程序打包为二进制文件。
PyOxidizer 是什么?
PyOxidizer 一套致力于优化 Python 应用程序打包和分发过程的库和工具集合,使用 PyOxidizer 能够将一个 Python 项目打包为独立可执行文件。PyOxidizer 项目有以下组件组成:
- oxidized_importer:一个使用 Rust 实现的 Python 模块,提供了高性能的模块和资源导入机制。用于从内存中导入 Python 模块和资源,从而使 Python 应用程序能够打包为单一文件的可执行文件。
- pyembed:一个用于在 Rust 应用程序中控制嵌入的 Python 解释器的库。通过实现额外的功能来增强嵌入的 Python 解释器的功能,例如与 oxidized_importer 的集成、配置特定的内存分配器、自动进行 terminfo 数据库解析等。
- PyOxidizer:PyOxidizer 是一个基于 Rust 语言开发的可分发 Python 应用程序的工具。
- PyOxy:提供替代 Python 运行环境的应用程序。可以将其视为
python
命令的另一种实现和重新设计。通过使用 PyOxy 可以使用为pyoxidizer
构建的部分技术(主要是 oxidized_importer 和 pyembed)而无需直接使用pyoxidizer
。 - Tugger:Tugger 是一个实现通用应用程序打包和发布功能的 umbrella 项目,定义了 Starlark 原语,用于脚本编写常见的应用程序打包和发布操作。在 PyOxidizer 中使用 Tugger 来执行与 Python 无关的功能。
其中 Tugger 由多个 Rust 库组成,每个库提供特定领域的功能,包括但不限于:
- Debian 打包
- 软件许可证
- Snapcraft 打包
- Apple 代码签名
- Rust 工具链安装
- Windows 安装程序生成
PyOxidizer 配置文件说明
PyOxidizer 配置文件使用 Starlark 语言定义,Starlark 是一种 Python 方言,主要用作 Bazel 构建系统的配置文件编写。
配置文件的结构和用途说明如下:
def make_exe(): # 该方法用于创建可执行文件并安装到目标位置
# 获取 Python 解释器
dist = default_python_distribution()
# 定义打包 Policy,Policy 限制如何构建可执行文件及资源文件如何添加到可执行文件中
policy = dist.make_python_packaging_policy()
# ... Policy 的具体配置
# 定义 Python 解释器的配置,包括内存分配方法、可执行文件的执行方式等
python_config = dist.make_python_interpreter_config()
# ... Config 的具体配置
# 从 Python 分发文件中构建可执行的 Python 文件,定义资源嵌入方法等
exe = dist.to_python_executable(
name="app",
packaging_policy=policy,
config=python_config,
)
# ... exe 的具体配置
return exe
def make_embedded_resources(exe):
return exe.to_embedded_resources()
def make_install(exe):
files = FileManifest()
files.add_python_resource(".", exe)
return files
def make_msi(exe): # 定义 Windows MSI 安装文件配置
return exe.to_wix_msi_builder(
"yanb_pdf", # Simple identifier of your app.
"yanb_pdf", # The name of your application.
"0.1.0", # The version of your application.
"user@example.com" # The author/manufacturer of your application.
)
def register_code_signers(): # macOS 应用程序签名配置
# You will need to run with `pyoxidizer build --var ENABLE_CODE_SIGNING 1` for this if block to be evaluated.
if not VARS.get("ENABLE_CODE_SIGNING"):
return
# ... 其他配置
# signer.activate()
register_code_signers()
register_target("exe", make_exe) # 注册构建目标:exe
register_target("resources", make_embedded_resources, depends=["exe"], default_build_script=True) # 注册构建目标:resources
register_target("install", make_install, depends=["exe"], default=True) # 注册构建目标 install
register_target("msi_installer", make_msi, depends=["exe"]) # 注册构建目标 msi_installer
resolve_targets()
pyoxidizer.bzl
使用 PyOxidizer 打包 Python 应用程序的实例
在使用 PyOxidizer 打包前,先确保安装了 Rust 开发环境。Rust 开发环境准备就绪后,使用 cargo
安装 PyOxidizer:
cargo install PyOxidizer
项目可以在 Github 上找到。这是一个简单的 Python 应用,其功能是使用 pdfplumber
读取命令行中指定的 pdf 文件并输出其文字内容。项目结构如下:
├── Makefile ├── poetry.lock ├── pyoxidizer.bzl # **PyOxidizer 配置文件** ├── pyproject.toml ├── requirements.txt └── app ├── __main__.py # 程序入口
功能相关的代码在 app
目录下。pyoxidizer.bzl
文件为 PyOxidizer 的配置文件,可以使用下面的脚本创建:
pyoxidizer init-config-file .
对于本项目,可以使用如下的 pyoxidizer.bzl
配置:
# This file defines how PyOxidizer application building and packaging is performed. See PyOxidizer's documentation at https://gregoryszorc.com/docs/pyoxidizer/stable/pyoxidizer.html for details of this configuration file format.
def make_exe():
dist = default_python_distribution()
policy = dist.make_python_packaging_policy()
# 定义资源加载位置,优先从内存中读取,如果读取不到则在文件系统中查找
policy.resources_location = "in-memory"
policy.resources_location_fallback = "filesystem-relative:prefix"
python_config = dist.make_python_interpreter_config()
python_config.run_module = "app"
exe = dist.to_python_executable(
name="app",
packaging_policy=policy,
config=python_config,
)
# 添加 pdfplumber 依赖
exe.add_python_resources(exe.pip_download(["pdfplumber"]))
# 或者使用 requirements.txt 文件添加
# exe.add_python_resources(exe.pip_install(["-r", "requirements.txt"]))
# 将当前目录下的 app 包作为嵌入的资源文件
exe.add_python_resources(exe.read_package_root(
path="/src/mypackage",
packages=["foo", "bar"],
))
return exe
def make_embedded_resources(exe):
return exe.to_embedded_resources()
def make_install(exe):
files = FileManifest()
files.add_python_resource(".", exe)
return files
def make_msi(exe):
return exe.to_wix_msi_builder(
"app",
"My Application",
"1.0",
"Alice Jones"
)
def register_code_signers():
return
register_code_signers()
register_target("exe", make_exe)
register_target("resources", make_embedded_resources, depends=["exe"], default_build_script=True)
register_target("install", make_install, depends=["exe"], default=True)
register_target("msi_installer", make_msi, depends=["exe"])
resolve_targets()
pyoxidizer.bzl
需要重点关注的内容是 make_exe
方法中:
- 配置
policy
,指定从内存中加载资源文件,无法加载的话再从文件系统中加载; - 配置
python_config
,使用python_config.run_module
让二进制文件在启动时运行指定的包(app/__main__.py
); - 在
exe
中为我们的项目添加必要的依赖,由于这个项目只有一个pdfplumber
依赖,因此直接使用pip_download
添加,也可以通过requirements.txt
文件或者读取virtual env
来添加; - 在
exe
中将项目的源码(app
目录)添加为应用程序资源文件。
完成配置文件的更新后,使用下面的脚本进行打包:
pyoxidizer build --release # 不添加 --release 选项的话默认以 debug 模式打包
如果没有错误,则可以在 build/<arch>/release/install
目录中找到打包完成的 app
文件。