在 Windows 系统 AMD 架构上编译静态的 Python 并使用 Nuitka 生成可执行文件
提示

本文编译的 Python 版本是  3.13.9+  ,在此版本上成功编译了一个在 AMD 架构上不需要任何额外拓展链接库的 Python,对于其他版本的 Python 略有不同,可作为参考。

0x00 参考文章

在 Windows 上编译静态的 Python 是一件不容易的事情,一定程度上,研究如何在 Windows 编译一个静态的 Python 是没有必要和无用功的,所以对其研究的人就甚少,但在某些特定场景下,需要一个便于迁移的程序而又不想拷贝大量的小文件的时候,静态的 Python 可能有那么一点点用途。

0x01 准备工作

编译静态的 Python 大致的走向是将其所有的链接库都编译为静态连接库,体现在最终生成的文件就是所有的 dll 消失,只留下了 exe ,另外,顺带一提的是,在 Windows 上并不是所有 lib 结尾的文件都属于静态链接库,实际上 lib 结尾链接库文件大多数属于动态链接,只是提供了基本的导出表,真正的实现还是交给了 dll ,区分静态和动态的 lib 就是看它们 的文件大小,如果不足 100KB 那大概率可能是动态的链接库。

Python 的源码编译时默认会采用动态的 lib 进行链接,并没有提供下载编译静态的 lib 的脚本,所以我们需要自己去找和去编译这些 lib ,为了方便产生静态链接库,推荐去安装 vcpkg,在编译静态链接库时,帮我省去了很多工作。此外,我使用的环境为Visual Studio 2022,这属于基本的环境配置了,确保拥有 C/C++ 的编译环境。

对于使用最终的 Python 打包 Python 程序使用的nuitka,我们需要对其的代码进行额外的修改,因为它在 Windows 平台上默认是不提供--static-libpython选项的,不过不用改太多,因为nuitka有个项目叫做Nuitka-Python(现在叫做 MonolithPy,项目地址是:https://github.com/Nuitka/MonolithPy),主要的就是在 Windows 上编译静态 Python 的,与本文的目标是一致的,不过由于缺乏社区支持,貌似开发者貌似也没有太多精力维护,现在仍然停留在 Python3.11 版本,此外,项目在一次改了名字之后,我测试下来不能顺利的一键构造静态的 Python 了,在项目没改名之前还是可以一键构建的,可能是部分代码没有更新导致的问题,这里就不过多赘述了,由于Nuitka-Python的存在, nuitka预留了一个接口,如果是Nuitka-Python就默认采用静态链接的逻辑打包程序,这点在后文会介绍。

接下来,正式开始编译静态 Python 了。

0x02 拉取完整代码与设置静态编译

前往 https://github.com/python/cpython 拉取需要编译的 Python 的源码,在源代码文件夹PCbuild下执行get_externals.bat脚本,该批处理会把 Python 编译时候需要的额外依赖下载到根目录下的externals文件夹下,可以说明的是,这个文件夹下的所有依赖几乎都是动态链接的,不过我们先暂时不处理这些依赖,我们先让 Python 自身先编译出来。

打开文件夹PCbuild下的pcbuild.sln,在选择上方的解决方案配置为 Release x64 ,在右侧解决方案资源管理器中选择所有项目,右键-属性,在弹出的窗口中,选择 配置属性- C/C++ -代码生成-运行库 ,选择“多线程 /MT”。

另外有一个可选操作,选择所有项目,配置属性- C/C++ -优化-全程序优化 选择 “否”,这样在 nuitka 编译的时候可以选择使用 clang 编译器编译。

现在需要把所有的额外拓展模块从 .pyd 修改为 .lib 导出,对于如下的项目,不要进行修改,不要进行修改,不要进行修改:

_freeze_module
_testembed
pylauncher
python
pythonw
python_uwp
pythonw_uwp
pywlauncher
sqlite3
venvlauncher
venvwlauncher

上述的项目是生成 exe 文件的,一个用于将 .py 转化为 .pyc (Python代码执行是提供缓存转化),一个用于测试,部分是 Python 本体,几个是 Python 虚拟环境要用的,这些不要执行下面的操作修改!

对于剩下的项目,全部选中,右键-属性-配置属性-常规-配置类型,选择静态库(.lib);再点击 配置属性-高级-目标文件拓展名 改为 .lib 。

接下来,需要修改 Python 的宏定义,来设置 Python 进行静态连接编译,具体有三个项目需要选择。

对于pythoncore,右键-属性-配置属性- C/C++ -预处理器-预处理器定义,添加:

Py_BUILD_CORE
Py_ENABLE_SHARED
_Py_HAVE_ZLIB
STATIC_BUILD

对于 python_testembed,右键-属性-配置属性- C/C++ -预处理器-预处理器定义,添加:

Py_BUILD_CORE
Py_NO_ENABLE_SHARED
_Py_HAVE_ZLIB
STATIC_BUILD

接着现在初步编译 pythoncore再编译 python,在 PCbuild/amd64 文件夹下将会产生若干个编译结果文件,其中 Python 已经是一个具有基本语法的静态编译的解释器了。

当然,这样是远远不够的,现在我们需要链接一些拓展的模块,比如 _ctypes_ssl,不然 python 是完全不能用的。

0x03 链接基本模块

回到 VS ,现在打开 pythoncore项目的 PC/config.c文件,这个文件设置了 python 的自带模块,我们现在需要将其他拓展模块一同编译到 Python 中去,这一步可以自助选择将部分模块导入到 python 中去,这里我默认全部导入,在这个文件的 #include "Python.h" 下方,加上:

extern PyObject* PyInit_unicodedata(void);
extern PyObject* PyInit__ctypes(void);
extern PyObject* PyInit_select(void);
extern PyObject* PyInit__socket(void);
extern PyObject* PyInit__ssl(void);
extern PyObject* PyInit__wmi(void);
extern PyObject* PyInit_winsound(void);
extern PyObject* PyInit__sqlite3(void);
extern PyObject* PyInit__zoneinfo(void);
extern PyObject* PyInit__uuid(void);
extern PyObject* PyInit__tkinter(void);
extern PyObject* PyInit__testsinglephase(void);
extern PyObject* PyInit__testmultiphase(void);
extern PyObject* PyInit__testlimitedcapi(void);
extern PyObject* PyInit__testinternalcapi(void);
extern PyObject* PyInit__testimportmultiple(void);
extern PyObject* PyInit__asyncio(void);
extern PyObject* PyInit__bz2(void);
extern PyObject* PyInit__decimal(void);
extern PyObject* PyInit__elementtree(void);
extern PyObject* PyInit__hashlib(void);
extern PyObject* PyInit__lzma(void);
extern PyObject* PyInit__multiprocessing(void);
extern PyObject* PyInit__overlapped(void);
extern PyObject* PyInit__queue(void);
extern PyObject* PyInit__testbuffer(void);
extern PyObject* PyInit__testclinic(void);
extern PyObject* PyInit__testclinic_limited(void);
extern PyObject* PyInit__testconsole(void);
extern PyObject* PyInit_pyexpat(void);

在结构体 _PyImport_Inittab 中加上:

{"unicodedata", PyInit_unicodedata},
{"_ctypes", PyInit__ctypes},
{"select", PyInit_select},
{"_socket", PyInit__socket},
{"_ssl", PyInit__ssl},
{"_wmi", PyInit__wmi},
{"winsound", PyInit_winsound},
{"_sqlite3", PyInit__sqlite3},
{"_zoneinfo", PyInit__zoneinfo},
{"_uuid", PyInit__uuid},
{"_tkinter", PyInit__tkinter},
{"_testsinglephase", PyInit__testsinglephase},
{"_testmultiphase", PyInit__testmultiphase},
{"_testlimitedcapi", PyInit__testlimitedcapi},
{"_testinternalcapi", PyInit__testinternalcapi},
{"_testimportmultiple", PyInit__testimportmultiple},
{"_asyncio", PyInit__asyncio},
{"_bz2", PyInit__bz2},
{"_decimal", PyInit__decimal},
{"_elementtree", PyInit__elementtree},
{"_hashlib", PyInit__hashlib},
{"_lzma", PyInit__lzma},
{"_multiprocessing", PyInit__multiprocessing},
{"_overlapped", PyInit__overlapped},
{"_queue", PyInit__queue},
{"_testbuffer", PyInit__testbuffer},
{"_testclinic", PyInit__testclinic},
{"_testclinic_limited", PyInit__testclinic_limited},
{"_testconsole", PyInit__testconsole},
{"pyexpat", PyInit_pyexpat},

此时,先编译一下除了 python 和 pythoncore 的所有项目,产生所有的 .lib 文件,然后修改 python_testembed 的链接,右键这个项目-配置属性-链接器-输入-附加依赖项,填上:

user32.lib
netapi32.lib
gdi32.lib
comctl32.lib
kernel32.lib
advapi32.lib
version.lib
MSVCRT.lib
python3.lib
python313.lib
bcrypt.lib
ws2_32.lib
pathcch.lib
unicodedata.lib
_ctypes.lib
select.lib
_socket.lib
iphlpapi.lib
rpcrt4.lib
_ssl.lib
crypt32.lib
_wmi.lib
comsuppw.lib
oleaut32.lib
wbemuuid.lib
ole32.lib
propsys.lib
winsound.lib
winmm.lib
_sqlite3.lib
_zoneinfo.lib
_uuid.lib
_tkinter.lib
_testsinglephase.lib
_testmultiphase.lib
_testlimitedcapi.lib
_testinternalcapi.lib
_testimportmultiple.lib
_asyncio.lib
_bz2.lib
_decimal.lib
_elementtree.lib
_hashlib.lib
_lzma.lib
_multiprocessing.lib
_overlapped.lib
_queue.lib
_testbuffer.lib
_testclinic.lib
_testclinic_limited.lib
_testconsole.lib
lzma.lib
pyexpat.lib

再编译这两个项目,可能会遇到:

_DllMain@12 already defined

的问题,在 https://www.blackh4t.org/post/how-to-compile-static-link-python/ 有提到,全解决方法搜索这个 DLLMain ,找到 dl_nt.c 这个文件,直接修改名称为 PythonDllMain 即可,其他编译的问题也可以去参考这个文章,上述提供的 lib 部分可能没有,如果属于是项目中的,需要先去生成对应的项目才能进行下一步编译,部分 DLL 可能是归属于系统的,需要自行手动补齐即可。

0x04 编译静态依赖

libffi-8

LibFFI(Foreign Function Interface)是一个便携的C库,允许程序在运行时调用具有已知参数和返回类型的函数。Pythonextern 文件夹中提供的默认是一个动态的 lib ,编译静态的方式也很简单,在命令行中输入:

vcpkg install libffi:x64-windows-static

等待编译结束之后,在 vcpkg 的工作目录中找到 packages\libffi_x64-windows-static 目录,其中的 lib 文件夹就是静态编译的 libffi-8.lib ,文件名为 ffi.lib ,虽然名称不一样,但是导出表是一样的,将这个文件复制到 PCbuild/amd64 并在 python_testembed项目的最开头链接这个文件,同时,可以将 packages\libffi_x64-windows-static\include 复制到 Cpython 的 externals\libffi-3.4.4\amd64\include 目录中,这对于 libffi 不是必须的,但是对于其他依赖可能需要进行这样的操作,尤其是使用与 Python 下载依赖不同的版本的依赖时。

SQLCipher

SQLCipher 是一个基于 SQLite 的开源扩展工具,获得其静态的 lib ,同样可以使用 vcpkg 编译安装:

vcpkg install sqlcipher:x64-windows-static

在最新版本的 vcpkg 中应该可以正常安装,比较老的 vcpkg 版本中存在一个 BUG 导致不能安装,在最新版本中已经修复。编译完成之后,再把 vcpkg 工作目录下的 packages\sqlcipher_x64-windows-static\lib\sqlcipher.lib 复制到 CPythonPCbuild/amd64 下,并在 python_testembed 项目的最开头链接这个文件,不需要拷贝头文件。

openssl

OpenSSL 是一个开源的安全套接字层密码库,也使用 vcpkg 进行安装:

vcpkg install openssl:x64-windows-static

再把 vcpkg 工作目录下的 packages\openssl_x64-windows-static\lib所有 lib (libcrypto.lib 和 libssl.lib)复制到 CPythonPCbuild/amd64 下和externals\openssl-bin-3.0.18\amd64下,并在 python_testembed 项目的最开头链接这两个文件,同时可能由于 openssl 的静态版本和 python 使用静态版本不同,需要把 packages\openssl_x64-windows-static\include\openssl拷到 Cpython 的 externals\openssl-bin-3.0.18\amd64\include\openssl并替换。

tk/tcl

主要是 Python 的 tkinter 模块需要,这个依赖是最麻烦的,不能用 vcpkg 安装,如果你的最终程序不会用到这个界面库可以直接跳过,并在链接基本模块的时候直接删掉 tkinter 的模块链接,lib的链接顺序查看这个章节的最后!

首先去 https://www.tcl-lang.org/software/tcltk/download.html 下载 8.6.17 版本的 tk 和 tcl 源代码,并解压到一个文件夹中。

先编译 TCL ,打开 Visual Studio 的开发者提示命令符,切换到 tcl8.6.17\win 文件夹,输入:

nmake -f makefile.vc OPTS=static,release,threads MSVCFLAGS="/DSTATIC_BUILD"
nmake -f makefile.vc OPTS=static,release,threads MSVCFLAGS="/DSTATIC_BUILD" INSTALLDIR=E:\tcl-install install

INSTALLDIR 替换为你实际需要安装的地址,

接下来编译 TK ,切换到 tk8.6.17\win 文件夹,输入:

nmake -f makefile.vc OPTS=static,release,threads MSVCFLAGS="/DSTATIC_BUILD"
nmake -f makefile.vc OPTS=static,release,threads MSVCFLAGS="/DSTATIC_BUILD" INSTALLDIR=E:\tk-install install

INSTALLDIR 替换为你实际需要安装的地址。

现在我们在 E:\tcl-install\libE:\tk-install\lib 可以获得需要的静态编译的 lib ,拷贝如下文件到 CPythonPCbuild/amd64 下:

tclstub86.lib
tkstub86.lib
tcl86ts.lib
tk86ts.lib

这个同样需要链接到 python_testembed 项目中,然后我们需要链接 res 资源,不然在使用 tkinter 的时候会提示:

FindResourceW() failed for buttons bitmap resource, resources in tk_base.rc must be linked into Tk dll or static executable

提示命令符切换到 tk8.6.17\win\rc 下,运行:

rc.exe /fo tk_base.res tk_base.rc

复制新产生的 tk_base.res 到 CPythonPCbuild/amd64 下,同样链接python_testembed 项目这个文件,这两个项目最后的链接表应该像这个样子的:

tclstub86.lib
tkstub86.lib
tcl86ts.lib
tk86ts.lib
tk_base.res
user32.lib
netapi32.lib
gdi32.lib
comctl32.lib
kernel32.lib
advapi32.lib
version.lib
MSVCRT.lib
python3.lib
python313.lib
bcrypt.lib
ws2_32.lib
pathcch.lib
unicodedata.lib
_ctypes.lib
select.lib
ffi.lib
_socket.lib
iphlpapi.lib
rpcrt4.lib
_ssl.lib
crypt32.lib
_wmi.lib
comsuppw.lib
oleaut32.lib
wbemuuid.lib
ole32.lib
propsys.lib
winsound.lib
winmm.lib
_sqlite3.lib
_zoneinfo.lib
_uuid.lib
_tkinter.lib
_testsinglephase.lib
_testmultiphase.lib
_testlimitedcapi.lib
_testinternalcapi.lib
_testimportmultiple.lib
_asyncio.lib
_bz2.lib
_decimal.lib
_elementtree.lib
_hashlib.lib
_lzma.lib
_multiprocessing.lib
_overlapped.lib
_queue.lib
_testbuffer.lib
_testclinic.lib
_testclinic_limited.lib
_testconsole.lib
sqlcipher.lib
libcrypto_static.lib
libssl.lib
lzma.lib
pyexpat.lib

注意!tk和tcl的stub要放在最前面,tk和tcl的其他链接内容要放在 _tkinter.lib 前面(前面四个lib顺序不要动),其他的lib可以自由调整,比如之前的sqlcipher.lib等。

接着,我们需要重新编译一下 _tkinter 项目,打开这个项目的 属性- C/C++ -预处理器 ,预处理器定义添加:

STATIC_BUILD

重新编译这个项目,最后重新编译 Python 即可,这样所有的链接工作就完成了。

上述所有的静态 lib 均会提供下载,在最后的提供的 Python 中有所有需要的内容。

0x04 组成基本的 Python

虽然现在 Python 已经可以独立运行,但是还不能安装模块,现在新建一个文件夹,比如 Python313 ,将 python.exe 拷进去。

将 Cpython 根目录下的 Lib 文件夹拷到 Python313\Lib 中(拓展额外库,不要拷已经 freeze 的 Lib,如果 Lib 文件夹中全是 pyd 文件不要拷,不然会影响 nuitka 打包),

将 PCbuild/amd64 下的所有 lib 文件全部复制到Python313\Libs中,tk 的一个 res 也要拷(用于 nuitka 和其他模块静态链接):

PCbuild/amd64> copy *.lib E:\Python313\libs
PCbuild/amd64> copy *.res E:\Python313\libs

将 Cpython 根目录下的 Include全部复制到 Python313\Include 中(用于编译),

PCbuild/amd64 下的 pyconfig.h 复制到Python313\Include中,

E:\tcl-install\libE:\tk-install\lib 一起拷到 Python313\tcl中(用于 tkinter 运行),

下面拷贝 nuitka 编译需要的头文件和依赖:

externals\openssl-bin-3.0.18\amd64\include 拷到 dependency_libs\openssl\include

externals\openssl-bin-3.0.18\amd64 下的 lib 文件拷到 dependency_libs\openssl\include\lib

externals\libffi-3.4.4\amd64\include 拷到 dependency_libs\libffi\include

现在开始安装 pip 和 nuitka ,在Python313文件夹中,启动 cmd ,输入:

python -m ensurepip
python -m pip install nuitka

0x05 修改 nuitka 并编译程序

打开 Python313目录下的 Lib\site-packages\nuitka\PythonFlavors.py,找到 isNuitkaPython() 的定义,强制 return True:

def isNuitkaPython():
    """Is this our own fork of CPython named Nuitka-Python."""

    return True

    # spell-checker: ignore nuitkapython

    if python_version >= 0x300:
        return sys.implementation.name == "nuitkapython"
    else:
        return sys.subversion[0] == "nuitkapython"

Python313根目录下新建一个文件叫做 link.json ,内容如下(没有 res 文件):

{
  "include_dirs": [
    "C:\\Users\\lovep\\Desktop\\python313\\Include"
  ],
  "macros": [
    [
      "Py_BUILD_CORE",
      null
    ]
  ],
    "libraries": [
        "tclstub86.lib",
        "tkstub86.lib",
        "tcl86ts.lib",
        "tk86ts.lib",
        "user32.lib",
        "netapi32.lib",
        "gdi32.lib",
        "comctl32.lib",
        "kernel32.lib",
        "advapi32.lib",
        "version.lib",
        "MSVCRT.lib",
        "python3.lib",
        "python313.lib",
        "bcrypt.lib",
        "ws2_32.lib",
        "pathcch.lib",
        "unicodedata.lib",
        "_ctypes.lib",
        "select.lib",
        "ffi.lib",
        "_socket.lib",
        "iphlpapi.lib",
        "rpcrt4.lib",
        "_ssl.lib",
        "crypt32.lib",
        "_wmi.lib",
        "comsuppw.lib",
        "oleaut32.lib",
        "wbemuuid.lib",
        "ole32.lib",
        "propsys.lib",
        "winsound.lib",
        "winmm.lib",
        "_sqlite3.lib",
        "_zoneinfo.lib",
        "_uuid.lib",
        "_tkinter.lib",
        "_testsinglephase.lib",
        "_testmultiphase.lib",
        "_testlimitedcapi.lib",
        "_testinternalcapi.lib",
        "_testimportmultiple.lib",
        "_asyncio.lib",
        "_bz2.lib",
        "_decimal.lib",
        "_elementtree.lib",
        "_hashlib.lib",
        "_lzma.lib",
        "_multiprocessing.lib",
        "_overlapped.lib",
        "_queue.lib",
        "_testbuffer.lib",
        "_testclinic.lib",
        "_testclinic_limited.lib",
        "_testconsole.lib",
        "sqlcipher.lib",
        "libcrypto_static.lib",
        "libssl.lib",
        "lzma.lib",
        "pyexpat.lib"
    ],
    "library_dirs": [
    "C:\\Users\\lovep\\Desktop\\python313",
    "C:\\Users\\lovep\\Desktop\\python313\\libs"
  ],
  "link_flags": [
    "/LTCG",
    "/NODEFAULTLIB:python3.lib",
    "/FORCE"
  ],
  "compile_flags": [
    
  ]
}

注意把 C:\\Users\\lovep\\Desktop\\python313 改为你实际创建 Python313 文件夹的路径!

现在,我们可以尝试编译自己的 Python 脚本了,对于平台依赖性不强的库如 requests 库实测可以正确被静态链接到最终的程序中,这里随便写一个脚本测试:

import requests
url = 'https://www.baidu.com/'
header = {
    'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
response = requests.get(url=url, headers=header)
response.raise_for_status()
response.encoding = response.apparent_encoding
concent = response.text
print(concent)

安装库:

python -m pip install requests
python -m pip install chardet

编译:

python313\python.exe -m nuitka test.py --onefile --standalone --static-libpython=yes

可以正常运行。

最后,提供一个可以编译的静态 Python

下载之后,请自行执行 0x04-0x05 部分的内容,安装 pip 和 修改nuitka 与 link.json ,所有的文件拷贝工作已经全部完成。

链接:https://www.123865.com/s/zbTrVv-fR6CA?pwd=rpwG# 提取码:rpwG

上一篇
下一篇