# 11.3 通過PyPI發布使用CMake/CFFI構建C/Fortran/Python項目
**NOTE**:*此示例代碼可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-11/recipe-03 中找到,其中有一個C++和Fortran示例。該示例在CMake 3.5版(或更高版本)中是有效的,并且已經在GNU/Linux、macOS和Windows上進行過測試。*
基于第9章第6節的示例,我們將重用前一個示例中的構建塊,不過這次使用Python CFFI來提供Python接口,而不是pybind11。這個示例中,我們通過PyPI共享一個Fortran項目,這個項目可以是C或C++項目,也可以是任何公開C接口的語言,非Fortran就可以。
## 準備工作
項目將使用如下的目錄結構:
```shell
.
├── account
│ ├── account.h
│ ├── CMakeLists.txt
│ ├── implementation
│ │ └── fortran_implementation.f90
│ ├── __init__.py
│ ├── interface_file_names.cfg.in
│ ├── test.py
│ └── version.py
├── CMakeLists.txt
├── MANIFEST.in
├── README.rst
└── setup.py
```
主`CMakeLists.txt`文件和`account`下面的所有源文件(`account/CMakeLists.txt`除外)與第9章中的使用方式相同。`README.rst`文件與前面的示例相同。`setup.py`腳本比上一個示例多了一行(包含`install_require =['cffi']`的那一行):
```python
# ... up to this line the script is unchanged
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],
install_requires=['cffi'],
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()})
```
`MANIFEST.in`應該與Python模塊和包一起安裝,并包含以下內容:
```txt
include README.rst CMakeLists.txt
recursive-include account *.h *.f90 CMakeLists.txt
```
`account`子目錄下,我們看到兩個新文件。一個`version.py`文件,其為`setup.py`保存項目的版本信息:
```python
__version__ = '0.0.0'
```
子目錄還包含`interface_file_names.cfg.in`文件:
```cmake
[configuration]
header_file_name = account.h
library_file_name = $<TARGET_FILE_NAME:account>
```
## 具體實施
討論一下實現打包的步驟:
1. 示例基于第9章第6節,使用Python CFFI擴展了`account/CMakeLists.txt`,增加以下指令:
```cmake
file(
GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg
INPUT ${CMAKE_CURRENT_SOURCE_DIR}/interface_file_names.cfg.in
)
set_target_properties(account
PROPERTIES
PUBLIC_HEADER "account.h;${CMAKE_CURRENT_BINARY_DIR}/account_export.h"
RESOURCE "${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg"
)
install(
TARGETS
account
LIBRARY
DESTINATION account/lib
RUNTIME
DESTINATION account/lib
PUBLIC_HEADER
DESTINATION account/include
RESOURCE
DESTINATION account
)
```
安裝目標和附加文件準備好之后,就可以測試安裝了。為此,會在某處創建一個新目錄,我們將在那里測試安裝。
2. 新創建的目錄中,我們從本地路徑運行pipenv install。調整本地路徑,指向`setup.py`腳本保存的目錄:
```shell
$ pipenv install /path/to/fortran-example
```
3. 現在在Pipenv環境中生成一個Python shell:
```shell
$ pipenv run python
```
4. Python shell中,可以測試CMake包:
```shell
>>> import account
>>> account1 = account.new()
>>> account.deposit(account1, 100.0)
>>> account.deposit(account1, 100.0)
>>> account.withdraw(account1, 50.0)
>>> print(account.get_balance(account1))
150.0
```
## 工作原理
使用Python CFFI和CMake安裝混合語言項目的擴展與第9章第6節的例子相對比,和使用Python CFFI的Python包多了兩個額外的步驟:
1. 需要`setup.py`s
2. 安裝目標時,CFFI所需的頭文件和動態庫文件,需要安裝在正確的路徑中,具體路徑取決于所選擇的Python環境
`setup.py`的結構與前面的示例幾乎一致,唯一的修改是包含`install_require =['cffi']`,以確保安裝示例包時,也獲取并安裝了所需的Python CFFI。`setup.py`腳本會自動安裝`__init__.py`和`version.py`。`MANIFEST.in `中的改變不僅有`README.rst`和CMake文件,還有頭文件和Fortran源文件:
```txt
include README.rst CMakeLists.txt
recursive-include account *.h *.f90 CMakeLists.txt
```
這個示例中,使用Python CFFI和`setup.py`打包CMake項目時,我們會面臨三個挑戰:
* 需要將`account.h`和`account_export.h`頭文件,以及動態庫復制到系統環境中Python模塊的位置。
* 需要告訴`__init__.py`,在哪里可以找到這些頭文件和庫。第9章第6節中,我們使用環境變量解決了這些問題,不過使用Python模塊時,不可能每次去都設置這些變量。
* Python方面,我們不知道動態庫文件的確切名稱(后綴),因為這取決于操作系統。
讓我們從最后一點開始說起:不知道確切的名稱,但在CMake生成構建系統時是知道的,因此我們在`interface_file_names.cfg,in`中使用生成器表達式,對占位符進行展開:
```txt
[configuration]
header_file_name = account.h
library_file_name = $<TARGET_FILE_NAME:account>
```
輸入文件用來生成`${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg`:
```cmake
file(
GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg
INPUT ${CMAKE_CURRENT_SOURCE_DIR}/interface_file_names.cfg.in
)
```
然后,將兩個頭文件定義為`PUBLIC_HEADER`(參見第10章),配置文件定義為`RESOURCE`:
```cmake
set_target_properties(account
PROPERTIES
PUBLIC_HEADER "account.h;${CMAKE_CURRENT_BINARY_DIR}/account_export.h"
RESOURCE "${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg"
)
```
最后,將庫、頭文件和配置文件安裝到`setup.py`定義的安裝路徑中:
```cmake
install(
TARGETS
account
LIBRARY
DESTINATION account/lib
RUNTIME
DESTINATION account/lib
PUBLIC_HEADER
DESTINATION account/include
RESOURCE
DESTINATION account
)
```
注意,我們為庫和運行時都設置了指向`account/lib`的目標。這對于Windows很重要,因為動態庫具有可執行入口點,因此我們必須同時指定這兩個入口點。
Python包將能夠找到這些文件,要使用`account/__init__.py `來完成:
```python
# this interface requires the header file and library file
# and these can be either provided by interface_file_names.cfg
# in the same path as this file
# or if this is not found then using environment variables
_this_path = Path(os.path.dirname(os.path.realpath(__file__)))
_cfg_file = _this_path / 'interface_file_names.cfg'
if _cfg_file.exists():
config = ConfigParser()
config.read(_cfg_file)
header_file_name = config.get('configuration', 'header_file_name')
_header_file = _this_path / 'include' / header_file_name
_header_file = str(_header_file)
library_file_name = config.get('configuration', 'library_file_name')
_library_file = _this_path / 'lib' / library_file_name
_library_file = str(_library_file)
else:
_header_file = os.getenv('ACCOUNT_HEADER_FILE')
assert _header_file is not None
_library_file = os.getenv('ACCOUNT_LIBRARY_FILE')
assert _library_file is not None
```
本例中,將找到`_cfg_file`并進行解析,`setup.py`將找到`include`下的頭文件和`lib`下的庫,并將它們傳遞給CFFI,從而構造庫對象。這也是為什么,使用`lib`作為安裝目標`DESTINATION`,而不使用`CMAKE_INSTALL_LIBDIR`的原因(否則可能會讓`account/__init__.py `混淆)。
## 更多信息
將包放到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 留下評論——讓其他讀者知道你的想法