目录#
- 什么是 Python 的 Wheel (.whl)
- 为什么要用 Cython 和 C++ 写 Python 包
- 项目结构和 pybind11/Cython 简介
- 如何构建跨平台的 Wheel(.whl)文件
- GitHub Actions 自动构建三平台的 Wheel
- 总结与建议
1. 什么是 Python 的 Wheel 文件?#
.whl
是 Python 包的 二进制发布格式。- 它是已经 编译好 的包(比如包含
.so
,.dll
的扩展模块),不需要用户安装时再编译。 - 优点:
- 安装快:
pip install xxx.whl
只需要解压,不需要编译 - 跨平台发布:可针对 macOS / Linux / Windows 分别打包上传到 PyPI
- 安装快:
2. 为什么用 Cython 和 C++?#
有两个核心理由:
✅ 性能#
- Python 本身解释执行慢
- 用 Cython 或 pybind11 + C++ 可以把性能关键部分编译为 原生代码(C/C++)
✅ 与底层库集成#
- 比如有很多 C/C++ 算法代码已有积累,不想重写
- 或需要调用 SIMD、OpenMP、CUDA 等加速库
3. 项目结构 & 技术介绍#
📁 项目结构(示例):#
sequenzo/├── __init__.py├── clustering/│ └── utils/│ ├── point_biserial.pyx├── dissimilarity_measures/│ ├── src/│ │ └── PAMonce.cpp│ ├── utils/│ │ └── seqlength.pyx├── setup.py├── pyproject.toml
🛠 技术栈说明:#
- Cython:用
.pyx
写代码,接近 Python 语法,但会被编译为 C/C++。 - pybind11:现代 C++ 绑定 Python 的神器。
- setup.py / pyproject.toml:定义编译行为,告诉 setuptools 如何构建扩展。
- cibuildwheel:构建跨平台的 wheel 文件,支持 Linux、Windows、macOS。
- GitHub Actions:CI/CD 平台,全自动打包构建上传。
4. 如何构建 Wheel 文件?#
🔧 本地构建(只适用于当前系统)#
# 先构建 C/Cython 模块python setup.py build_ext --inplace
# 然后生成 wheelpython -m build
会生成:
dist/├── sequenzo-0.1.4-cp311-cp311-macosx_11_0_arm64.whl
🌍 跨平台构建(cibuildwheel)#
pip install cibuildwheel
# Linux 环境cibuildwheel --output-dir dist
# 可设置变量跳过 PyPy:CIBW_SKIP="pp*" cibuildwheel --output-dir dist
5. GitHub Actions 实现三平台自动打包#
以下是 workflow 的核心思路:
- 设置 matrix 构建:多平台 × 多 Python 版本
- 用
cibuildwheel
构建.whl
- 用
twine check
校验格式 - 自动上传构建产物
✅ 构建完成后就可以上传到 PyPI 或手动下载使用了。
6. 总结 & 建议#
- 如果你的包用到了 Cython/C++,建议提供
.whl
,否则用户安装时可能因为没有编译环境而失败 - 使用 cibuildwheel + GitHub Actions 可以极大降低打包和维护成本
- 不同平台使用不同构建策略是常见实践,比如:
- macOS 用
python -m build
- Linux 和 Windows 用
cibuildwheel
- macOS 用
你提的这个问题非常好!我们来具体说说这两个命令的区别,以及你 什么时候需要执行、什么时候可以跳过:
✅ 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
,建议:
# 一次性开发环境装好、也能自动编译pip install -e .
之后你每次修改 .py
文件是自动生效的,不用重装;只有你改了 .pyx
/ .cpp
文件时,重新运行:
python setup.py build_ext --inplace # 可选,只是编译,不装
或者就直接:
pip install -e . # 重新装
⚠️ 注意#
- 如果你使用
pip install .
(没有-e
),那它会装一个普通的副本,后续你改代码不会生效,必须重新安装; - 如果你有
pyproject.toml
(你有),pip install -e .
会使用setuptools.build_meta
调用构建钩子,所以 其实就隐式包含了 build_ext 的逻辑。
💡 总结建议:#
# 最推荐的开发安装方式(自动包含编译 + 路径挂载)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 模块,并支持:
- macOS 平台(M1 / M2 / Intel 全覆盖)
- Pybind11 + Cython 混合扩展模块
- 支持 editable install (
pip install -e .
) - 支持 GitHub Actions 上打通用 wheel (
arm64 + x86_64
)
原以为“只要有 pyproject.toml + setup.py 就能搞定”,结果——噩梦开始了 🌀
😵 问题复现#
在 M1 芯片 Mac 上,执行:
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 架构冲突#
原因分析:#
- Apple Silicon 默认使用
arm64
- 老的 Python 环境或某些包是用
x86_64
安装的 - 编译阶段用了
-arch x86_64
- 链接阶段却用的是
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
#
✅ 关键逻辑:#
- 使用
uname -m
获取当前机器架构(arm64
/x86_64
) - 动态设置
-arch
- 为
.cpp
文件添加-std=c++17
,而.c
文件不添加 - 对 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
分支,不需要真实存在。
🧪 验证方式#
✅ 本地开发环境测试#
pip install -e .
✅ 构建跨架构 wheel:#
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
或pip install .
- 使用
setuptools-scm
自动管理版本号 - 发布到 PyPI 或用
wheel
构建.whl
文件
那么这些是必需的构建依赖,应该出现在:
[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
的[build-system]
中 - ❌ 不要出现在
requirements-3.x.txt
中,除非你打算在 CI 环境手动安装它们来构建源码包
✅ 总结:#
依赖 | 在 pyproject.toml | 在 requirements.txt |
---|---|---|
setuptools | ✅ 必须 | ❌ 通常不需要 |
wheel | ✅ 推荐 | ❌ 不需要 |
setuptools-scm | ✅ 若用版本自动化 | ❌ 不需要 |
是否需要我同时检查你 [build-system]
块是否配置正确?如果你愿意,也可以直接让我生成最小化的 build+runtime TOML 配置。