本文将介绍如何将一个 Python 应用程序编译成可执行文件,同时规避动态链接库的依赖问题,实现单文件分发部署。

请注意,尽管是「静态」可执行文件,你仍需要考虑目标机器的平台和架构。

本文不涉及交叉编译。

背景

众所周知,Python 属于动态语言。

Python 应用程序不需要编译为可执行文件即可运行,但需要目标机器上安装有相应的 Python 解释器和 Python 应用程序需要的外部模块。显然,这对于应用程序的分发部署来说十分不便。

这里,我介绍一种将 Python 应用程序编译成「静态」可执行文件的方法 (PyInstaller + StaticX),可以规避动态链接库的依赖问题,实现单文件分发部署。

编译「静态」可执行文件

本文将使用 PyInstaller 和 StaticX。

PyInstaller

PyInstaller 将一个 Python 应用程序和它的所有依赖项捆绑成一个单一的软件包。用户无需安装 Python 解释器或任何模块就可以运行打包后的应用程序。

首先,我们需要安装 PyInstaller 包。

pip install -U pyinstaller

打包应用程序和它的所有依赖项成一个单一的软件包

pyinstaller main.py --onefile

现在,你可以在 main.py 同目录下的 dist 文件夹中找到打包好的「动态」可执行文件。

StaticX

通过上一步,我们已经得到了打包好的「动态」可执行文件。若我们直接将该文件部署到其他机器,兴许能够成功运行。因为,即使是在同平台和同架构的情况下,你仍有极大概率因动态链接库的依赖问题而翻车,特别是 Linux 平台。

PyInstaller 构建的可执行文件不是完全静态的,因为它仍然依赖于系统的 libc。在 Linux 下,GLIBC 的 ABI 是向后兼容的,但不是向前兼容的。因此,如果你用较新的 GLIBC 链接,你就不能在较旧的系统上运行编译好的可执行文件。

你此时可能想说:“那么,使用 PyInstaller 前指定旧的 GLIBC 不就好了吗?”

问题在于,Python 应用程序最关键的 Python 解释器 libpython.so 和其他动态库仍然依赖于较新的 GLIBC。一种解决方法是在你身边最老的系统上编译 Python 解释器及其模块。这确实可行但有些麻烦,好在我们还有其他手段。

既然动态链接库存在依赖问题,那么直接将 Python 应用程序编译成「静态」可执行文件就是最简单粗暴的解决方法。这里,我们需要使用如 StaticX 这样的工具。

首先,我们需要安装 StaticX 包。

pip install -U staticx

若要使用 StaticX,除了安装 StaticX 包,你还需要确保你的开发环境中已安装如下命令:

  • ldd
  • readelf
  • objcopy
  • patchelf

你可以在 Shell 中执行 command -v <COMMAND> 来检查命令是否存在。如有缺失,这些命令也都可以通过 Linux 发行版的包管理器轻松安装。

使用 StaticX 构建「静态」可执行文件:

staticx <动态可执行文件路径> <静态可执行文件路径>

现在,你可以在你所指定的静态可执行文件路径找到最终生成的「静态」可执行文件,不再受 GLIBC 所困。

存在的问题

StaticX 生成的「静态」可执行文件在运行时将在系统的 /tmp/ 路径下创建随机临时文件夹,其中存放有应用程序运行所需的库,并且该目录将在应用程序退出时被自动清除。

应用程序在运行时以该临时文件夹为其工作目录,这意味着诸如 __file__ 变量和 os, pathlib 等库所获取到的路径将出现异常。

总结

Python 应用程序分发部署时存在不便,而仅仅使用 PyInstaller 将其和它的所有依赖项捆绑成一个单一的软件包又会碰上动态链接库的依赖问题

本文介绍的使用 PyInstaller + StaticX 将 Python 应用程序编译成「静态」可执行文件的方法,是一种简单粗暴却又行之有效的解决方法。