【问题标题】:differential update of pyinstaller executable (modify embedded PYZ-00.pyz)pyinstaller 可执行文件的差异更新(修改嵌入式 PYZ-00.pyz)
【发布时间】:2021-07-05 05:53:29
【问题描述】:

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

想象一下,后来我在我的一个 python 模块中发现了一个错误。 有没有办法只传输/复制修改后的字节码,用新的字节码替换原来的字节码。

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

示例:我有一个包含两个文件的项目: 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 重新编译 完整目录(tared 和 gzipped)大小约为 10M 文件dist/prog/prog的大小约为1M

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

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

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

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

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

【问题讨论】:

    标签: python pyinstaller archive zlib difference


    【解决方案1】:

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

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

    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,以便在后续运行时捆绑。

    根据您捆绑和运行项目的方式,您可能会遇到当前工作目录关闭的问题,当您尝试加载pycs 时,这将导致FileNotFound。我不能给你一个灵丹妙药来找到你想要的路径,因为它取决于你最终如何做事,但我通常用来找到应该是当前工作目录的绝对路径的方法是 os.path.dirname(sys.executable) 和 @ 987654334@.

    【讨论】:

    • 是的,这是一个相当部分的解决方案,并且对于包含数百个文件的现有代码库来说不容易实现,因为每个文件都必须修改。它也没有显示如何将 .pyc 文件移出 .PYZ 文件并移入目录。我将发布一个我不太满意的答案,但它似乎提供了一个可行的解决方案。虽然它既不修补 .PYZ 文件也不使用 zip 文件。
    • 有了这个,pyc 文件一开始就永远不会出现在 PYZ 存档中,因为 PyInstaller 不知道导入。虽然我不确定这将如何与更复杂的导入机制一起工作,比如模块,这可能与这么多文件相关。
    • 是的,我明白了。我有一个包含数百个文件和许多第三方依赖项的现有项目,因此这种方法实际上并不可行。我依靠Pyinstaller 的分析阶段来了解必须打包的内容。我昨天刚刚提出的替代解决方案远非很好,但不需要更改源代码(或者如果我想使用 zip 文件,则只需要更改顶级程序)或者如果我想将所有代码放入专用子目录
    • 现在是 Stackoverflow 特定的问题。我想接受我的回答,因为它可能对将来遇到这个问题的人更有帮助。但是,我想将赏金归功于您,因为您是唯一的答案,我想奖励您的努力。实现这一目标的 SO 机制是什么?
    • 啊,我之前错过了这个。我真的不确定 - 我以前从未做过赏金。我很欣赏这个想法,但如果它不容易奏效,请不要担心。
    【解决方案2】:

    此解决方案既不能“修补”.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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-10-20
      • 2021-12-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多