如何用setuptools打包C++扩展并包含stub.pyi文件
项目文件结构:
project/ ├─ cpp_src/ │ ├─ src/ │ │ ├─ cpp source files │ ├─ test/ │ │ ├─ cpp test files │ ├─ CMakeLists.txt │ ├─ stub.pyi ├─ python_src/ │ ├─ ... ├─ build.py
使用setuptools配合自定义build_ext命令编译打包cpp_src中的C++扩展,但无法将stub.pyi文件包含进包内。允许调整文件结构(比如在cpp_src中新增setup.py),同时使用Poetry管理虚拟环境,也可更换更简便的构建系统。
简化后的build.py代码:
import subprocess from pathlib import Path from setuptools import Extension, setup from setuptools.command.build_ext import build_ext class CMakeBuild(build_ext): def build_extension(self, ext: Extension) -> None: # Determine where the extension should be transferred to after it has been # compiled current_dir = Path.cwd() build_dir = current_dir.joinpath(self.get_ext_fullpath(ext.name)).parent # Determine the profile to build the CMake extension with profile = "Release" # Make sure the build directory exists build_temp = Path(self.build_temp).joinpath(ext.name) if not build_temp.exists(): build_temp.mkdir(parents=True) # Compile and build the CMake extension subprocess.run( [ "cmake", current_dir.joinpath(ext.sources[0]), f"-DDO_TESTS=false", f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{profile.upper()}={build_dir}", ], cwd=build_temp, check=True, ) subprocess.run( ["cmake", "--build", ".", f"--config {profile}"], cwd=build_temp, check=True ) def main(): setup( name="hades_extensions", script_args=["bdist_wheel"], ext_modules=[Extension("hades_extensions", ["cpp_src"])], cmdclass={"build_ext": CMakeBuild}, ) if __name__ == "__main__": main()
需求:修改setuptools命令以包含stub.pyi文件。
方法一:修改现有build.py,添加package_data配置
Setuptools的setup函数支持通过package_data指定要包含的额外文件。由于扩展模块名为hades_extensions,需要把stub.pyi重命名为hades_extensions.pyi,让Python能正确关联到扩展模块。
修改main()函数中的setup调用:
def main(): setup( name="hades_extensions", script_args=["bdist_wheel"], ext_modules=[Extension("hades_extensions", ["cpp_src"])], cmdclass={"build_ext": CMakeBuild}, # 配置要包含的存根文件 package_data={ "hades_extensions": ["hades_extensions.pyi"], }, # 声明包目录(即使没有__init__.py) packages=["hades_extensions"], )
注意:需要在cpp_src目录下创建空的__init__.py,或者直接通过packages参数声明hades_extensions为包,这样setuptools才能识别并处理package_data配置。
方法二:在cpp_src目录下新增setup.py
如果允许调整文件结构,可以把构建逻辑和文件配置放在cpp_src目录下的setup.py中,结构更清晰:
cpp_src/setup.py:
import subprocess from pathlib import Path from setuptools import Extension, setup from setuptools.command.build_ext import build_ext class CMakeBuild(build_ext): def build_extension(self, ext: Extension) -> None: current_dir = Path(__file__).parent build_dir = Path(self.get_ext_fullpath(ext.name)).parent profile = "Release" build_temp = Path(self.build_temp).joinpath(ext.name) if not build_temp.exists(): build_temp.mkdir(parents=True) subprocess.run( [ "cmake", current_dir, f"-DDO_TESTS=false", f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{profile.upper()}={build_dir}", ], cwd=build_temp, check=True, ) subprocess.run( ["cmake", "--build", ".", f"--config {profile}"], cwd=build_temp, check=True ) setup( name="hades_extensions", ext_modules=[Extension("hades_extensions", ["."])], cmdclass={"build_ext": CMakeBuild}, package_data={ "hades_extensions": ["hades_extensions.pyi"], }, packages=["hades_extensions"], )
将cpp_src下的stub.pyi重命名为hades_extensions.pyi,然后执行python cpp_src/setup.py bdist_wheel即可完成打包。
方法三:结合Poetry简化构建
既然用Poetry管理虚拟环境,可以直接通过Poetry配置构建流程:
- 项目根目录创建
pyproject.toml(若未存在),添加以下配置:
[tool.poetry] name = "hades_extensions" version = "0.1.0" description = "" authors = [] [tool.poetry.dependencies] python = "^3.8" [tool.poetry.build] script = "build.py" [build-system] requires = ["poetry-core>=1.0.0", "setuptools"] build-backend = "poetry.core.masonry.api"
- 修改build.py,添加
package_data配置(同方法一),同时在CMakeBuild的build_extension方法末尾添加存根文件复制逻辑:
def build_extension(self, ext: Extension) -> None: # ... 原有编译逻辑 # 复制stub文件到扩展目录 stub_src = Path(__file__).parent / "cpp_src" / "stub.pyi" stub_dst = build_dir / "hades_extensions.pyi" stub_dst.write_text(stub_src.read_text())
执行poetry build即可完成打包,stub文件会被正确包含。
内容的提问来源于stack exchange,提问作者Aspect11

