使用pypa build构建带Cython二进制的Python包生成通用wheel问题
我正尝试构建一个含Cython代码的Python分发包,希望在上传至PyPI前将Cython代码编译为二进制文件。使用pypa的build工具,在项目根目录执行python -m build,该命令可完成代码的Cython化并生成适配本地系统的二进制文件,同时在dist目录创建sdist和wheel包。但生成的wheel命名为--py3-none-any.whl,解压后虽能找到对应二进制文件(如cycode.cp39-win_amd64.pyd),但当计划在GitHub workflow中为多Python版本、多操作系统构建二进制包时,不同系统生成的wheel同名,上传PyPI时会出现覆盖或版本重复错误。且跨系统从PyPI安装时,因缺少对应系统的二进制文件,又因是wheel包不会重新编译Cython代码,导致出现“模块找不到”错误。
使用环境为64位Windows、MacOS、Ubuntu,Python版本为3.8-3.10,另有少量依赖包。简化后的项目结构与配置文件如下:
简化项目结构
Tests\ Project\ __init__.py pycode.py cymod\ __init__.py _cycode.pyx _build.py pyproject.toml
pyproject.toml
[project] name='Project' version = '0.1.0' description = 'My Project' authors = ... requires-python = ... dependencies = ... [build-system] requires = [ 'setuptools>=64.0.0', 'numpy>=1.22', 'cython>=0.29.30', 'wheel>=0.38' ] build-backend = "setuptools.build_meta" [tool.setuptools] py-modules = ["_build"] include-package-data = true packages = ["Project", "Project.cymod"] [tool.setuptools.cmdclass] build_py = "_build._build_cy"
_build.py
import os from setuptools.extension import Extension from setuptools.command.build_py import build_py as _build_py class _build_cy(_build_py): def run(self): self.run_command("build_ext") return super().run() def initialize_options(self): super().initialize_options() import numpy as np from Cython.Build import cythonize print('!-- Cythonizing') if self.distribution.ext_modules == None: self.distribution.ext_modules = [] # Add to ext_modules list self.distribution.ext_modules.append( Extension( 'Project.cymod.cycode', sources=[os.path.join('Project', 'cymod', '_cycode.pyx')], include_dirs=[os.path.join('Project', 'cymod'), np.get_include()] ) ) # Add cythonize ext_modules self.distribution.ext_modules = cythonize( self.distribution.ext_modules, compiler_directives={'language_level': "3"}, include_path=['.', np.get_include()] ) print('!-- Finished Cythonizing')
核心问题分析
生成py3-none-any.whl这种通用wheel的原因是:自定义的build_py命令逻辑导致setuptools无法正确识别扩展模块的平台依赖属性,进而默认生成不包含平台/版本标识的通用wheel,最终引发跨平台安装失败、多版本包上传覆盖的问题。
具体修复步骤
1. 重构构建逻辑,直接声明扩展模块
移除通过_build.py动态添加扩展模块的方式,改用setup.py直接声明Cython扩展,让setuptools能正确识别模块的平台属性。
新增setup.py文件:
import os import numpy as np from setuptools import setup, Extension from Cython.Build import cythonize # 声明Cython扩展模块 ext_modules = [ Extension( 'Project.cymod.cycode', sources=[os.path.join('Project', 'cymod', '_cycode.pyx')], include_dirs=[os.path.join('Project', 'cymod'), np.get_include()] ) ] # Cythonize扩展模块 ext_modules = cythonize( ext_modules, compiler_directives={'language_level': "3"}, include_path=['.', np.get_include()] ) # 传递扩展模块给setuptools setup( ext_modules=ext_modules )
2. 更新pyproject.toml,移除自定义build_py配置
删除原配置中自定义的build_py命令,让setuptools默认处理扩展模块的构建与wheel生成:
[project] name='Project' version = '0.1.0' description = 'My Project' authors = ... requires-python = ... dependencies = ... [build-system] requires = [ 'setuptools>=64.0.0', 'numpy>=1.22', 'cython>=0.29.30', 'wheel>=0.38' ] build-backend = "setuptools.build_meta" [tool.setuptools] include-package-data = true packages = ["Project", "Project.cymod"] # 移除[tool.setuptools.cmdclass]相关配置
3. 验证平台特定wheel生成
现在执行python -m build,会生成带平台和Python版本标识的wheel,例如:
- Windows平台:
Project-0.1.0-cp39-cp39-win_amd64.whl - Ubuntu平台:
Project-0.1.0-cp39-cp39-manylinux_2_17_x86_64.whl - macOS平台:
Project-0.1.0-cp39-cp39-macosx_10_9_x86_64.whl
这类带标识的wheel上传PyPI后不会互相覆盖,用户跨系统安装时会自动匹配对应平台的二进制包。
4. GitHub Actions多平台构建优化(可选)
如果需要批量构建多平台、多Python版本的wheel,推荐使用cibuildwheel工具,它能自动处理不同环境的编译配置。示例.github/workflows/build.yml配置:
name: Build Wheels on: [push, pull_request] jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.9' - name: Install cibuildwheel run: python -m pip install cibuildwheel==2.16.5 - name: Build wheels run: python -m cibuildwheel --output-dir dist - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: wheels path: dist/*.whl
内容的提问来源于stack exchange,提问作者Oniow

