zipapp —— 管理可執(zhí)行的 Python zip 打包文件?

3.5 新版功能.

源代碼: Lib/zipapp.py


本模塊提供了一套管理工具,用于創(chuàng)建包含 Python 代碼的壓縮文件,這些文件可以 直接由 Python 解釋器執(zhí)行。 本模塊提供 命令行接口Python API。

簡單示例?

下述例子展示了用 命令行接口 根據(jù)含有 Python 代碼的目錄創(chuàng)建一個可執(zhí)行的打包文件。 運(yùn)行后該打包文件時,將會執(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 是個目錄,將根據(jù) source 的內(nèi)容創(chuàng)建一個打包文件。如果 source 是個文件,則應(yīng)為一個打包文件,將會復(fù)制到目標(biāo)打包文件中(如果指定了 -info 選項,將會顯示 shebang 行的內(nèi)容)。

可以接受以下參數(shù):

-o <output>, --output=<output>?

將程序的輸出寫入名為 output 的文件中。若未指定此參數(shù),輸出的文件名將與輸入的 source 相同,并添加擴(kuò)展名 .pyz。如果顯式給出了文件名,將會原樣使用(因此必要時應(yīng)包含擴(kuò)展名 .pyz)。

如果 source 是個打包文件,必須指定一個輸出文件名(這時 output 必須與 source 不同)。

-p <interpreter>, --python=<interpreter>?

給打包文件加入 #! 行,以便指定 解釋器 作為運(yùn)行的命令行。另外,還讓打包文件在 POSIX 平臺上可執(zhí)行。默認(rèn)不會寫入 #! 行,也不讓文件可執(zhí)行。

-m <mainfn>, --main=<mainfn>?

在打包文件中寫入一個 __main__.py 文件,用于執(zhí)行 mainfn。mainfn 參數(shù)的形式應(yīng)為 “pkg.mod:fn”,其中 “pkg.mod”是打包文件中的某個包/模塊,“fn”是該模塊中的一個可調(diào)用對象。__main__.py 文件將會執(zhí)行該可調(diào)用對象。

在復(fù)制打包文件時,不能設(shè)置 --main 參數(shù)。

-c, --compress?

利用 deflate 方法壓縮文件,減少輸出文件的大小。默認(rèn)情況下,打包文件中的文件是不壓縮的。

在復(fù)制打包文件時,--compress 無效。

3.7 新版功能.

--info?

顯示嵌入在打包文件中的解釋器程序,以便診斷問題。這時會忽略其他所有參數(shù),SOURCE 必須是個打包文件,而不是目錄。

-h, --help?

打印簡短的用法信息并退出。

Python API?

該模塊定義了兩個快捷函數(shù):

zipapp.create_archive(source, target=None, interpreter=None, main=None, filter=None, compressed=False)?

source 創(chuàng)建一個應(yīng)用程序打包文件。source 可以是以下形式之一:

  • 一個目錄名,或指向目錄的 path-like object ,這時將根據(jù)目錄內(nèi)容新建一個應(yīng)用程序打包文件。

  • 一個已存在的應(yīng)用程序打包文件名,或指向這類文件的 path-like object,這時會將該文件復(fù)制為目標(biāo)文件(會稍作修改以反映出 interpreter 參數(shù)的值)。必要時文件名中應(yīng)包括 .pyz 擴(kuò)展名。

  • 一個以字節(jié)串模式打開的文件對象。該文件的內(nèi)容應(yīng)為應(yīng)用程序打包文件,且假定文件對象定位于打包文件的初始位置。

target 參數(shù)定義了打包文件的寫入位置:

  • 若是個文件名,或是 path-like object,打包文件將寫入該文件中。

  • 若是個打開的文件對象,打包文件將寫入該對象,該文件對象必須在字節(jié)串寫入模式下打開。

  • 如果省略了 target (或為 None),則 source 必須為一個目錄,target 將是與 source 同名的文件,并加上 .pyz 擴(kuò)展名。

參數(shù) interpreter 指定了 Python 解釋器程序名,用于執(zhí)行打包文件。這將以 “釋伴(shebang)”行的形式寫入打包文件的頭部。在 POSIX 平臺上,操作系統(tǒng)會進(jìn)行解釋,而在 Windows 平臺則會由 Python 啟動器進(jìn)行處理。省略 interpreter 參數(shù)則不會寫入釋伴行。如果指定了解釋器,且目標(biāo)為文件名,則會設(shè)置目標(biāo)文件的可執(zhí)行屬性位。

參數(shù) main 指定某個可調(diào)用程序的名稱,用作打包文件的主程序。僅當(dāng) source 為目錄且不含 __main__.py 文件時,才能指定該參數(shù)。main 參數(shù)應(yīng)采用 “pkg.module:callable”的形式,通過導(dǎo)入“pkg.module”并不帶參數(shù)地執(zhí)行給出的可調(diào)用對象,即可執(zhí)行打包文件。如果 source 是目錄且不含``__main__.py`` 文件,省略 main 將會出錯,生成的打包文件將無法執(zhí)行。

可選參數(shù) filter 指定了回調(diào)函數(shù),將傳給代表被添加文件路徑的 Path 對象(相對于源目錄)。如若文件需要加入打包文件,則回調(diào)函數(shù)應(yīng)返回 True。

可選參數(shù) compressed 指定是否要壓縮打包文件。若設(shè)為 True,則打包中的文件將用 deflate 方法進(jìn)行壓縮;否則就不會壓縮。本參數(shù)在復(fù)制現(xiàn)有打包文件時無效。

sourcetarget 指定的是文件對象,則調(diào)用者有責(zé)任在調(diào)用 create_archive 之后關(guān)閉這些文件對象。

當(dāng)復(fù)制已有的打包文件時,提供的文件對象只需 readreadline 方法,或 write 方法。當(dāng)由目錄創(chuàng)建打包文件時,若目標(biāo)為文件對象,將會將其傳給 類,且必須提供 zipfile.ZipFile 類所需的方法。

3.7 新版功能: 加入了 filtercompressed 參數(shù)。

zipapp.get_interpreter(archive)?

返回打包文件開頭的 行指定的解釋器程序。如果沒有 #! 行,則返回 None。參數(shù) archive 可為文件名或在字節(jié)串模式下打開以供讀取的文件類對象。#! 行假定是在打包文件的開頭。

例子?

將目錄打包成一個文件并運(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 平臺上直接執(zhí)行,需要指定所用的解釋器。

$ python -m zipapp myapp -p "/usr/bin/env python"
$ ./myapp.pyz
<output from myapp>

若要替換已有打包文件中的釋伴行,請用 create_archive() 函數(shù)另建一個修改好的打包文件:

>>>
>>> import zipapp
>>> zipapp.create_archive('old_archive.pyz', 'new_archive.pyz', '/usr/bin/python3')

若要原地更新打包文件,可用 BytesIO 對象在內(nèi)存中進(jìn)行替換,然后再覆蓋源文件。注意,原地覆蓋文件會有風(fēng)險,出錯時會丟失原文件。這里沒有考慮出錯情況,但生產(chǎn)代碼則應(yīng)進(jìn)行處理。另外,這種方案僅當(dāng)內(nèi)存足以容納打包文件時才有意義:

>>>
>>> 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 啟動器支持大多數(shù)常見的 POSIX #! 行,但還需要考慮一些其他問題。

  • 如果采用“/usr/bin/env python”(或其他格式的 python 調(diào)用命令,比如“/usr/bin/python”),需要考慮默認(rèn)版本既可能是 Python 2 又可能是 Python 3,應(yīng)讓代碼在兩個版本下均能正常運(yùn)行。

  • 如果用到的 Python 版本明確,如“/usr/bin/env python3”,則沒有該版本的用戶將無法運(yùn)行應(yīng)用程序。(如果代碼不兼容 Python 2,可能正該如此)。

  • 因為無法指定“python X.Y以上版本”,所以應(yīng)小心“/usr/bin/env python3.4”這種精確版本的指定方式,因為對于 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)用程序代碼和所有依賴項一起放入打包文件中。

創(chuàng)建獨(dú)立運(yùn)行打包文件的步驟如下:

  1. 照常在某個目錄中創(chuàng)建應(yīng)用程序,于是會有一個 myapp 目錄,里面有個``__main__.py`` 文件,以及所有支持性代碼。

  2. 用 pip 將應(yīng)用程序的所有依賴項裝入 myapp 目錄。

    $ python -m pip install -r requirements.txt --target myapp
    

    (這里假定在 requirements.txt 文件中列出了項目所需的依賴項,也可以在 pip 命令行中列出依賴項)。

  3. pip 在 myapp 中創(chuàng)建的 .dist-info 目錄,是可以刪除的。這些目錄保存了 pip 用于管理包的元數(shù)據(jù),由于接下來不會再用到 pip,所以不是必須存在,當(dāng)然留下來也不會有什么壞處。

  4. 用以下命令打包:

    $ python -m zipapp -p "interpreter" myapp
    

這會生成一個獨(dú)立的可執(zhí)行文件,可在任何裝有合適解釋器的機(jī)器上運(yùn)行。詳情參見 指定解釋器程序??梢詥蝹€文件的形式分發(fā)給用戶。

在 Unix 系統(tǒng)中,myapp.pyz 文件將以原有文件名執(zhí)行。如果喜歡 “普通”的命令名,可以重命名該文件,去掉擴(kuò)展名 .pyz 。在 Windows 系統(tǒng)中,myapp.pyz[w] 是可執(zhí)行文件,因為 Python 解釋器在安裝時注冊了擴(kuò)展名``.pyz`` 和 .pyzw 。

制作 Windows 可執(zhí)行文件?

在 Windows 系統(tǒng)中,可能沒有注冊擴(kuò)展名 .pyz,另外有些場合無法“透明”地識別已注冊的擴(kuò)展(最簡單的例子是,subprocess.run(['myapp']) 就找不到——需要明確指定擴(kuò)展名)。

因此,在 Windows 系統(tǒng)中,通常最好 由zipapp 創(chuàng)建一個可執(zhí)行文件。雖然需要用到 C 編譯器,但還是相對容易做到的?;咀龇ㄓ匈囉谝韵率聦?,即 zip 文件內(nèi)可預(yù)置任意數(shù)據(jù),Windows 的 exe 文件也可以附帶任意數(shù)據(jù)。因此,創(chuàng)建一個合適的啟動程序并將 .pyz 文件附在后面,最后就能得到一個單文件的可執(zhí)行文件,可運(yùn)行 Python 應(yīng)用程序。

合適的啟動程序可以簡單如下:

#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ù)處理器符號 WINDOWS,上述代碼將會生成一個 GUI 可執(zhí)行文件。若未定義則生成一個可執(zhí)行的控制臺文件。

直接使用標(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")

生成的啟動程序用到了 “受限 ABI”,所以可在任意版本的 Python 3.x 中運(yùn)行。只要用戶的 PATH 中包含了 Python(python3.dll)路徑即可。

若要得到完全獨(dú)立運(yùn)行的發(fā)行版程序,可將附有應(yīng)用程序的啟動程序,與“內(nèi)嵌版” Python 打包在一起即可。這樣在架構(gòu)匹配(32位或64位)的任一 PC 上都能運(yùn)行。

注意事項?

要將應(yīng)用程序打包為單個文件,存在一些限制。大多數(shù)情況下,無需對應(yīng)用程序進(jìn)行重大修改即可解決。

  1. 如果應(yīng)用程序依賴某個帶有 C 擴(kuò)展的包,則此程序包無法由打包文件運(yùn)行(這是操作系統(tǒng)的限制,因為可執(zhí)行代碼必須存在于文件系統(tǒng)中,操作系統(tǒng)才能加載)。這時可去除打包文件中的依賴關(guān)系,然后要求用戶事先安裝好該程序包,或者與打包文件一起發(fā)布并在 __main__.py 中增加代碼,將未打包模塊的目錄加入 sys.path 中。采用增加代碼方式時,一定要為目標(biāo)架構(gòu)提供合適的二進(jìn)制文件(可能還需在運(yùn)行時根據(jù)用戶的機(jī)器選擇正確的版本加入 sys.path)。

  2. 若要如上所述發(fā)布一個 Windows 可執(zhí)行文件,就得確保用戶在 PATH 中包含``python3.dll`` 的路徑(安裝程序默認(rèn)不會如此),或者應(yīng)把應(yīng)用程序與內(nèi)嵌版 Python 一起打包。

  3. 上述給出的啟動程序采用了 Python 嵌入 API。 這意味著應(yīng)用程序?qū)?sys.executable ,而*不是*傳統(tǒng)的 Python 解釋器。代碼及依賴項需做好準(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 腳本,父級(這里指打包文件)將放入 sys.path ,因此可從打包文件中導(dǎo)入更多的模塊。

zip 文件格式允許在文件中預(yù)置任意數(shù)據(jù)。利用這種能力,zip 應(yīng)用程序格式在文件中預(yù)置了一個標(biāo)準(zhǔn)的 POSIX “釋伴”行(#!/path/to/interpreter)。

因此,Python zip 應(yīng)用程序的格式會如下所示:

  1. 可選的釋伴行,包含字符 b'#!',后面是解釋器名,然后是換行符 (b'\n')。 解釋器名可為操作系統(tǒng) “釋伴”處理所能接受的任意值,或為 Windows 系統(tǒng)中的 Python 啟動程序。解釋器名在 Windows 中應(yīng)用 UTF-8 編碼,在 POSIX 中則用 sys.getfilesystemencoding()。

  2. 標(biāo)準(zhǔn)的打包文件由 zipfile 模塊生成。其中 必須 包含一個名為``__main__.py`` 的文件(必須位于打包文件的“根”目錄——不能位于某個子目錄中)。打包文件中的數(shù)據(jù)可以是壓縮或未壓縮的。

如果應(yīng)用程序的打包文件帶有釋伴行,則在 POSIX 系統(tǒng)中可能需要啟用可執(zhí)行屬性,以允許直接執(zhí)行。

不一定非要用本模塊中的工具創(chuàng)建應(yīng)用程序打包文件,本模塊只是提供了便捷方案,上述格式的打包文件可用任何方式創(chuàng)建,均可被 Python 接受。