虽然在 Linux 上创建静态链接的嵌入式 Python 可执行文件相对容易(例如,参见 SO-post),但在 Windows 上要复杂得多。而你可能不想这样做。
结果可能不是人们所期望的:由于 dll 与 Linux 的共享对象相比的限制,生成的静态 python 版本将无法使用/加载任何其他 c 扩展,因为支持的-in 在编译/链接期间。
我也不建议从 vcruntime-dll 切换到其静态版本 - 只有当 所有内容(exe、c-extensions、其他依赖于 vcruntime 的 dll)都是静态的时才有意义链接到一个巨大的可执行文件中。
第一个绊脚石:虽然在 Linux 上的 Python 发行版通常已经发布了静态 Python 库,但 Windows 发行版只有 dll,无法静态链接。
因此需要在 Windows 上构建一个静态库。一个很好的起点是link。
下载正确 Python 版本 (git clone --depth=1 --branch v3.8.0 https://github.com/python/cpython.git) 的源代码后,您可以转到 cpython\PCBuild 并按照文档中的说明构建 cpython(可能因版本而异)。
在我的情况下是
cd cpython/PCbuild
.\build.bat -e -p x64
不,我们有一个正常运行的 Python3.8 安装,可以在 cpython/PCbuild/amd64 中找到。创建文件夹cpython/PCbuild/static_amd64并添加以下pyx文件:
#hello.pyx
print("I'm standalone")
暂时将python38.dll复制到static_amd64。
现在让我们用嵌入式 python 解释器构建我们的程序:
cython --embed -3 hello.pyx
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64
cl /c hello.c /Fohello.obj /nologo /Ox /W3 /GL /DNDEBUG /MD -I<path_to_code>\cpython\include -I<path_to_code>\cpython\PC
link hello.obj python38.lib /OUT:hello_prog.exe /nologo "/LIBPATH:<path_to_code>\cpython\PCbuild\amd64"
在开始之后,hello_prog.exe 对我们说谎,因为它并不是真正独立的。好消息是:它找到了所需的 Python 安装,例如 here。
现在让我们创建一个静态 python38 库。为此,我们在 cpython/PCbuild-folder 中打开 pcbuild.sln 并更改 pythoncore-project 的设置以在 PCbuild\amd64_static-folder 中生成静态库。重建它。
现在我们可以构建 Embedded-python-exe:
cl /c hello.c /Fohello.obj /D "Py_NO_ENABLE_SHARED" /nologo /Ox /W3 /GL /DNDEBUG /MD -I<path_to_code>\cpython\include -I<path_to_code>\cpython\PC
link hello.obj python38.lib "version.lib" "shlwapi.lib" "ws2_32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" /OUT:hello_prog.exe /nologo "/LIBPATH:<path_to_code>\cpython\PCbuild\static_amd64"
与针对 dll 的构建相比,我们必须更改以下内容:
-
Py_NO_ENABLE_SHARED(即/D "Py_NO_ENABLE_SHARED")被添加到预处理器定义中,否则链接器会查找错误的符号。
- 由python-dll带来的Windows依赖项(即
version.lib等)现在需要显式传递给链接器(这可以在pythoncore-project的链接器命令行中查找)。
- lib 路径显示到静态文件夹,即
"/LIBPATH:<path_to_code>\cpython\PCbuild\static_amd64" now。
- 可能还有其他较小的问题(不同的优化级别、链接时间代码生成、禁用整个程序优化等),具体取决于您的确切工具链。
我们现在可以从static_amd64 中删除python38.dll,hello_prog.exe 仍然有效。
在 Linux 上,这将是“任务完成”,在 Windows 上,我们才刚刚开始......
确保cpython-folder 有一个DLLs-folder,其中包含所有正确的pyd-files,否则从PCbuild/amd64-folder 创建并复制所有pyd-files。
让我们的 pyx 文件稍微复杂一点:
import _decimal
print("I'm standalone")
_decimal 是 decimal 模块的快速实现,它是一个 C 扩展,可以在 DLL 文件夹中找到。
在 cythonizing 和构建之后,运行 hello_prog.exe 会导致以下错误消息:
import _decimal
ImportError: DLL load failed while importing _decimal: The specified module could not be found.
问题很容易找到:
dumpbin /DEPENDENTS ../amd64/_decimal.pyd
...
python38.dll
...
我们安装的扩展仍然依赖于 python-dll。让我们针对静态库重新构建它们——我们需要将库路径从amd64 更改为static_amd64,添加预处理器定义Py_NO_ENABLE_SHARED 和所有缺少的windows-libraries(即“version.lib”& Co.)和将/EXPORT:PyInit__decimal添加到链接选项,否则,由于Py_NO_ENABLE_SHARED它becomes invisible。结果不依赖于python-dll!我们将它复制到DLLs文件夹并...
hello_prog.exe
# crash/stopped worked
发生了什么?我们违反了一个定义规则 (ODR),最终得到了两个 Python 解释器:一个来自 hello_prog.exe,它已初始化,另一个来自 _decimal.pyd,它未初始化。 _decimal.pyd 对其未初始化的解释器“说话”,并且发生了不好的事情。
与 Linux 的区别在于共享对象和 dll 之间的区别:虽然共享对象可以使用 exe 中的符号(如果 exe 是使用正确的选项构建的),但 dll 不能因此必须依赖于 dll(我们不想要)或需要有自己的版本。
为了避免违反 ODR,我们只有一个出路:它必须直接链接到我们的 hello_word-executable。因此,让我们将_decimal 的项目更改为静态库并在static_amd64-文件夹中重建它。从“DLLs”文件夹中删除 pyd 并将/WHOLEARCHIVE:_decimal.lib 添加到链接器命令行(整个存档,否则链接器将丢弃_decimal.lib,因为它的符号在某处都没有被引用),导致一个可执行文件,它出现以下错误:
ModuleNotFoundError: No module named '_decimal'
这是意料之中的 - 我们需要告诉解释器,模块 _decimal 已被备份,不应在 python 路径上搜索。
这个问题通常的解决方案是在Py_Initialize之前使用PyImport_AppendInittab,这意味着我们需要更改cython生成的c文件(可能有workarounds,但由于multi-phase initialization它是没那么容易。因此,嵌入 Python 的一种更明智的方法可能是提供的 here 或 here 是 main 不是由 Cython 编写的)。 c 文件应如下所示:
//decalare init-functions
extern PyObject* PyInit__decimal();
...
int main(int argc, char** argv) {
...
if (argc && argv)
Py_SetProgramName(argv[0]);
PyImport_AppendInittab("_decimal", PyInit__decimal); // HERE WE GO
// BEFORE Py_Initialize
Py_Initialize();
现在构建所有内容会生成一个打印的 exe
I'm standalone
这一次不是在骗我们!
现在我们必须为我们需要的所有其他内置扩展重复最后的步骤。
以上意味着静态构建的 python-interpreter 有一些限制:所有内置模块都需要备份到可执行文件中,我们不能使用 numpy/scipy 之类的库来扩展解释器(但可以直接这样做在编译/链接时)。
摆脱 vcruntime-dll 更容易:以上所有步骤都必须完成with /MT option instead of MD-option。但是,由于使用其他 dll(例如 _ctypes 需要 ffi-dll)可能会出现一些问题,这些 dll 是使用 dll 版本构建的(因此我们再次违反了 ODR) - 所以我不推荐它。