ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MinGW 替代 MSVC 编译 Python C 扩展:setuptools 与 distutils 配置实战

MinGW 替代 MSVC 编译 Python C 扩展:setuptools 与 distutils 配置实战 最近被一个老朋友问了个很实际的问题他在 MSYS2 里用 mingw64 工具链编译 ffmpeg、libx265 都行云流水结果用 pip 装一个带 C 扩展的 Python 包时被拦住了报错“error: Microsoft Visual C 14.0 or greater is required”。他不想为了一个包去装几 GB 的 MSVC 工具链问能不能让 pip 在安装编译时直接调用 mingw64 的 gcc。先说结论能但这个“能”是有前提的而且前提比大多数人想的更苛刻。pip 本身根本不参与编译真正决定用哪个编译器的是 setuptools 底下的 distutils 构建框架。在 Windows 上这个框架默认只认 MSVCmingw32 编译器前端虽然存在但命运多舛在 setuptools 69 之后基本属于“名存实亡”。所以整篇文章我会按五个部分讲distutils 选编译器的逻辑、老版本 setuptools 激活 mingw32 的完整步骤、现代环境下绕开 MSVC 的务实路线、MinGW 和 MSVC 混用时的 ABI 边界以及我实际踩过的坑速查表。这篇文章适合已经会用 MSYS2/mingw64、但不想再为 Python 单独装一套 MSVC 的开发者也适合被各种 DLL 加载错误折磨过、想搞明白为什么的人。1. 为什么 pip 在 Windows 上默认只认 MSVCdistutils 的编译器选择逻辑1.1 pip 不负责编译构建后端才是关键先把这个最容易被误解的概念掰开。pip install 包名启动之后pip 做的事情非常有限解析依赖、下载源码包或 wheel、然后调用构建后端去生成 wheel。构建后端在 pyproject.toml 的[build-system]里声明最常见的自然是setuptools。源码包被下载后pip 会创建一个隔离的构建环境在里面运行prepare_metadata_for_build_wheel之类的钩子最终生成 wheel 再装进目标环境。所以“pip 安装编译时调用编译器”这个说法准确地说应该是“setuptools 在构建 wheel 时调用了编译器”。我们在技术上做的一切动作本质都是影响 setuptools/distutils 在构建阶段的编译器探测结果。你把 CC 环境变量设成 gcc 也没用因为 Windows 平台的 distutils 根本不走 Unixcompiler 那条路。1.2 Windows 下 distutils 的编译器探测顺序distutils 的build_ext命令在os.name nt时默认返回的编译器是 MSVC。它会去做下面这几件事查找 Visual Studio / Build Tools 的安装路径定位vcvarsall.bat或者VsDevCmd.bat通过它设置cl.exe、link.exe、lib.exe的环境如果找不到就抛出error: Microsoft Visual C 14.0 or greater is required。这个错误就是老朋友遇到的那条。它不是在说“你代码写错了”而是说“这台机器上没有任何 MSVC 环境”。反过来说只要构建系统能找到cl.exe并启动它就会用 MSVC 编译 C/C 扩展。当年 distutils 里还留了一个老式编译器前端叫mingw32你可以通过python setup.py build_ext --compilermingw32或者配置文件强制指定。那个前端会去 PATH 里找mingw32-gcc、gcc调用逻辑和 Unix 下的unixcompiler很像但针对 Windows 做了特殊处理比如自动加上-mthreads、链接pythonXY.lib之类的参数。问题在于默认情况下这个编译器选项永远不会被自动选中必须手动指定。1.3 mingw32 这个“老古董”编译器选项是怎么被移除的这件事的来龙去脉挺有意思。Python 2.x 时代distutils 确实支持 mingw32 作为 Windows 下的备选编译器这也是当时很多没有 Visual Studio 的人编译扩展的唯一希望。CPython 3.5 之后官方 Windows 发行版全部改用 MSVC 构建同时把 CRT 切换到了 UCRTdistutils 文档里对 mingw32 的态度也逐渐变成“deprecated”。真正致命的一击来自 setuptools。从 setuptools 60 开始distutils 被 setuptools 自己重写走的是setuptools._distutils这条内部路径。到 setuptools 69这个重写基本完成mingw32ccompiler这个模块直接不再可用。现在的报错很有代表性error: compiler mingw32 is not supported by setuptools这就意味着你如果在现代 setuptools 环境下直接指定--compilermingw32大概率会碰一鼻子灰。所以我们需要用配套老版本的方法来激活或者彻底换一条思路绕开。2. 旧版 setuptools distutils.cfg 激活 Mingw32 编译器的完整流程这套方案是我在本地反复验证过的传统路线。它的核心是用setuptools69恢复 distutils 里的 mingw32 编译器前端再用distutils.cfg让所有构建默认走 gcc。适合你控制构建环境的场景比如本地私有包、公司内部包或者你明确知道某个包不会强制要求新版 setuptools。2.1 准备 MSYS2 的 mingw64 工具链第一步是确保机器上真的有一套可用的 gcc。我这里默认你已经装了 MSYS2如果还没有去官网下载安装包装完打开 MSYS2 MinGW x64 的终端执行pacman -Syu pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-g mingw-w64-x86_64-pkgconf安装完成后把 MSYS2 的 mingw64 bin 目录加进系统 PATH最方便的做法是在 Windows 环境变量里追加C:\msys64\mingw64\bin然后新开一个 cmd 或 PowerShell确认gcc --version看到gcc.exe的版本信息就可以了。这里有一个细节MSYS2 安装后通常同时提供msvcrt运行时版本和ucrt运行时版本。Python 3.5 之后的官方 Windows 发行版用 UCRT所以如果你要跟官方 Python 混用我会优先建议装mingw-w64-ucrt-x86_64-gcc套装而不是老式的mingw-w64-x86_64-gcc这样 CRT 层面的差异会更小。至于这背后的原因后面 ABI 那节详细讲。2.2 让 gcc 能直接链接 Python 的导入库这是整套流程里最容易卡住的地方。Python 官方 Windows 安装包里面C 扩展要链接的 Python 符号表在安装目录的libs子目录下文件名类似C:\Python312\libs\python312.lib这个python312.lib是 MSVC 格式的 COFF 导入库gcc 理论上能读 COFF但老版本 mingw32 编译器前端在链接时经常按libpython312.a这个名字去找库。找不到就报gcc: error: cannot find -lpython312解决办法是手工生成一个 MinGW 能识别的导入库。在 MSYS2 MinGW x64 终端里执行cd $(python -c import sys; print(sys.prefix))/libs gendef $(python -c import sys; print(sys.prefix))/python312.dll dlltool --dllname python312.dll --def python312.def --output-lib libpython312.agendef会根据 DLL 里的导出符号生成.def文件dlltool再把.def转成.a。这个转出来的libpython312.a就是 gcc 在链接-lpython312时要找的东西。如果你觉得麻烦还有个更省的土办法直接把python312.lib复制一份改名成libpython312.a因为文件本身都是 COFF 导入库gcc 按扩展名找文件也能吃进去。但我实测下来dlltool生成的方法最干净撞鬼的概率最低。2.3 锁住 setuptools 版本并写入 distutils.cfg现在的 Python 环境通常自带一套较新的 setuptools我们先建一个干净的虚拟环境把 setuptools 钉在 68.xpython -m venv .venv .venv\Scripts\activate pip install setuptools69接下来找到setuptools._distutils所在的目录因为只有这里的 distutils.cfg 才对“构建后端”真正生效python -c import setuptools._distutils as d, os; print(os.path.dirname(d.__file__))输出类似...\site-packages\setuptools\_distutils在这个目录下手动创建一个distutils.cfg内容只有三行[build] compilermingw32[build] compilermingw32的意思是所有 build 系命令默认用 mingw32 编译器前端。这个配置文件的位置很关键因为它属于 setuptools 内部的 distutils 包当你锁住版本不再升级时它就一直存在。但你也要记住一旦 setuptools 升级到 69 以上这个目录可能整个被替换配置也就失效了。2.4 实测编译一个最小的 C 扩展先做最小验证避免一上来就编译 numpy 之类的大型包。建一个hellocext目录里面放两个文件。hellocext.c#define PY_SSIZE_T_CLEAN #include Python.h static PyObject* hello(PyObject* self, PyObject* args) { if (!PyArg_ParseTuple(args, )) return NULL; return PyUnicode_FromString(hello from mingw); } static PyMethodDef methods[] { {hello, hello, METH_NOARGS, say hello}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef moduledef { PyModuleDef_HEAD_INIT, hellocext, NULL, -1, methods }; PyMODINIT_FUNC PyInit_hellocext(void) { return PyModule_Create(moduledef); }setup.pyfrom setuptools import setup, Extension setup( namehellocext, version0.1.0, ext_modules[Extension(hellocext, [hellocext.c])], )然后在项目目录里执行pip install --no-build-isolation --no-binary :all: .这里有两个参数特别关键。--no-binary :all:是强迫 pip 不要偷懒下载现成 wheel必须走源码编译否则你可能根本观察不到编译过程就装完了。--no-build-isolation是关闭构建隔离让构建过程使用当前虚拟环境里那份 setuptools 68而不是 pip 临时拉取的隔离环境——如果开着隔离你放在setuptools._distutils下的distutils.cfg根本不会被读取。编译如果成功日志里会出现gcc的调用比如building hellocext extension creating build\temp... gcc -mdll -O2 -Wall -I... -c hellocext.c -o ... gcc -shared ... -lpython312 -o ...最后验证python -c import hellocext; print(hellocext.hello())看到hello from mingw就说明装了 gcc 编译器前端并且成功链接了 Python 的导入库。这套流程能跑通接下来你就可以尝试一些更复杂的包。但我必须提前泼一盆冷水这套方案对“绑定死了新版 setuptools 的包”无效。很多现代包在pyproject.toml里直接写requires [setuptools70]pip 在隔离环境里依然会拉新版你的配置照样失效。碰到这种包要么认栽用 MSVC要么试试下一部分的三条绕开路线。3. 现代 setuptools 下绕开 MSVC 的三个务实方向如果你不想为了一两个包装 MSVC又受不了上面那套“锁 setuptools 版本”的别扭操作其实还有三条更省事的路。它们的共同点是不强行让官方 Python 的构建后端去适应 mingw而是直接改变编译的发生场所。3.1 先问一句这个包真的需要现场编译吗很多新手看到报错说“需要 MSVC”第一反应就是去装编译器但其实 pip 默认的行为是优先下载现成的 wheel。只有当包没有提供cp312-cp312-win_amd64这样的匹配 wheel或者你显式加了参数才会触发源码编译。判断方法很简单安装时看 pip 日志。如果看到Downloading xxx-1.0-cp312-cp312-win_amd64.whl说明用的就是预编译二进制跟本机编译器没有任何关系。如果你压根不想在 Windows 上碰编译可以直接跑pip install --only-binary :all: 包名这样 pip 只会接受 wheel没有就报错不会去触发编译。反过来如果你怀疑某个包编译有问题想复现就用--no-binary :all:强制源码编译。这两个参数我一天至少用十次排查环境问题时非常好用。3.2 直接用 MSYS2 自己的 Python编译器天然就是 gcc如果你本来就重度依赖 MSYS2/mingw64 环境那么我强烈建议一艘次别折腾官方 Python 了直接装 MSYS2 提供的 mingw Python也就是mingw-w64-x86_64-python。在里面执行pacman -S mingw-w64-x86_64-python mingw-w64-x86_64-python-pip然后在“MSYS2 MinGW x64”终端里确认which python正常情况下你会看到路径指向C:\msys64\mingw64\bin\python.exe这就是一个由 gcc 编译出来的完整 Python。这套 Python 在安装任何 C 扩展时走的都是 mingw 生态默认编译器就是 gcc根本不需要 MSVC。你在里面跑pip install --no-binary :all: 某个纯C扩展包大概率直接就过了因为构建系统知道你当前是 mingw 环境编译器探测走的不是 MSVC 那条路。但这条路也有代价MSYS2 的 Python 生态跟 PyPI 的预编译 wheel 基本不兼容。PyPI 上win_amd64的 wheel 大多是 MSVC 编译的mingw Python 装上去之后很容易在 import 阶段 DLL 加载失败。所以这个环境里最稳的安装方式是优先pacman -S找现成包找不到再用 pip 源码编译。它更适合“我就想在 mingw 环境里写点 Python 和 C 混编的东西”的场景。3.3 换到 conda 生态让别人的预编译二进制替你干活如果说上面两种方案还在“想办法编译”conda 的思路干脆就是“不编译”。以 Anaconda 或 Miniconda 为代表的环境里绝大多数常见包都由 conda-forge 频道提供预编译版本安装时底层会解析依赖并下载合适的二进制包整个过程不需要本机存在任何编译器。实际操作很直接conda create -n py_env python3.12 conda activate py_env conda install -c conda-forge 某个包对纯 Python 用户来说这是最省心的路线。它唯一的门槛是你需要接受 conda 那套独立的 Python 环境。如果你需要 C 扩展包同时又不愿意折腾编译器conda 的优先级在我心里比前两个方案高得多。4. MinGW 编译官方 Python 扩展的 ABI 边界与实测经验4.1 为什么官方 Python 默认要求 MSVC不只是强迫症官方 Python 的 Windows 发行版是 MSVC 编译的这意味着它的 C API 函数导出、结构体内存布局、CRT 分配器管理都遵循 MSVC 的规则。mingw 的 gcc 在 Windows 下虽然能生成相同的 PE/COFF 格式但它携带的是另一套 C 运行时的行为。混用的时候最常冒出来的问题有三个结构体布局差异。Python.h 定义了一堆结构体比如PyObject、PyTypeObjectMSVC 和 gcc 的默认对齐、位域处理在绝大多数场景一致但一旦涉及__declspec(align)或特定宏展开就可能出现偏移不一致。CRT 内存分配器不一致。一个模块用 gcc 的 malloc 分配了内存另一个模块用 msvcrt 的 free 释放轻则慢重则崩溃。UCRT 方案的统一性更好MSYS2 里mingw-w64-ucrt-x86_64-gcc就是为了减少这种差异。符号修饰和导入机制。MSVC 下 Python.h 用__declspec(dllimport)导出extern符号mingw 下也能处理但个别老库、老头文件在宏判断上会走不同分支。所以“mingw 编出来的扩展能不能用”这个问题答案不是绝对的而是分层的。4.2 我实测过的可编译范围纯 C 和简单 Cython 相对安全纯 C 接口的扩展只要不牵涉复杂的 C 对象跨 DLL 边界用 mingw 编译的失败率其实不高。举几个我实测过或者观察过的例子regex、simplejson、zstandard这一类核心是纯 C 的包强制 mingw 编译后 import、跑功能测试结果和官方 wheel 基本一致。Cython 生成的 C 代码也属于这一类因为 Cython 输出的是标准 C 接口不涉及 C ABI。你只要在setup.py里用Extension声明普通参数mingw 编译器前端就能处理。但只要进入 C 领域情况迅速恶化。pybind11写出来的扩展石榴裙下藏着一整套 libstdc 和 MSVC C 标准库之间的 ABI 鸿沟。具体表现是编译能过import 也正常但 Python 对象生命周期管理、异常传播、字符串跨模块传递这些场景稍不留神就是诡异崩溃。网上能搜到大量类似“pybind11 在 mingw 下编译成功但运行崩溃”的案例。如果你要用 pybind11我个人强烈建议老老实实上 MSVC这不是编译能力的问题而是标准库二进制兼容性的问题。4.3 拿到ImportError: DLL load failed while importing xxx怎么定位这个报错太常见了而且它几乎不告诉你原因。我处理这类问题的第一步永远是看 pyd 依赖了哪些 DLL。在 MinGW 环境里用objdump -p 你的包.pyd | grep DLL Name或者用 Dependencies 这类图形工具打开 pyd 文件。你会看到类似DLL Name: python312.dll DLL Name: VCRUNTIME140.dll DLL Name: MSVCP140.dll如果依赖列表里出现VCRUNTIME140.dll和MSVCP140.dll说明这个扩展链接了 MSVC 的 C 运行库。mingw 编译出来的扩展很少主动依赖这两个 DLL一旦出现就要怀疑是不是构建过程中混用了 MSVC 工具链的导入库或者下载的 wheel 是 MSVC 版却被硬塞进了 mingw Python。定位清楚之后最常见的处理路径是确认当前环境 Python 版本和编译时一致确认扩展的所有依赖 DLL 都在 PATH 里再不行就换方案。盲目重编译十次不如静下心看一次依赖树。4.4 什么情况我建议你直接放弃 MinGW我给你一个很诚实的判断标准。如果出现下面任一情况别在 mingw 上浪费时间目标包是 pybind11 或者重度 C 扩展目标包的 C 部分大量依赖第三方 C 库而这些库本身只有 MSVC 版预编译产物你需要编译 OpenMP/MPI/Fortran 混合的扩展你只是想装个包并不想深究 ABI。这些场景下装 MSVC Build Tools 或转 conda 生态成本远远低于在 mingw 上排查诡异崩溃。技术方案的评估不只是“能不能编过”还要算“后面维护和排错的代价”。5. 踩坑记录常见报错、原因与补救手段速查表我在做这整套配置和测试的过程中把出现过的问题归纳成一个速查表。你以后遇到类似报错直接对着找原因比大海捞针快得多。报错或现象根本原因我的处理方式error: Microsoft Visual C 14.0 or greater is required构建系统没找到 MSVC 环境要么装 MSVC Build Tools要么走绕开路线compiler mingw32 is not supported by setuptoolssetuptools 69 之后移除了 mingw32 编译器前端使用setuptools69或者放弃显式指定gcc: error: cannot find -lpython312缺少libpython312.a导入库用gendefdlltool生成或复制改名python312.liberror: python312.lib: No such file or directory老 distutils 直接把.lib当输入文件但路径不对确认libs目录存在并把目录加进LIBRARY_PATHImportError: DLL load failed while importing xxx依赖 DLL 缺失或 MSVC/mingw ABI 混用用objdump -p列依赖确认 Python 版本一致不行就换方案ModuleNotFoundError: No module named distutils新 Python 环境没有 distutils 或 setuptools 未装pip install setuptools69其中自带 distutils 实现pip 隔离构建时 distutils.cfg 不生效构建隔离环境使用的是临时 setuptools安装时加--no-build-isolation最后再补一个我个人的排错习惯遇到任何编译相关的问题第一步先用pip install --only-binary :all: 目标包确认它是不是真的没有 pre-built wheel。有这个 wheel 就不存在编译问题没有再决定走哪条路线。第二步看日志确认到底是 MSVC 探测失败还是导入库链接失败这两类问题的解决方向完全不同。第三步才是动手改环境。顺着这个顺序来你在“mingw 编译 Python 扩展”这条路上踩坑的次数会少很多。如果你只是在几个包之间反复横跳我的优先级建议是有 wheel 用 wheel没有就上 conda再不行就 MSYS2 的 Python最后才轮到在官方 Python 里强行激活 mingw32。这套顺序不是因为我保守而是因为 ABI 坑的成本往往比装个编译器高得多。
返回列表