You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用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/includefoo/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

火山引擎 最新活动