PEP 518下带纯Python回退的Python绑定打包最佳实践
现有以CMake子项目形式分发的静态C++库libFoo,需开发名为pyFoo的Python包:既要通过pybind11提供libFoo的Python绑定能力,也要在目标平台无法编译libFoo时,提供对应功能的纯Python实现作为回退方案。
+--+ libFoo | +--- CMakeLists.txt (导出静态库目标 foo::foo) | +--- src/..., inc/... (libFoo的C++源码) | +--+ pyFoo +--- pyproject.toml +--- ... (其他元数据/包文件、测试用例等) +--+ foo (主包目录) +--+ _native | +--- CMakeLists.txt (将libFoo/CMakeLists.txt作为子项目导入,完成配置后添加pybind11模块) | +--- foo_bindings.cpp (基于导入的foo::foo目标编写pybind11绑定逻辑) | +--- _fallback/... (libFoo功能的纯Python兼容实现) +--- __init__.py (核心逻辑为:try: from ._native.foo_bindings import *; except ImportError: from ._fallback import *)
上述结构在开发环境下可正常运行:执行ninja install时,pyFoo/foo/_native路径下的CMake逻辑会将编译好的foo_bindings移动到对应位置,但这套逻辑无法适配发行版、wheel包构建场景下的原生C++部分构建流程。
目前调研到的同类项目仅存在两类实现方案:
- 包内仅包含依赖库+绑定层,无纯Python回退实现,安装阶段必须完成原生编译,否则安装直接失败
- 包通过ctypes/cffi导入外部预安装的依赖库(如
libssl.so),导入失败则回退到Python实现,对构建系统而言这类包属于纯Python包,无需编译原生扩展
让pyFoo适配尽可能多的运行场景,覆盖用户无C++源码编译环境(如未安装MSVC的Windows系统)、当前平台无可用预编译wheel包的情况,采用符合PEP 518规范的现代工具链方案实现需求(不要把所有逻辑硬编码到setup.py中),同时保持对外接口简洁清晰。
执行pip install pyfoo时按如下优先级决策:
- 若当前平台存在适配的预编译wheel包,直接安装使用该wheel
- 若无可用wheel包,尝试通过CMake从源码构建
foo._native.bindings(包含其直接依赖libfoo):- 若构建成功,安装完成后默认使用
foo._native.bindings作为功能实现 - 若构建失败,不中断安装流程,仅排除
foo._native包,完成其余纯Python部分的安装即可,运行时自动走回退逻辑
- 若构建成功,安装完成后默认使用
参考示例:Python标准库datetime模块的实现逻辑就是类似方案——若C实现(
_datetimemodule.c)不可用,Python会自动回退到datetime.py的纯Python实现,但官方打包指南并未给出这类带可选原生编译+失败回退场景的具体落地方案。
- 构建后端选择:选用
scikit-build-core作为PEP 518构建后端,它原生支持CMake构建pybind11扩展,同时支持配置可选组件的构建失败容错逻辑,不需要手写复杂的setup.py逻辑。 - 构建配置调整:
- 在
pyproject.toml中配置scikit-build-core的构建选项,将foo._native标记为可选组件:配置构建失败时不抛出致命错误,仅跳过该扩展的安装,同时自动排除_native目录的打包 - 配置CMake参数,直接引用仓库根目录下的
libFoo子项目作为静态依赖,不需要额外的依赖下载逻辑 - 配置wheel构建规则,在有编译环境的CI平台上全平台构建预编译wheel,上传到PyPI时同时上传纯Python wheel(即不包含
_native扩展的通用wheel),注意给纯Python wheel设置更低的优先级,保证有适配原生wheel的平台优先安装原生版本
- 在
- 运行时逻辑:保持现有
__init__.py的导入容错设计即可,原生扩展存在时优先加载,不存在时自动加载回退实现。
若foo._fallback存在foo/foo._native不需要的额外包依赖,不要直接将这些依赖加入pyproject.toml的核心依赖列表,否则会给所有使用原生实现的用户强加不必要的依赖。推荐处理方式:
- 将回退实现需要的额外依赖标记为可选依赖,放到
[project.optional-dependencies]下的fallback分组中 - 在
__init__.py触发回退导入时增加依赖缺失的友好提示:如果导入回退模块时抛出依赖缺失错误,提示用户可通过pip install pyfoo[fallback]安装回退实现所需的完整依赖 - 如果希望用户无感知使用回退能力,也可以在构建脚本检测到原生扩展构建失败时,自动在安装配置中追加回退所需依赖,但该方案会增加构建逻辑复杂度,优先推荐可选依赖分组方案。
内容的提问来源于stack exchange,提问作者Marandil

