Py学习  »  Python

将Python打包为可执行文件的4种尝试

Python程序员 • 4 年前 • 524 次点击  

几年前,我研究了如何创建Python应用程序的单文件可执行文件。当时的目标是创建一个将其他文件和二进制文件包含在一个bundle(包)中的GUI应用程序。使用PyInstaller,我构建了一个可以跨平台执行的二进制文件,看起来就像任何其他应用程序一样。


快进到今天,我仍然有一个类似的需求,但是使用情况不同。我想在一个Docker容器中运行Python代码,但是这个容器镜像无法安装Python。


我决定研究更多的替代方案,并在这里讨论,而不是盲目地重复我上次的尝试。


注意:请不要把这篇文章读成“项目x是最好的”或“解决方案y很烂”,而是要试着从这个过程中学习。这里的信息可能会在你自己尝试时帮助你克服过去的问题。


环境


所有的项目都有限制,这绝对不是例外。理解这些是很重要的,因为它有助于你在解决问题时集中精力。让我们来看看我们的限制。

最终打包的文件将运行在Docker上,这意味着其镜像几乎可以保证是Linux派生的。现在还没有必要担心Windows,尽管我尝试的所有打包系统都支持为Windows、Mac和Linux构建——保持多种可能性总是好的。


构建包是在Ubuntu 16.04虚拟机上执行的,但它不是一个干净的安装。它使用的是从源代码编译的Python 3.7.4, 存在有web代理、阻塞端口、ssh问题和网络问题。编译环境混合了gcc和clang。至少有十几个单独的Python虚拟环境。它同时使用了Git和Mercurial 仓库——是的,它们确实存在!还有在hg和git之间转换的插件——是的,人们也这样做。自定义docker镜像,nginx反向代理,工作。


我不知道你的情况,但这就是在大公司中作为一个软件开发人员的现实。


我敢肯定,如果使用只有我才能访问的原始虚拟机,尝试所有这些东西将会变得无比容易。如果能以稳定状态配置运行在网络服务和硬件上。而且不依赖VPN、SSH隧道或跨地理连接。


然而,在我近20年的职业生涯中,我从未体验过这样的环境。至少在IT纳粹党找到你的流氓VM并杀死它以及你所有的希望和梦想之前的一两个月之前是这样:D。


尝试


我在选择以下四个系统时没有太多的考虑。基于对打包问题的普遍回答,我提出的解决方案能够满足我的需求。

Cython


Cython作为一个将python代码编译成C模块的方法和语言来说是非常流行的。它给了你与其他C代码集成的能力,以及与运行解释器相比的典型速度优势。它有两大特性,既不让Python代码可以从C调用,又能够生成可以像其他Python模块一样直接导入的C库。


首先,我测试了一个简单的hello-world脚本,它会将一些内容打印到屏幕上,以及导入subprocess并调用run()——这是我在最终解决方案中需要做的事情。


Cython并不能为你解决所有问题。它的主要功能是将一个包含调整过的Python代码的.pyx文件转换(因为Cython定义了更多的语言结构)到一个.c文件中。它不会为你编译该文件,但是有一个选项可以将Python解释器嵌入到该文件中。


如果你从未编译过C代码,这是一个分两步的过程:编译和链接。一个生成一个C模块.o文件,另一个则创建一个可执行文件。以下是完成这个过程的步骤:



第一行是转换成C的实际cython调用,我们告诉它--embed(嵌入)解释器并使用-3来指明Python版本。-o是它写入的输出文件的名称。


这两个编译步骤还需要额外的信息来帮助编译器找到组成Python本身的必要代码片段,并将它们包含到生成的文件中。当你使用虚拟环境时,这会变得很棘手,如果你是从源代码安装的Python,那自定义标志就会更加复杂。


在决定使用哪个标志时,我发现了python-config命令。在你的虚拟环境中运行它将会指向正确的信息。具体来说,你将需要python-config cflags和python-config ldflags。它们的输出可以直接传递到上面第2行和第3行中的gcc编译器命令中。


我能够生成一个独立的单文件包。它运行得非常好,直到我开始将Python代码分割成单独的模块。看起来Cython不会抓取你的导入项去找出编译所需要的东西。你必须自己手动完成,或者至少我不能在我有时间的情况下找到一种自动完成的方法。


Cython并不是真正按照我需要的方式来打包代码的。如前所述,它的主要目的是C和Python之间的集成。进入我们的下一个方法:nuitka!


Nuitka


Nuitka已经存在很长时间了,几乎和Cython本身一样长。这个系统的存在主要是为了将Python模块转换成可执行文件——这正是我们的目标。它还像Cython一样编译代码,但使用的是自己的算法。这意味着执行速度仍然有可能获得一些提升。


我过去曾使用Nuitka作为一个概念的验证,我将一个flask应用程序编译成一个运行良好的单文件。我在这次尝试中使用的命令是:



不幸的是,我在添加必要的C include时遇到了一些麻烦。我看到它抓取了我代码中的所有导入模块,以及这些模块导入的模块,等等。但我就是无法通过来自缺少标准库函数的一组“未定义”的错误。


我怀疑我的虚拟环境设置有问题,或者只是缺少指向正确路径的环境变量。不管怎样,我在研究解决方案的时候已经没有时间了。


PyOxidizer


我特别提到了前边几个打包选项中的编译步骤,因为后面两个的选项稍有不同。


使用Python解释器编译模块或包(到目前为止的尝试)与将解释器和Python代码“捆绑”到一个文件中是不同的。这最后一部分是接下来的两个机制所做的事情。


PYOxidzer是新来者。它利用了为Rust编程语言开发的打包系统。与其它所有东西一样,它可以为任何操作系统生成包,但是它的工作方式是将Python解释器、代码及其依赖项从一个新的虚拟环境捆绑到可执行文件中。


执行该二进制文件会将相关文件提取到内存中。正如你所知,从内存中读取要比文件系统快得多,因此你将拥有更快的导入语句和加载时间。其它解决方案,如PyInstaller(下面将更详细地描述),也做类似的事情,但是它们是将代码提取到文件系统中。


它可以以3种不同的模式打包东西:


  1. repl -这真的很简洁,我认为它是一个其自身的“可交付的”类别。它允许你分发一个预先配置了任何和所有依赖项的单文件交互式Python REPL。可以将Jupyter Notebook看作是一个可执行的命令行。

  2. eval——使用你选择的Python命令运行一个单行字符串。其文档使用import uuid; print(uuid.uuid4())为例。它可以很好地处理执行简单任务或调用具有简单接口的模块的代码。

  3. module——将一个模块加载为__main__并执行它。我预计这是大型应用程序的常见选择。


我在使用PyOxidizer的过程中做了一些测试,效果很好。TOML配置文件中提供的打包选项允许进行大量自定义设置。


我特别喜欢处理模块依赖关系的4个选项: 单文件pip安装、使用需求文件、提供根目录包含或简单地指向一个配置了所有依赖关系的虚拟环境。


当我开始做更复杂的事情时,在尝试导入requests时遇到了这个问题(https://github.com/indygreg/PyOxidizer/issues/69 )。它的要点: 运行打包的代码时__file__属性不会被设置。


PyOxidizer开发者们在这里(https://pyoxidizer.readthedocs.io/en/latest/packaging_pitfalls.html#no-file  )做了详细的解释。他们指出,Python环境不需要这个属性,因此任何人如果编写了一个需要它的模块时,其代码的运行环境就会受到限制。


实际上,我以前在使用PyInstaller绑定外部资源时就处理过__file__问题,所以我对它并不感到惊讶。


似乎有几个库依赖于这个属性。或者至少是一些被更多高级库使用的基本库,使得这些高级库也依赖于它。还记得我们什么时候讨论过模块化和依赖树是如何使开放源码变得复杂的吗?


这个问题使所有的努力都白费了。我四处寻找解决方案,但在我超量的时间内并没有找到。然而,在写这篇文章时,我确实碰到了它们文档的这一部分,其中描述了如何通过在配置文件中添加一些额外的设置来避免这种情况。因此,希望并没有丧失。


PyInstaller


PyInstaller已经存在了好几年了。我曾经多次使用它成功地为Linux和OSX生成单文件桌面应用程序二进制文件。你甚至可以在打包文件中包含其他软件,如Chromium或一个内置的Unity3D游戏。


如前所述,该方法非常类似于PyOxidizer。它的最终可执行文件包含一个绑定的解释器和运行应用程序所需的所有文件。执行它会将打包的文件提取到一个目录中,并从那里加载其余的文件。


在我的测试中,代码要求访问Python共享库。在我的情况中,这稍微复杂一些,因为我使用的是Python源代码发行版。


在配置Python源代码以启用这些库时,我必须做出明智的决定。你在运行源代码构建的./configure步骤时,可以通过添加--enable-shared选项来配置共享库。


以这种方式安装Python之后,你必须设置一个指向共享库的LD_LIBRARY_PATH环境变量。或者,你也可以在/etc/ld.so.conf.d下的一个文件中配置共享库路径,并运行ldconfig。


由于我仍然喜欢使用virtualenv和virtualenvwrapper来管理虚拟环境,所以另一个问题就随之而来。


原来PyInstaller在内部运行时如何交互存在一个bug。问题在于为distutils确定正确的路径,但是我在这个问题注释中找到了一个快速修复方法。没有什么太复杂的。


生成最终的可执行文件是通过以下命令完成的,另外还有几个选项,我没有在这里列出,因为它们是关于指定输出和构建目录的:



其他几项注意事项:


  1. 环境变量——为了确保能正确的加载,PyInstaller会自动将其进程中的LD_LIBRARY_PATH变量更新为一个临时路径。

  2. 路径(再次)——在确定执行代码的路径时,你可能会发现一些类似于PyOxidizer的问题,在前面链接的桌面应用程序的文章中可以找到这些问题的解决方案。


LD_LIBRARY_PATH问题并不是所有情况下的问题,但是我的应用程序使用subprocess来执行一些依赖于该路径的命令。由于派生的进程继承父环境,因此你不得不调整该变量,并将正确的变量作为子进程调用的env参数传入。你可以通过一个名为LD_LIBRARY_PATH_ORIG的不同的环境变量来访问原始值。


今后


Cython不是为解决我的用例而构建的,但是Nuitka是。我确信如果有更多的时间,我就可以使Nuitka编译过程正常运行。


然而,PyInstaller再次解决了我所有的问题,所以这是我选择的解决方案。我已经连续几周在自动化程序中反复使用它,没有出现任何问题。


PyOxidizer很有前途。它的文档非常好,甚至包括与其他工具的比较,其中简要介绍了每种工具及其差异。如果你仅仅只知道所有不同的可能性的名字的话,那你应该看一下它。


英文原文:https://tryexceptpass.org/article/package-python-as-executable/ 
译者:测试
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/37804
 
524 次点击