作者:Tarek Ziadé,翻譯:張吉
對(duì)于如何安裝軟件,目前有兩種思想流派。第一種是說(shuō)軟件應(yīng)該自給自足,不依賴于其它任何部件,這點(diǎn)在Windows和Mac OS X系統(tǒng)中很流行。這種方式簡(jiǎn)化了軟件的管理:每個(gè)軟件都有自己獨(dú)立的“領(lǐng)域”,安裝和卸載它們不會(huì)對(duì)操作系統(tǒng)產(chǎn)生影響。如果軟件依賴一項(xiàng)不常見(jiàn)的類(lèi)庫(kù),那么這個(gè)類(lèi)庫(kù)一定是包含在軟件安裝包之中的。
第二種流派,主要在類(lèi)Linux的操作系統(tǒng)中盛行,即軟件應(yīng)該是由一個(gè)個(gè)獨(dú)立的、小型的軟件包組成的。類(lèi)庫(kù)被包含在軟件包中,包與包之間可以有依賴關(guān)系。安裝軟件時(shí)需要查找和安裝它所依賴的其他特定版本的軟件包。這些依賴包通常是從一個(gè)包含所有軟件包的中央倉(cāng)庫(kù)中獲取的。這種理念也催生了Linux發(fā)行版中那些復(fù)雜的依賴管理工具,如dpkg
和RPM
。它們會(huì)跟蹤軟件包的依賴關(guān)系,并防止兩個(gè)軟件使用了版本相沖突的第三方包。
以上兩種流派各有優(yōu)劣。高度模塊化的系統(tǒng)可以使得更新和替換某個(gè)軟件包變的非常方便,因?yàn)槊總€(gè)類(lèi)庫(kù)都只有一份,所有依賴于它的應(yīng)用程序都能因此受益。比如,修復(fù)某個(gè)類(lèi)庫(kù)的安全漏洞可以立刻應(yīng)用到所有程序中,而如果應(yīng)用程序使用了自帶的類(lèi)庫(kù),那安全更新就很難應(yīng)用進(jìn)去了,特別是在類(lèi)庫(kù)版本不一致的情況下更難處理。
不過(guò)這種“模塊化”也被一些開(kāi)發(fā)者視為缺點(diǎn),因?yàn)樗麄儫o(wú)法控制應(yīng)用程序的依賴關(guān)系。他們希望提供一個(gè)獨(dú)立和穩(wěn)定的軟件運(yùn)行環(huán)境,這樣就不會(huì)在系統(tǒng)升級(jí)后遭遇各種依賴方面的問(wèn)題。
在安裝程序中包含所有依賴包還有一個(gè)優(yōu)點(diǎn):便于跨平臺(tái)。有些項(xiàng)目在這點(diǎn)上做到了極致,它們將所有和操作系統(tǒng)的交互都封裝了起來(lái),在一個(gè)獨(dú)立的目錄中運(yùn)行,甚至包括日志文件的記錄位置。
Python的打包系統(tǒng)使用的是第二種設(shè)計(jì)思想,并盡可能地方便開(kāi)發(fā)者、管理員、用戶對(duì)軟件的管理。不幸的是,這種方式導(dǎo)致了種種問(wèn)題:錯(cuò)綜復(fù)雜的版本結(jié)構(gòu)、混亂的數(shù)據(jù)文件、難以重新打包等等。三年前,我和其他一些Python開(kāi)發(fā)者決定研究解決這個(gè)問(wèn)題,我們自稱為“打包別動(dòng)隊(duì)”,本文就是講述我們?cè)谶@個(gè)問(wèn)題上做出的努力和取得的成果。
在Python中,?包?表示一個(gè)包含Python文件的目錄。Python文件被稱為?模塊?,這樣一來(lái),使用“包”這個(gè)單詞就顯得有些模糊了,因?yàn)樗3S脕?lái)表示某個(gè)項(xiàng)目的?發(fā)行版本?。
Python開(kāi)發(fā)者有時(shí)也對(duì)此表示不能理解。為了更清晰地進(jìn)行表述,我們用“Python包(package)”來(lái)表示一個(gè)包含Python文件的目錄,用“發(fā)行版本(release)”來(lái)表示某個(gè)項(xiàng)目的特定版本,用“發(fā)布包(distribution)”來(lái)表示某個(gè)發(fā)行版本的源碼或二進(jìn)制文件,通常是Tar包或Zip文件的形式。
大多數(shù)Python開(kāi)發(fā)者希望自己的程序能夠在任何環(huán)境中運(yùn)行。他們還希望自己的軟件既能使用標(biāo)準(zhǔn)的Python類(lèi)庫(kù),又能使用依賴于特定系統(tǒng)類(lèi)型的類(lèi)庫(kù)。但除非開(kāi)發(fā)者使用現(xiàn)有的各種打包工具生成不同的軟件包,否則他們打出的軟件安裝包就必須在一個(gè)安裝有Python環(huán)境的系統(tǒng)中運(yùn)行。這樣的軟件包還希望做到以下幾點(diǎn):
要做到以上幾點(diǎn)往往是不可能的。舉例來(lái)說(shuō),Plone這一功能全面的CMS系統(tǒng),使用了上百個(gè)純Python語(yǔ)言編寫(xiě)的類(lèi)庫(kù),而這些類(lèi)庫(kù)并不一定在所有的打包系統(tǒng)中提供。這就意味著Plone必須將它所依賴的軟件包都集成到自己的安裝包中。要做到這一點(diǎn),他們選擇使用zc.buildout
這一工具,它能夠?qū)⑺械囊蕾嚢际占饋?lái),生成一個(gè)完整的應(yīng)用程序文件,在獨(dú)立的目錄中運(yùn)行。它事實(shí)上是一個(gè)二進(jìn)制的軟件包,因?yàn)樗蠧語(yǔ)言代碼都已經(jīng)編譯好了。
這對(duì)開(kāi)發(fā)者來(lái)說(shuō)是福音:他們只需要描述好依賴關(guān)系,然后借助zc.buildout
來(lái)發(fā)布自己的程序即可。但正如上文所言,這種發(fā)布方式在系統(tǒng)層面構(gòu)筑了一層屏障,這讓大多數(shù)Linux系統(tǒng)管理員非常惱火。Windows管理員不會(huì)在乎這些,但CentOS和Debian管理員則會(huì),因?yàn)榘凑账麄兊墓芾碓瓌t,系統(tǒng)中的所有文件都應(yīng)該被注冊(cè)和歸類(lèi)到現(xiàn)有的管理工具中。
這些管理員會(huì)想要將你的軟件按照他們自己的標(biāo)準(zhǔn)重新打包。問(wèn)題在于:Python有沒(méi)有這樣的打包工具,能夠自動(dòng)地按照新的標(biāo)準(zhǔn)重新打包?如果有,那么Python的任何軟件和類(lèi)庫(kù)就能夠針對(duì)不同的目標(biāo)系統(tǒng)進(jìn)行打包,而不需要額外的工作。這里,“自動(dòng)”一詞并不是說(shuō)打包過(guò)程可以完全由腳本來(lái)完成——這點(diǎn)上RPM
和dpkg
的使用者已經(jīng)證實(shí)是不可能的了,因?yàn)樗麄兛倳?huì)需要增加額外的信息來(lái)重新打包。他們還會(huì)告訴你,在重新打包的過(guò)程中會(huì)遇到一些開(kāi)發(fā)者沒(méi)有遵守基本打包原則的情況。
我們來(lái)舉一個(gè)實(shí)際例子,如何通過(guò)使用現(xiàn)有的Python打包工具來(lái)惹惱那些想要重新打包的管理員:在發(fā)布一個(gè)名為“MathUtils”的軟件包時(shí)使用“Fumanchu”這樣的版本號(hào)名字。撰寫(xiě)這個(gè)類(lèi)庫(kù)的數(shù)學(xué)家想用自家貓咪的名字來(lái)作為版本號(hào),但是管理員怎么可能知道“Fumanchu”是他家第二只貓的名字,第一只貓叫做“Phil”,所以“Fumanchu”版本要比“Phil”版本來(lái)得高?
可能這個(gè)例子有些極端,但是在現(xiàn)有的打包工具和規(guī)范中是可能發(fā)生的。最壞的情況是easy_install
和pip
使用自己的一套標(biāo)準(zhǔn)來(lái)追蹤已安裝的文件,并使用字母順序來(lái)比較“Fumanchu”和“Phil”的版本高低。
另一個(gè)問(wèn)題是如何處理數(shù)據(jù)文件。比如,如果你的軟件使用了SQLite數(shù)據(jù)庫(kù),安裝時(shí)被放置在包目錄中,那么在程序運(yùn)行時(shí),系統(tǒng)會(huì)阻止你對(duì)其進(jìn)行讀寫(xiě)操作。這樣做還會(huì)破壞Linux系統(tǒng)的一項(xiàng)慣例,即/var
目錄下的數(shù)據(jù)文件是需要進(jìn)行備份的。
在現(xiàn)實(shí)環(huán)境中,系統(tǒng)管理員需要能夠?qū)⒛愕奈募胖玫剿麄兿胍牡胤剑⑶也黄茐某绦虻耐暾?,這就需要你來(lái)告訴他們各類(lèi)文件都是做什么用的。讓我們換一種方式來(lái)表述剛才的問(wèn)題:Python是否有這樣一種打包工具,它可以提供各類(lèi)信息,足以讓第三方打包工具能據(jù)此重新進(jìn)行打包,而不需要閱讀軟件的源碼?
Python標(biāo)準(zhǔn)庫(kù)中提供的Distutils
打包工具充斥了上述的種種問(wèn)題,但由于它是一種標(biāo)準(zhǔn),所以人們要么繼續(xù)忍受并使用它,或者轉(zhuǎn)向更先進(jìn)的工具Setuptools
,它在Distutils之上提供了一些高級(jí)特性。另外還有Distribute
,它是Setuptools
的衍生版本。Pip
則是一種更為高級(jí)的安裝工具,它依賴于Setuptools
。
但是,這些工具都源自于Distutils
,并繼承了它的種種問(wèn)題。有人也想過(guò)要改進(jìn)Distutils
本身,但是由于它的使用范圍已經(jīng)很廣很廣,任何小的改動(dòng)都會(huì)對(duì)Python軟件包的整個(gè)生態(tài)系統(tǒng)造成沖擊。
所以,我們決定凍結(jié)Distutils
的代碼,并開(kāi)始研發(fā)Distutils2
,不去考慮向前兼容的問(wèn)題。為了解釋我們所做的改動(dòng),首先讓我們近距離觀察一下Distutils
。
Distutils
由一些命令組成,每條命令都是一個(gè)包含了run
方法的類(lèi),可以附加若干參數(shù)進(jìn)行調(diào)用。Distutils
還提供了一個(gè)名為Distribution
的類(lèi),它包含了一些全局變量,可供其他命令使用。
當(dāng)要使用Distutils
時(shí),Python開(kāi)發(fā)者需要在項(xiàng)目中添加一個(gè)模塊,通常命名為setup.py
。這個(gè)模塊會(huì)調(diào)用Distutils
的入口函數(shù):setup
。這個(gè)函數(shù)有很多參數(shù),這些參數(shù)會(huì)被Distribution
實(shí)例保存起來(lái),供后續(xù)使用。下面這個(gè)例子中我們指定了一些常用的參數(shù),如項(xiàng)目名稱和版本,它所包含的模塊等:
from distutils.core import setup
setup(name='MyProject', version='1.0', py_modules=['mycode.py'])
這個(gè)模塊可以用來(lái)執(zhí)行Distutils
的各種命令,如sdist
。這條命令會(huì)在dist
目錄中創(chuàng)建一個(gè)源代碼發(fā)布包:
$ python setup.py sdist
這個(gè)模塊還可以執(zhí)行install
命令:
$ python setup.py install
Distutils
還提供了一些其他命令:
upload
?將發(fā)布包上傳至在線倉(cāng)庫(kù)register
?向在線倉(cāng)庫(kù)注冊(cè)項(xiàng)目的基本信息,而不上傳發(fā)布包bdist
?創(chuàng)建二進(jìn)制發(fā)布包bdist_msi
?創(chuàng)建.msi
安裝包,供Windows系統(tǒng)使用我們還可以使用其他一些命令來(lái)獲取項(xiàng)目的基本信息。
所以在安裝或獲取應(yīng)用程序信息時(shí)都是通過(guò)這個(gè)文件調(diào)用Distutils
實(shí)現(xiàn)的,如獲取項(xiàng)目名稱:
$ python setup.py --name
MyProject
setup.py
是一個(gè)項(xiàng)目的入口,可以通過(guò)它對(duì)項(xiàng)目進(jìn)行構(gòu)建、打包、發(fā)布、安裝等操作。開(kāi)發(fā)者通過(guò)這個(gè)函數(shù)的參數(shù)信息來(lái)描述自己的項(xiàng)目,并使用它進(jìn)行各種打包任務(wù)。這個(gè)文件同樣用于在目標(biāo)系統(tǒng)中安裝軟件。
圖14.2:PyPI倉(cāng)庫(kù)
你可以通過(guò)Classifies
(類(lèi)別)來(lái)瀏覽,獲取項(xiàng)目作者的名字和主頁(yè)。同時(shí),Requires
可以用來(lái)定義Python模塊的依賴關(guān)系。requires
選項(xiàng)可以向元信息文件的Requires
字段添加信息:
from distutils.core import setup
setup(name='foo', version='1.0', requires=['ldap'])
這里聲明了對(duì)ldap
模塊的依賴,這種依賴并沒(méi)有實(shí)際效力,因?yàn)闆](méi)有安裝工具會(huì)保證這個(gè)模塊真實(shí)存在。如果說(shuō)Python代碼中會(huì)使用類(lèi)似Perl的require
關(guān)鍵字來(lái)定義依賴關(guān)系,那還有些作用,因?yàn)檫@時(shí)安裝工具會(huì)檢索PyPI上的信息并進(jìn)行安裝,其實(shí)這也就是CPAN的做法。但是對(duì)于Python來(lái)說(shuō),ldap
模塊可以存在于任何項(xiàng)目之中,因?yàn)?code>Distutils是允許開(kāi)發(fā)者發(fā)布一個(gè)包含多個(gè)模塊的軟件的,所以這里的元信息字段并無(wú)太大作用。
Metadata
的另一個(gè)缺點(diǎn)是,因?yàn)樗怯蒔ython腳本創(chuàng)建的,所以會(huì)根據(jù)腳本執(zhí)行環(huán)境的不同而產(chǎn)生特定信息。比如,運(yùn)行在Windows環(huán)境下的一個(gè)項(xiàng)目會(huì)在setup.py
文件中有以下描述:
from distutils.core import setup
setup(name='foo', version='1.0', requires=['win32com'])
這樣配置相當(dāng)于是默認(rèn)該項(xiàng)目只會(huì)運(yùn)行在Windows環(huán)境下,即使它可能提供了跨平臺(tái)的方案。一種解決方法是根據(jù)不同的平臺(tái)來(lái)指定requires
參數(shù):
from distutils.core import setup
import sys
if sys.platform == 'win32':
setup(name='foo', version='1.0', requires=['win32com'])
else:
setup(name='foo', version='1.0')
但這種做法往往會(huì)讓事情更糟。要注意,這個(gè)腳本是用來(lái)將項(xiàng)目的源碼包發(fā)布到PyPI上的,這樣寫(xiě)就說(shuō)明它向PyPI上傳的Metadata
文件會(huì)因?yàn)樵撃_本運(yùn)行環(huán)境的不同而不同。換句話說(shuō),這使得我們無(wú)法在元信息文件中看出這個(gè)項(xiàng)目依賴于特定的平臺(tái)。
http://python.org/peps/pep-0214.html
在這里我想感謝所有為打包標(biāo)準(zhǔn)的制定做出貢獻(xiàn)的人們,你可以在PEP中找到他們的名字。我還要特別感謝“打包別動(dòng)隊(duì)”的成員們。還要謝謝Alexis Metaireau、Toshio Kuratomi、Holger Krekel、以及Stefane Fermigier,感謝他們對(duì)本文提供的反饋。
本章中討論的項(xiàng)目有:
更多建議: