Skip to content

Python包诞生记(一):Python 打包轮子(.whl 文件)与 Cython/C++ 混合编程实践

· 11 min

目录#

  1. 什么是 Python 的 Wheel (.whl)
  2. 为什么要用 Cython 和 C++ 写 Python 包
  3. 项目结构和 pybind11/Cython 简介
  4. 如何构建跨平台的 Wheel(.whl)文件
  5. GitHub Actions 自动构建三平台的 Wheel
  6. 总结与建议

1. 什么是 Python 的 Wheel 文件?#


2. 为什么用 Cython 和 C++?#

有两个核心理由:

✅ 性能#

✅ 与底层库集成#


3. 项目结构 & 技术介绍#

📁 项目结构(示例):#

sequenzo/
├── __init__.py
├── clustering/
│ └── utils/
│ ├── point_biserial.pyx
├── dissimilarity_measures/
│ ├── src/
│ │ └── PAMonce.cpp
│ ├── utils/
│ │ └── seqlength.pyx
├── setup.py
├── pyproject.toml

🛠 技术栈说明:#


4. 如何构建 Wheel 文件?#

🔧 本地构建(只适用于当前系统)#

Terminal window
# 先构建 C/Cython 模块
python setup.py build_ext --inplace
# 然后生成 wheel
python -m build

会生成:

dist/
├── sequenzo-0.1.4-cp311-cp311-macosx_11_0_arm64.whl

🌍 跨平台构建(cibuildwheel)#

Terminal window
pip install cibuildwheel
# Linux 环境
cibuildwheel --output-dir dist
# 可设置变量跳过 PyPy:
CIBW_SKIP="pp*" cibuildwheel --output-dir dist

5. GitHub Actions 实现三平台自动打包#

以下是 workflow 的核心思路:

✅ 构建完成后就可以上传到 PyPI 或手动下载使用了。


6. 总结 & 建议#


你提的这个问题非常好!我们来具体说说这两个命令的区别,以及你 什么时候需要执行、什么时候可以跳过


✅ TL;DR:你只需要运行 pip install -e . 就够了!#

pip install -e . 会自动调用 python setup.py build_ext --inplace 背后的逻辑,除非你只是想局部地测试 Cython/C++ 构建,而不安装整个包


🔍 命令对比说明:#

命令作用是否必须何时用
python setup.py build_ext --inplace编译 Cython 和 C++ 代码,并将 .so 编译文件生成在原目录中不是必须你只想测试 C/Cython 编译模块、提前看哪些 .so 会被生成
pip install -e .安装整个包为“开发模式”(包括编译扩展)推荐主用安装整个包用于开发,能直接 import 使用修改后的代码,不用重复安装

✅ 推荐工作流#

如果你在开发 Sequenzo,建议:

Terminal window
# 一次性开发环境装好、也能自动编译
pip install -e .

之后你每次修改 .py 文件是自动生效的,不用重装;只有你改了 .pyx / .cpp 文件时,重新运行:

Terminal window
python setup.py build_ext --inplace # 可选,只是编译,不装

或者就直接:

Terminal window
pip install -e . # 重新装

⚠️ 注意#


💡 总结建议:#

Terminal window
# 最推荐的开发安装方式(自动包含编译 + 路径挂载)
pip install -e .
# 只在你单独测试 .cpp / .pyx 编译时运行
python setup.py build_ext --inplace

除非你明确知道你要干嘛,否则推荐只跑 pip install -e .,会更自动、安全、统一。

当然可以!你踩的这个坑其实是很多做 Cython / C++ 扩展的 macOS 用户,尤其是同时支持 Intel + Apple Silicon 架构时的通病,写成一篇博文非常有价值 💡。

我来帮你写一个清晰、实用又稍微带点故事性的技术博文草稿,适合发布到像 CSDN、知乎、Medium 这样的平台。如果你有风格倾向可以告诉我,这里我先给出一个「专业、亲切又实用」的风格版本👇


🧱 在 Mac 上用 Pybind11 + Cython 编译 C++ 扩展的踩坑记录:支持 Apple Silicon + Intel 架构#

作者:Yuqi Liang 梁彧祺
时间:2025-02-27


📌 背景#

在开发我的 Python 包 Sequenzo 时,我们需要编译一些高性能的 C++ 和 Cython 模块,并支持:

原以为“只要有 pyproject.toml + setup.py 就能搞定”,结果——噩梦开始了 🌀


😵 问题复现#

在 M1 芯片 Mac 上,执行:

Terminal window
pip install -e .

报错信息中出现:

ld: warning: ignoring file .../DHDdistance.o, building for macOS-arm64 but attempting to link with file built for macOS-x86_64
...
error: invalid argument '-std=c++17' not allowed with 'C'

这是两个 常见又让人迷惑 的问题:


🔍 问题一:x86_64 vs arm64 架构冲突#

原因分析:#

🔧 解决方案:

使用 ARCHFLAGS-arch 参数,让编译架构和链接架构一致


🔍 问题二:.c 文件误用了 -std=c++17#

原因分析:#

有的扩展模块是 .c 文件,但我们统一给 extra_compile_args 加了 -std=c++17,这会导致:

error: invalid argument '-std=c++17' not allowed with 'C'

🔧 解决方案:

区分 .cpp.c/.pyx 文件,分别设置不同的编译参数


✅ 最终解决方案:改造 setup.py#

✅ 关键逻辑:#

  1. 使用 uname -m 获取当前机器架构(arm64 / x86_64
  2. 动态设置 -arch
  3. .cpp 文件添加 -std=c++17,而 .c 文件不添加
  4. 对 Cython 扩展逐个设置编译参数,避免报错

✅ 完整代码片段:#

def get_compile_args_for_file(filename):
base_cflags = ['-Wall', '-Wextra']
base_cppflags = ['-std=c++17'] + base_cflags
arch = subprocess.check_output(['uname', '-m']).decode().strip()
arch_flags = ['-arch', arch] if arch in ('x86_64', 'arm64') else []
return base_cppflags + arch_flags if filename.endswith('.cpp') else base_cflags + arch_flags

然后每个 extension 都这样用:

extra_compile_args=get_compile_args_for_file("dummy.cpp"),

⚠️ "dummy.cpp" 只是为了触发 .cpp 分支,不需要真实存在。


🧪 验证方式#

✅ 本地开发环境测试#

Terminal window
pip install -e .

✅ 构建跨架构 wheel:#

Terminal window
ARCHFLAGS="-arch arm64" python setup.py bdist_wheel

并可用 GitHub Actions 打包 wheel(记得多平台测试)。


📦 架构安全构建实践建议#

项目推荐做法
开发环境ARCHFLAGS 配合 setup.py
发布环境使用 GitHub Actions 构建双架构 wheel
包内 .c 文件避免添加 -std=c++17
.cpp 文件明确指定 -std=c++17
Cython每个 .pyx 设置独立 extra_compile_args

🧠 总结#

这次经验告诉我,Mac 跨架构构建不是简单的加个 -arch 就完事的,要从源文件类型、系统架构、build 环境一一考虑清楚。

但一旦你配好了:

✅ C++ 模块 + Cython 模块混编
✅ 支持 pip install -e .
✅ 多平台打 wheel 没压力
✅ 支持 Apple Silicon + Intel 全覆盖

香!真的香!


💬 如果你也遇到了类似问题,欢迎评论交流!#

或者你在 CI / 构建 Pybind11 扩展方面还有更多坑,我也乐意一起讨论!


需要我帮你把这篇转成 Markdown 或者发布到你用的平台的格式也可以说!如果有你项目链接,我也可以帮你插入到文末「项目地址」部分~

这三个依赖属于 构建工具链,是否需要保留,取决于你项目的打包/发布方式:


需要保留的情况(推荐)#

如果你使用的是 标准的 pyproject.toml 构建系统,特别是:

那么这些是必需的构建依赖,应该出现在:

[build-system]
requires = [
"setuptools>=64",
"wheel>=0.40.0",
"setuptools-scm[toml]>=7.0.0"
]
build-backend = "setuptools.build_meta"

不需要出现在 requirements.txt 中的情况#

这三个包是构建时依赖(build-system),而不是项目运行时依赖。

所以:


✅ 总结:#

依赖在 pyproject.toml在 requirements.txt
setuptools✅ 必须❌ 通常不需要
wheel✅ 推荐❌ 不需要
setuptools-scm✅ 若用版本自动化❌ 不需要

是否需要我同时检查你 [build-system] 块是否配置正确?如果你愿意,也可以直接让我生成最小化的 build+runtime TOML 配置。