因为工作需要,需要把一个Python脚本打包封装后在公司内推广,原先以为代码写完,功能正常就完事了,没想到Python跨平台打包是一件很麻烦的事情。

公司内部同事有用Linux,有用Mac OS的,还有大量Windows用户,所以需要Python跨平台打包。

最初调研的时候,确定了几个被选工具,py2exe,PyinstallerCx_freeze.后来又在google上搜索到了Nuitka也可以完成python打包的任务。

Pyinstaller和Nuitka都号称跨平台,但其实顶多只能算是工具本身跨平台,实际体验中不仅打包产生的文件不能跨平台,能否成功打包本身也不确定。

本篇博文主要总结下在调研使用以上工具中遇到的坑和解决方法。

先放结论,如果对安全性和速度要求不是那么高,推荐使用Pyinstaller,而不是Nuitka,具体原因会在下文给出。

常见Python打包工具汇总比较

上面的网址给出了常见打包工具的简单比较,从参数上来说真正可以做到“跨平台”的只有bbFreeze,cx_Freeze,Pyinstaller这几款。以及可以算作编译器的Nuitka。

Nuitka

Nuitka直接将python编译成C++代码 ,再编译C++代码产生可执行文件,完全不存在反向解析的问题,非常安全,而且由于可执行文件由C++编译而来,运行速度也会获得提升。

但是在实际体验Nuitka的过程中发现了很多问题.

使用Nuitka的具体指令如下

nuitka --standalone --nofreeze-stdlib gclt.py --output-dir=/home/why/python/gcli_executable

打包完成后在本地Ubuntu 16.04 LTS 下运行一切正常,但是到了一台CentOS 6的服务器上执行就会产生下面的错误

gclt: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by /root/gclt/libz.so.1)

可能是因为本地打包时使用的C++编译器比服务器上的新引起了不兼容的问题,毕竟CentOS 6已经很古旧了。

那如果在服务器上打包,能在本地正常运行吗,本地的新包应该能兼容旧版本的库?
由于服务器上自带Python 2.6,先使用Virtualenv
事实证明不行,服务器上打包后的文件在本地运行会遇到如下的错误:

Traceback (most recent call last):
  File "/home/why/gclt.dist/clt_hdfs.py", line 71, in <module>
  File "/home/why/gclt.dist/clt_hdfs.py", line 51, in execute
  File "/home/why/gclt.dist/requests_client.py", line 101, in post_hdfs_async
  File "/home/why/gclt.dist/requests_client.py", line 20, in post_auth_json_request
  File "/home/why/gclt.dist/requests/api.py", line 110, in post
  File "/home/why/gclt.dist/requests/api.py", line 56, in request
  File "/home/why/gclt.dist/requests/sessions.py", line 471, in request
  File "/home/why/gclt.dist/requests/sessions.py", line 581, in send
  File "/home/why/gclt.dist/requests/adapters.py", line 481, in send

requests.exceptions.ConnectionError: HTTPConnectionPool(host='gclt_test.udp.10101111.com', port=80): Max retries exceeded with url: /clt_server/api/v1.0/hdfs (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7f9bb293e8d0>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution',))

关于这个错误网上基本查不到资料,查看报错源码sessions.py 第581行是如下代码,

r = adapter.send(request, **kwargs)

报错中提到的网址 gclt_test.udp.10101111.com 通过未打包的代码和在服务器上运行打包后的可执行文件都可以正确访问,所以这个问题就很吊诡了。
至今也没有发现解决的方法。

Nuitka的关键问题还不在于此,在执行上面的打包命令时,由于短时间大量输出,所以并没有注意输出中夹杂的报错或者警告信息,实际上输出中是有很多警告信息的:

Nuitka:WARNING:/home/hadoop/code/trunk/client/ENV/lib/python2.7/site-packages/poster/encode.py:29: Cannot find 'email.Header' in package 'poster' as relative or absolute import.

Nuitka:WARNING:/home/hadoop/code/trunk/client/ENV/lib/python2.7/site-packages/requests/compat.py:58: Cannot find 'http.cookies' in package 'requests' as relative or absolute import. 

........

类似上面的警告信息还有很多,基本都是同样的格式,如下:

Cannot find 'http.cookies' in package 'requests' as relative or absolute import.

实际上这个问题的起因是Nuitka只支持在代码开头import别的库,默认并不支持动态_import_()这种方式,而Python由于存在2.x版本和3.x版本的兼容性问题,在很多Python库中都使用了SWIG这种方式来基于当前系统动态引入对应版本号的包,但是理论上如果使用的是Python2.7,Nuitka编译时找不到动态引入的Python 3.x包,应该不会对最后打包产生的可执行文件产生影响啊?

本着强迫症追求完美的想法,我注释了所有警告信息对应代码,然后又出现了如下错误:

Indentation error

可能是因为注释掉的代码有些属于try:catch代码块。

到这一步我已经决定放弃使用Nuitka,纵使它安全高效。其实高效也只是指产生的可执行文件运行速度会快,实际编译时需要近20分钟,在之后使用Pyinstaller时,则只需要几十秒,考虑到我的脚本中并没有什么复杂逻辑,并没有Nuitka的用武之地。

Pyinstaller

相比较Nuitka,Pyinstaller真的是好用太多
简单的使用

pyinstaller myscript.py

即可得到可执行文件,虽然也存在本机编译后的文件不能在服务器执行的问题:

gclt: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by /root/gclt/libz.so.1)

但是在服务器上编译的代码是可以在本机成功执行,当然了,所谓的跨平台,其实是个伪命题,Linux下编译产生的可执行文件并不能在Windows和Mac OS下执行,前者会产生类似下面的错误(使用Nuitka时也是这样)

Too big to fit in memory

而在Mac OS下则会直接提示找不到相关可执行文件。

最后,分别在Linux,Windows,Mac OS下分别使用Pyinstaller完成任务。

经历过这次打包风波,我强烈建议大家在没有特殊需求时使用Pyinstaller而不是Nuitka,虽然Nuitka的功能感觉很高大上,先转译为C++再编译为可执行,但是这风骚的功能背后,不仅增加了打包时长,还可能带来很多一想不到的问题。

如故再给我一次机会,脚本应该用Java来写,虽然写起来麻烦点,但是跨平台支持比Python好太多了。