首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >pyinstaller可执行文件的差异更新(修改嵌入式PYZ-00.pyz)

pyinstaller可执行文件的差异更新(修改嵌入式PYZ-00.pyz)
EN

Stack Overflow用户
提问于 2021-04-09 10:41:46
回答 2查看 591关注 0票数 2

我计划创建一个巨大的可执行目录,并将其安装在一些设备上。

想象一下,后来我在我的python模块中发现了一个bug。是否有任何方法只传输/复制修改过的字节码并用新的字节码替换原始字节码。

我想这么做的原因是,在我的上下文中,带宽非常昂贵,我想远程修补代码。

示例:我有一个带有两个文件的项目:prog.py:(有以下三行)

代码语言:javascript
复制
import mod1
if __name__ == "__main__":
    mod1.hello()

mod1.py:(以下两行)

代码语言:javascript
复制
def hello():
    print("hello old world")

现在我使用PYTHONHASHSEED=2 pyinstaller prog.py创建我的目录,然后复制到我的设备上。

现在我修改mod1.py

代码语言:javascript
复制
def hello():
    print("hello new world")

我用PYTHONHASHSEED=2 pyinstaller prog.py重新编译了完整的目录,其大小约为10M,dist/prog/prog文件的大小约为1M。

使用pyi-archive_viewer,我可以在PYZ-00.pyz中从可执行的dist/prog/prog中提取PYZ-00.pyz,我可以找到并提取只使用133个字节的mod1

现在,如果我将该文件复制到我的设备上,如何才能更新旧的dist/prog/prog,使其具有新的PYZ-00.pyz:mod1字节码。

我可以使用什么代码来分解,在替换了一个特定的文件(模块)之后,我可以使用什么代码来重新组装?

替代方案:将pyc文件移动到zip文件中,启动性能并不那么关键。我还可以使用另一种解决方案,即不创建PYZ文件并将其添加到可执行文件中,但是dist目录包含一个包含所有.pyc文件的zip文件。

另一种选择:将.pyc文件复制到应用程序目录中,这将导致__file__具有与PYZ模式完全相同的值。性能方面可能没有那么好,并且创建了大量的文件,但是如果增量更新是关键的话,可能是处理它的一个选项。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2021-08-12 09:23:01

这种解决方案既不能“修补”.PYZ文件,也不能将所有.pyc文件放入zip文件。

但到目前为止,这是我找到的唯一可行的解决方案,它适用于具有大量第三方依赖项的大型项目。

其想法是删除所有(或.PYZ文件中的大多数文件),并将相应的.pyc文件复制到工作目录中。

随着时间的推移,我将加强和详细阐述这一答案。我还在做实验:

我通过修改规范文件来实现这一点:

  • 确定规范文件位于
  • 中的目录MYDIR创建目录,MYDIR/srca.pure的所有文件复制到
  • ,将所有文件从a.pure复制到MYDIR/src。(子目录对应于模块的名称。例如,模块mypackage.mod.common将通过文件存储在afterwards.
  • create中,并将它们编译成.pyc文件,并删除.py文件afterwards.
  • create(仅包含未复制的文件的PYZ文件)。(在我的测试用例中,在PYZ)
  • create exe中不要使用修改后的PYZ
  • collect -所有应该收集的文件加上来自MYDIR/src的所有文件(例如,使用a.datas + Tree("src")

)。

文件更改:在开头

代码语言:javascript
复制
import os
MYDIR = os.path.realpath(SPECPATH)
sys.path.append(MYDIR)
import mypyinsthelpers  # allows to reuse the code in multiple projects

然后,在(未修改的) a = Analysis(...部分之后添加。

代码语言:javascript
复制
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

更改收集阶段:

而不是

代码语言:javascript
复制
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
...

我写道:

代码语言:javascript
复制
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas + Tree("src"),
...

这里是mypyinsthelpers.mk_copy_n_compile()的声明

代码语言:javascript
复制
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
票数 0
EN

Stack Overflow用户

发布于 2021-08-05 05:24:35

这是一个相当复杂的问题,但我认为这可能至少是你正在寻找的一部分。

根据您的示例,我更改了prog.py,以便它在从源代码运行时非常正常地导入,但是当使用pyinstaller冻结时,它直接从pyc文件中运行。

代码语言:javascript
复制
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,以便在以后的运行中绑定。

根据打包和运行项目的方式,您可能会遇到当前工作目录关闭的问题,当您试图加载pyc的时候,这可能会导致一个pyc。我不能给您一个找到所需路径的灵丹妙药,因为这取决于您是如何完成任务的,但是我通常用来查找当前工作目录的绝对路径的方法是os.path.dirname(sys.executable)os.path.dirname(os.path.abspath(__file__))

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/67019608

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档