在 Linux 上为 Windows 进行交叉编译扩展

6

我成功在Linux上使用MinGW构建了一些DLL,这些DLL对于我的Python扩展非常必要。大致如下:

from setuptools.command.build_py import build_py

class BuildGo(build_py):

    def run(self):
        if # need to build windows binaries
            self.build_win()
        build_py.run(self)

    def build_win(self):
        if # compilers and toolchain available
            try:
                # builds extra libraries necessary for this extension
            except subprocess.CalledProcessError as e:
                print(e.stderr)
                raise
            try:
                result = subprocess.check_output([
                    'x86_64-w64-mingw32-gcc-win32',
                    '-shared',
                    '-pthread',
                    '-o',
                    EXTRA_DLL,
                    FAKE_WIN_BINDINGS,
                    ARCHIVE_GENERATED_IN_PREVIOUS_STEP,
                    '-lwinmm',
                    '-lntdll',
                    '-lws2_32',
                ])
                print(result)
            except subprocess.CalledProcessError as e:
                print(e.stderr)
                raise

我现在希望能够避免像之前那样痛苦地扩展`build_ext`,以便在Windows上交叉编译Cython代码... 我研究了“setuptools、distutils和cython的优雅相互作用”的深渊,但在深渊有机会回头看我的时候...是否有一种方式只需指定一些标志,比如所需平台的编译器名称和Python二进制文件名,然后它就可以自动完成呢?
我读了这篇文章:http://whatschrisdoing.com/blog/2009/10/16/cross-compiling-python-extensions/ - 它已经快10年了。它让我想哭...自那以后有什么变化吗?或者这些步骤更多或更少是我必须为除了我正在运行的平台之外的平台进行编译的步骤吗?
还是说,有一个在网上的示例项目可以做到这一点吗?
目标
我的终极目标是生成一个egg包,其中包含PE和ELF二进制文件,并在安装时通过pip或pipenv将它们正确地安装在任一平台上的正确位置。它应该在Linux上编译(在MS Windows上编译它并不必要)。

2
一个问题是,Windows 上的 Python 与特定版本的 MSVC 和使用 mingw 编译的模块通常不兼容。(最近有一些努力,试图制作一个能够创建兼容 Python 模块的 mingw 版本,但它并不完美。例如,在 Windows 上尝试使用 gfortran 和 Python 就会带来一系列有趣的问题) - DavidW
这是不可能被可靠地完成的,否则会以可怕、难以排除故障的方式破裂。在Windows上使用与该Python版本相同的MSVC编译扩展。混合使用不同编译器构建的共享对象/模块通常是不可行的。即使只有GCC也需要使用相同的版本,更不用说MingW和MSVC之间了。 - danny
我对Ruby一无所知,但是这篇文章表明Ruby也适用于这个问题。我认为不同之处可能在于他们选择了Mingw作为默认选项,这使得在Linux上进行交叉编译变得非常简单。Python开发人员可能会认为MSVC是“本地”编译器,并且可以免费获得,因此是一个明智的选择。 - DavidW
好的,Ruby有DevKit,它会为您提供正确的编译器,为用户编译扩展,还可以将您的代码从Linux交叉编译到Windows和从Windows交叉编译到Linux。对于Python来说,这些都不是真的。它并不完全相同。Python声称是一种自由语言,但现在似乎如果我想编写一个适用于Windows用户的扩展,我必须使用非自由软件... - wvxvw
是的,Python在Windows上选择使用本地编译器MSVC。这使得在该平台上使用其他编译器进行交叉编译变得不可能。这个说法适用于任何平台上由不同编译器编译的所有内容,而不仅仅是Python扩展。这就是所谓的“不特定于Python”,即链接使用不同编译器构建的模块时出现的问题。 - danny
显示剩余4条评论
4个回答

3
根据 https://docs.python.org/3/distutils/builtdist.html ,截至目前(3.7),distutils 仅支持在 win32win_amd64 之间进行交叉编译。

此外,使用 Python 构建时的其他编译器构建扩展不受官方支持

从理论上讲,可以通过获取适用于 win32/64 的 Linux 工具链(包括必要的头文件和链接库)、一组必要的 Python for Windows 二进制文件来链接,然后setup.py 中编写编译器和链接器路径及/或选项,但这仍然是未受支持的设置。

因此,最好使用 Windows VM 或在线构建服务(例如 AppVeyor)。

1
我早就放弃了这个想法。我没有更新我的问题,但实际上我写信给Python邮件列表,只是发现这基本上是不可能的。目前,我已经转向Anaconda,因为它比python.org拥有更好的工具链。但是,将来我会尽可能地避免使用Python。 - wvxvw

3

我将这个帖子发布为社区Wiki,因为它并不是一个非常令人满意的答案:它只告诉你为什么很难,而不是提供真正的解决方案。

Windows上的官方Python发行版使用Microsoft Visual C (MSVC)编译,当编译Python扩展时,通常需要使用与Python编译版本相同的版本。这说明准确的编译器匹配非常重要。

可以获得使用Mingw编译的Python版本,这些版本将与使用Mingw编译的模块兼容。这可能可以在Linux上作为交叉编译器工作,但是这些模块只对拥有此自定义Python版本的非常小的一部分人有用(因此不能创建一个有用的可分发的.egg文件)。

也已经做出了一些努力,制作了一个可以在Windows上构建兼容Python扩展的Mingw版本:https://mingwpy.github.io/(我认为还有https://anaconda.org/msys2/m2w64-toolchain)。主要驱动因素似乎是Windows上缺乏与MSVC兼容的自由Fortran编译器,因此构建Fortran模块的能力非常有用。在我的经验中,mingwpy工具链表现得相当不错,直到Python 3.4时切换到更高版本的MSVC带来了一整套新问题兼容性问题

我认为任何可行的解决方案可能都基于这些基本可行的Mingw编译器,适用于Windows操作系统。


只是补充一下,conda 是专门为解决这个问题而设计的。它提供了一个跨平台工具链,可以构建 Linux、OSX 和 Windows 的软件包,并极大地简化了构建和发布跨平台本地代码扩展和其他本地代码二进制文件和库的过程。它的缺点是它不是任何操作系统的“本地”功能,因此用户必须先安装它,并且它提供与 pip 二进制轮相似的功能。话虽如此,它的引导程序简单而无痛苦。 - danny
如果您愿意,欢迎将其编辑到此答案中(或单独发布)。虽然它并没有完全解决问题,但肯定是一个相关的替代方案。 - DavidW

2
这里是一个概念证明,用于在Linux上交叉编译(Cython-)扩展到Windows,大致遵循构建使用mingw-w64在Windows上的步骤。
但首先要警告一下:虽然可能,但这个工作流程并没有得到真正的支持(从事实开始,唯一支持的Windows编译器是MSVC),因此随着未来版本的更改,它可能会被破坏。我使用Python 3.7进行64位处理,其他版本可能会有(稍微)不同。
可能存在合法的跨平台编译方案,但Python世界似乎没有太多需要,因此在大多数情况下,跨平台编译可能不是正确的方向。 前提条件: 编译器:截至撰写本文时,唯一的真正替代品是MinGW-w64(例如sudo apt-get install mingw-w64)- 64位编译器是x86_64-w64-mingw32-gcc
头文件:Linux和Windows上的Python头文件不同(例如pyconfig.h),这意味着交叉编译需要从Windows版本复制Python头文件。最简单的方法是从应该构建扩展名的Windows版本中复制它们。
动态链接库:Windows和Linux上的动态库处理方式不同。在Linux上,不需要共享的Python库(甚至使用它都是错误的,因为Python符号是由使用-Xlinker -export-dynamic`构建的Python可执行文件提供的)进行链接,但在Windows可执行文件中需要它。mingw-w64的链接器与MSVC的工作方式不同:需要Python-dll而不是Python-lib。
“distutils”不支持“mingw-w64”,因此我们将手动执行所有步骤。
1. C代码生成
让我们来看一个简单的Cython扩展“foo.pyx”。
print("It is me!")


可以通过以下方式将其转换为C代码:
>>> cython -3 foo.pyx

它创建了foo.c文件。
2. 编译
编译步骤如下:
>>> x86_64-w64-mingw32-gcc -c foo.c -o foo.o -I <path_to_windows_includes> -DMS_WIN64  -O2 <other compile flags>

我猜在大多数情况下,人们可以采用极简主义并只使用-O2编译标志。然而,重要的是要定义MS_WIN64宏(例如通过-DMS_WIN64)。为了在Windows上构建x64,必须设置它,但它只适用于MSVC(定义_WIN64可能会有稍微不同的结果)。
#ifdef _WIN64
#define MS_WIN64
#endif

3. 链接
链接命令为:
>>> x86_64-w64-mingw32-gcc -shared foo.o -o foo.pyd -L <path_to_windows_dll> -lpython37

重要的是,Python库(`python37`)应该是dll本身,而不是`lib`(请参见这个SO-post)。
可能需要为生成的pyd文件添加适当的后缀,我在这里为了简单起见使用旧的约定。
4. 运行:
将pyd文件复制到Windows中,现在:
import foo
# prints "It is me!"

完成!
嵌入式Python:
如果需要嵌入Python,即通过cython -3 --embed foo.pyx生成C代码,则编译步骤与上述相同。
链接步骤如下:
>>> x86_64-w64-mingw32-gcc foo.o -o foo.exe -L <path_to_windows_dll> -lpython37 -municode


有两个明显的不同点:
  • 不再使用-shared,因为结果不再是动态库(这就是 *.pyd 文件的本质),而是一个可执行文件。
  • 需要 -municode,因为对于 Windows,在 Cython 中定义了 int wmain(int argc, wchar_t **argv) 而不再是 int main(int argc, char** argv)。如果没有该选项,则会出现类似于 In function 'main': /build/mingw-w64-_1w3Xm/mingw-w64-4.0.4/mingw-w64-crt/crt/crt0_c.c:18: undefined reference to 'WinMain' collect2: error: ld returned 1 exit status 的错误消息(参见此 SO-post 获取更多信息)。

注意:为了使生成的可执行文件能够运行,需要整个 Python 发行版(而不仅仅是 DLL)(另请参见此 SO-post)。


1

我曾经遇到过同样的问题,但我只是使用虚拟机来编译我最依赖微软的程序。

https://developer.microsoft.com/en-us/windows/downloads/virtual-machines

如果您没有访问Windows计算机的权限,或者您的程序使用非常特定的机器,例如优化的Fortran编译器、某些POSIX依赖项或来自VS可再发行版本的最新功能,则最好尝试基于虚拟机的编译系统。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接