windows下基于MSYS2+GCC构建原生MSVC python的C Extension Wheel:踩坑十六条

windows下基于MSYS2+GCC构建原生MSVC python的C Extension Wheel:踩坑十六条

背景

正在将jq库的python binding jq.py移植到windows。难度在于jq的构建工具链只能在unix-like环境下运行,不支持windows原生的MSVC编译器。

采取的方案是利用MSYS2在windows下提供构建工具链运行环境。第一个方案是完全在MSYS2下使用内置安装的gcc和python构建,但最终构建的wheel会有名称和链接问题,不能直接安装到MSYS外的windows原生python(虽然有一些workaround但是不推荐)。

第二种成功的方案是只在MSYS2下构建jq库,而后切换到windows原生python环境安装cibuildwheel等构建工具,通过环境变量借用MSYS2中的gcc完成链接预编译jq库和最终wheel 的构建。

如果需要完整的示例可以看我对jq.py的fork(主要关注setup.py和用于github action自动化的build.yml)或者PR内容。需要注意mingw gcc与MSVC仍然不是完全兼容,因此构建的扩展可能具有潜在的问题。

这里只记录在上述过程中踩的各种坑及解决方法。

提前吐槽:

单setuptools就有pyproject.toml, setup.py, setup.cfg, distutils.cfg一堆各种历史遗留积攒的格式不同的配置文件,相互之间交糅错杂。构建/测试工具也有setup.py/build/pip wheel/cibuildwheel/tox/nox 这么多种,每个的行为和读的配置还都不一样,甚至还可能相互调用,每调用一层都非要各自建一个venv,再下一遍依赖包。

python版本从3.6到3.12, 同样的构建流程每个python版本都能给我报出不一样的错来TAT。

方案一:完全在MSYS2使用内置gcc和python构建(不推荐)

cmp not found

编译过程中出现报错,但并不会终止编译:

1
cmp: command not found

通过pacman -S diffutils解决。

sys/select.h: No such file or directory

1
2
3
4
5
6
7
8
building 'jq' extension
gcc ... -c jq.c -o /tmp/tmp1008y2y9.build-temp/jq.o
In file included from C:/msys64/usr/include/python3.11/Python.h:38,
               from jq.c:6:
C:/msys64/usr/include/python3.11/pyport.h:236:10: fatal error: sys/select.h: No such file or directory
236 | #include <sys/select.h>
    |          ^~~~~~~~~~~~~~
compilation terminated.

解决:卸载MSYS的python包。换用mingw-w64-ucrt-x86_64-python

_winapi:系统找不到指定的文件

1
2
3
hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      FileNotFoundError: [WinError 2] 系统找不到指定的文件

这是由于windows创建子进程时的参数必须是可执行文件,不能是脚本。在本例中的setup.py中需要执行configure脚本构建,所以在setup.py中的command部分添加sh来执行configure脚本:

1
2
3
["./configure",...
#改为:
["sh", "./configure",...

–plat-name must be one of (‘win32’, ‘win-amd64’, ‘win-arm32’, ‘win-arm64’)

1
2
3
4
5
6
 building 'jq' extension
      Traceback (most recent call last):
        ...
        File "C:/msys64/tmp/pip-build-env-f2drlbw6/overlay/lib/python3.11/site-packages/setuptools/_distutils/_msvccompiler.py", line 246, in initialize
          raise DistutilsPlatformError(
      distutils.errors.DistutilsPlatformError: --plat-name must be one of ('win32', 'win-amd64', 'win-arm32', 'win-arm64')

解决方案

1
env SETUPTOOLS_USE_DISTUTILS=stdlib _virtualenv/bin/python -m build

安装时:not supported wheel on this platform

完全在MSYS2下构建的Python wheel在windows原生的MSVC 构建python上安装时,会报错:

1
ERROR: jq-1.7.0-cp311-cp311-mingw_x86_64_ucrt.whl is not a supported wheel on this platform.

原生的python接受的后缀是win_amd64

解决:首先确认已安装wheel(pip install wheel),使用wheel包repack wheel:

1
2
3
4
5
6
$ ../_virtualenv/bin/wheel.exe unpack jq-1.7.0-cp311-cp311-mingw_x86_64_ucrt.whl
Unpacking to: jq-1.7.0...OK
$ mv jq-1.7.0/jq.cp311-mingw_x86_64_ucrt.pyd jq-1.7.0/jq.cp311-win_amd64.pyd
$ perl -i  -pe 's/cp311-cp311-mingw_x86_64_ucrt/cp311-cp311-win_amd64/' jq-1.7.0/jq-1.7.0.dist-info/WHEEL
$ ../_virtualenv/bin/wheel.exe pack jq-1.7.0
Repacking wheel as ./jq-1.7.0-cp311-cp311-win_amd64.whl...OK

此时可正常安装wheel,但仍有问题仍未解决。

ImportError: DLL load failed

pyd文件就是dll文件。使用软件Dependencies检查发现缺少了libpython3.11.dll。查看系统python版本,实际的文件名称是python311.dll。直接复制重命名的文件到wheel中会导致import后程序闪退。

检查MSYS环境中的/ucrt/bin,其中的文件名是libpython3.11.dll。这是由于MSYS2遵守了Unix下的命名约定。

尝试:修改dll:

观察MSYS2环境/ucrt64/bin中libpython3.11.dll和原生安装的python目录下的python311.dll具有相同的sha校验和,只是名称问题,手动使用CEF Explorer(普通的Hex Editor替换字符串是无效的)将pyd中的dll依赖名称修改,重新打包安装,现在import正常。但该方法过于奇技淫巧,不推荐。

方案二:MSYS2+GCC构建C库 ,使用原生python构建wheel

error: C:\Program Files (x86)\Microsoft Visual Studio… \cl.exe failed with exit code 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
creating build\temp.win-amd64-3.11\Release
C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.39.33519\bin\HostX86\x64\cl.exe /c /nologo /Ox /W3 /GL ... /Tcjq.c /Fobuild\temp.win-amd64-3.11\Release\jq.obj
jq.c
C:\msys64\tmp\build-via-sdist-kx6g69ip\jq-1.7.0\_deps\build\jq-1.7.1\src\jv.h(129): error C2061: 语法错误: 标识符“JV_VPRINTF_LIKE”
C:\msys64\tmp\build-via-sdist-kx6g69ip\jq-1.7.0\_deps\build\jq-1.7.1\src\jv.h(129): error C2059: 语法错误:“;C:\msys64\tmp\build-via-sdist-kx6g69ip\jq-1.7.0\_deps\build\jq-1.7.1\src\jv.h(129): error C2059: 语法错误:“常数”
C:\msys64\tmp\build-via-sdist-kx6g69ip\jq-1.7.0\_deps\build\jq-1.7.1\src\jv.h(130): error C2061: 语法错误: 标识符“JV_PRINTF_LIKE”
C:\msys64\tmp\build-via-sdist-kx6g69ip\jq-1.7.0\_deps\build\jq-1.7.1\src\jv.h(130): error C2059: 语法错误:“;C:\msys64\tmp\build-via-sdist-kx6g69ip\jq-1.7.0\_deps\build\jq-1.7.1\src\jv.h(130): error C2059: 语法错误:“常数”
jq.c(3537): warning C4244: “=”: 从“Py_ssize_t”转换到“int”,可能丢失数据
jq.c(7085): warning C4244: “函数”: 从“Py_ssize_t”转换到“int”,可能丢失数据
jq.c(8792): warning C4090: “=”: 不同的“const”限定符
error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\VC\\Tools\\MSVC\\14.39.33519\\bin\\HostX86\\x64\\cl.exe' failed with exit code 2

ERROR Backend subprocess exited when trying to invoke build_wheel

看起来是自动调用了MSVC编译器。

解决方案:创建配置文件【推荐】:项目目录下新建setup.cfg,将如下内容写入。

1
2
[build]
compiler=mingw32

上述方案在使用cibuildwheel构建时也有效。

如果只是临时测试,对于部分构建工具可以使用参数--compiler=mingw32。 例如:

1
python -m build --compiler=mingw32

error: enumerator value for ‘__pyx_check_sizeof_voidp’ is not an integer constant

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
./winenv/Scripts/python setup.py build --compiler=mingw32
running build
running build_ext
building 'jq' extension
gcc -mdll -O -Wall ... -c jq.c -o build\temp.win-amd64-cpython-311\Release\jq.o
jq.c:272:41: warning: division by zero [-Wdiv-by-zero]
  272 |     enum { __pyx_check_sizeof_voidp = 1 / (int)(SIZEOF_VOID_P == sizeof(void*)) };
      |                                         ^
jq.c:272:12: error: enumerator value for '__pyx_check_sizeof_voidp' is not an integer constant
  272 |     enum { __pyx_check_sizeof_voidp = 1 / (int)(SIZEOF_VOID_P == sizeof(void*)) };
      |            ^~~~~~~~~~~~~~~~~~~~~~~~
jq.c: In function '__pyx_f_2jq_jv_string_to_py_string':
jq.c:8792:24: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
...           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: command 'C:\\msys64\\ucrt64\\bin\\gcc.exe' failed with exit code 1

解决方案:修改setup.py,增加宏定义:

1
2
3
4
5
jq_extension = Extension(
   ...
    define_macros=[("MS_WIN64", 1)] if os.name == 'nt' else None,
   ...
)

Unknown MS Compiler version 1916

在本例中只有为python 3.6版本构建时才会报错。

    File "C:\...\3.6.8\lib\distutils\cygwinccompiler.py", line 86, in get_msvcr
      raise ValueError("Unknown MS Compiler version %s " % msc_ver)
ValueError: Unknown MS Compiler version 1916

解决方案:在报错的cygwincompiler.py文件中增加:

1
2
3
4
5
6
7
elif msc_ver == '1600':
    # VS2010 / MSVC 10.0
    return ['msvcr100']
elif msc_ver >= '1900':
    return ['vcruntime140']
else:
    raise ValueError("Unknown MS Compiler version %s " % msc_ver)

但是该方案具有侵入性,不推荐。

32位构建错误集合

skipping incompatible pythonxxx

1
2
3
4
5
creating build\lib.win32-cpython-311
      gcc -shared -s ... -lpython311 -lvcruntime140 -o build\lib.win32-cpython-311\jq.cp311-win32.pyd -lm -Wl,-Bstatic -lpthread -lshlwapi -static-libgcc
	...
      C:/.../ld.exe: cannot find -lvcruntime140: No such file or directory
      C:/.../ld.exe: skipping incompatible C:\Users\maxsun\AppData\Local\pypa\cibuildwheel\Cache\nuget-cpython\pythonx86.3.11.8\tools/vcruntime140.dll when searching for -lvcruntime140

问题原因:为32位构建时仍然使用的64位gcc编译器。例如MSYS2中x86_64-w64-mingw32编译器目标平台仍然是x64,需要使用i686-w64-mingw32-gcc编译目标才是32位。如果不知道当前默认的gcc对应什么架构,可以使用gcc -v查看输出中的target machine。

解决方案:先将C:/msys64/mingw32/bin加入PATH**(重要)**,然后使用环境变量CC=i686-w64-mingw32-gcc.exe指定编译器。

对于cibuildwheel需要通过CIBW_ENVIRONMENT=“CC=..."的形式指定。

error: enumerator value for ‘__pyx_check_sizeof_voidp’ is not an integer constant

1
2
3
4
5
6
7
8
gcc -mdll -O -Wall -DMS_WIN64=1 ... -c jq.c -o build\temp.win32-cpython-311\Release\jq.o
      jq.c:272:41: warning: division by zero [-Wdiv-by-zero]
        272 |     enum { __pyx_check_sizeof_voidp = 1 / (int)(SIZEOF_VOID_P == sizeof(void*)) };
            |                                         ^
      jq.c:272:12: error: enumerator value for '__pyx_check_sizeof_voidp' is not an integer constant
        272 |     enum { __pyx_check_sizeof_voidp = 1 / (int)(SIZEOF_VOID_P == sizeof(void*)) };
            |            ^~~~~~~~~~~~~~~~~~~~~~~~
      jq.c: In function '__pyx_f_2jq_jv_string_to_py_string':

解决需要两个方面:首先判断32bit时也不定义MS_WIN64

1
2
3
4
5
jq_extension = Extension(
    "jq",
    sources=["jq.c"],
    define_macros=[("MS_WIN64", 1)] if os.name == 'nt' and sys.maxsize > 2**32 else None,
    ...

第二是观察到输出中同样使用的是gcc(64位),不是此前指定的i686 GCC,需要重新检查编译器的安装和环境变量的设置。设置正确目标架构的GCC’后问题解决。

undefined reference to xxx

仅在链接预编译库时报错:

1
2
3
4
5
gcc -shared -s ... -lpython311 -lvcruntime140 -o build\lib.win32-cpython-311\jq.cp311-win32.pyd -lm -Wl,-Bstatic -lpthread -lshlwapi -static-libgcc
      C:/.../ld.exe: build\temp.win32-cpython-311\Release\jq.o:jq.c:(.text+0x1fa3): undefined reference to `jv_parser_free'
      C:/.../ld.exe: build\temp.win32-cpython-311\Release\jq.o:jq.c:(.text+0x2096): undefined reference to `jq_teardown'
      C:/.../ld.exe: build\temp.win32-cpython-311\Release\jq.o:jq.c:(.text+0x34f6): undefined reference to `jv_copy'
      ...

排查:当目标是32位wheel,而链接的预编译的库是在64位目标构建时会出现此情况。

若不确定预编译的库的架构,使用dumpbin(需要预先安装Visual Studio 或者Visual Studio 2022 生成工具)查看libjq.dll的目标架构。打开Tools Command Prompt for VS 2022:

dumpbin /headers \path\to\xxx.dll

输出中会有x86或是x64的一行。如果是64位,重新用正确的32位gcc编译一次jq库。

[WinError 2] 系统找不到指定的文件/The system cannot find the file specified

1
2
3
4
5
6
[6 lines of output]
running bdist_wheel
running build
running build_ext
error: [WinError 2] The system cannot find the file specified
[end of output]

解决:主要的可能是找不到CC指定的编译器。例如CC环境变量中只使用了单反斜杠在某些构建工具中造成错误的反斜杠转义,修改为\\或者/【推荐】。

gcc.exe failed with exit code 1

1
2
3
4
5
 building 'jq' extension
      creating build\temp.win32-cpython-39
      creating build\temp.win32-cpython-39\Release
      C:/msys64/mingw32/bin/i686-w64-mingw32-gcc.exe -mdll -O -Wall ... -c jq.c -o build\temp.win32-cpython-39\Release\jq.o
      error: command 'C:/msys64/mingw32/bin/i686-w64-mingw32-gcc.exe' failed with exit code 1

和64位的情况报错不同,gcc没有任何报错信息直接退出,也无法通过-DMS_WIN64解决。

解决:此前忘记了将使用的编译器所在的目录也加入PATH,本例中是C:/msys64/mingw32/bin;

PyPy

ld.exe: cannot find -lvcruntime140

cibuildwheel为PyPy构建时报错(凭记忆还原,可能有出入):

1
ld.exe: cannot find -lvcruntime140: No such file or directory

普通的CPython运行时安装根目录都会有vcruntime140.dll,检查cibuildwheel安装的PyPy运行时,确实没有。

解决(暂时):手动复制vcruntime140.dll到PyPy运行时安装根目录。但仍有问题未解决。

undefined reference to `__imp___p__environ’【未解决】

不确定是预编译库本身不支持pypy的问题还是编译流程的问题。

1
2
3
4
5
6
 C:/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe -shared -s build\temp.win-amd64-pypy310\Release\jq.o ... -lpython310 -lvcruntime140 -o build\lib.win-amd64-pypy310\jq.pypy310-pp73-win_amd64.pyd -lm -Wl,-Bstatic -lpthread -lshlwapi -static-libgcc
      C:/.../ld.exe: C:\Users\maxsun\Desktop\notimon-all\jq.py\_deps\build\jq-1.7.1\.libs/libjq.a(builtin.o):builtin.c:(.text+0xc767): undefined reference to `__imp___p__environ'
      ...
      C:/.../ld.exe: C:\Users\maxsun\Desktop\notimon-all\jq.py\_deps\build\jq-1.7.1\.libs/libjq.a(compile.o):compile.c:(.text+0x6d2c): more undefined references to `__imp___p__environ' follow
      collect2.exe: error: ld returned 1 exit status
      error: command 'C:/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe' failed with exit code 1
0%