Linux环境下使用Pyinstaller打包Django项目

1 环境

Linux环境:CentOS7
Python 3.7.4
PyInstaller 3.6 及其依赖包:setuptools altgraph dis3
项目代码:django 2.1 项目,存放路径:/mypath/hello/

[root@localhost mypath]# tree
.
└── hello
    ├── db.sqlite3
    ├── hello
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── hooks
    │   ├── hook-django_redis.py
    │   ├── hook-my_package.py
    │   └── hook-rest_framework.py
    ├── manage.py
    ├── my_package
    │   ├── __init__.py
    │   ├── sub_package
    │   │   ├── __init__.py
    │   │   ├── mgr_package
    │   │   │   ├── __init__.py
    │   │   │   └── test_mgr.py
    │   │   ├── test_mgr.py
    │   │   ├── urls.py
    │   │   └── views.py
    │   ├── urls.py
    │   └── views.py
    └── templates
        └── base_template.html

7 directories, 19 files

2 使用方式

(一)不安装,直接解压,当成脚本使用,可以自由指定打包使用的解释器,解释器上必须安装了依赖包

# tar -xzvf /usr/.../PyInstaller-3.6.tar.gz
# python3 /usr/.../PyInstaller-3.6/pyinstaller.py -F /usr/.../hello/mange.py

(二)安装,当成命令使用,装到了哪个解释器上就只能用该解释器打包,参数-F表示打包成单个可执行文件

[root@localhost ~]# pip3 install /usr/.../PyInstaller-3.6.tar.gz
[root@localhost ~]# cd /mypath/hello/
[root@localhost hello]# pyinstaller -F manage.py
66 INFO: PyInstaller: 3.6
67 INFO: Python: 3.7.4
67 INFO: Platform: Linux-3.10.0-1062.12.1.el7.x86_64-x86_64-with-centos-7.7.1908-Core
68 INFO: wrote /mypath/hello/manage.spec
70 INFO: UPX is not available.
72 INFO: Extending PYTHONPATH with paths
['/mypath/hello', '/mypath/hello']
72 INFO: checking Analysis
72 INFO: Building Analysis because Analysis-00.toc is non existent
72 INFO: Initializing module dependency graph...
73 INFO: Caching module graph hooks...
79 INFO: Analyzing base_library.zip ...
3574 INFO: Caching module dependency graph...
3678 INFO: running Analysis Analysis-00.toc
3700 INFO: Analyzing /mypath/hello/manage.py
3749 INFO: Processing pre-find module path hook   distutils
3750 INFO: distutils: retargeting to non-venv dir '/usr/local/python3/lib/python3.7'
8421 INFO: Processing module hooks...
8421 INFO: Loading module hook "hook-django.core.mail.py"...
8487 INFO: Loading module hook "hook-django.py"...
Traceback (most recent call last):
  File "<string>", line 41, in <module>
  File "<string>", line 36, in walk_packages
  File "<string>", line 36, in walk_packages
  File "<string>", line 20, in walk_packages
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/admin/__init__.py", line 5, in <module>
    from django.contrib.gis.admin.options import GeoModelAdmin, OSMGeoAdmin
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/admin/options.py", line 2, in <module>
    from django.contrib.gis.admin.widgets import OpenLayersWidget
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/admin/widgets.py", line 3, in <module>
    from django.contrib.gis.gdal import GDALException
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/gdal/__init__.py", line 28, in <module>
    from django.contrib.gis.gdal.datasource import DataSource
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/gdal/datasource.py", line 39, in <module>
    from django.contrib.gis.gdal.driver import Driver
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/gdal/driver.py", line 5, in <module>
    from django.contrib.gis.gdal.prototypes import ds as vcapi, raster as rcapi
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/gdal/prototypes/ds.py", line 9, in <module>
    from django.contrib.gis.gdal.libgdal import GDAL_VERSION, lgdal
  File "/usr/local/python3/lib/python3.7/site-packages/django/contrib/gis/gdal/libgdal.py", line 43, in <module>
    % '", "'.join(lib_names)
django.core.exceptions.ImproperlyConfigured: Could not find the GDAL library (tried "gdal", "GDAL", "gdal2.2.0", "gdal2.1.0", "gdal2.0.0", "gdal1.11.0", "gdal1.10.0", "gdal1.9.0"). Is GDAL installed? If it is, try setting GDAL_LIBRARY_PATH in your settings.
9553 INFO: Determining a mapping of distributions to packages...
18246 INFO: Packages required by django:
['pytz']
18246 INFO: Django root directory /mypath/hello/hello
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/utils/hooks/subproc/django_import_finder.py", line 39, in <module>
    list(settings.TEMPLATE_LOADERS) + \
  File "/usr/local/python3/lib/python3.7/site-packages/django/conf/__init__.py", line 58, in __getattr__
    val = getattr(self._wrapped, name)
AttributeError: 'Settings' object has no attribute 'TEMPLATE_CONTEXT_PROCESSORS'
18766 INFO: Collecting Django migration scripts.
23531 INFO: Processing pre-safe import module hook   setuptools.extern.six.moves
23941 INFO: Processing pre-find module path hook   site
23942 INFO: site: retargeting to fake-dir '/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/fake-modules'
27219 INFO: Loading module hook "hook-xml.etree.cElementTree.py"...
27220 INFO: Loading module hook "hook-pkg_resources.py"...
27725 INFO: Processing pre-safe import module hook   win32com
27731 INFO: Excluding import '__main__'
27732 INFO:   Removing import of __main__ from module pkg_resources
27733 INFO: Loading module hook "hook-xml.py"...
27734 INFO: Loading module hook "hook-numpy.py"...
27734 INFO: Loading module hook "hook-django.core.cache.py"...
27845 INFO: Loading module hook "hook-sysconfig.py"...
27857 INFO: Loading module hook "hook-encodings.py"...
27912 INFO: Loading module hook "hook-pytz.py"...
27937 INFO: Loading module hook "hook-setuptools.py"...
28485 INFO: Loading module hook "hook-pydoc.py"...
28486 INFO: Loading module hook "hook-jinja2.py"...
28504 INFO: Loading module hook "hook-django.db.backends.py"...
29109 WARNING: Hidden import "django.db.backends.__pycache__.base" not found!
29220 INFO: Loading module hook "hook-distutils.py"...
29222 INFO: Loading module hook "hook-xml.dom.domreg.py"...
29223 INFO: Loading module hook "hook-django.core.management.py"...
29234 INFO: Import to be excluded not found: 'tkinter'
29234 INFO: Import to be excluded not found: 'matplotlib'
29234 INFO: Import to be excluded not found: 'IPython'
29236 INFO: Loading module hook "hook-lib2to3.py"...
29238 INFO: Loading module hook "hook-numpy.core.py"...
29347 INFO: Loading module hook "hook-sqlite3.py"...
29400 INFO: Loading module hook "hook-django.db.backends.oracle.base.py"...
29402 WARNING: Hidden import "django.db.backends.oracle.compiler" not found!
29402 INFO: Loading module hook "hook-django.db.backends.mysql.base.py"...
29477 INFO: checking Tree
29477 INFO: Building Tree because Tree-00.toc is non existent
29477 INFO: Building Tree Tree-00.toc
29492 INFO: Looking for ctypes DLLs
29659 INFO: Analyzing run-time hooks ...
29676 INFO: Including run-time hook 'pyi_rth_pkgres.py'
29678 INFO: Including run-time hook 'pyi_rth_multiprocessing.py'
29682 INFO: Including run-time hook 'pyi_rth_django.py'
29704 INFO: Looking for dynamic libraries
30355 INFO: Looking for eggs
30355 INFO: Using Python library /usr/local/python3/lib/libpython3.7m.so.1.0
30374 INFO: Warnings written to /mypath/hello/build/manage/warn-manage.txt
30523 INFO: Graph cross-reference written to /mypath/hello/build/manage/xref-manage.html
30789 INFO: checking PYZ
30790 INFO: Building PYZ because PYZ-00.toc is non existent
30790 INFO: Building PYZ (ZlibArchive) /mypath/hello/build/manage/PYZ-00.pyz
32363 INFO: Building PYZ (ZlibArchive) /mypath/hello/build/manage/PYZ-00.pyz completed successfully.
32416 INFO: checking PKG
32416 INFO: Building PKG because PKG-00.toc is non existent
32416 INFO: Building PKG (CArchive) PKG-00.pkg
59730 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
59950 INFO: Bootloader /usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/bootloader/Linux-64bit/run
59950 INFO: checking EXE
59950 INFO: Building EXE because EXE-00.toc is non existent
59950 INFO: Building EXE from EXE-00.toc
59951 INFO: Appending archive to ELF section in EXE /mypath/hello/dist/manage
60035 INFO: Building EXE from EXE-00.toc completed successfully.

最后两句说明打包完成,生成了一个可执行文件,存放在/mypath/hello/dist/manage目录下。可以看到日志中有几条WARNING和报错信息,第7节会分析。

3 打包失败

如果日志没有走到最后两句,说明打包失败,可能是遇到了以下报错:

3.1 未找到libpython3.7.so.1.0等库文件

OSError: Python library not found: libpython3.7.so.1.0, libpython3.7m.so.1.0, libpython3.7mu.so.1.0

This would mean your Python installation doesn't come with proper library files.
This usually happens by missing development package, or unsuitable build parameters of Python installation.

* On Debian/Ubuntu, you would need to install Python development packages
  * apt-get install python3-dev
  * apt-get install python-dev
* If you're building Python by yourself, please rebuild your Python with `--enable-shared` (or, `--enable-framework` on Darwin)

解决思路(一)
先检索以下系统中是否已有这几个文件:libpython3.7.so.1.0, libpython3.7m.so.1.0, libpython3.7mu.so.1.0

[root@localhost ~]# find / -name libpython*

如果能找到,直接复制过去就行。

[root@localhost ~]# cp /....../libpython3.7.so.1.0  /usr/lib

解决思路(二)
重新编译python解释器,增加--enable-shared选项。

解决思路(三)
安装python3-devel-3.7的包。Debian/Ubuntu下包名为python3-dev,Centos下包名为python3-devel,注意选择3.7的版本。CentOS7的环境中基本上只能选择通过外网yum安装,因为CentOS7 Everything的ISO镜像源中提供的版本最高是3.6,而这个包的依赖层又非常多。

3.2 error while loading shared libraries

有libpython*.so文件,但是error while loading shared libraries报错

[root@localhost ~]# find / -name libpython*
/usr/local/python3/lib/libpython3.so
/usr/local/python3/lib/libpython3.7m.so.1.0
/usr/local/python3/lib/libpython3.7m.so
[root@localhost ~]# vim /etc/ld.so.conf
include ld.so.conf.d/*.conf
/usr/local/lib
/usr/local/python3/lib
[root@localhost ~]# ldconfig

4 项目启动失败

打包完成后,在当前目录中生成了2个子目录distbuild,1个文件manage.spec。其中,dist下有一个可执行文件manage,授予其可执行权限即可启动项目。其他都是打包过程的中间产物,可删除。

[root@localhost hello]# cd dist
[root@localhost dist]# chmod a+x manage
[root@localhost dist]# ./manage runserver 0.0.0.0:8888
Performing system checks...

Unhandled exception in thread started by <function check_errors.<locals>.wrapper at 0x7f9bb41d4680>
Traceback (most recent call last):
  File "site-packages/django/utils/autoreload.py", line 225, in wrapper
  File "/tmp/_MEIWComW3/django/core/management/commands/runserver.py", line 117, in inner_run
    self.check(display_num_errors=True)
  File "site-packages/django/core/management/base.py", line 379, in check
  File "site-packages/django/core/management/base.py", line 366, in _run_checks
  File "site-packages/django/core/checks/registry.py", line 71, in run_checks
  File "site-packages/django/core/checks/urls.py", line 40, in check_url_namespaces_unique
  File "site-packages/django/core/checks/urls.py", line 57, in _load_all_namespaces
  File "site-packages/django/utils/functional.py", line 37, in __get__
  File "site-packages/django/urls/resolvers.py", line 533, in url_patterns
  File "site-packages/django/utils/functional.py", line 37, in __get__
  File "site-packages/django/urls/resolvers.py", line 526, in urlconf_module
  File "importlib/__init__.py", line 127, in import_module
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/loader/pyimod03_importers.py", line 623, in exec_module
    exec(bytecode, module.__dict__)
  File "hello/urls.py", line 21, in <module>
  File "site-packages/django/urls/conf.py", line 34, in include
  File "importlib/__init__.py", line 127, in import_module
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'my_package'

4.1 报错找不到自己写的my_package

项目启动失败,报错找不到我们自己写的my_package,是因为我们自己写的项目没有作为一个第三方包安装解释器上,即在路径/usr/local/python3/lib/python3.7/site-packages/下找不到my_package
解决思路(一)
为项目写一个setup.py,把我们自己的项目作为一个第三方包安装到解释器上。

解决思路(二)
在打包的时候使用参数--hidden-import告诉Pyinstaller项目中有一个隐式导入。参数--clean清除之前打包产生的缓存和临时文件。

[root@localhost hello]# pyinstaller --clean -F manage.py --hidden-import my_package

日志中会出现一行:

INFO: Analyzing hidden import 'my_package'

再次运行,发现还是报错,但是报错内容变了,找不到my_package.urls

[root@localhost hello]# .//dist/manage runserver 0.0.0.0:8888
Performing system checks...

Unhandled exception in thread started by <function check_errors.<locals>.wrapper at 0x7f95afc2d680>
Traceback (most recent call last):
  File "site-packages/django/utils/autoreload.py", line 225, in wrapper
  File "/tmp/_MEIf9QhvP/django/core/management/commands/runserver.py", line 117, in inner_run
    self.check(display_num_errors=True)
  File "site-packages/django/core/management/base.py", line 379, in check
  File "site-packages/django/core/management/base.py", line 366, in _run_checks
  File "site-packages/django/core/checks/registry.py", line 71, in run_checks
  File "site-packages/django/core/checks/urls.py", line 40, in check_url_namespaces_unique
  File "site-packages/django/core/checks/urls.py", line 57, in _load_all_namespaces
  File "site-packages/django/utils/functional.py", line 37, in __get__
  File "site-packages/django/urls/resolvers.py", line 533, in url_patterns
  File "site-packages/django/utils/functional.py", line 37, in __get__
  File "site-packages/django/urls/resolvers.py", line 526, in urlconf_module
  File "importlib/__init__.py", line 127, in import_module
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/loader/pyimod03_importers.py", line 623, in exec_module
    exec(bytecode, module.__dict__)
  File "hello/urls.py", line 21, in <module>
    path('', include('my_package.urls'))
  File "site-packages/django/urls/conf.py", line 34, in include
  File "importlib/__init__.py", line 127, in import_module
  File "importlib/__init__.py", line 127, in import_module
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'my_package.urls'

解决思路(一)
打包的时候再增加一个参数--hidden-import,这种方法的缺点是如果my_package包含了很多模块,则每个模块都要加一个--hidden-import参数。

[root@localhost hello]# pyinstaller -F manage.py --hidden-import my_package --hidden-import my_package.urls

解决思路(二)
如果my_package包含了很多模块,则更简便的方法是写一个钩子文件,一次找出my_package中的所有子包及模块。使用参数--additional-hooks-dir把钩子文件所在目录告诉Pyinstaller。
注意:
1.钩子文件必须严格按照hook-包名.py,或者hook-包名.模块名.py的格式命名。
2.必须同时使用参数--hidden-import把包名传入,Pyinstaller才会去加载对应的钩子文件。

[root@localhost hello]# mkdir hooks
[root@localhost hello]# vim hooks/hook-my_package.py

from PyInstaller.utils.hooks import collect_submodules
hiddenimports = collect_submodules('my_package')
print('hiddenimports: %s' % hiddenimports)

[root@localhost hello]# pyinstaller --clean -F manage.py --hidden-import my_package --additional-hooks-dir /mypath/hello/hooks

日志中会出现一行:

INFO: Loading module hook "hook-my_package.py"...

由于在钩子文件中加了打印语句,日志中还能够看到:

hiddenimports: ['my_package.sub_package.urls', 'my_package.views', 'my_package.sub_package.test_mgr', 'my_package.sub_package.views', 'my_package.urls', 'my_package.sub_package.mgr_package.test_mgr', 'my_package.sub_package.mgr_package', 'my_package.sub_package', 'my_package']

4.2 报错找不到rest_framework.authentication

再次运行,出现了新的报错,找不到rest_framework.authentication,是因为打包过程中的一个报错导致的(第7.4节分析了原因)。

[root@localhost hello]# .//dist/manage runserver 0.0.0.0:8007
Performing system checks...

Unhandled exception in thread started by <function check_errors.<locals>.wrapper at 0x7f4c0e6957a0>
Traceback (most recent call last):
  File "site-packages/rest_framework/settings.py", line 177, in import_from_string
  File "site-packages/django/utils/module_loading.py", line 17, in import_string
  File "importlib/__init__.py", line 127, in import_module
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'rest_framework.authentication'

同样的解决思路:增加--hidden-import和钩子文件。

[root@localhost hello]# vim hooks/-rest_framework.py

from PyInstaller.utils.hooks import collect_submodules
hiddenimports = collect_submodules('rest_framework')
print('hiddenimports: %s' % hiddenimports)

[root@localhost hello]# pyinstaller --clean -F manage.py --hidden-import my_package --hidden-import rest_framework --additional-hooks-dir /mypath/hello/hooks

4.3 报错找不到django_redis.serializers

如果项目中使用了django_redis包,并且也对应增加了参数--hidden-import和钩子文件,但是运行时发现报错:找不到django_redis.serializers

ModuleNotFoundError: No module named 'django_redis.serializers'

查看hooke-django_redis.py中打印的hiddenimports,发现没有django_redis.serializersdjango_redis.compressors这两个子包,这也是因为打包过程中的一个报错导致的(第7.4节分析了原因)。

INFO: Loading module hook "hook-django_redis.py"...
hiddenimports: ['django_redis', 'django_redis.cache', 'django_redis.client']

解决方法是为django_redis.serializersdjango_redis.compressors这两个模块也对应增加参数--hidden-import和钩子文件。

5 项目能启动,打不开页面

再运行一下,发现项目能启动了

[root@localhost hello]# .//dist/manage runserver 0.0.0.0:8888                       
Performing system checks...

System check identified no issues (0 silenced).


June 15, 2020 - 03:45:43
Django version 2.1, using settings 'hello.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C.

访问一个页面试试,发现报错TemplateDoesNotExist: base_template.html,这是因为Pyinstaller无法自动发现html等非py文件。

Internal Server Error: /test/
Traceback (most recent call last):
  File "site-packages/django/core/handlers/exception.py", line 34, in inner
  File "site-packages/django/core/handlers/base.py", line 126, in _get_response
  File "site-packages/django/core/handlers/base.py", line 124, in _get_response
  File "site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
  File "site-packages/django/views/generic/base.py", line 68, in view
  File "site-packages/rest_framework/views.py", line 505, in dispatch
  File "site-packages/rest_framework/views.py", line 465, in handle_exception
  File "site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
  File "site-packages/rest_framework/views.py", line 502, in dispatch
  File "my_package/views.py", line 11, in get
    return render(request, 'base_template.html', context)
  File "site-packages/django/shortcuts.py", line 36, in render
  File "site-packages/django/template/loader.py", line 61, in render_to_string
  File "site-packages/django/template/loader.py", line 19, in get_template
django.template.exceptions.TemplateDoesNotExist: base_template.html
[15/Jun/2020 03:45:48] "GET /test/ HTTP/1.1" 500 76244

解决方法
通过参数--add-data把存放base_template.html的路径告诉Pyinstaller。
注意:
1.参数--add-data的值由两部分组成,冒号左侧是文件当前实际存放的目录,冒号右侧是希望把该文件打包到可执行文件内部哪个目录下,项目运行时将从该目录下读取文件,采用相对路径。
2.在Linux上冒号是分隔符,在Windows上分号是分隔符。

[root@localhost hello]# pyinstaller --clean -F manage.py --hidden-import my_package --hidden-import rest_framework --additional-hooks-dir /mypath/hello/hooks --add-data /mypath/hello/templates/:./templates/

日志中会出现一行:

INFO: Appending 'datas' from .spec

6 找不到二进制文件

如果项目中使用了或依赖于*.dll*.dyliblib*.so等二进制文件,一般来讲Pyinstaller会自动搜索到。
例如,在日志中可以看到:

INFO: Looking for ctypes DLLs
INFO: Looking for dynamic libraries
INFO: Using Python library /usr/local/python3/lib/libpython3.7m.so.1.0

但有时可能会搜索不到,则需要使用参数--add-binary把存放二进制文件的路径告诉Pyinstaller。使用方法与参数--add-data相同。

7 能打包成功,但是有几条WARNING和报错信息

7.1 找不到GDAL库

INFO: Loading module hook "hook-django.py"...
....
django.core.exceptions.ImproperlyConfigured: Could not find the GDAL library (tried "gdal", "GDAL", "gdal2.2.0", "gdal2.1.0", "gdal2.0.0", "gdal1.11.0", "gdal1.10.0", "gdal1.9.0"). Is GDAL installed? If it is, try setting GDAL_LIBRARY_PATH in your settings.

GDAL是地理空间数据抽象库,Django框架通过django.contrib.gis包提供GIS(Geographic Information Systems)支持的扩展,该包需要调用GDAL库。举个例子,它允许你的Django模型保存地理学数据并执行地理学查询。这个报错是在加载Pyinstaller自带的钩子文件hook-django.py时打印的,该钩子文件会尝试把Django的所有子包都打包到可执行文件中。只要确认我们的项目中不需要使用gis包,就可以忽略这个报错。

7.2 Settings没有TEMPLATE_CONTEXT_PROCESSORS属性

INFO: Loading module hook "hook-django.py"...
INFO: Django root directory /mypath/hello/hello
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/utils/hooks/subproc/django_import_finder.py", line 39, in <module>
    list(settings.TEMPLATE_LOADERS) + \
  File "/usr/local/python3/lib/python3.7/site-packages/django/conf/__init__.py", line 58, in __getattr__
    val = getattr(self._wrapped, name)
AttributeError: 'Settings' object has no attribute 'TEMPLATE_CONTEXT_PROCESSORS'
INFO: Collecting Django migration scripts.

这个报错是在加载Pyinstaller自带的钩子文件hook-django.py时打印的,该钩子文件内部经过层层调用,走到django_import_finder.py的39行报错了。查看发现该文件固定了几个隐式导入。

hiddenimports = list(settings.INSTALLED_APPS) + \
                 list(settings.TEMPLATE_CONTEXT_PROCESSORS) + \
                 list(settings.TEMPLATE_LOADERS) + \
                 [settings.ROOT_URLCONF]

其中settings.TEMPLATE_CONTEXT_PROCESSORSsettings.TEMPLATE_LOADERS是Django 1.7 即以前版本的模板配置项,从Django 1.8 版本开始修改了。删掉这两行,发现新的报错,找不到django.core.resolvers

INFO: Loading module hook "hook-django.py"...
INFO: Django root directory /mypath/hello/hello
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/utils/hooks/subproc/django_import_finder.py", line 104, in <module>
    from django.core.resolvers import URLPattern as RegexURLPattern
ModuleNotFoundError: No module named 'django.core.resolvers'

这个报错也是因为Pyinstaller没有针对Django版本差异更新钩子文件造成的。解决方法:把报错django_import_finder.py的104行修改为:

from django.urls.resolvers import URLPattern as RegexURLPattern
from django.urls.resolvers import URLResolver as RegexURLResolver

再次打包,程序继续在django_import_finder.py中往下走,出现了几条新的WARNING

WARNING: Hidden import "django.contrib.auth.templatetags" not found!
WARNING: Hidden import "rest_framework.context_processors" not found!
WARNING: Hidden import "django.contrib.messages.templatetags" not found!
WARNING: Hidden import "django.contrib.sessions.templatetags" not found!
WARNING: Hidden import "django.contrib.sessions.context_processors" not found!
WARNING: Hidden import "django.contrib.admin.context_processors" not found!
WARNING: Hidden import "django.contrib.contenttypes.templatetags" not found!
WARNING: Hidden import "django.contrib.staticfiles.context_processors" not found!
WARNING: Hidden import "django.contrib.contenttypes.context_processors" not found!

查看一下django_import_finder.py,发现第97行固定为INSTALLED_APPS中的每个app都添加了两个隐式导入:.templatetags.context_processors

# Add templatetags and context processors for each installed app.
for app in settings.INSTALLED_APPS:
    app_templatetag_module = app + '.templatetags'
    app_ctx_proc_module = app + '.context_processors'
    hiddenimports.append(app_templatetag_module)
    hiddenimports += collect_submodules(app_templatetag_module)
    hiddenimports.append(app_ctx_proc_module)

而新增的这些WARNING说明我们采用的Django 2.1中并没有这些模块,自然会提示找不到。这些WARNING同样也是因为Pyinstaller没有针对Django版本差异更新钩子文件造成的,忽略即可。

7.3 找不到django.db.backends.pycache.base

INFO: Loading module hook "hook-django.db.backends.py"...
WARNING: Hidden import "django.db.backends.__pycache__.base" not found!

这个WARNING是在加载Pyinstaller自带的钩子文件hook-django.db.backends.py时打印的,查看一下发现该钩子文件是采用组装路径的方式查找隐式导入的,导致把Python解释器的缓存目录__pycache__也当成包了。可以选择修改这个钩子文件,也可以直接忽略这条WARNING。

[root@localhost hello]# find / -name hook-django.db.backends.py
/usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/hooks/hook-django.db.backends.py
[root@localhost hello]# cat /usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/hooks/hook-django.db.backends.py
import os
import glob

from PyInstaller.utils.hooks import get_module_file_attribute

# Compiler (see class BaseDatabaseOperations)
hiddenimports = ['django.db.models.sql.compiler']

# Include all available Django backends.
modpath = os.path.dirname(get_module_file_attribute('django.db.backends'))
for fn in glob.glob(os.path.join(modpath, '*')):
    if os.path.isdir(fn):
        fn = os.path.basename(fn)
        hiddenimports.append('django.db.backends.' + fn + '.base')

7.4 找不到django.db.backends.oracle.compiler

INFO: Loading module hook "hook-django.db.backends.oracle.base.py"...
WARNING: Hidden import "django.db.backends.oracle.compiler" not found!

这个WARNING是在加载Pyinstaller自带的钩子文件django.db.backends.oracle.compiler时打印的,查看一下发现该钩子文件中固定了一个隐式导入,而这个模块在Django 2.1中是没有的,自然找不到。可以选择删除这个钩子文件,也可以直接忽略这条WARNING。

[root@localhost hello]# find / -name hook-django.db.backends.oracle.base.py                                     /usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/hooks/hook-django.db.backends.oracle.base.py
[root@localhost hello]# cat /usr/local/python3/lib/python3.7/site-packages/PyInstaller-3.6-py3.7.egg/PyInstaller/hooks/hook-django.db.backends.oracle.base.py
hiddenimports = ["django.db.backends.oracle.compiler"]
[root@localhost hello]# ll /usr/local/python3/lib/python3.7/site-packages/django/db/backends/oracle
total 116
-rw-r--r--. 1 root root 23101 Jun 12 11:22 base.py
-rw-r--r--. 1 root root   331 Jun 12 11:22 client.py
-rw-r--r--. 1 root root 18751 Jun 12 11:22 creation.py
-rw-r--r--. 1 root root  2052 Jun 12 11:22 features.py
-rw-r--r--. 1 root root   768 Jun 12 11:22 functions.py
-rw-r--r--. 1 root root     0 Jun 12 11:22 __init__.py
-rw-r--r--. 1 root root 10485 Jun 12 11:22 introspection.py
-rw-r--r--. 1 root root 24643 Jun 12 11:22 operations.py
drwxr-xr-x. 2 root root  4096 Jun 12 11:22 __pycache__
-rw-r--r--. 1 root root  7501 Jun 12 11:22 schema.py
-rw-r--r--. 1 root root  1489 Jun 12 11:22 utils.py
-rw-r--r--. 1 root root   860 Jun 12 11:22 validation.py

7.5 django未正确配置

INFO: Loading module hook "hook-django_redis.py"...
Traceback (most recent call last):
  File "<string>", line 41, in <module>
  File "<string>", line 20, in walk_packages
  File "/usr/local/python3/lib/python3.7/site-packages/django_redis/client/__init__.py", line 2, in <module>
    from .herd import HerdClient
  File "/usr/local/python3/lib/python3.7/site-packages/django_redis/client/herd.py", line 23, in <module>
    CACHE_HERD_TIMEOUT = getattr(settings, 'CACHE_HERD_TIMEOUT', 60)
  File "/usr/local/python3/lib/python3.7/site-packages/django/conf/__init__.py", line 57, in __getattr__
    self._setup(name)
  File "/usr/local/python3/lib/python3.7/site-packages/django/conf/__init__.py", line 42, in _setup
    % (desc, ENVIRONMENT_VARIABLE))
django.core.exceptions.ImproperlyConfigured: Requested setting CACHE_HERD_TIMEOUT, but settings are not configured. You must eitheLE or call settings.configure() before accessing settings.
hiddenimports: ['django_redis.cache', 'django_redis.client', 'django_redis']

这个报错是在加载我们自定义的钩子文件hook-django_redis.py时出现的,该钩子文件会导入子包django_redis.clientherd.py模块,而这个模块内有一个顶级的全局变量CACHE_HERD_TIMEOUT,导入该模块时会运行这一行代码,进入到django,被django捕获到未正确配置。由于我们不是通过正常的django项目启动方式走到django/conf/__init__.py的,当然也就读不到我们在settings.py中的配置项。基本上在加载钩子文件时出现django.core.exceptions.ImproperlyConfigured的报错都是这个原因。从最后一行看到我们钩子文件中打印的hiddenimports只有3个,而实际上django_redis的导入远不止这3个,说明程序走到这里报错退出,无法查找到django_redis的其他导入。遇到这种情况,只能为那些没有导入的子包单独增加参数--hidden-import和钩子文件。例如:为django_redis.serializers,django_redis.compressors这两个模块也对应增加参数–hidden-import和钩子文件。

另外一个比较容易出现django未正确配置的包是rest_framework

django.core.exceptions.ImproperlyConfigured: Requested setting REST_FRAMEWORK, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.
hiddenimports: ['rest_framework.generics', 'rest_framework.permissions', 'rest_framework.filters', 'rest_framework.documentation', 'rest_framework.authtoken.models', 'rest_framework.reverse', 'rest_framework.routers', 'rest_framework.authtoken.views', 'rest_framework.authtoken.migrations', 'rest_framework.request', 'rest_framework.authtoken.migrations.0001_initial', 'rest_framework.negotiation', 'rest_framework.parsers', 'rest_framework.management', 'rest_framework.renderers', 'rest_framework.checks', 'rest_framework.management.commands', 'rest_framework.decorators', 'rest_framework.management.commands.generateschema', 'rest_framework.authtoken.management.commands', 'rest_framework.fields', 'rest_framework.schemas', 'rest_framework', 'rest_framework.authtoken.admin', 'rest_framework.apps', 'rest_framework.compat', 'rest_framework.authtoken', 'rest_framework.authtoken.management', 'rest_framework.authentication', 'rest_framework.metadata', 'rest_framework.response', 'rest_framework.authtoken.apps', 'rest_framework.pagination', 'rest_framework.mixins', 'rest_framework.exceptions', 'rest_framework.authtoken.serializers', 'rest_framework.authtoken.migrations.0002_auto_20160226_1747', 'rest_framework.authtoken.management.commands.drf_create_token', 'rest_framework.relations']

从钩子文件中打印的hiddenimports来看,似乎已经导入了很多子包,但是对照rest_framework包源码的目录来看,还是缺少了templatetagsutils两个子包,以及根目录下的一些模块。由于有些模块和子包我们的项目中可能没有用到,为减少工作量,直接运行打包好的可执行文件,全面测试一遍我们的项目,如果项目运行中遇到ModuleNotFoundError,再对应增加参数--hidden-import和钩子文件。

8 选项参数

8.1 常规选项

-h, --help显示帮助消息。
-v, --version显示程序版本信息。
--distpath DIR指定生成的独立可执行文件的存放路径(默认值:./dist,在当前目录下自动创建一个dist子目录,生成的可执行文件存放在这里)。
--workpath WORKPATH指定打包过程中产生的所有临时工作文件的存放路径(默认值:./build,在当前目录下自动创建一个build子目录),打包完成后该目录下的文件都可删除。
-y, --noconfirm 当输出目录下存在同名文件或目录时,不要询问,直接覆盖。
--upx-dir UPX_DIR指定UPX工具所在的路径(默认值:在打包的执行路径下搜索)。如果未使用此选项,且默认路径下也搜索不到UPX工具,则不对可执行文件进行压缩。UPX (the Ultimate Packer for eXecutables)是一款先进的可执行程序文件压缩器,压缩过的可执行文件体积缩小50%-70% ,这样减少了磁盘占用空间、网络上传下载的时间和其它分布以及存储费用。
-a, --ascii无需支持unicode编码(默认值:尽可能支持unicode编码)
--clean在本次打包之前先清理之前打包产生的缓存并删除临时文件。
--log-level LEVEL指定打包过程中输出到控制台的日志的详细程度。LEVEL的可选值为TRACE,DEBUG,INFO,WARN,ERROR,CRITICAL(默认值:INFO)。

8.2 生成什么形式的可执行文件?

-D, --onedir生成包含可执行文件的单文件夹(默认)。
-F, --onefile生成单个可执行文件。
--specpath DIR指定打包过程中生成的spec文件的存放路径(默认:当前目录)。
-n NAME, --name NAME指定可执行文件和spec文件的名称(默认值:被打包项目入口文件的名称)。

8.3 还要打包哪些文件?在哪里搜索依赖包?

--add-data <SRC;DEST or SRC:DEST>把指定的二进制文件也打包到可执行文件内部的指定目录中,即项目运行时需要的.conf.ini.html.py.so.dll文件,此选项可以多次使用。

--add-data=/path/templates/*:./templates/

冒号是分隔符,冒号左侧是文件当前所在的路径,冒号右侧是项目运行时从哪个目录(可执行文件内部的指定目录)读取这些文件。Windows系统的分隔符是分号

--add-binary <SRC;DEST or SRC:DEST>把指定的二进制文件也打包到可执行文件内部的指定目录中,即项目运行时需要的*.dll*.dyliblib*.so文件。此选项可以多次使用,使用方法同上。
-p DIR, --paths DIR提供其他搜索导入包的路径,默认会搜索PYTHONPATH。如果需要提供多个路径,则以冒号分隔,或多次使用此选项。

--hidden-import MODULENAME, --hiddenimport MODULENAME指定隐式导入的包名称,此选项可以多次使用。importfrom ... import属于显式导入,Pyinstaller一般能够自动识别出这些显式导入,并将其打包到可执行文件中。但是如果项目代码中使用了其他导入方式,例如__import__, imp.find_module()execeval,即隐式导入,则Pyinstaller无法自动识别,须使用此选项告知这些隐式导入的包名称,Pyinstaller才能将其打包到可执行文件中。

--additional-hooks-dir HOOKSPATH提供其他搜索钩子文件的路径,默认只搜索Pyinstaller源码中自带的钩子文件目录,此选项可以多次使用。如果一个隐式导入的包里面有很多子包,则需要把子包的名称也逐一通过--hidden-import选项提供给Pyinstaller。但是这样显然很麻烦,Pyinstaller提供了一种便捷的方式,按照规定的命名格式编写一个简单的钩子文件。例如:编写hook-gunicorn.py,必须严格按照hook-包名.pyhook-包名.模块名.py这种格式命名钩子文件,并通过此选项把这些钩子文件的所在目录提供给Pyinstaller,钩子才能被正确识别。

from PyInstaller.utils.hooks import collect_submodules
hiddenimports = collect_submodules('gunicorn')

--runtime-hook RUNTIME_HOOKS指定需要在可执行文件运行时加载的钩子文件的所在路径,此选项可以多次使用。如果项目运行之前需要动态设置一些特殊环境,则可以编写运行时钩子文件,该文件将打包到可执行文件中,文件中的代码将在项目运行之前执行。该选项很少需要使用。

--exclude-module EXCLUDES指定需要排除的包名,这些包将不会被打包到可执行文件中,此选项可以多次使用。如果python解释器上安装了本项目不需要使用的包,则可以使用此选项,从而减少可执行文件的大小。

--key KEY指定用于加密Python字节码的密钥,秘钥需为一个长度为16的字符串。如果使用了此选项,则Pyinstaller将调用依赖包pycrypto来对字节码加密。

8.4 其他选项

-d <all,imports,bootloader,noarchive>, --debug <all,imports,bootloader,noarchive>
指定可执行文件运行时需要输出到控制台的调试信息。可以多次提供此参数以选择以下几个选项。
all:以下三个选项。
imports:指定基础Python解释器的-v选项,使该模块在每次模块初始化时打印一条消息,显示加载该文件的位置(文件名或内置模块)。
bootloader:告诉引导程序在初始化和启动可执行文件时发出进度消息。用于诊断缺少导入的问题。
noarchive:不要将所有打包的Python源文件作为archive存储在生成的可执行文件内部,而应将它们作为文件存储在生成的输出目录中。
-s, --strip将符号表条应用于可执行文件和共享库(对于Windows不建议)。
--noupx即使搜索到有可用的UPX工具,也不要使用UPX。
--upx-exclude FILE使用upx时不压缩指定的二进制文件。使用此选项时为了避免upx在压缩过程中破坏了某些二进制文件。FILE是不带路径的二进制文件名。此选项可以多次使用。

9 命令汇总

9.1 完成打包的三种方式

1.一条命令完成:生成spec文件并完成打包。

[root@localhost hello]# pyinstaller --clean -F manage.py --hidden-import my_package

2.两条命令完成:先单独生成spec文件,一般是为了手动修改spec文件,一点点测试需要哪些选项,然后再根据该spec文件完成打包。

[root@localhost hello]# pyi-makespec manage.py -F --hidden-import my_package
[root@localhost hello]# vim manage.spec

[root@localhost hello]# pyinstaller manage.spec

3.编写打包脚本
通常使用方式2测试好了需要的选项后,编写成脚本,自动化打包过程。

import PyInstaller.__main__

PyInstaller.__main__.run([
	os.path.join('mypath', 'hello', 'manage.py'),
    '--onefile',
    '--hidden-import=%s' % 'my_package',    
])

9.2 命令汇总

1、pyinstaller 生成spec文件并完成打包,或根据指定的spec文件进行打包。
2、 pyi-makespec 只生成spec文件
3、 pyi-archive-viewer
检查Archives(归档文件)内部包含的文件,可用于检查.pyz.pkg.exe,或检查ELFCOFF形式的可执行文件。通过此命令能够在可执行文件内部查看到项目源码对应的.pyc文件。
pyi-archive-viewer命令的选项参数:
-h, --help 显示帮助。
-l, --log 快速内容日志。
-b, --brief 打印内容文件名的python可评估列表。
-r, --recursive 与-l或-b一起使用时,将应用递归行为。

# pyi-archive-viewr manage -b -r

使用pyi-archive-viewer命令会进入交互式shell,通过以下四个子命令来浏览可执行文件manage内部包含的文件:
O name 打开嵌套的文件,例如:可执行文件内部包含一个.pyz文件,使用此命令可以嵌套打开这个.pyz文件,从而查看该.pyz文件内部包含的文件。
U 当使用O命令进入嵌套文件内部后(称为下一层),可以使用此命令返回上一层。
X name 将指定文件另存为到当前目录中,会提示输入另存为的文件名,如果不输入另存为的文件名,则直接在stdout中打印该文件内容。
Q 退出交互式shell。
4、 pyi-bindepend:检查可执行文件或动态库(DLL)的二进制依赖项,并打印到stdout中。使用该命令能够跟踪二进制扩展的依赖关系链。

# pyi-bindepend manage 

10 zipapp与Pyinstaller区别

从Python 3.5开始,Python定义了.pyz.pyzw分别作为“Python Zip应用”和“Windows下Python Zip应用”的扩展名。
同时,新增了内置zipapp模块,主要用于将 Python 应用打包成一个 .pyz 文件。无论开发 Python 应用时有多少源文件和依赖包,使用 zipapp 都可以将它们打包成一个 .pyz 文件,不足之处是该文件依然需要 Python 环境来执行,即目标机器上必须要安装 Python 解释器环境。

PyInstaller 工具则更强大,它可以直接将 Python 程序打包成可执行程序,前该工具跨平台,使用非常方便。使用 PyInstaller 打包出来的程序,完全可以被分发到对应平台的的目标机器上直接运行,无须在目标机器上安装 Python 解释器环境。

Python zipapp打包教程
zipapp—管理可执行的Python zip存档

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值