PyInstaller是一個跨平臺的Python應用打包工具,支持 Windows/Linux/MacOS三大主流平臺,可以把 Python 腳本及其所在的 Python 解釋器打包成可執行文件,從而容許最終用戶在無需安裝 Python 的狀況下執行應用程序。
PyInstaller 製做出來的執行文件並非跨平臺的,若是須要爲不一樣平臺打包,就要在相應平臺上運行PyInstaller進行打包。html
pip install PyInstaller
react
pyinstaller main.py
PyInstaller 最簡單使用只須要指定做爲程序入口的腳本文件。PyInstaller 執行打包程序後會在當前目錄下建立下列文件和目錄:
main.spec 文件,其前綴和腳本名相同,指定了打包時所需的各類參數;
build 子目錄,其中存放打包過程當中生成的臨時文件。warnxxxx.txt文件記錄了生成過程當中的警告/錯誤信息。若是 PyInstaller 運行有問題,須要檢查warnxxxx.txt文件來獲取錯誤的詳細內容。xref-xxxx.html文件輸出 PyInstaller 分析腳本獲得的模塊依賴關係圖。
dist子目錄,存放生成的最終文件。若是使用單文件模式將只有單個執行文件;若是使用目錄模式的話,會有一個和腳本同名的子目錄,其內纔是真正的可執行文件以及附屬文件。web
PyInstaller命令行選項能夠經過幫助信息查看:pyinstaller --help
-y | --noconfirm:直接覆蓋輸出文件,而無需提示,在屢次重複運行命令時可避免反覆確認。
-D | --onedir:生成包含執行文件的目錄(默認行爲)。
-F | --onefile:生成單一的可執行文件,不推薦使用。
-i | --icon [.ico | .exe | .icns]:爲 Windows/Mac 平臺的執行文件指定圖標。
--version-file [filename]:添加文件版本信息。
-c | --console | --nowindowed:經過控制檯窗口運行程序 而且分配標準輸入/輸出,(默認行爲)。
-w | --windowed | --noconsole:不建立控制檯窗口,也不分配標準輸入/輸出,主要用來運行 GUI 程序。沒有輸入輸出會給調試帶來必定困難,所以即使是 GUI 程序,建議在調試時禁用本選項,在最終發佈時再打開。--add-data [file:dir]
:添加數據文件。若是有多個文件須要添加,本選項能夠出現屢次。參數的格式爲文件名+輸出目錄名,用路徑分隔符分割,在 Windows 下使用 ;,其它系統下則使用 :。 若是輸出到和腳本相同的目錄,則使用 . 做爲輸出目錄。--add-binary [file:dir]
:添加二進制文件,即運行程序所需的.exe/.dll/.so 等。django
單目錄模式是 PyInstaller 將 Python 程序編譯爲同一個目錄下的多個文件,其中 xxxx.exe 是程序入口點(xxxx 是腳本文件名稱,能夠經過命令行修改)。單目錄模式是 PyInstaller 的默認模式,能夠本身加上 -D 或者 --onedir 開關顯式開啓。
單目錄模式打包生成的目錄除可執行文件外,還包括 Python 解釋器(PythonXX.dll)、系統運行庫(ucrtbase.dll 以及其它 apixx.dll),以及一些編譯後的 Python 模塊(.pyd 文件)。api
單文件模式是將整個程序編譯爲單一的可執行文件。須要在命令行添加 -F 或者 --onefile 開關開啓。
Python腳本是解釋型程序,而不是 原生的編譯程序,並不能產生出真正單一的可執行文件。若是使用單文件模式,PyInstaller打包生成的是自動解壓程序,須要先把全部文件解壓到一個臨時目錄(一般名爲_MEIxxxx
,xxxx是隨機數字),再從臨時目錄加載解釋器和附屬文件。程序運行完畢後,若是一切正常,會將臨時目錄再刪除。
PyInstaller會對運行時的Python解釋器修改。若是直接運行 Python 腳本,那麼sys.frozen 變量不存在,若是經過 PyInstaller 生成的可執行文件運行,PyInstaller 會設置sys.frozen 變量爲 True;若是使用單文件模式,sys._MEIPASS
變量包含了PyInstaller 自動建立的臨時目錄名。
單文件模式由於有臨時目錄和解壓文件過程,因此程序啓動速度會比較慢。若是程序運行到一半崩潰,則臨時目錄將沒有機會被刪除。ide
PyInstaller 在生成文件的同時會建立一個相應的.spec 文件,.spec 文件本質上是一個特殊的 Python 腳本,記錄了生成所需的指令。工具
使用pyinstaller [options] xxx.py進行打包時,PyInstaller 會首先根據選項生成對應的 .spec 文件,而後執行 .spec 文件所指定的過程生成最終文件。所以,能夠直接指定spec文件執行打包過程。pyinstaller [options] xxx.spec
ui
單目錄模式生成的spec 文件格式以下:命令行
a = Analysis(...) pyz = PYZ(...) exe = EXE(...) coll = COLLECT(...)
單文件模式生成的spec 文件格式以下:調試
a = Analysis(...) pyz = PYZ(...) exe = EXE(...)
單文件模式是將全部內容統一打包到 .exe,而單目錄模式除了生成 .exe 外,還須要拷貝其它附屬文件。
Analysis用於分析腳本的引用關係,並將全部查找到的相關內容記錄在內部結構中,供後續步驟使用;
PYZ將全部 Python 腳本模塊編譯爲對應的 .pyd 並打包;
EXE:將打包後的 Python 模塊及其它文件一塊兒生成可執行的文件結構;
COLLECT:將引用到的附屬文件拷貝到生成目錄的對應位置。
若是數據文件不少致使 Analysis 太長,則能夠提取爲單獨的變量。
data_files = [(...)] a = Analysis(..., datas=data_files, ...)
能夠爲數據/二進制文件指定通配符,從而匹配同一類型的多個文件。
a = Analysis(..., datas=[('media/*.mp3', 'media')], ...)
能夠將指定文件和指定目錄打包進行打包,以下:
a = Analysis(..., datas=[('config.ini', '.'), ('data', 'data')], ...)
將config.ini文件打包當可執行文件當前目錄下,將data目錄打包到可執行文件當前目錄下。
PyInstaller 使用遞歸方法,從入口的腳本文件逐個分析,獲取依賴模塊。
PyInstaller 能識別 ctypes、SWIG、Cython 等形式的模塊調用,但文件名必須爲字面值。但PyInstaller 沒法識別動態和調用,例如 import、exec、eval,以及以變量爲參數的調用。
當 PyInstaller 識別完全部模塊後,會在內部構成一個樹形結構表示調用關係圖,調用關係在生成目標時也會一併輸出(xref-xxxx.html 文件)。PYZ 步驟會將全部識別到的模塊聚集起來,若是有必要會編譯成.pyd,而後將文件打包。但仍然存在如下問題:
(1)因爲動態模塊調用未必能夠自動識別到,所以不會打包到文件中,執行時確定會出現問。
(2)有些模塊並不是是以模塊的形式,而是經過文件系統去訪問 .py 文件,代碼在運行時一樣會出現問題。
爲了解決上述問題,PyInstaller引入了Hooks機制,對於兩種問題引入了兩種類型的 Hook。兩種 Hook 主要是按照加載時間區分,第一種Hook在 PyInstaller 文檔中沒有明確的命名,是在生成過程當中,導入特定模塊時調用的,稱爲 Import Hook;第二種是Runtime Hook,是在執行文件啓動期間、加載特定模塊時調用的。
PyInstaller 定義的全部 Hook 在 PyInstaller 安裝目錄的 hooks 子目錄下,文件的命名均爲 hook-[模塊名].py 的形式,即爲 Import Hook。
當 PyInstaller 生成過程當中找到特定的導入模塊,就會到hooks目錄下查找是否存在對應的Hook,若是存在,則執行之。
hook-PyQt5.py文件以下:
import os from PyInstaller.utils.hooks import collect_system_data_files from PyInstaller.utils.hooks.qt import pyqt5_library_info, get_qt_binaries # Ensure PyQt5 is importable before adding info depending on it. if pyqt5_library_info.version: hiddenimports = [ # PyQt5.10 and earlier uses sip in an separate package; 'sip', # PyQt5.11 and later provides SIP in a private package. Support both. 'PyQt5.sip' ] # Collect the ``qt.conf`` file. datas = [x for x in collect_system_data_files(pyqt5_library_info.location['PrefixPath'], 'PyQt5') if os.path.basename(x[0]) == 'qt.conf'] # Collect required Qt binaries. binaries = get_qt_binaries(pyqt5_library_info)
hiddenimports是PyInstaller 用來描述並不是經過 import 明確導入,而是經過其它動態機制加載的模塊。
Runtime Hooks均位於 PyInstaller 安裝目錄下的loader\rthooks 子目錄下,而且命名方式是 pyi_rth_[模塊名稱].py
(rth 表明 run time hook)。
loader\rthooks.dat內容是一個字典,記錄了系統中全部支持的 Runtime Hooks。rthooks.dat文件以下:
{ 'certifi': ['pyi_rth_certifi.py'], 'django': ['pyi_rth_django.py'], 'enchant': ['pyi_rth_enchant.py'], 'gi': ['pyi_rth_gi.py'], 'gi.repository.Gio': ['pyi_rth_gio.py'], 'gi.repository.GLib': ['pyi_rth_glib.py'], 'gi.repository.GdkPixbuf': ['pyi_rth_gdkpixbuf.py'], 'gi.repository.Gtk': ['pyi_rth_gtk.py'], 'gi.repository.Gst': ['pyi_rth_gstreamer.py'], 'gst': ['pyi_rth_gstreamer.py'], 'kivy': ['pyi_rth_kivy.py'], 'kivy.lib.gstplayer': ['pyi_rth_gstreamer.py'], 'matplotlib': ['pyi_rth_mplconfig.py', 'pyi_rth_mpldata.py'], 'osgeo': ['pyi_rth_osgeo.py'], 'pkg_resources': ['pyi_rth_pkgres.py'], 'PyQt4': ['pyi_rth_qt4plugins.py'], 'PyQt5': ['pyi_rth_pyqt5.py'], 'PyQt5.QtWebEngineWidgets': ['pyi_rth_pyqt5webengine.py'], 'PySide': ['pyi_rth_qt4plugins.py'], 'PySide2': ['pyi_rth_pyside2.py'], 'PySide2.QtWebEngineWidgets': ['pyi_rth_pyside2webengine.py'], '_tkinter': ['pyi_rth__tkinter.py'], 'traitlets': ['pyi_rth_traitlets.py'], 'twisted.internet.reactor': ['pyi_rth_twisted.py'], 'usb': ['pyi_rth_usb.py'], 'win32com': ['pyi_rth_win32comgenpy.py'], 'multiprocessing': ['pyi_rth_multiprocessing.py'], 'nltk': ['pyi_rth_nltk.py'], }
Runtime Hooks 是在執行文件運行期間執行的。PyInstaller 修改了模塊加載機制,當運行期間加載任何模塊時,PyInstaller 會檢查是否有對應的 Runtime Hook,若是有,則運行相應Hook。所以,Runtime Hooks 是和腳本一塊兒編譯到可執行文件中的。
pyi_rth_pyqt5.py文件以下:
import os import sys # The path to Qt's components may not default to the wheel layout for # self-compiled PyQt5 installations. Mandate the wheel layout. See # ``utils/hooks/qt.py`` for more details. pyqt_path = os.path.join(sys._MEIPASS, 'PyQt5', 'Qt') os.environ['QT_PLUGIN_PATH'] = os.path.join(pyqt_path, 'plugins') os.environ['QML2_IMPORT_PATH'] = os.path.join(pyqt_path, 'qml')
使用PyInstaller進行打包時,最多見的錯誤是Failed to execute script xxx,一般作法是先使用pyinstaller -c xxx.py將應用打包爲控制檯應用,在命令行執行相應可執行程序查看錯誤輸出,進而逐個排除錯誤。