# 11.2 通過PyPI發布使用CMake/pybind11構建的C++/Python項目
**NOTE**:*此示例代碼可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-11/recipe-02 中找到。該示例在CMake 3.11版(或更高版本)中是有效的,并且已經在GNU/Linux、macOS和Windows上進行過測試。*
本示例中,我們將以第9章第5節的代碼的pybind11為例,為其添加相關的安裝目標和pip打包信息,并將項目上傳到PyPI。我們要實現一個可以使用pip安裝,并運行CMake從而獲取底層pybind11依賴項的項目。
## 準備工作
要通過PyPI分發包的話,需要一個https://pypi.org 帳戶。當然,也可以先從本地路徑進行安裝練習。
**TIPS**:*建議使用Pipenv (https://docs.pipenv.org )或虛擬環境(https://virtualenv.pypa )安裝這個包和其他的Python包。*
我們基于第9章第5節的項目,它包含一個主`CMakeLists.txt`文件和一個`account/CMakeLists.txt`文件,配置帳戶示例目標時,使用如下的項目樹:
```shell
.
├── account
│ ├── account.cpp
│ ├── account.hpp
│ ├── CMakeLists.txt
│ s└── test.py
└── CMakeLists.txt
```
示例中,`account.cpp`,` account.hpp`和`test.py`沒有任何變化。修改`account/CMakeLists.txt`,并為pip添加幾個文件,以便能夠構建安裝包。為此,需要根目錄中的另外三個文件:`README.rst`,`MANIFEST.in`和`setup.py`。
`README.rst`中包含關于項目的s文檔:
```txt
Example project
===============
Project description in here ...
```
`MANIFEST.in`列出了需要安裝的Python模塊:
```txt
include README.rst CMakeLists.txt
recursive-include account *.cpp *.hpp CMakeLists.txt
```
最后,`setup.py`包含構建指令和安裝項目的說明:
```python
import distutils.command.build as _build
import os
import sys
from distutils import spawn
from distutils.sysconfig import get_python_lib
from setuptools import setup
def extend_build():
class build(_build.build):
def run(self):
cwd = os.getcwd()
if spawn.find_executable('cmake') is None:
sys.stderr.write("CMake is required to build this package.\n")
sys.exit(-1)
_source_dir = os.path.split(__file__)[0]
_build_dir = os.path.join(_source_dir, 'build_setup_py')
_prefix = get_python_lib()
try:
cmake_configure_command = [
'cmake',
'-H{0}'.format(_source_dir),
'-B{0}'.format(_build_dir),
'-DCMAKE_INSTALL_PREFIX={0}'.format(_prefix),
]
_generator = os.getenv('CMAKE_GENERATOR')
if _generator is not None:
cmake_configure_command.append('-
G{0}'.format(_generator))
spawn.spawn(cmake_configure_command)
spawn.spawn(
['cmake', '--build', _build_dir, '--target', 'install'])
os.chdir(cwd)
except spawn.DistutilsExecError:
sys.stderr.write("Error while building with CMake\n")
sys.exit(-1)
_build.build.run(self)
return build
_here = os.path.abspath(os.path.dirname(__file__))
if sys.version_info[0] < 3:
with open(os.path.join(_here, 'README.rst')) as f:
long_description = f.read()
else:
with open(os.path.join(_here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()
_this_package = 'account'
version = {}
with open(os.path.join(_here, _this_package, 'version.py')) as f:
exec(f.read(), version)
setup(
name=_this_package,
version=version['__version__'],
description='Description in here.',
long_description=long_description,
author='Bruce Wayne',
author_email='bruce.wayne@example.com',
url='http://example.com',
license='MIT',
packages=[_this_package],
include_package_data=True,
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Science/Research',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.6'
],
cmdclass={'build': extend_build()})
```
`account`子目錄中放置一個`__init__.py`腳本:
```python
from .version import __version__
from .account import Account
__all__ = [
'__version__',
'Account',
]
```
再放一個`version.py`腳本:
```python
__version__ = '0.0.0'
```
項目的文件結構如下:
```shell
.
├── account
│ ├── account.cpp
│ ├── account.hpp
│ ├── CMakeLists.txt
│ ├── __init__.py
│ ├── test.py
│ └── version.py
├── CMakeLists.txt
├── MANIFEST.in
├── README.rst
└── setup.py
```
## 具體實施
本示例基于第9章第5節項目的基礎上。
首先,修改`account/CMakeLists.txt`,添加安裝目標:
```cmake
install(
TARGETS
account
LIBRARY
DESTINATION account
)
```
安裝目標時,`README.rst`, `MANIFEST.in`,`setup.py`、`__init__.py`和`version.py`將放置在對應的位置上,我們準備使用pybind11測試安裝過程:
1. 為此,在某處創建一個新目錄,我們將在那里測試安裝。
2. 在創建的目錄中,從本地路徑運行`pipenv install`。調整本地路徑,指向`setup.py`的目錄:
```shell
$ pipenv install /path/to/cxx-example
```
3. 在Pipenv環境中打開一個Python shell:
```shell
$ pipenv run python
```
4. Python shell中,可以測試我們的CMake包:
```shell
>>> from account import Account
>>> account1 = Account()
>>> account1.deposit(100.0)
>>> account1.deposit(100.0)
>>> account1.withdraw(50.0)
>>> print(account1.get_balance())
150.0
```
## 工作原理
`${CMAKE_CURRENT_BINARY_DIR}`目錄包含編譯后的`account.cpython-36m-x86_64-linux-gnu.so`,這個動態庫就是使用pybind11構建Python模塊。但是請注意,它的名稱取決于操作系統(本例中是64位Linux)和Python環境(本例中是Python 3.6)。`setup.py`s腳本將運行CMake,并根據所選的Python環境(系統Python,Pipenv或虛擬環境)將Python模塊安裝到正確的路徑下。
不過,在安裝模塊時面臨兩個挑戰:
* 名稱可變
* CMake外部設置路徑
可以使用下面的安裝目標來解決這個問題,將在setup.py中定義安裝目標位置:
```cmake
install(
TARGETS
account
LIBRARY
DESTINATION account
)
```
指示CMake將編譯好的Python模塊文件安裝到相對于安裝目標位置的`account`子目錄中(第10章中詳細討論了如何設置目標位置)。`setup.py`將通過設置`CMAKE_INSTALL_PREFIX`來設置安裝位置,并根據Python環境指向正確的路徑。
讓我們看看`setup.py`如何實現的。自下而上來看一下腳本:
```python
setup(
name=_this_package,
version=version['__version__'],
description='Description in here.',
long_description=long_description,
author='Bruce Wayne',
author_email='bruce.wayne@example.com',
url='http://example.com',
license='MIT',
packages=[_this_package],
include_package_data=True,
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Science/Research',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.6'
],
cmdclass={'build': extend_build()})
```
該腳本包含許多占位符,還包含一些自解釋的語句。這里我們將重點介紹最后一個指令`cmdclass`。這個指令中,通過自定義`extend_build`函數擴展默認的構建步驟。這個默認的構建步驟如下:
```python
def extend_build():
class build(_build.build):
def run(self):
cwd = os.getcwd()
if spawn.find_executable('cmake') is None:
sys.stderr.write("CMake is required to build this package.\n")
sys.exit(-1)
_source_dir = os.path.split(__file__)[0]
_build_dir = os.path.join(_source_dir, 'build_setup_py')
_prefix = get_python_lib()
try:
cmake_configure_command = [
'cmake',
'-H{0}'.format(_source_dir),
'-B{0}'.format(_build_dir),
'-DCMAKE_INSTALL_PREFIX={0}'.format(_prefix),
]
_generator = os.getenv('CMAKE_GENERATOR')
if _generator is not None:
cmake_configure_command.append('-
G{0}'.format(_generator))
spawn.spawn(cmake_configure_command)
spawn.spawn(
['cmake', '--build', _build_dir, '--target', 'install'])
os.chdir(cwd)
except spawn.DistutilsExecError:
sys.stderr.write("Error while building with CMake\n")
sys.exit(-1)
_build.build.run(self)
return build
```
首先,檢查CMake是否可用。函數執行了兩個CMake命令:
```python
cmake_configure_command = [
'cmake',
'-H{0}'.format(_source_dir),
'-B{0}'.format(_build_dir),
'-DCMAKE_INSTALL_PREFIX={0}'.format(_prefix),
]
_generator = os.getenv('CMAKE_GENERATOR')
if _generator is not None:
cmake_configure_command.append('-
G{0}'.format(_generator))
spawn.spawn(cmake_configure_command)
spawn.spawn(
['cmake', '--build', _build_dir, '--target', 'install'])
```
我們可以設置`CMAKE_GENERATOR`環境變量來修改生成器。安裝目錄如下方式設置:
```shell
_prefix = get_python_lib()
```
從安裝目錄的根目錄下,通過`distutils.sysconfig`導入`get_python_lib`函數。`cmake --build _build_dir --target install`命令以一種可移植的方式,構建和安裝我們的項目。使用`_build_dir`而不使用`build`的原因是,在測試本地安裝時,項目可能已經包含了一個`build`目錄,這將與新安裝過程發生沖突。對于已經上傳到PyPI的包,構建目錄的名稱并不會帶來什么影響。
## 更多信息
現在我們已經測試了本地安裝,準備將包上傳到PyPI。在此之前,請確保`setup.py`中的元數據(例如:項目名稱、聯系方式和許可協議信息)是合理的,并且項目名稱沒有與PyPI已存在項目重名。在上傳到https://pypi.org 之前,先測試PyPI(https://test.pypi.org )上,進行上載和下載的嘗試。
上傳之前,我們需要在主目錄中創建一個名為`.pypirc`的文件,其中包含(替換成自己的`yourusername`和`yourpassword `):
```
[distutils]account
index-servers=
pypi
pypitest
[pypi]
username = yourusername
password = yourpassword
[pypitest]
repository = https://test.pypi.org/legacy/
username = yourusername
password = yourpassword
```
我們將分兩步進行。首先,我們在本地創建Release包:
```shell
$ python setup.py sdist
```
第二步中,使用Twine上傳生成的分布數據(我們將Twine安裝到本地的Pipenv中):
```shell
$ pipenv run twine upload dist/* -r pypitest
Uploading distributions to https://test.pypi.org/legacy/
Uploading yourpackage-0.0.0.tar.gz
```
下一步,從測試實例到,將包安裝到一個隔離的環境中:
```python
$ pipenv shell
$ pip install --index-url https://test.pypi.org/simple/ yourpackage
```
當一切正常,就將我們的包上傳到了PyPI:
```shell
$ pipenv run twine upload dist/* -r pypi
```
- Introduction
- 前言
- 第0章 配置環境
- 0.1 獲取代碼
- 0.2 Docker鏡像
- 0.3 安裝必要的軟件
- 0.4 測試環境
- 0.5 上報問題并提出改進建議
- 第1章 從可執行文件到庫
- 1.1 將單個源文件編譯為可執行文件
- 1.2 切換生成器
- 1.3 構建和鏈接靜態庫和動態庫
- 1.4 用條件句控制編譯
- 1.5 向用戶顯示選項
- 1.6 指定編譯器
- 1.7 切換構建類型
- 1.8 設置編譯器選項
- 1.9 為語言設定標準
- 1.10 使用控制流
- 第2章 檢測環境
- 2.1 檢測操作系統
- 2.2 處理與平臺相關的源代碼
- 2.3 處理與編譯器相關的源代碼
- 2.4 檢測處理器體系結構
- 2.5 檢測處理器指令集
- 2.6 為Eigen庫使能向量化
- 第3章 檢測外部庫和程序
- 3.1 檢測Python解釋器
- 3.2 檢測Python庫
- 3.3 檢測Python模塊和包
- 3.4 檢測BLAS和LAPACK數學庫
- 3.5 檢測OpenMP的并行環境
- 3.6 檢測MPI的并行環境
- 3.7 檢測Eigen庫
- 3.8 檢測Boost庫
- 3.9 檢測外部庫:Ⅰ. 使用pkg-config
- 3.10 檢測外部庫:Ⅱ. 自定義find模塊
- 第4章 創建和運行測試
- 4.1 創建一個簡單的單元測試
- 4.2 使用Catch2庫進行單元測試
- 4.3 使用Google Test庫進行單元測試
- 4.4 使用Boost Test進行單元測試
- 4.5 使用動態分析來檢測內存缺陷
- 4.6 預期測試失敗
- 4.7 使用超時測試運行時間過長的測試
- 4.8 并行測試
- 4.9 運行測試子集
- 4.10 使用測試固件
- 第5章 配置時和構建時的操作
- 5.1 使用平臺無關的文件操作
- 5.2 配置時運行自定義命令
- 5.3 構建時運行自定義命令:Ⅰ. 使用add_custom_command
- 5.4 構建時運行自定義命令:Ⅱ. 使用add_custom_target
- 5.5 構建時為特定目標運行自定義命令
- 5.6 探究編譯和鏈接命令
- 5.7 探究編譯器標志命令
- 5.8 探究可執行命令
- 5.9 使用生成器表達式微調配置和編譯
- 第6章 生成源碼
- 6.1 配置時生成源碼
- 6.2 使用Python在配置時生成源碼
- 6.3 構建時使用Python生成源碼
- 6.4 記錄項目版本信息以便報告
- 6.5 從文件中記錄項目版本
- 6.6 配置時記錄Git Hash值
- 6.7 構建時記錄Git Hash值
- 第7章 構建項目
- 7.1 使用函數和宏重用代碼
- 7.2 將CMake源代碼分成模塊
- 7.3 編寫函數來測試和設置編譯器標志
- 7.4 用指定參數定義函數或宏
- 7.5 重新定義函數和宏
- 7.6 使用廢棄函數、宏和變量
- 7.7 add_subdirectory的限定范圍
- 7.8 使用target_sources避免全局變量
- 7.9 組織Fortran項目
- 第8章 超級構建模式
- 8.1 使用超級構建模式
- 8.2 使用超級構建管理依賴項:Ⅰ.Boost庫
- 8.3 使用超級構建管理依賴項:Ⅱ.FFTW庫
- 8.4 使用超級構建管理依賴項:Ⅲ.Google Test框架
- 8.5 使用超級構建支持項目
- 第9章 語言混合項目
- 9.1 使用C/C++庫構建Fortran項目
- 9.2 使用Fortran庫構建C/C++項目
- 9.3 使用Cython構建C++和Python項目
- 9.4 使用Boost.Python構建C++和Python項目
- 9.5 使用pybind11構建C++和Python項目
- 9.6 使用Python CFFI混合C,C++,Fortran和Python
- 第10章 編寫安裝程序
- 10.1 安裝項目
- 10.2 生成輸出頭文件
- 10.3 輸出目標
- 10.4 安裝超級構建
- 第11章 打包項目
- 11.1 生成源代碼和二進制包
- 11.2 通過PyPI發布使用CMake/pybind11構建的C++/Python項目
- 11.3 通過PyPI發布使用CMake/CFFI構建C/Fortran/Python項目
- 11.4 以Conda包的形式發布一個簡單的項目
- 11.5 將Conda包作為依賴項發布給項目
- 第12章 構建文檔
- 12.1 使用Doxygen構建文檔
- 12.2 使用Sphinx構建文檔
- 12.3 結合Doxygen和Sphinx
- 第13章 選擇生成器和交叉編譯
- 13.1 使用CMake構建Visual Studio 2017項目
- 13.2 交叉編譯hello world示例
- 13.3 使用OpenMP并行化交叉編譯Windows二進制文件
- 第14章 測試面板
- 14.1 將測試部署到CDash
- 14.2 CDash顯示測試覆蓋率
- 14.3 使用AddressSanifier向CDash報告內存缺陷
- 14.4 使用ThreadSaniiser向CDash報告數據爭用
- 第15章 使用CMake構建已有項目
- 15.1 如何開始遷移項目
- 15.2 生成文件并編寫平臺檢查
- 15.3 檢測所需的鏈接和依賴關系
- 15.4 復制編譯標志
- 15.5 移植測試
- 15.6 移植安裝目標
- 15.7 進一步遷移的措施
- 15.8 項目轉換為CMake的常見問題
- 第16章 可能感興趣的書
- 16.1 留下評論——讓其他讀者知道你的想法