CMake 秘籍(六)(3)https://developer.aliyun.com/article/1525060
工作原理
CPack 可以用来生成许多不同类型的包用于分发。在生成构建系统时,我们在CMakeCPack.cmake
中列出的 CPack 指令用于在构建目录中生成一个CPackConfig.cmake
文件。当运行 CMake 命令为package
或package_source
目标时,CPack 会自动使用自动生成的配置文件作为参数调用。确实,这两个新目标只是简单地包装了对 CPack 的调用。就像 CMake 一样,CPack 也有生成器的概念。在 CMake 的上下文中,生成器是用于生成原生构建脚本的工具,例如 Unix Makefiles 或 Visual Studio 项目文件,而在 CPack 的上下文中,这些是用于打包的工具。我们列出了这些,特别注意不同的平台,使用CPACK_SOURCE_GENERATOR
和CPACK_GENERATOR
变量为源和二进制包。因此,Debian 打包工具将被调用用于DEB
包生成器,而在给定平台上适当的存档工具将被调用用于TGZ
生成器。我们可以直接从build
目录调用 CPack,并使用-G
命令行选项选择要使用的生成器。RPM 包可以通过以下方式生成:
$ cd build $ cpack -G RPM CPack: Create package using RPM CPack: Install projects CPack: - Run preinstall target for: recipe-01 CPack: - Install project: recipe-01 CPack: Create package CPackRPM: Will use GENERATED spec file: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/_CPack_Packages/Linux/RPM/SPECS/recipe-01.spec CPack: - package: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/recipe-01-1.0.0-Linux.rpm generated.
对于任何分发,无论是源还是二进制,我们只需要打包最终用户严格需要的那些内容,因此整个构建目录和与版本控制相关的任何其他文件都必须从要打包的文件列表中排除。在我们的示例中,排除列表是通过以下命令声明的:
set(CPACK_SOURCE_IGNORE_FILES "${PROJECT_BINARY_DIR};/.git/;.gitignore")
我们还需要指定有关我们包的基本信息,例如名称、简短描述和版本。这些信息是通过 CMake 变量设置的,然后在包含相应的模块时传递给 CPack。
自 CMake 3.9 起,project()
命令接受一个DESCRIPTION
字段,其中包含对项目的简短描述。CMake 将设置一个PROJECT_DESCRIPTION
,可以用来设置CPACK_PACKAGE_DESCRIPTION_SUMMARY
。
让我们详细看看我们为示例项目可以生成的不同类型的包的说明。
源代码存档
在我们的示例中,我们决定为源归档使用TGZ
和ZIP
生成器。这将分别产生.tar.gz
和.zip
归档文件。我们可以检查生成的.tar.gz
文件的内容:
$ tar tzf recipe-01-1.0.0-Source.tar.gz recipe-01-1.0.0-Source/opt/ recipe-01-1.0.0-Source/opt/recipe-01/ recipe-01-1.0.0-Source/opt/recipe-01/cmake/ recipe-01-1.0.0-Source/opt/recipe-01/cmake/coffee.icns recipe-01-1.0.0-Source/opt/recipe-01/cmake/Info.plist.in recipe-01-1.0.0-Source/opt/recipe-01/cmake/messageConfig.cmake.in recipe-01-1.0.0-Source/opt/recipe-01/CMakeLists.txt recipe-01-1.0.0-Source/opt/recipe-01/src/ recipe-01-1.0.0-Source/opt/recipe-01/src/Message.hpp recipe-01-1.0.0-Source/opt/recipe-01/src/CMakeLists.txt recipe-01-1.0.0-Source/opt/recipe-01/src/Message.cpp recipe-01-1.0.0-Source/opt/recipe-01/src/hello-world.cpp recipe-01-1.0.0-Source/opt/recipe-01/LICENSE recipe-01-1.0.0-Source/opt/recipe-01/tests/ recipe-01-1.0.0-Source/opt/recipe-01/tests/CMakeLists.txt recipe-01-1.0.0-Source/opt/recipe-01/tests/use_target/ recipe-01-1.0.0-Source/opt/recipe-01/tests/use_target/CMakeLists.txt recipe-01-1.0.0-Source/opt/recipe-01/tests/use_target/use_message.cpp recipe-01-1.0.0-Source/opt/recipe-01/INSTALL.md
正如预期的那样,只有源树的内容被包括在内。注意,INSTALL.md
和LICENSE
文件也被包括在内,这是通过CPACK_PACKAGE_DESCRIPTION_FILE
和CPACK_RESOURCE_FILE_LICENSE
变量指定的。
package_source
目标不被 Visual Studio 系列的生成器理解:gitlab.kitware.com/cmake/cmake/issues/13058
。
二进制归档文件
在创建二进制归档文件时,CPack 将根据我们的CMakeCPack.cmake
文件中描述的安装说明,将目标的内容打包。因此,在我们的示例中,hello-world 可执行文件、消息共享库以及相应的头文件都将被打包在.tar.gz
和.zip
格式中。此外,CMake 配置文件也将被打包。这对于需要链接到我们库的其他项目非常有用。在包中使用的安装前缀可能与从构建树安装项目时使用的前缀不同。可以使用CPACK_PACKAGING_INSTALL_PREFIX
变量来实现这一点。在我们的示例中,我们将其设置为系统上的特定位置:/opt/recipe-01
。
我们可以分析生成的.tar.gz
归档文件的内容:
$ tar tzf recipe-01-1.0.0-Linux.tar.gz recipe-01-1.0.0-Linux/opt/ recipe-01-1.0.0-Linux/opt/recipe-01/ recipe-01-1.0.0-Linux/opt/recipe-01/bin/ recipe-01-1.0.0-Linux/opt/recipe-01/bin/hello-world recipe-01-1.0.0-Linux/opt/recipe-01/share/ recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/ recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/ recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/messageConfig.cmake recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/messageTargets-hello-world.cmake recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/messageConfigVersion.cmake recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/messageTargets-hello-world-release.cmake recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/messageTargets-release.cmake recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe-01/messageTargets.cmake recipe-01-1.0.0-Linux/opt/recipe-01/include/ recipe-01-1.0.0-Linux/opt/recipe-01/include/message/ recipe-01-1.0.0-Linux/opt/recipe-01/include/message/Message.hpp recipe-01-1.0.0-Linux/opt/recipe-01/include/message/messageExport.h recipe-01-1.0.0-Linux/opt/recipe-01/lib64/ recipe-01-1.0.0-Linux/opt/recipe-01/lib64/libmessage.so recipe-01-1.0.0-Linux/opt/recipe-01/lib64/libmessage.so.1
平台原生二进制安装程序
我们预计每个平台原生二进制安装程序的配置会有所不同。这些差异可以在一个CMakeCPack.cmake
中通过 CPack 进行管理,正如我们在示例中所做的那样。
对于 GNU/Linux,该节配置了DEB
和RPM
生成器:
if(UNIX) if(CMAKE_SYSTEM_NAME MATCHES Linux) list(APPEND CPACK_GENERATOR "DEB") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "robertodr") set(CPACK_DEBIAN_PACKAGE_SECTION "devel") set(CPACK_DEBIAN_PACKAGE_DEPENDS "uuid-dev") list(APPEND CPACK_GENERATOR "RPM") set(CPACK_RPM_PACKAGE_RELEASE "1") set(CPACK_RPM_PACKAGE_LICENSE "MIT") set(CPACK_RPM_PACKAGE_REQUIRES "uuid-devel") endif() endif()
我们的示例依赖于 UUID 库,CPACK_DEBIAN_PACKAGE_DEPENDS
和CPACK_RPM_PACKAGE_REQUIRES
选项允许我们在我们的包和其他数据库中的包之间指定依赖关系。我们可以使用dpkg
和rpm
程序分别分析生成的.deb
和.rpm
包的内容。
请注意,CPACK_PACKAGING_INSTALL_PREFIX
也会影响这些包生成器:我们的包将被安装到/opt/recipe-01
。
CMake 确实提供了对跨平台和便携式构建系统的支持。以下节将使用 Nullsoft Scriptable Install System(NSIS)创建一个安装程序:
if(WIN32 OR MINGW) list(APPEND CPACK_GENERATOR "NSIS") set(CPACK_NSIS_PACKAGE_NAME "message") set(CPACK_NSIS_CONTACT "robertdr") set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) endif()
最后,如果我们正在 macOS 上构建项目,以下节将启用 Bundle 打包器:
if(APPLE) list(APPEND CPACK_GENERATOR "Bundle") set(CPACK_BUNDLE_NAME "message") configure_file(${PROJECT_SOURCE_DIR}/cmake/Info.plist.in Info.plist @ONLY) set(CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) set(CPACK_BUNDLE_ICON ${PROJECT_SOURCE_DIR}/cmake/coffee.icns) endif()
在 macOS 示例中,我们首先需要为包配置一个属性列表文件,这可以通过configure_file
命令实现。然后,Info.plist
的位置和包的图标被设置为 CPack 的变量。
你可以在这里阅读更多关于属性列表格式的信息:en.wikipedia.org/wiki/Property_list
。
还有更多
我们没有像之前为了简化而将 CPack 配置设置列在CMakeCPack.cmake
中,而是可以将CPACK_*
变量的每个生成器设置放在一个单独的文件中,例如CMakeCPackOptions.cmake
,并使用set(CPACK_PROJECT_CONFIG_FILE "${PROJECT_SOURCE_DIR}/CMakeCPackOptions.cmake")
将这些设置包含到CMakeCPack.cmake
中。这个文件也可以在 CMake 时配置,然后在 CPack 时包含,提供了一种干净的方式来配置多格式包生成器(另请参见:cmake.org/cmake/help/v3.6/module/CPack.html
)。
与 CMake 家族中的所有工具一样,CPack 功能强大且多才多艺,提供了比本食谱中展示的更多的灵活性和选项。感兴趣的读者应阅读 CPack 的官方文档,了解命令行界面的详细信息(cmake.org/cmake/help/v3.6/manual/cpack.1.html
)以及详细介绍 CPack 如何使用额外生成器打包项目的 man 页面(cmake.org/cmake/help/v3.6/module/CPack.html
)。
通过 PyPI 分发使用 CMake/pybind11 构建的 C++/Python 项目
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-11/recipe-02
找到。该食谱适用于 CMake 版本 3.11(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
在本食谱中,我们将以第九章,混合语言项目,第 5 个食谱,使用 pybind11 构建 C++和 Python 项目中的 pybind11 示例为起点,添加相关的安装目标和 pip 打包信息,并将项目上传到 PyPI。我们的目标将是得到一个可以使用 pip 安装的项目,并在幕后运行 CMake 并获取 pybind11 依赖项。
准备就绪
要通过 PyPI 分发包,您需要在pypi.org
上注册一个用户账户,但也可以先从本地路径进行安装练习。
我们还普遍建议使用 pip 安装此包和其他 Python 包,使用 Pipenv(docs.pipenv.org
)或虚拟环境(virtualenv.pypa.io/en/stable/
)而不是安装到系统环境中。
我们的起点是来自第九章,混合语言项目,第 5 个食谱,使用 pybind11 构建 C++和 Python 项目的 pybind11 示例,其中包含一个顶级CMakeLists.txt
文件和一个account/CMakeLists.txt
文件,该文件配置了账户示例目标并使用以下项目树:
. ├── account │ ├── account.cpp │ ├── account.hpp │ ├── CMakeLists.txt │ └── test.py └── CMakeLists.txt
在这个配方中,我们将保持account.cpp
,account.hpp
和test.py
脚本不变。我们将修改account/CMakeLists.txt
并添加一些文件,以便 pip 能够构建和安装包。为此,我们需要在根目录中添加三个额外的文件:README.rst
,MANIFEST.in
和setup.py
。
README.rst
包含有关项目的文档:
Example project =============== Project description in here ...
MANIFEST.in
列出了应与 Python 模块和包一起安装的文件:
include README.rst CMakeLists.txt recursive-include account *.cpp *.hpp CMakeLists.txt
最后,setup.py
包含构建和安装项目的指令:
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()})
我们将把__init__.py
放入account
子目录中:
from .version import __version__ from .account import Account __all__ = [ '__version__', 'Account', ]
我们还将把version.py
放入account
子目录中:
__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
如何做到这一点
这个配方建立在第九章,混合语言项目,配方 5,使用 pybind11 构建 C++和 Python 项目的基础上。让我们详细看看:
首先,我们扩展account/CMakeLists.txt
。唯一的添加是最后一个指令,它指定了安装目标:
install( TARGETS account LIBRARY DESTINATION account )
就是这样!有了安装目标和README.rst
,MANIFEST.in
,setup.py
,__init__.py
和version.py
文件,我们就可以测试使用 pybind11 接口的示例代码的安装了:
- 为此,在你的计算机上创建一个新的目录,我们将在那里测试安装。
- 在新创建的目录中,我们从本地路径运行
pipenv install
。调整本地路径以指向包含setup.py
脚本的目录:
$ pipenv install /path/to/cxx-example
- 现在我们在 Pipenv 环境中启动一个 Python shell:
$ pipenv run python
- 在 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}
目录包含使用 pybind11 编译的account.cpython-36m-x86_64-linux-gnu.so
Python 模块,但请注意,其名称取决于操作系统(在这种情况下,64 位 Linux)和 Python 环境(在这种情况下,Python 3.6)。setup.py
脚本将在后台运行 CMake,并将 Python 模块安装到正确的路径,具体取决于所选的 Python 环境(系统 Python 或 Pipenv 或虚拟环境)。但现在我们在安装模块时面临两个挑战:
- 命名可能会改变。
- 路径是在 CMake 之外设置的。
我们可以通过使用以下安装目标来解决这个问题,其中setup.py
将定义安装目标位置:
install( TARGETS account LIBRARY DESTINATION account )
在这里,我们指导 CMake 将编译后的 Python 模块文件安装到相对于安装目标位置的account
子目录中(第十章,编写安装程序,详细讨论了如何设置目标位置)。后者将由setup.py
通过定义CMAKE_INSTALL_PREFIX
指向正确的路径,这取决于 Python 环境。
现在让我们检查一下我们是如何在setup.py
中实现这一点的;我们将从脚本的底部开始:
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
。这个函数是默认构建步骤的子类:
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 命令:
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 上尚未被占用。在将包上传到pypi.org
之前,先测试上传到 PyPI 测试实例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
我们将分两步进行。首先,我们在本地创建分发:
$ python setup.py sdist
在第二步中,我们使用 Twine(我们将其安装到本地 Pipenv 中)上传生成的分发数据:
$ pipenv run twine upload dist/* -r pypitest Uploading distributions to https://test.pypi.org/legacy/ Uploading yourpackage-0.0.0.tar.gz
接下来,尝试从测试实例安装到一个隔离的环境中:
$ pipenv shell $ pip install --index-url https://test.pypi.org/simple/ yourpackage
一旦这个工作正常,我们就可以准备上传到生产 PyPI 了:
$ pipenv run twine upload dist/* -r pypi
通过 PyPI 分发使用 CMake/CFFI 构建的 C/Fortran/Python 项目
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-11/recipe-03
找到,并包含 C++和 Fortran 示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
这个配方是之前的配方和第九章,*混合语言项目,*配方 6,使用 Python CFFI 混合 C、C++、Fortran 和 Python的混合体。我们将重用之前配方的许多构建块,但不是使用 pybind11,而是使用 Python CFFI 来提供 Python 接口。在这个配方中,我们的目标是通过 PyPI 共享一个 Fortran 项目,但它同样可以是 C 或 C++项目,或者任何暴露 C 接口的语言项目。
准备工作
我们将从以下文件树开始:
. ├── 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
,与第九章,混合语言项目,配方 6,使用 Python CFFI 混合 C、C++、Fortran 和 Python中出现的方式相同。我们很快会讨论需要应用到account/CMakeLists.txt
的小改动。README.rst
文件与之前的配方相同。setup.py
脚本与之前的配方相比包含一条额外的行(包含install_requires=['cffi']
的行):
# ... 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 模块和包一起安装的文件,并包含以下内容:
include README.rst CMakeLists.txt recursive-include account *.h *.f90 CMakeLists.txt
在account
子目录下,我们看到了两个新文件。同样,有一个version.py
文件,它保存了setup.py
的项目版本:
__version__ = '0.0.0'
子目录还包含interface_file_names.cfg.in
文件,我们很快就会讨论它:
[configuration] header_file_name = account.h library_file_name = $<TARGET_FILE_NAME:account>
如何操作
让我们讨论实现打包所需的步骤:
- 我们扩展了第九章,混合语言项目,配方 6,使用 Python CFFI 混合 C、C++、Fortran 和 Python中的
account/CMakeLists.txt
。唯一的添加指令如下:
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 )
就这样!安装目标和附加文件就位后,我们就可以开始测试安装了。为此,在你的电脑上创建一个新的目录,我们将在那里进行安装测试。
- 在新创建的目录中,我们从本地路径运行
pipenv install
。调整本地路径以指向包含setup.py
脚本的目录:
$ pipenv install /path/to/fortran-example
- 现在我们在 Pipenv 环境中启动一个 Python shell:
$ pipenv run python
- 在 Python shell 中,我们可以测试我们的 CMake 包:
>>> 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
它是如何工作的
与第九章,混合语言项目,配方 6,使用 Python CFFI 混合 C、C++、Fortran 和 Python相比,使用 Python CFFI 和 CMake 安装混合语言项目的扩展包括两个额外步骤:
- 我们需要
setup.py
层。 - 我们安装目标,以便 CFFI 层所需的头文件和共享库文件根据所选 Python 环境安装在正确的路径中。
setup.py
的结构与之前的食谱几乎相同,我们请您参考之前的食谱来讨论这个文件。唯一的增加是包含install_requires=['cffi']
的行,以确保安装我们的示例包也会获取并安装所需的 Python CFFI。setup.py
脚本将自动安装__init__.py
和version.py
,因为这些是从setup.py
脚本引用的。MANIFEST.in
稍作修改,以打包不仅包括README.rst
和 CMake 文件,还包括头文件和 Fortran 源文件:
include README.rst CMakeLists.txt recursive-include account *.h *.f90 CMakeLists.txt
在本食谱中,我们将面临三个挑战,即打包使用 Python CFFI 和setup.py
的 CMake 项目:
- 我们需要将
account.h
和account_export.h
头文件以及共享库复制到依赖于 Python 环境的 Python 模块位置。 - 我们需要告诉
__init__.py
在哪里找到这些头文件和库。在第九章,混合语言项目,第 6 个食谱,使用 Python CFFI 混合 C、C++、Fortran 和 Python中,我们通过使用环境变量解决了这些问题,但每次我们计划使用 Python 模块时设置这些变量是不切实际的。 - 在 Python 方面,我们不知道共享库文件的确切名称(后缀),因为它取决于操作系统。
让我们从最后一点开始:我们不知道确切的名称,但在生成构建系统时 CMake 知道,因此我们在interface_file_names.cfg.in
中使用生成器表达式来扩展占位符:
[configuration] header_file_name = account.h library_file_name = $<TARGET_FILE_NAME:account>
此输入文件用于生成${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg
:
file( GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/interface_file_names.cfg INPUT ${CMAKE_CURRENT_SOURCE_DIR}/interface_file_names.cfg.in )
然后,我们将两个头文件定义为PUBLIC_HEADER
(另请参见第十章,编写安装程序),并将配置文件定义为RESOURCE
:
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
定义的路径的结构中:
install( TARGETS account LIBRARY DESTINATION account/lib RUNTIME DESTINATION account/lib PUBLIC_HEADER DESTINATION account/include RESOURCE DESTINATION account )
请注意,我们将DESTINATION
设置为LIBRARY
和RUNTIME
,指向account/lib
。这对于 Windows 来说很重要,因为共享库具有可执行入口点,因此我们必须同时指定两者。
Python 包将能够通过account/__init__.py
中的这一部分找到这些文件:
# 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
感到困惑。
CMake 秘籍(六)(5)https://developer.aliyun.com/article/1525062