zipapp —— 管理可執行的 Python zip 打包文件?

3.5 新版功能.

源代碼: Lib/zipapp.py


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

簡(jiǎn)單示例?

下述例子展示了用 命令行接口 根據含有 Python 代碼的目錄創(chuàng )建一個(gè)可執行的打包文件。 運行后該打包文件時(shí),將會(huì )執行 myapp 模塊中的 main 函數。

$ python -m zipapp myapp -m "myapp:main"
$ python myapp.pyz
<output from myapp>

命令行接口?

若要從命令行調用,則采用以下形式:

$ python -m zipapp source [options]

如果 source 是個(gè)目錄,將根據 source 的內容創(chuàng )建一個(gè)打包文件。如果 source 是個(gè)文件,則應為一個(gè)打包文件,將會(huì )復制到目標打包文件中(如果指定了 -info 選項,將會(huì )顯示 shebang 行的內容)。

可以接受以下參數:

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

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

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

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

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

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

在打包文件中寫(xiě)入一個(gè) __main__.py 文件,用于執行 mainfn。mainfn 參數的形式應為 “pkg.mod:fn”,其中 “pkg.mod”是打包文件中的某個(gè)包/模塊,“fn”是該模塊中的一個(gè)可調用對象。__main__.py 文件將會(huì )執行該可調用對象。

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

-c, --compress?

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

在復制打包文件時(shí),--compress 無(wú)效。

3.7 新版功能.

--info?

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

-h, --help?

打印簡(jiǎn)短的用法信息并退出。

Python API?

該模塊定義了兩個(gè)快捷函數:

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

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

  • 一個(gè)目錄名,或指向目錄的 path-like object ,這時(shí)將根據目錄內容新建一個(gè)應用程序打包文件。

  • 一個(gè)已存在的應用程序打包文件名,或指向這類(lèi)文件的 path-like object,這時(shí)會(huì )將該文件復制為目標文件(會(huì )稍作修改以反映出 interpreter 參數的值)。必要時(shí)文件名中應包括 .pyz 擴展名。

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

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

  • 若是個(gè)文件名,或是 path-like object,打包文件將寫(xiě)入該文件中。

  • 若是個(gè)打開(kāi)的文件對象,打包文件將寫(xiě)入該對象,該文件對象必須在字節串寫(xiě)入模式下打開(kāi)。

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

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

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

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

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

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

當復制已有的打包文件時(shí),提供的文件對象只需 readreadline 方法,或 write 方法。當由目錄創(chuàng )建打包文件時(shí),若目標為文件對象,將會(huì )將其傳給 類(lèi),且必須提供 zipfile.ZipFile 類(lèi)所需的方法。

3.7 新版功能: 加入了 filtercompressed 參數。

zipapp.get_interpreter(archive)?

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

例子?

將目錄打包成一個(gè)文件并運行它。

$ python -m zipapp myapp
$ python myapp.pyz
<output from myapp>

同樣還可用 create_archive() 函數完成:

>>>
>>> import zipapp
>>> zipapp.create_archive('myapp', 'myapp.pyz')

要讓?xiě)贸绦蚰茉?POSIX 平臺上直接執行,需要指定所用的解釋器。

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

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

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

若要原地更新打包文件,可用 BytesIO 對象在內存中進(jìn)行替換,然后再覆蓋源文件。注意,原地覆蓋文件會(huì )有風(fēng)險,出錯時(shí)會(huì )丟失原文件。這里沒(méi)有考慮出錯情況,但生產(chǎn)代碼則應進(jìn)行處理。另外,這種方案僅當內存足以容納打包文件時(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ā)布應用程序打包文件,需要確保所用到的解釋器是可移植的。Windows 的 Python 啟動(dòng)器支持大多數常見(jiàn)的 POSIX #! 行,但還需要考慮一些其他問(wèn)題。

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

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

  • 因為無(wú)法指定“python X.Y以上版本”,所以應小心“/usr/bin/env python3.4”這種精確版本的指定方式,因為對于 Python 3.5 的用戶(hù)就得修改釋伴行,比如:

通常應該用“/usr/bin/env python2”或“/usr/bin/env python3”的格式,具體根據代碼適用于 Python 2 還是 3 而定。

用 zipapp 創(chuàng )建獨立運行的應用程序?

利用 zipapp 模塊可以創(chuàng )建獨立運行的 Python 程序,以便向最終用戶(hù)發(fā)布,僅需在系統中裝有合適版本的 Python 即可運行。操作的關(guān)鍵就是把應用程序代碼和所有依賴(lài)項一起放入打包文件中。

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

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

  2. 用 pip 將應用程序的所有依賴(lài)項裝入 myapp 目錄。

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

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

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

  4. 用以下命令打包:

    $ python -m zipapp -p "interpreter" myapp
    

這會(huì )生成一個(gè)獨立的可執行文件,可在任何裝有合適解釋器的機器上運行。詳情參見(jiàn) 指定解釋器程序??梢詥蝹€(gè)文件的形式分發(fā)給用戶(hù)。

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

制作 Windows 可執行文件?

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

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

合適的啟動(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);
}

若已定義了預處理器符號 WINDOWS,上述代碼將會(huì )生成一個(gè) GUI 可執行文件。若未定義則生成一個(gè)可執行的控制臺文件。

直接使用標準的 MSVC 命令行工具,或利用 distutils 知道如何編譯 Python 源代碼,即可編譯可執行文件:

>>>
>>> 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 中運行。只要用戶(hù)的 PATH 中包含了 Python(python3.dll)路徑即可。

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

注意事項?

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

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

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

  3. 上述給出的啟動(dòng)程序采用了 Python 嵌入 API。 這意味著(zhù)應用程序將會(huì )是 sys.executable ,而*不是*傳統的 Python 解釋器。代碼及依賴(lài)項需做好準備。例如,如果應用程序用到了 multiprocessing 模塊,就需要調用 multiprocessing.set_executable() 來(lái)讓模塊知道標準 Python 解釋器的位置。

Python 打包應用程序的格式?

自 2.6 版開(kāi)始,Python 即能夠執行包含 文件的打包文件了。為了能被 Python 執行,應用程序的打包文件必須為包含 __main__.py 文件的標準 zip 文件,__main__.py 文件將作為應用程序的入口運行。類(lèi)似于常規的 Python 腳本,父級(這里指打包文件)將放入 sys.path ,因此可從打包文件中導入更多的模塊。

zip 文件格式允許在文件中預置任意數據。利用這種能力,zip 應用程序格式在文件中預置了一個(gè)標準的 POSIX “釋伴”行(#!/path/to/interpreter)。

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

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

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

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

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