# 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`文件，配置帐户示例目标时，使用如下的项目树：

```
.
├── 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文档：

```
Example project
===============

Project description in here ...
```

`MANIFEST.in`列出了需要安装的Python模块：

```
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'
```

项目的文件结构如下：

```
.
├── 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`，添加安装目标：

```
install(
  TARGETS
      account
  LIBRARY
      DESTINATION account
  )
```

安装目标时，`README.rst`, `MANIFEST.in`，`setup.py`、`__init__.py`和`version.py`将放置在对应的位置上，我们准备使用pybind11测试安装过程：

1. 为此，在某处创建一个新目录，我们将在那里测试安装。
2. 在创建的目录中，从本地路径运行`pipenv install`。调整本地路径，指向`setup.py`的目录：

   ```
   $ pipenv install /path/to/cxx-example
   ```
3. 在Pipenv环境中打开一个Python shell：

   ```
   $ pipenv run python
   ```
4. Python shell中，可以测试我们的CMake包：

   ```
   >>> 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中定义安装目标位置：

```
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`环境变量来修改生成器。安装目录如下方式设置：

```
_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包：

```
$ python setup.py sdist
```

第二步中，使用Twine上传生成的分布数据(我们将Twine安装到本地的Pipenv中):

```
$ 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：

```
$ pipenv run twine upload dist/* -r pypi
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chenxiaowei.gitbook.io/cmake-cookbook/11.0-chinese/11.2-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
