cover

使用 PyOxidizer 打包 Python 应用

本文的主要内容是如何使用 PyOxidizer 将 Python 应用程序打包为二进制文件。

2024-04-12

将 Python 应用程序打包为二进制文件有许多中方法,一些常用的库如下方表格中所示:

跨平台支持资源文件支持可配置性活跃度
PyOxidizerLinux、Windows、macOS良好支持高度可配置活跃开发中
PyInstallerLinux、Windows、macOS支持中等可配置活跃
NuitkaLinux、Windows、macOS支持一些可配置选项活跃
Py2ExeWindows支持有限的可配置选项不太活跃
Py2AppmacOS支持有限的可配置选项相对不活跃
ShivLinux、Windows、macOS支持可配置相对较新
BriefcaseLinux、Windows、macOS、iPhone/iPad、Android、Web支持中等可配置活跃

在 M1 Mac 上尝试使用 PyInstaller 和 Nuitka 打包 Python 应用程序,但是结果并不好:

  1. PyInstaller 能够成功打包,但是二进制文件响应很慢,似乎大部分时间都花在了加载相关代码上;
  2. Nuitka 在 M1 Mac 上编译失败。

最后尝试使用 PyOxidizer 进行打包,虽然文件体积稍大,但是二进制文件在使用上没有其他问题。接下来简单介绍下 PyOxidizer 的一些基础概念,并使用例子展示如何使用 PyOxidizer 将一个 Python 应用程序打包为二进制文件。

heading

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 安装程序生成
heading

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
heading

使用 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 方法中:

  1. 配置 policy,指定从内存中加载资源文件,无法加载的话再从文件系统中加载;
  2. 配置 python_config,使用 python_config.run_module 让二进制文件在启动时运行指定的包(app/__main__.py);
  3. exe 中为我们的项目添加必要的依赖,由于这个项目只有一个 pdfplumber 依赖,因此直接使用 pip_download 添加,也可以通过 requirements.txt 文件或者读取 virtual env 来添加;
  4. exe 中将项目的源码(app 目录)添加为应用程序资源文件。

完成配置文件的更新后,使用下面的脚本进行打包:

pyoxidizer build --release # 不添加 --release 选项的话默认以 debug 模式打包

如果没有错误,则可以在 build/<arch>/release/install 目录中找到打包完成的 app 文件。