pyinstaller 可执行文件的差异更新(修改嵌入式 PYZ-00.pyz)
differential update of pyinstaller executable (modify embedded PYZ-00.pyz)
我打算创建一个巨大的可执行目录并将其安装在某些设备上。
想象一下,后来我在我的 python 模块之一中发现了一个错误。
有没有办法transfer/copy只修改字节码,用新字节码替换原来的字节码
我想这样做的原因是,在我的上下文中带宽非常昂贵,我想远程修补代码。
示例:我有一个包含两个文件的项目:
prog.py
: (以下三行)
import mod1
if __name__ == "__main__":
mod1.hello()
mod1.py
: (以下两行)
def hello():
print("hello old world")
现在我使用 PYTHONHASHSEED=2 pyinstaller prog.py
创建我的目录并将其复制到我的设备
现在我修改mod1.py
:
def hello():
print("hello new world")
然后我用 PYTHONHASHSEED=2 pyinstaller prog.py
重新编译
完整目录的大小约为 10M
文件dist/prog/prog
大小约1M
使用 pyi-archive_viewer
我可以从我的可执行文件 dist/prog/prog
中提取 PYZ-00.pyz
在 PYZ-00.pyz
中,我可以找到并提取仅使用 133 个字节的 mod1
。
现在,如果我将该文件复制到我的设备,我该如何更新旧文件
dist/prog/prog
这样,它就有了新的 PYZ-00.pyz:mod1
字节码。
替换一个特定文件(模块)后,我可以用什么代码分解,我可以用什么代码重新组装?
备选方案:将 pyc 文件移动到 zip 文件
启动性能并不是那么重要。我也可以使用替代解决方案,其中没有创建 PYZ 文件并将其添加到可执行文件中,但 dist 目录包含一个包含所有 .pyc 文件的 zip 文件
另一种选择:将 .pyc 文件复制到应用程序目录中
这将导致 __file__
具有与 PYZ 模式中完全相同的值。性能方面可能不是那么好并且会创建大量文件,但如果增量更新至关重要,也许可以选择一种方法来处理它。
这是一个相当复杂的问题,但我认为这可能至少是您正在寻找的问题的一部分。
根据您的示例,我更改了 prog.py
,因此当从源 运行ning 时它可以正常导入,但是当使用来自 pyc
文件的 pyinstaller 运行s 冻结时直接。
import sys
def import_pyc(name):
import py_compile
import types
import marshal
pyversion = f"{sys.version_info.major}{sys.version_info.minor}"
filename = f"{name}.cpython-{pyversion}.pyc"
with open(filename, "rb") as pyc_file:
# pyc files have 16 bytes reserved at the start in python 3.7+
# due to https://www.python.org/dev/peps/pep-0552/
# may change again in the future
pyc_file.seek(16)
code_obj = marshal.load(pyc_file)
module = types.ModuleType(name)
exec(code_obj, module.__dict__)
globals()[name] = module
def import_py(name):
import importlib
globals()[name] = importlib.import_module("mod1")
def import2(name):
if getattr(sys, "frozen", False):
import_pyc(name)
else:
import_py(name)
import2("mod1")
if __name__ == "__main__":
mod1.hello()
这在很大程度上基于精彩的回答 here。
这意味着 mod.py
没有被 PyInstaller 打包,您必须将 mod1.cpython-38.pyc
作为数据文件包含在内。
一个方便的方法是使用命令 PyInstaller --add-data "__pycache__/*;." prog.py
(如果您不使用 Windows,请将分号切换为冒号)。这会将 __pycache__
文件夹中的所有内容,所有导入的模块,放入结尾的 dist/prog
文件夹中。请注意,如果您 运行 这多次,PyInstaller 会在 __pycache__
中为主 python 文件夹放置一个 pyc,以便在后续的 运行 中捆绑。
根据您捆绑的方式和 运行 您的项目,您可能会 运行 遇到当前工作目录关闭的问题,这将导致 FileNotFound
当您尝试加载 pyc
s。我不能给你找到你想要的路径的灵丹妙药,因为这取决于你最终如何做事,但我通常用来找到应该是当前工作目录的绝对路径的方法是 os.path.dirname(sys.executable)
和 os.path.dirname(os.path.abspath(__file__))
.
此解决方案既不能 'patching' 一个 .PYZ
文件,也不能将所有 .pyc
文件放入一个 zip 文件。
但到目前为止,这是我找到的唯一可行的解决方案,适用于具有大量第三方依赖项的大型项目。
想法是删除所有(或 .PYZ 文件中的大部分文件)并将相应的 .pyc 文件复制到工作目录中。
随着时间的推移,我会加强和详细说明这个答案。我还在试验:
我通过修改规范文件来实现:
- 确定spec文件所在目录
MYDIR
- 创建一个目录,
MYDIR/src
将 a.pure
中的所有文件复制到
- 将所有文件从 a.pure 复制到
MYDIR/src
。 (子目录与模块名称相对应。例如,模块 mypackage.mod.common
将存储在 MYDIR/src/mypackage/mod/common.py
中)
- 遍历文件并将它们编译为
.pyc
文件,然后删除 .py
文件。
- 创建一个
PYZ
文件,其中仅包含未复制的文件。 (在我的测试用例中,PYZ
中没有 .pyc
文件)
- 用修改后的
PYZ
创建 exe
- 收集应收集的所有文件以及来自
MYDIR/src
的所有文件(例如 a.datas + Tree("src")
规范文件更改:
一开始
import os
MYDIR = os.path.realpath(SPECPATH)
sys.path.append(MYDIR)
import mypyinsthelpers # allows to reuse the code in multiple projects
然后在(未修改的)a = Analysis(...
部分之后添加。
to_rmv_from_pyc = mypyinsthelpers.mk_copy_n_compile(a.pure, MYDIR)
# modified creation of pyz`
pyz = PYZ(a.pure - to_rmv_from_pyc, a.zipped_data,
cipher=block_cipher)
我会在下面详细介绍功能mypyinsthelpers.mk_copy_n_compile
更改收集阶段:
而不是
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
...
我写:
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas + Tree("src"),
...
这里是mypyinsthelpers.mk_copy_n_compile()
的声明
import compileall
import os
import shutil
from pathlib import Path
def mk_copy_n_compile(toc, src_tree):
"""
- copy source files to a destination directory
- compile them as pyc
- delete source
"""
dst_base_path = os.path.join(src_tree, "src")
to_rm = []
# copy files to destination tree
for entry in toc:
modname, src, typ = entry
assert typ == "PYMODULE"
assert src.endswith(".py") or src.endswith(".pyw")
# TODO: might add logic to skip some files (keep them in PYC)
to_rm.append(entry)
if src.endswith("__init__.py"):
modname += ".__init__"
m_split = modname.split(".")
m_split[-1] += ".py"
dst_dir = os.path.join(dst_base_path, *m_split[:-1])
dst_path = os.path.join(dst_dir, m_split[-1])
if not os.path.isdir(dst_dir):
os.makedirs(dst_dir)
print(entry[:2], dst_path)
shutil.copy(src, dst_path)
# now compile all files and rmv src
top_tree = src_tree
src_tree = os.path.join(src_tree, "src")
curdir = os.getcwd()
os.chdir(dst_base_path)
for path in Path(dst_base_path).glob("**/*.py"):
# TODO: might add code to keep some files as source
compileall.compile_file(
str(path.relative_to(dst_base_path)), quiet=1, legacy=True)
path.unlink()
os.chdir(curdir)
return to_rm
我打算创建一个巨大的可执行目录并将其安装在某些设备上。
想象一下,后来我在我的 python 模块之一中发现了一个错误。 有没有办法transfer/copy只修改字节码,用新字节码替换原来的字节码
我想这样做的原因是,在我的上下文中带宽非常昂贵,我想远程修补代码。
示例:我有一个包含两个文件的项目:
prog.py
: (以下三行)
import mod1
if __name__ == "__main__":
mod1.hello()
mod1.py
: (以下两行)
def hello():
print("hello old world")
现在我使用 PYTHONHASHSEED=2 pyinstaller prog.py
创建我的目录并将其复制到我的设备
现在我修改mod1.py
:
def hello():
print("hello new world")
然后我用 PYTHONHASHSEED=2 pyinstaller prog.py
重新编译
完整目录的大小约为 10M
文件dist/prog/prog
大小约1M
使用 pyi-archive_viewer
我可以从我的可执行文件 dist/prog/prog
中提取 PYZ-00.pyz
在 PYZ-00.pyz
中,我可以找到并提取仅使用 133 个字节的 mod1
。
现在,如果我将该文件复制到我的设备,我该如何更新旧文件
dist/prog/prog
这样,它就有了新的 PYZ-00.pyz:mod1
字节码。
替换一个特定文件(模块)后,我可以用什么代码分解,我可以用什么代码重新组装?
备选方案:将 pyc 文件移动到 zip 文件 启动性能并不是那么重要。我也可以使用替代解决方案,其中没有创建 PYZ 文件并将其添加到可执行文件中,但 dist 目录包含一个包含所有 .pyc 文件的 zip 文件
另一种选择:将 .pyc 文件复制到应用程序目录中
这将导致 __file__
具有与 PYZ 模式中完全相同的值。性能方面可能不是那么好并且会创建大量文件,但如果增量更新至关重要,也许可以选择一种方法来处理它。
这是一个相当复杂的问题,但我认为这可能至少是您正在寻找的问题的一部分。
根据您的示例,我更改了 prog.py
,因此当从源 运行ning 时它可以正常导入,但是当使用来自 pyc
文件的 pyinstaller 运行s 冻结时直接。
import sys
def import_pyc(name):
import py_compile
import types
import marshal
pyversion = f"{sys.version_info.major}{sys.version_info.minor}"
filename = f"{name}.cpython-{pyversion}.pyc"
with open(filename, "rb") as pyc_file:
# pyc files have 16 bytes reserved at the start in python 3.7+
# due to https://www.python.org/dev/peps/pep-0552/
# may change again in the future
pyc_file.seek(16)
code_obj = marshal.load(pyc_file)
module = types.ModuleType(name)
exec(code_obj, module.__dict__)
globals()[name] = module
def import_py(name):
import importlib
globals()[name] = importlib.import_module("mod1")
def import2(name):
if getattr(sys, "frozen", False):
import_pyc(name)
else:
import_py(name)
import2("mod1")
if __name__ == "__main__":
mod1.hello()
这在很大程度上基于精彩的回答 here。
这意味着 mod.py
没有被 PyInstaller 打包,您必须将 mod1.cpython-38.pyc
作为数据文件包含在内。
一个方便的方法是使用命令 PyInstaller --add-data "__pycache__/*;." prog.py
(如果您不使用 Windows,请将分号切换为冒号)。这会将 __pycache__
文件夹中的所有内容,所有导入的模块,放入结尾的 dist/prog
文件夹中。请注意,如果您 运行 这多次,PyInstaller 会在 __pycache__
中为主 python 文件夹放置一个 pyc,以便在后续的 运行 中捆绑。
根据您捆绑的方式和 运行 您的项目,您可能会 运行 遇到当前工作目录关闭的问题,这将导致 FileNotFound
当您尝试加载 pyc
s。我不能给你找到你想要的路径的灵丹妙药,因为这取决于你最终如何做事,但我通常用来找到应该是当前工作目录的绝对路径的方法是 os.path.dirname(sys.executable)
和 os.path.dirname(os.path.abspath(__file__))
.
此解决方案既不能 'patching' 一个 .PYZ
文件,也不能将所有 .pyc
文件放入一个 zip 文件。
但到目前为止,这是我找到的唯一可行的解决方案,适用于具有大量第三方依赖项的大型项目。
想法是删除所有(或 .PYZ 文件中的大部分文件)并将相应的 .pyc 文件复制到工作目录中。
随着时间的推移,我会加强和详细说明这个答案。我还在试验:
我通过修改规范文件来实现:
- 确定spec文件所在目录
MYDIR
- 创建一个目录,
MYDIR/src
将a.pure
中的所有文件复制到 - 将所有文件从 a.pure 复制到
MYDIR/src
。 (子目录与模块名称相对应。例如,模块mypackage.mod.common
将存储在MYDIR/src/mypackage/mod/common.py
中) - 遍历文件并将它们编译为
.pyc
文件,然后删除.py
文件。 - 创建一个
PYZ
文件,其中仅包含未复制的文件。 (在我的测试用例中,PYZ
中没有.pyc
文件) - 用修改后的
PYZ
创建 exe
- 收集应收集的所有文件以及来自
MYDIR/src
的所有文件(例如a.datas + Tree("src")
规范文件更改: 一开始
import os
MYDIR = os.path.realpath(SPECPATH)
sys.path.append(MYDIR)
import mypyinsthelpers # allows to reuse the code in multiple projects
然后在(未修改的)a = Analysis(...
部分之后添加。
to_rmv_from_pyc = mypyinsthelpers.mk_copy_n_compile(a.pure, MYDIR)
# modified creation of pyz`
pyz = PYZ(a.pure - to_rmv_from_pyc, a.zipped_data,
cipher=block_cipher)
我会在下面详细介绍功能mypyinsthelpers.mk_copy_n_compile
更改收集阶段:
而不是
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
...
我写:
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas + Tree("src"),
...
这里是mypyinsthelpers.mk_copy_n_compile()
import compileall
import os
import shutil
from pathlib import Path
def mk_copy_n_compile(toc, src_tree):
"""
- copy source files to a destination directory
- compile them as pyc
- delete source
"""
dst_base_path = os.path.join(src_tree, "src")
to_rm = []
# copy files to destination tree
for entry in toc:
modname, src, typ = entry
assert typ == "PYMODULE"
assert src.endswith(".py") or src.endswith(".pyw")
# TODO: might add logic to skip some files (keep them in PYC)
to_rm.append(entry)
if src.endswith("__init__.py"):
modname += ".__init__"
m_split = modname.split(".")
m_split[-1] += ".py"
dst_dir = os.path.join(dst_base_path, *m_split[:-1])
dst_path = os.path.join(dst_dir, m_split[-1])
if not os.path.isdir(dst_dir):
os.makedirs(dst_dir)
print(entry[:2], dst_path)
shutil.copy(src, dst_path)
# now compile all files and rmv src
top_tree = src_tree
src_tree = os.path.join(src_tree, "src")
curdir = os.getcwd()
os.chdir(dst_base_path)
for path in Path(dst_base_path).glob("**/*.py"):
# TODO: might add code to keep some files as source
compileall.compile_file(
str(path.relative_to(dst_base_path)), quiet=1, legacy=True)
path.unlink()
os.chdir(curdir)
return to_rm