zipapp
—— 管理可執(zhí)行的 Python zip 打包文件?
3.5 新版功能.
源代碼: Lib/zipapp.py
本模塊提供了一套管理工具,用于創(chuàng)建包含 Python 代碼的壓縮文件,這些文件可以 直接由 Python 解釋器執(zhí)行。 本模塊提供 命令行接口 和 Python API。
簡(jiǎn)單示例?
下述例子展示了用 命令行接口 根據(jù)含有 Python 代碼的目錄創(chuàng)建一個(gè)可執(zhí)行的打包文件。 運(yùn)行后該打包文件時(shí),將會(huì)執(zhí)行 myapp
模塊中的 main
函數(shù)。
$ python -m zipapp myapp -m "myapp:main"
$ python myapp.pyz
<output from myapp>
命令行接口?
若要從命令行調(diào)用,則采用以下形式:
$ python -m zipapp source [options]
如果 source 是個(gè)目錄,將根據(jù) source 的內(nèi)容創(chuàng)建一個(gè)打包文件。如果 source 是個(gè)文件,則應(yīng)為一個(gè)打包文件,將會(huì)復(fù)制到目標(biāo)打包文件中(如果指定了 -info 選項(xiàng),將會(huì)顯示 shebang 行的內(nèi)容)。
可以接受以下參數(shù):
- -o <output>, --output=<output>?
將程序的輸出寫入名為 output 的文件中。若未指定此參數(shù),輸出的文件名將與輸入的 source 相同,并添加擴(kuò)展名
.pyz
。如果顯式給出了文件名,將會(huì)原樣使用(因此必要時(shí)應(yīng)包含擴(kuò)展名.pyz
)。如果 source 是個(gè)打包文件,必須指定一個(gè)輸出文件名(這時(shí) output 必須與 source 不同)。
- -p <interpreter>, --python=<interpreter>?
給打包文件加入
#!
行,以便指定 解釋器 作為運(yùn)行的命令行。另外,還讓打包文件在 POSIX 平臺(tái)上可執(zhí)行。默認(rèn)不會(huì)寫入#!
行,也不讓文件可執(zhí)行。
- -m <mainfn>, --main=<mainfn>?
在打包文件中寫入一個(gè)
__main__.py
文件,用于執(zhí)行 mainfn。mainfn 參數(shù)的形式應(yīng)為 “pkg.mod:fn”,其中 “pkg.mod”是打包文件中的某個(gè)包/模塊,“fn”是該模塊中的一個(gè)可調(diào)用對(duì)象。__main__.py
文件將會(huì)執(zhí)行該可調(diào)用對(duì)象。在復(fù)制打包文件時(shí),不能設(shè)置
--main
參數(shù)。
- -c, --compress?
利用 deflate 方法壓縮文件,減少輸出文件的大小。默認(rèn)情況下,打包文件中的文件是不壓縮的。
在復(fù)制打包文件時(shí),
--compress
無效。3.7 新版功能.
- --info?
顯示嵌入在打包文件中的解釋器程序,以便診斷問題。這時(shí)會(huì)忽略其他所有參數(shù),SOURCE 必須是個(gè)打包文件,而不是目錄。
- -h, --help?
打印簡(jiǎn)短的用法信息并退出。
Python API?
該模塊定義了兩個(gè)快捷函數(shù):
- zipapp.create_archive(source, target=None, interpreter=None, main=None, filter=None, compressed=False)?
由 source 創(chuàng)建一個(gè)應(yīng)用程序打包文件。source 可以是以下形式之一:
一個(gè)目錄名,或指向目錄的 path-like object ,這時(shí)將根據(jù)目錄內(nèi)容新建一個(gè)應(yīng)用程序打包文件。
一個(gè)已存在的應(yīng)用程序打包文件名,或指向這類文件的 path-like object,這時(shí)會(huì)將該文件復(fù)制為目標(biāo)文件(會(huì)稍作修改以反映出 interpreter 參數(shù)的值)。必要時(shí)文件名中應(yīng)包括
.pyz
擴(kuò)展名。一個(gè)以字節(jié)串模式打開的文件對(duì)象。該文件的內(nèi)容應(yīng)為應(yīng)用程序打包文件,且假定文件對(duì)象定位于打包文件的初始位置。
target 參數(shù)定義了打包文件的寫入位置:
若是個(gè)文件名,或是 path-like object,打包文件將寫入該文件中。
若是個(gè)打開的文件對(duì)象,打包文件將寫入該對(duì)象,該文件對(duì)象必須在字節(jié)串寫入模式下打開。
如果省略了 target (或?yàn)?
None
),則 source 必須為一個(gè)目錄,target 將是與 source 同名的文件,并加上.pyz
擴(kuò)展名。
參數(shù) interpreter 指定了 Python 解釋器程序名,用于執(zhí)行打包文件。這將以 “釋伴(shebang)”行的形式寫入打包文件的頭部。在 POSIX 平臺(tái)上,操作系統(tǒng)會(huì)進(jìn)行解釋,而在 Windows 平臺(tái)則會(huì)由 Python 啟動(dòng)器進(jìn)行處理。省略 interpreter 參數(shù)則不會(huì)寫入釋伴行。如果指定了解釋器,且目標(biāo)為文件名,則會(huì)設(shè)置目標(biāo)文件的可執(zhí)行屬性位。
參數(shù) main 指定某個(gè)可調(diào)用程序的名稱,用作打包文件的主程序。僅當(dāng) source 為目錄且不含
__main__.py
文件時(shí),才能指定該參數(shù)。main 參數(shù)應(yīng)采用 “pkg.module:callable”的形式,通過導(dǎo)入“pkg.module”并不帶參數(shù)地執(zhí)行給出的可調(diào)用對(duì)象,即可執(zhí)行打包文件。如果 source 是目錄且不含``__main__.py`` 文件,省略 main 將會(huì)出錯(cuò),生成的打包文件將無法執(zhí)行。可選參數(shù) filter 指定了回調(diào)函數(shù),將傳給代表被添加文件路徑的 Path 對(duì)象(相對(duì)于源目錄)。如若文件需要加入打包文件,則回調(diào)函數(shù)應(yīng)返回
True
。可選參數(shù) compressed 指定是否要壓縮打包文件。若設(shè)為
True
,則打包中的文件將用 deflate 方法進(jìn)行壓縮;否則就不會(huì)壓縮。本參數(shù)在復(fù)制現(xiàn)有打包文件時(shí)無效。若 source 或 target 指定的是文件對(duì)象,則調(diào)用者有責(zé)任在調(diào)用 create_archive 之后關(guān)閉這些文件對(duì)象。
當(dāng)復(fù)制已有的打包文件時(shí),提供的文件對(duì)象只需
read
和readline
方法,或write
方法。當(dāng)由目錄創(chuàng)建打包文件時(shí),若目標(biāo)為文件對(duì)象,將會(huì)將其傳給 類,且必須提供zipfile.ZipFile
類所需的方法。3.7 新版功能: 加入了 filter 和 compressed 參數(shù)。
例子?
將目錄打包成一個(gè)文件并運(yùn)行它。
$ python -m zipapp myapp
$ python myapp.pyz
<output from myapp>
同樣還可用 create_archive()
函數(shù)完成:
>>> import zipapp
>>> zipapp.create_archive('myapp', 'myapp.pyz')
要讓應(yīng)用程序能在 POSIX 平臺(tái)上直接執(zhí)行,需要指定所用的解釋器。
$ python -m zipapp myapp -p "/usr/bin/env python"
$ ./myapp.pyz
<output from myapp>
若要替換已有打包文件中的釋伴行,請(qǐng)用 create_archive()
函數(shù)另建一個(gè)修改好的打包文件:
>>> import zipapp
>>> zipapp.create_archive('old_archive.pyz', 'new_archive.pyz', '/usr/bin/python3')
若要原地更新打包文件,可用 BytesIO
對(duì)象在內(nèi)存中進(jìn)行替換,然后再覆蓋源文件。注意,原地覆蓋文件會(huì)有風(fēng)險(xiǎn),出錯(cuò)時(shí)會(huì)丟失原文件。這里沒有考慮出錯(cuò)情況,但生產(chǎn)代碼則應(yīng)進(jìn)行處理。另外,這種方案僅當(dāng)內(nèi)存足以容納打包文件時(shí)才有意義:
>>> import zipapp
>>> import io
>>> temp = io.BytesIO()
>>> zipapp.create_archive('myapp.pyz', temp, '/usr/bin/python2')
>>> with open('myapp.pyz', 'wb') as f:
>>> f.write(temp.getvalue())
指定解釋器程序?
注意,如果指定了解釋器程序再發(fā)布應(yīng)用程序打包文件,需要確保所用到的解釋器是可移植的。Windows 的 Python 啟動(dòng)器支持大多數(shù)常見的 POSIX #!
行,但還需要考慮一些其他問題。
如果采用“/usr/bin/env python”(或其他格式的 python 調(diào)用命令,比如“/usr/bin/python”),需要考慮默認(rèn)版本既可能是 Python 2 又可能是 Python 3,應(yīng)讓代碼在兩個(gè)版本下均能正常運(yùn)行。
如果用到的 Python 版本明確,如“/usr/bin/env python3”,則沒有該版本的用戶將無法運(yùn)行應(yīng)用程序。(如果代碼不兼容 Python 2,可能正該如此)。
因?yàn)闊o法指定“python X.Y以上版本”,所以應(yīng)小心“/usr/bin/env python3.4”這種精確版本的指定方式,因?yàn)閷?duì)于 Python 3.5 的用戶就得修改釋伴行,比如:
通常應(yīng)該用“/usr/bin/env python2”或“/usr/bin/env python3”的格式,具體根據(jù)代碼適用于 Python 2 還是 3 而定。
用 zipapp 創(chuàng)建獨(dú)立運(yùn)行的應(yīng)用程序?
利用 zipapp
模塊可以創(chuàng)建獨(dú)立運(yùn)行的 Python 程序,以便向最終用戶發(fā)布,僅需在系統(tǒng)中裝有合適版本的 Python 即可運(yùn)行。操作的關(guān)鍵就是把應(yīng)用程序代碼和所有依賴項(xiàng)一起放入打包文件中。
創(chuàng)建獨(dú)立運(yùn)行打包文件的步驟如下:
照常在某個(gè)目錄中創(chuàng)建應(yīng)用程序,于是會(huì)有一個(gè)
myapp
目錄,里面有個(gè)``__main__.py`` 文件,以及所有支持性代碼。用 pip 將應(yīng)用程序的所有依賴項(xiàng)裝入
myapp
目錄。$ python -m pip install -r requirements.txt --target myapp
(這里假定在
requirements.txt
文件中列出了項(xiàng)目所需的依賴項(xiàng),也可以在 pip 命令行中列出依賴項(xiàng))。pip 在
myapp
中創(chuàng)建的.dist-info
目錄,是可以刪除的。這些目錄保存了 pip 用于管理包的元數(shù)據(jù),由于接下來不會(huì)再用到 pip,所以不是必須存在,當(dāng)然留下來也不會(huì)有什么壞處。用以下命令打包:
$ python -m zipapp -p "interpreter" myapp
這會(huì)生成一個(gè)獨(dú)立的可執(zhí)行文件,可在任何裝有合適解釋器的機(jī)器上運(yùn)行。詳情參見 指定解釋器程序??梢詥蝹€(gè)文件的形式分發(fā)給用戶。
在 Unix 系統(tǒng)中,myapp.pyz
文件將以原有文件名執(zhí)行。如果喜歡 “普通”的命令名,可以重命名該文件,去掉擴(kuò)展名 .pyz
。在 Windows 系統(tǒng)中,myapp.pyz[w]
是可執(zhí)行文件,因?yàn)?Python 解釋器在安裝時(shí)注冊(cè)了擴(kuò)展名``.pyz`` 和 .pyzw
。
制作 Windows 可執(zhí)行文件?
在 Windows 系統(tǒng)中,可能沒有注冊(cè)擴(kuò)展名 .pyz
,另外有些場(chǎng)合無法“透明”地識(shí)別已注冊(cè)的擴(kuò)展(最簡(jiǎn)單的例子是,subprocess.run(['myapp'])
就找不到——需要明確指定擴(kuò)展名)。
因此,在 Windows 系統(tǒng)中,通常最好 由zipapp 創(chuàng)建一個(gè)可執(zhí)行文件。雖然需要用到 C 編譯器,但還是相對(duì)容易做到的?;咀龇ㄓ匈囉谝韵率聦?shí),即 zip 文件內(nèi)可預(yù)置任意數(shù)據(jù),Windows 的 exe 文件也可以附帶任意數(shù)據(jù)。因此,創(chuàng)建一個(gè)合適的啟動(dòng)程序并將 .pyz
文件附在后面,最后就能得到一個(gè)單文件的可執(zhí)行文件,可運(yùn)行 Python 應(yīng)用程序。
合適的啟動(dòng)程序可以簡(jiǎn)單如下:
#define Py_LIMITED_API 1
#include "Python.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#ifdef WINDOWS
int WINAPI wWinMain(
HINSTANCE hInstance, /* handle to current instance */
HINSTANCE hPrevInstance, /* handle to previous instance */
LPWSTR lpCmdLine, /* pointer to command line */
int nCmdShow /* show state of window */
)
#else
int wmain()
#endif
{
wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
myargv[0] = __wargv[0];
memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
return Py_Main(__argc+1, myargv);
}
若已定義了預(yù)處理器符號(hào) WINDOWS
,上述代碼將會(huì)生成一個(gè) GUI 可執(zhí)行文件。若未定義則生成一個(gè)可執(zhí)行的控制臺(tái)文件。
直接使用標(biāo)準(zhǔn)的 MSVC 命令行工具,或利用 distutils 知道如何編譯 Python 源代碼,即可編譯可執(zhí)行文件:
>>> from distutils.ccompiler import new_compiler
>>> import distutils.sysconfig
>>> import sys
>>> import os
>>> from pathlib import Path
>>> def compile(src):
>>> src = Path(src)
>>> cc = new_compiler()
>>> exe = src.stem
>>> cc.add_include_dir(distutils.sysconfig.get_python_inc())
>>> cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
>>> # First the CLI executable
>>> objs = cc.compile([str(src)])
>>> cc.link_executable(objs, exe)
>>> # Now the GUI executable
>>> cc.define_macro('WINDOWS')
>>> objs = cc.compile([str(src)])
>>> cc.link_executable(objs, exe + 'w')
>>> if __name__ == "__main__":
>>> compile("zastub.c")
生成的啟動(dòng)程序用到了 “受限 ABI”,所以可在任意版本的 Python 3.x 中運(yùn)行。只要用戶的 PATH
中包含了 Python(python3.dll
)路徑即可。
若要得到完全獨(dú)立運(yùn)行的發(fā)行版程序,可將附有應(yīng)用程序的啟動(dòng)程序,與“內(nèi)嵌版” Python 打包在一起即可。這樣在架構(gòu)匹配(32位或64位)的任一 PC 上都能運(yùn)行。
注意事項(xiàng)?
要將應(yīng)用程序打包為單個(gè)文件,存在一些限制。大多數(shù)情況下,無需對(duì)應(yīng)用程序進(jìn)行重大修改即可解決。
如果應(yīng)用程序依賴某個(gè)帶有 C 擴(kuò)展的包,則此程序包無法由打包文件運(yùn)行(這是操作系統(tǒng)的限制,因?yàn)榭蓤?zhí)行代碼必須存在于文件系統(tǒng)中,操作系統(tǒng)才能加載)。這時(shí)可去除打包文件中的依賴關(guān)系,然后要求用戶事先安裝好該程序包,或者與打包文件一起發(fā)布并在
__main__.py
中增加代碼,將未打包模塊的目錄加入sys.path
中。采用增加代碼方式時(shí),一定要為目標(biāo)架構(gòu)提供合適的二進(jìn)制文件(可能還需在運(yùn)行時(shí)根據(jù)用戶的機(jī)器選擇正確的版本加入sys.path
)。若要如上所述發(fā)布一個(gè) Windows 可執(zhí)行文件,就得確保用戶在 PATH 中包含``python3.dll`` 的路徑(安裝程序默認(rèn)不會(huì)如此),或者應(yīng)把應(yīng)用程序與內(nèi)嵌版 Python 一起打包。
上述給出的啟動(dòng)程序采用了 Python 嵌入 API。 這意味著應(yīng)用程序?qū)?huì)是
sys.executable
,而*不是*傳統(tǒng)的 Python 解釋器。代碼及依賴項(xiàng)需做好準(zhǔn)備。例如,如果應(yīng)用程序用到了multiprocessing
模塊,就需要調(diào)用multiprocessing.set_executable()
來讓模塊知道標(biāo)準(zhǔn) Python 解釋器的位置。
Python 打包應(yīng)用程序的格式?
自 2.6 版開始,Python 即能夠執(zhí)行包含 文件的打包文件了。為了能被 Python 執(zhí)行,應(yīng)用程序的打包文件必須為包含 __main__.py
文件的標(biāo)準(zhǔn) zip 文件,__main__.py
文件將作為應(yīng)用程序的入口運(yùn)行。類似于常規(guī)的 Python 腳本,父級(jí)(這里指打包文件)將放入 sys.path
,因此可從打包文件中導(dǎo)入更多的模塊。
zip 文件格式允許在文件中預(yù)置任意數(shù)據(jù)。利用這種能力,zip 應(yīng)用程序格式在文件中預(yù)置了一個(gè)標(biāo)準(zhǔn)的 POSIX “釋伴”行(#!/path/to/interpreter
)。
因此,Python zip 應(yīng)用程序的格式會(huì)如下所示:
可選的釋伴行,包含字符
b'#!'
,后面是解釋器名,然后是換行符 (b'\n'
)。 解釋器名可為操作系統(tǒng) “釋伴”處理所能接受的任意值,或?yàn)?Windows 系統(tǒng)中的 Python 啟動(dòng)程序。解釋器名在 Windows 中應(yīng)用 UTF-8 編碼,在 POSIX 中則用sys.getfilesystemencoding()
。標(biāo)準(zhǔn)的打包文件由
zipfile
模塊生成。其中 必須 包含一個(gè)名為``__main__.py`` 的文件(必須位于打包文件的“根”目錄——不能位于某個(gè)子目錄中)。打包文件中的數(shù)據(jù)可以是壓縮或未壓縮的。
如果應(yīng)用程序的打包文件帶有釋伴行,則在 POSIX 系統(tǒng)中可能需要啟用可執(zhí)行屬性,以允許直接執(zhí)行。
不一定非要用本模塊中的工具創(chuàng)建應(yīng)用程序打包文件,本模塊只是提供了便捷方案,上述格式的打包文件可用任何方式創(chuàng)建,均可被 Python 接受。