使用pip构建CMake扩展:兼容python setup.py与pip的路径问题问询
解决C++/Python混合项目中setup.py与pip调用的兼容性问题
我之前也碰到过这个问题——pip的构建隔离机制会把foo/python目录单独复制到临时环境,导致CMake找不到顶层的C++源码和根目录的CMakeLists.txt。我们可以通过两个关键调整来解决,同时保持你原有的setup.py调用CMake的逻辑:
1. 用MANIFEST.in让pip带上顶层C++源码
首先在foo/python目录下创建MANIFEST.in文件,告诉pip在构建时把顶层的C++相关文件也复制到临时环境:
# 包含项目根目录的CMakeLists.txt include ../CMakeLists.txt # 递归包含顶层头文件和源码目录 recursive-include ../include * recursive-include ../source * # 包含Python绑定的C++源码目录 recursive-include source *
这样pip在创建临时构建目录时,就会把foo/include、foo/source和根目录的CMakeLists.txt都带上,不会再出现文件缺失的问题。
2. 修改setup.py:动态定位项目根目录
不管setup.py是在原目录还是pip的临时目录运行,我们都可以通过__file__属性动态找到项目根目录,然后把这个路径传递给CMake。修改后的setup.py代码如下:
import os import sys import subprocess from setuptools import setup, Extension, build_ext def get_project_root(): # 获取当前setup.py的绝对路径 setup_path = os.path.abspath(__file__) setup_dir = os.path.dirname(setup_path) # 项目根目录是setup.py所在目录的父级(原环境下是foo/,临时环境下是临时构建根目录) project_root = os.path.dirname(setup_dir) # 验证路径正确性,避免定位出错 if not os.path.exists(os.path.join(project_root, "CMakeLists.txt")): raise RuntimeError("Could not find project root: Missing CMakeLists.txt in expected path") return project_root class CMakeExtension(Extension): def __init__(self, name, sourcedir=""): Extension.__init__(self, name, sources=[]) self.sourcedir = os.path.abspath(sourcedir) class CMakeBuild(build_ext): def run(self): try: subprocess.check_output(['cmake', '--version']) except OSError: raise RuntimeError("CMake must be installed and available in your PATH") for ext in self.extensions: self.build_extension(ext) def build_extension(self, ext): if not os.path.exists(self.build_temp): os.makedirs(self.build_temp) # 获取项目根目录,传递给CMake project_root = get_project_root() self._setup(ext, project_root) self._build(ext) def _setup(self, ext, project_root): # 构造CMake命令,把项目根目录作为参数传递 cmake_cmd = [ 'cmake', ext.sourcedir, f'-DPROJECT_ROOT={project_root}', # 可选:指定Python解释器,确保pybind11匹配当前环境 f'-DPYTHON_EXECUTABLE={os.path.abspath(sys.executable)}' ] # 可选:添加构建类型参数(Release/Debug) # if self.debug: # cmake_cmd.append('-DCMAKE_BUILD_TYPE=Debug') # else: # cmake_cmd.append('-DCMAKE_BUILD_TYPE=Release') subprocess.check_call(cmake_cmd, cwd=self.build_temp) def _build(self, ext): # 执行CMake构建,可选添加并行编译加速 cmake_build_cmd = [ 'cmake', '--build', '.', '--', '-j4' ] subprocess.check_call(cmake_build_cmd, cwd=self.build_temp) # 项目setup配置 setup( name="foo", version="0.1.0", packages=["foo"], ext_modules=[CMakeExtension("_foo_cpp", sourcedir="source")], cmdclass={"build_ext": CMakeBuild}, include_package_data=True, # 启用MANIFEST.in的文件规则 )
3. 调整Python绑定的CMakeLists.txt
最后修改foo/python/source/CMakeLists.txt,使用传递过来的PROJECT_ROOT变量来引用顶层的C++资源:
cmake_minimum_required(VERSION 3.15) project(_foo_cpp) # 确保PROJECT_ROOT参数已传入 if(NOT DEFINED PROJECT_ROOT) message(FATAL_ERROR "Please set PROJECT_ROOT via -DPROJECT_ROOT when running CMake") endif() # 添加顶层头文件目录 include_directories(${PROJECT_ROOT}/include) # 如果需要编译顶层C++源码,添加子目录 add_subdirectory(${PROJECT_ROOT}/source ${CMAKE_BINARY_DIR}/source) # 查找pybind11依赖 find_package(pybind11 REQUIRED) # 创建Python绑定模块 pybind11_add_module(_foo_cpp _foo_cpp.cpp) # 链接顶层编译出的C++库(假设顶层源码生成了libfoo) target_link_libraries(_foo_cpp PRIVATE foo) # 设置模块输出目录,确保能被Python包找到 set_target_properties(_foo_cpp PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../foo )
验证两种调用方式
现在你可以同时使用两种方式完成安装/构建:
- 直接运行setup.py:
cd foo/python python setup.py install - 使用pip构建或安装:
cd foo/python pip wheel -w wheelhouse --no-deps . # 或者直接安装 pip install .
核心逻辑说明
- MANIFEST.in:解决pip构建时的文件缺失问题,确保临时环境包含完整的项目源码。
- 动态根目录定位:通过
__file__获取setup.py的绝对路径,不受运行环境影响,总能找到项目根目录。 - CMake参数传递:把项目根目录作为
PROJECT_ROOT参数传给CMake,让绑定代码的CMakeLists.txt能准确定位顶层资源。
内容的提问来源于stack exchange,提问作者Peter




