Setuptools
和 distutils
都是用於編譯、分發和安裝 python 包的一個工具,特別是在包依賴問題場景下很是有用,它是一個強大的包管理工具。Setuptools 是 distutils 的增強版。編譯、分發和安裝 python 包的一個關鍵的事就是編寫 setup 腳本。setup 腳本的主要做用在於向包管理工具 Setuptools
或 distutils
說明你的模塊分發細節,因此 Setuptools
支持大量的命令以操做你的包。setup 腳本主要調用一個 setup()
方法,許多提供給 Setuptools
的信息都以 keyword arguments
的參數形式提供給 setup()
方法。html
對於包開發者和使用者,所須要作的事:python
python setup.py sdist
,python setup.py bdist
對於包使用者,只須要 python setup.py install
,即可以成功安裝 python 包。linux
import
語句導入到另外一個 module;module 分爲:pure python module
(純 python 模塊)、extension module
(擴展模塊)和 package
(包).py
文件做爲一個模塊使用,也就是一個 .py
能夠稱爲模塊了distutils
只支持 C/C++ 和 Objective-C,不支持 Java 編寫擴展模塊;可是 python 提供了一個 JCC
這樣一個用於生成訪問 Java 類接口的 C++ 代碼的膠水模塊,應該也是可使用 Java 編寫模塊的。__init__.py
文件的文件夾,用於包含其餘模塊__init__.py
文件。大量的標準庫位於 root package
,由於它們不屬於一個任何更大的模塊集合了。實際上,每個 sys.path
列舉出來的文件夾都是 root package
,你能夠在這些文件夾中找到大量的模塊。distribution
。setup.py
所在的位置。源碼分發文件是將包分享給其餘人更爲推薦的一種形式,由於源碼分發文件比二進制分發文件更適合跨平臺,這樣使用者能夠在本身的機器上經過編譯獲得本身的機器相關的包代碼而且進行安裝。ios
py_modules
選項一個一個地指定;py_modules
一個一個指定比較麻煩,特別是模塊位於多個包中,那麼你可使用 packages
指定整個包,另外只須要另外指定 package_dir
,位於 distribution root 下的模塊文件也能夠被處理;package_dir
和 py_modules
也能夠支持分發任何沒有包含 __init__.py
文件夾下的模塊,經測試安裝過程沒有報錯,可是沒有包含 __init__.py
的文件夾下的模塊是沒有被正確安裝的!所以,若是 python 模塊分佈在不一樣的文件夾,最好是在該文件夾下建立一個 __init__.py
文件,以表示它是一個包。舉個簡單的例子,你須要發佈兩個模塊 foo 以及 bar.bar,以供別人使用(import foo 和 import bar.bar)。
其目錄樹以下:git
pure_module ├── bar │ └── bar.py │ └── __init__.py ├── foo.py └── setup.py
如上圖所示,pure_module 目錄下包含了一個 foo 模塊以及一個 bar 包,同時在 bar 包下還包含了一個 bar 模塊。算法
一個僅使用 py_modules
的 setup 腳本能夠這樣寫:windows
from setuptools import setup NAME = 'foo' VERSION = '1.0' PY_MODULES = ['foo', 'bar.bar'] setup(name = NAME , version = VERSION , py_modules = PY_MODULES)
py_modules 指定了 foo 模塊以及 bar.bar 模塊。app
經過 python setup.py install --user --prefix=
進行安裝後,即可以直接經過 import foo 和 import bar.bar
直接使用了。curl
.py
文件位於其餘文件夾,該文件夾須要建立一個容許爲空的 __init__.py
文件,表示爲一個 package,不然安裝後不能正常使用其餘文件夾的模塊。上一節的例子中,bar.bar 屬於 bar 包,foo 位於 distribution root,安裝後屬於 root package,在 Setuptools 中 "" 能夠用於表示 root package。因此下面展現兩種 setup 腳本的寫法:
pure_module ├── bar │ └── bar.py │ └── __init__.py ├── foo.py └── setup.py
僅使用 packages
的 setup.py 文件以下:
from setuptools import setup NAME = 'foo' VERSION = '1.0' PACKAGES = ['', 'bar'] setup(name = NAME , version = VERSION , packages = PACKAGES )
如上,packages 包含了 package 的列表 root package 以及 bar,這樣便能輕鬆覆蓋到 distribution root 下的 foo.py 和 bar 文件夾下的 bar.py 了,在存在大量模塊的狀況下,省去像 py_modules
同樣窮舉模塊的麻煩。在 python 中,默認的狀況下,包的名字和目錄的名字是一致的,好比 bar 包對應了 bar 目錄,且包的路徑表示是相對於 distribution root 的(也就是 setup.py 所在目錄)。好比 packages = ['foo']
,Setuptools 會在 setup.py 所在目錄下尋找 foo/__init__.py
,並將 foo/ 下的全部模塊包含進去。
另一個關鍵字是 package_dir
,它的做用是將 package 映射到其餘目錄,這樣的一個好處是方便將 package 移到其餘目錄而不用修改 packages
的參數值。舉個例子,假設咱們如今須要把 bar 移到 foobar 目錄下,按照原來的腳本,Setuptools 是沒法成功找到 bar 包的。
package_dir ├── foobar │ ├── bar.py │ └── __init__.py ├── foo.py └── setup.py
經過 package_dir = = {'bar':'foobar'}
,將原來的 bar package 映射到 foobar 下。完整的腳本以下:
from setuptools import setup NAME = 'foo' VERSION = '1.0' PACKAGE_DIR = {'bar':'foobar'} PACKAGES = ['', 'bar'] setup(name = NAME , version = VERSION , package_dir = PACKAGE_DIR , packages = PACKAGES )
package_dir 是一個字典,它的 key 是 package 名("" 表示 root package),value 是相對於 distribution root 的目錄名。在上面的例子中, package_dir = = {'bar':'foobar'}
改變了 packages
中 package 對應的目錄位置,這樣當 Setuptools 在找 bar package 時,會在 foobar 目錄下找相應的 __init__.py
文件。
packages =['bar', 'bar.lib']
,package_dir 不只會影響全部和 bar 有關的 package,bar.lib
也會相應被映射到 foorbar.lib
。擴展模塊須要使用 ext_modules
參數。上面所說的 package_dir
和 packages
都是針對純 python 模塊,而 ext_modules
針對的是使用 C/C++ 底層語言所寫的模塊。下面舉個最簡單的例子,擴展模塊僅包含一個 foo.cpp 文件,其中定義了可供 python 調用的 myPrint 函數。
#include <iostream> #include <string> using namespace std; void myPrint(string text) { cout << text << endl; }
. ├── setup.py └── src ├── foo.cpp ├── foo.h └── PythonWrapAPI.cpp
如今,咱們想要發佈擴展模塊供別人使用咱們的 myPrint 方法。想要 python 中成功導入你的包,須要利用額外的代碼封裝將被調用的方法。這裏的 PythonWrapAPI.cpp 的做用就是使用 Python 提供的庫封裝你所寫的接口,它是處在 python 和 C++ 間的膠水庫,當 python 調用你的 C++ 方法時,因爲語言類型的差異,須要作轉換。
PythonWrapAPI.cpp 以下:
#include "foo.h" #include <string> #include <python2.6/Python.h> using namespace std; /* Notice:Python Interface Wrap */ static PyObject *_myPrint(PyObject *self, PyObject *args) { char *text; // 解析 Python 傳過來的參數 if (!PyArg_ParseTuple(args, "s", &text)) return NULL; myPrint(text); return Py_None; } static PyMethodDef ExtestMethods[] = { { "myPrint", _myPrint, METH_VARARGS }, { NULL, NULL }, }; PyMODINIT_FUNC initmyprint(void) { (void)Py_InitModule("myprint", ExtestMethods); }
擴展模塊和純 python 模塊有點不太同樣,咱們導入的 package 名字與 setup() 的 packages
或者 package_dir
參數是一致的,可是擴展模塊的名字是由 Extension
實例的 name
參數決定的,且須要和導出函數對應的initxxx 名字以及 Py_InitModule
方法對應的第一個參數相同。
最後,咱們須要編寫 setup 腳本編譯咱們的 cpp 文件爲 so 動態連接庫,並進行相應的封裝。運行 python setup.py install
,setuptools 會幫咱們自動編譯。
from setuptools import setup, Extension, find_packages # package name, import NAME NAME = "foo" VERSION = '1.0.0' # an Extension instance list EXT_MODULES = [ Extension( name = 'myprint' , sources=['src/foo.cpp','src/PythonWrapAPI.cpp'] , include_dirs = ['src'] ) ] setup(name = NAME , version = VERSION , ext_modules = EXT_MODULES , )
ext_modules
是一個 Extension
實例列表,Extension 的參數 sources 用於指定全部源文件位置,include_dirs 指定頭文件位置,同時還可使用 library_dirs 和 libraries 指定外部連接庫,以及 extra_compile_args 指定額外的編譯參數。
在編寫一個 package 的時候,儘可能提供更多的元信息,這樣使用者更加可以瞭解到 package 的相關信息,而且有些信息會被 PyPi 使用。
元信息 | 類型 | 描述 | 說明 |
---|---|---|---|
name | short string | package 名字, 這裏用於 pypi 的顯示, 而不是用於 import, import 的包名與 packages 和 package_dir 參數一致 | 1 |
version | short string | package 的發佈版本, 建議:major.minor[.patch[.sub]] | 1 |
author | short string | package 做者名字 | 3 |
author_email | short string | package 做者郵箱 | 3 |
maintainer | short string | package 維護者名字 | 3 |
maintainer_email | short string | package 維護者郵箱 | 3 |
url | short string | package 項目地址 | 1 |
description | short string | 簡介 | |
long_description | long string | 顯示於 pypi 的介紹 | |
download_url | short string | package 下載地址 | 2 |
classifiers | strings list | 分類符, 這樣便於 pypi 索引, 由 pypi 固定提供, https://pypi.python.org/pypi?%3Aaction=list_classifiers | 2 |
platforms | strings list | 支持的平臺列表 | |
license | short string | 受權協議 |
py_modules 是一個字符串列表,用於指定全部的模塊,即 py 文件模塊。 若是你只是發佈幾個腳本文件而已,特別是它們邏輯上不屬於同一個 package。
好比,py_modules = ['mod1', 'pkg.mod2']
。這指定了兩個模塊,一個位於 root package
,而另外一個位於 pkg package
。若是沒有使用 package_dir
從新映射 package 和目錄的關係的話,那麼這兩個模塊分別對應了 mod1.py
以及 pkg/mod2.py
文件,而且在 pkg 文件夾下還存在 __init__.py
文件。
若是你須要發佈的模塊文件太多,使用 py_modules
一個一個指定比較麻煩,特別是模塊位於多個包中,那麼你可使用 packages
指定整個包。
packages 是一個包名列表,packages 參數告訴 Setuptools 處理列舉出的 package 下全部純 python 模塊。在文件系統中,默認地,package 的名字與目錄是一一對應的,也就是說,packages = ['foo']
,Setuptools 會去查找 foo/__init__.py
文件。
當你想要重命名你的 package 所在的文件夾,或者想要移動整個 package 到其餘目錄下,通常狀況下,一旦你的源代碼佈局改變,你須要從新修改 packages。可是 package_dir 能夠從新映射 package 和目錄的關係。好比你將 root package 下的模塊和 package 移到 lib 目錄下,那麼你只須要在 package_dir 中將 root package 映射到 lib 下。好比 package_dir = {'': 'lib'}
再舉個例子,好比一下目錄結果,當我使用 package_dir= {'bar':'foobar'}
和 packages= ['bar']時,Setuptools 根據 packages
參數查找 bar package 時,會在 foobar 文件夾下找相應的 __init__.py
文件。
package_dir ├── foobar │ ├── bar.py │ └── __init__.py └── setup.py
packages =['bar', 'bar.lib']
,package_dir 不只會影響全部和 bar 有關的 package,bar.lib
也會相應被映射到 foorbar.lib
。install_requires
能夠聲明 package 安裝時所需的依賴模塊及其版本。安裝 package 時,Setuptools 便可以從 PyPi 上自動下載其所依賴的模塊,而且將依賴信息包含進 Python Eggs 中。
好比咱們在本身的 package 中用到了一個 python 非標準庫 pycurl 和 xmltodict,當咱們的 package 在別的機器上使用時便會報錯。爲了解決這個問題,咱們可使用 install_requires = ['pycurl', 'xmltodict']
將 pycurl 和 xmltodict 加入 package 依賴。
install_requires
能夠是 string 或 string list,指明所須要依賴的模塊。當 install_requires
爲 string 類型,而且依賴多於 1 個時,每一個依賴的聲明都要另起一行。
最新版本的 Setuptools 的install_requires
有另外兩個做用:
前面說到,Setuptools 便可以從 PyPi 上自動下載其所依賴的模塊,可是在某些環境下沒法正常訪問 Pypi 下,咱們也能夠經過 dependency_links
參數 指定到本身的 python 源,這樣即可以解決下載問題。
好比,dependency_links = ['http://xxx/xmltodict', 'http://xxx/pycurl']
。
dependency_links 是一個字符串列表,包含了依賴的下載 URL。
Setuptools 對連接的支持比較強大!
下載的資源能夠知足如下條件:
URL 連接能夠是:
當包含資源下載連接的網頁 URL 中存在多個版本時,Setuptools 會根據版本要求下載合適的版本。
通常,比較好的方式是網頁 URL 方式。咱們也可使用 SourceForge 的 showfiles.php 連接來下載咱們所依賴的模塊。
若是依賴的模塊是一個 py 文件時,你必須在 URL 添加 "#egg=project-version"
後綴,以指出模塊的名字和版本,另外須要確保將模塊名和版本中出現的 -
替換爲 _
。EasyInstall 將會識別這個後綴而且自動建立一個 setup.py 腳本,將咱們的 py 文件包裝爲 egg 文件。若是爲 VCS,將會 checkout 對應的版本,建立一個臨時文件夾並執行 setup.py bdist_egg,安裝所需的依賴。
在使用 VCS 的狀況下,你也可使用 #egg=project-version
指定要使用的版本。你能夠經過在 #egg=project-version
前加入 @REV
來指定要 checkout 的版本。另外你也能夠經過在 URL 前加上如下標識顯式聲明 URL 使用 的是 VCS:
svn+URL
git+URL
hg+URL
所以使用 VCS 更復雜的一個示例爲: vcs+proto://host/path@revision#egg=project-version
Python 的可擴展性特別強,不只支持 python 語言的擴展模塊,並且支持其餘語言的擴展。
Python 調用 C++ 的詳細文檔能夠查看 https://docs.python.org/2/extending/building.html
這裏假設已經懂得怎麼調用 C++ 方法了,接下來只須要使用 ext_module 參數,使 Setuptools 可以編譯和安裝擴展模塊了。
ext_module
參數是一個 Extension 實例列表,Extension 相似於 gcc/g++ 的所需參數,包含了指定源文件、頭文件、依賴的靜態庫或動態庫、額外的編譯參數、宏定義等功能。
name
是一個字符串,用於指定擴展模塊的名字。
packages
和 package_dir
用於支持 python 語言編寫模塊,其 import 語句使用的包名與 packages
和 package_dir
中所指定的名字是一致的。可是擴展模塊的名字是由 Extension
實例的 name
參數決定的,且須要和導出函數對應的initxxx 名字以及 Py_InitModule
方法對應的第一個參數相同。定義好模塊的名字 xxx 後,咱們即可以使用 import xxx
使用咱們本身的模塊了。
sources
爲用於指定要編譯源文件的字符串列表,好比,sources=['foo/foo.cpp', 'bar/bar.cpp']
,Setuptools 支持 C/C++ 以及 Objective-C。
include_dirs
爲用於指定編譯須要的頭文件目錄的字符串列表,好比,include_dirs=['foo/include', 'bar/include']
。若是頭文件位於 distribution root 目錄,須要使用 '.'
表示頭文件位於當前目錄,不能爲 ''
,不然將找不到頭文件。
另外還支持 extra_objects
向連接程序傳遞 object 文件,好比 .o
文件。
gcc 支持在編譯的時候定義新的宏變量和取消某個宏變量的定義,具體的選項 [-Dmacro[=defn]...] [-Umacro]
。Extension 也支持這樣的選項。
你可使用 define_macros
和 undef_macros
定義新的宏變量和取消某個宏變量的定義。
define_macros
是一個 (name, value)
元組列表,其中的 name 爲宏變量名字符串,value 爲對應的值,能夠爲字符串、數字或爲 None 類型(說明:官方文檔沒有聲明 value
能夠爲數字,可是通過測試,只要是 python 支持的數字類型均可以用於 value
,可是最好仍是使用字符串的形式,這樣腳本的兼容性會更好).
好比,define_macros=[('DEBUG', None), ('FOO', '1'), ('BAR', 2), ('FOOBAR', '"abc"')]
,gcc 對應的編譯選項結果爲 -DDEBUG -DFOO=1 -DBAR=2 -DFOOBAR="abc"
。
undef_macros
比 define_macros
簡單得多,它就是一個宏變量字符串列表,舉個例子,咱們想要取消以上定義的宏變量,對應的 undef_macros 值爲 undef_macros=['DEBUG', 'FOO', 'BAR', 'FOOBAR']
。
Setuptools 對 C/C++ 庫的引用方法和 gcc 同樣,具體的規則能夠參考 gcc。
libraries 爲要添加的庫的名字字符串列表,而 library_dirs 爲要添加的庫所在的目錄,舉個例子:
. ├── setup.py └── curl ├── include ├── curl.h ├── test.h ├── lib ├── libcurl.a ├── libtest.a
其對應的參數爲 libraries=['curl', 'test']
、library_dirs=['curl/lib']
、include_dirs=[‘curl/include’]
注意:在實際的使用過程當中碰到過一個連接錯誤的坑,Setuptools 在編譯的時候報錯:
libcurl.a : relocation against .rodata can not be used when making a shared object:recompile with -fPIC libcurl.a : could not read symbols:Bad value
前面提到,python 在建立擴展模塊時會將源文件編譯爲動態連接庫,動態連接庫在加載的時候,內存位置是不固定的,因此咱們連接的外部庫代碼也須要所有使用相對地址,這樣代碼即可以加載到內存的任意位置。由於有的庫沒有使用 -fPIC
選項進行編譯,致使庫最終在連接到 so 文件時報錯。
解決方案是使用 -fPIC
從新編譯 libcurl.a
庫。
在編譯擴展模塊時,Setuptools 會自動指定編譯參數,好比下面一個模塊的編譯:
gcc -pthread -fno-strict-aliasing -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -DNDEBUG -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DDEBUG -DFOO=1 -DBAR=2 -DFOOBAR="abc" -Isrc -I/usr/include/python2.6 -c src/foo.cpp -o build/temp.linux-x86_64-2.6/src/foo.o gcc -pthread -fno-strict-aliasing -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -DNDEBUG -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -DDEBUG -DFOO=1 -DBAR=2 -DFOOBAR="abc" -Isrc -I/usr/include/python2.6 -c src/PythonWrapAPI.cpp -o build/temp.linux-x86_64-2.6/src/PythonWrapAPI.o g++ -pthread -shared build/temp.linux-x86_64-2.6/src/foo.o build/temp.linux-x86_64-2.6/src/PythonWrapAPI.o -L/usr/lib64 -lpython2.6 -o build/lib.linux-x86_64-2.6/myprint.so
這麼多的編譯參數絕大部分是 Setuptools 自動指定的,可是若是咱們還想要在每一個文件的編譯再加上額外的編譯選項,可使用 extra_compile_args
和 extra_link_args
,其中 extra_link_args
選項用於連接。
extra_compile_args
是一個編譯選項字符串列表,每一個編譯選項都要單獨做爲一個字符串,不能並在一塊兒,不然會報錯。
建議使用源碼分發的形式發佈你的包,而不是二進制發佈形式,這樣包將更方便跨平臺。
建立源碼分發的命令爲:python setup.py sdist
,命令執行後會建立 dist 目錄,收集一些必要的文件以及 setup 腳本,生成一個壓縮文件,用戶安裝時,只須要解壓,而後執行 python setup.py install
命令,將進行編譯和安裝,將相應的文件存放到 python 第三方庫目錄下。
sdist 比較經常使用的一個選項是 --format
,選擇壓縮的格式。好比,使用 zip 進行壓縮,python setup.py sdist --format=zip
。
格式 | 後綴 |
---|---|
zip | .zip |
gztar | .tar.gz |
bztar | .tar.bz2 |
ztar | .tar.Z |
tar | .tar |
說明:python setup.py sdist --format=zip, tar
,Setuptools 會分別使用 zip 和 tar 進行壓縮,將同時產生兩個壓縮文件。
setuptools 和 distutils 對於文件查找的算法是同樣的:
py_modules
和 packages
指定的對應模塊文件ext_modules
和 libraries
選項指定的源文件和庫scripts
選項指定的腳本文件package_data
選項指定的文件data_files
選項指定的文件另外在使用過程當中,遇到 Setuptools 的一個巨坑,確實能夠包含文件,可是它並不總能包含文件,這是有前提的。
bdist是發佈二進制文件,sdist是發佈源文件。而在舊版本的 python 中(2.7 之前), package_data只有在使用 bdist 時候纔有用,也就是若是使用 sdist,是沒法正確包含文件的。而在新版本中,會自動把package_data 裏面的內容添加到 MANIFEST 文件中。
當咱們使用 sdist 進行分發包時,若是須要包含額外的文件,可使用 MANIFEST.in
文件,在該文件中列舉出須要包含的文件。當咱們執行 sdist 時,將會對 MANIFEST.in
文件進行檢查,讀取解釋並生成 MANIFEST 文件,該文件列舉了全部須要包含進包的文件。位於 distribution root 下的 MANIFEST.in 文件每行對應一條包含一系列文件的命令。