使用PyInstaller捆绑数据文件(--onefile)

时间:2011-10-06 13:20:34

标签: python pyinstaller

我正在尝试使用PyInstaller构建一个单文件EXE,它包含一个图像和一个图标。我无法让我的生活与--onefile一起使用。

如果我做--onedir它的效果非常好。 当我使用--onefile时,它找不到引用的附加文件(运行已编译的EXE时)。它找到了DLL和其他一切都很好,而不是两个图像。

我查看了运行EXE时生成的temp-dir(例如\Temp\_MEI95642\),文件确实在那里。当我将EXE放入该临时目录时,它会找到它们。非常困惑。

这是我添加到.spec文件

的内容
a.datas += [('images/icon.ico', 'D:\\[workspace]\\App\\src\\images\\icon.ico',  'DATA'),
('images/loaderani.gif','D:\\[workspace]\\App\\src\\images\\loaderani.gif','DATA')]     

我应该补充一点,我也试过不把它们放在子文件夹中,没有什么区别。

编辑: 由于PyInstaller更新,将较新的答案标记为正确。

14 个答案:

答案 0 :(得分:111)

较新版本的PyInstaller不再设置env变量,因此Shish的优秀answer将不起作用。现在路径设置为sys._MEIPASS

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

答案 1 :(得分:49)

pyinstaller将您的数据解压缩到一个临时文件夹中,并将此目录路径存储在_MEIPASS2环境变量中。要在打包模式下获取_MEIPASS2目录并在解压缩(开发)模式下使用本地目录,我使用:

def resource_path(relative):
    return os.path.join(
        os.environ.get(
            "_MEIPASS2",
            os.path.abspath(".")
        ),
        relative
    )

输出:

# in development
>>> resource_path("app_icon.ico")
"/home/shish/src/my_app/app_icon.ico"

# in production
>>> resource_path("app_icon.ico")
"/tmp/_MEI34121/app_icon.ico"

答案 2 :(得分:14)

如果应用程序不是PyInstalled(即未设置@echo off @echo off rem http://technet.microsoft.com/en-us/library/bb490890.aspx SETLOCAL EnableDelayedExpansion :: findJDK.bat rem start:-> Run a command in a separate process, or run a file with its default associated application rem REGEDIT.EXE: -> Export to a (.REG) file: rem REGEDIT.EXE [ /L:system | /R:user ] /E exportfile.REG "registry_key" start /w regedit /e reg1.txt "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\JavaSoft\Java Development Kit" rem TYPE: -> Display text file content in console rem FIND: -> Search files or standard output rem reserved key words regarding reg-key -> "JavaHome" "MicroVersion" "RuntimeLib" rem | Reads the output from one command and writes it to the input of another command. Also known as a pipe. type reg1.txt | find "JavaHome" > reg2.txt if errorlevel 1 goto ERROR for /f "tokens=2 delims==" %%x in (reg2.txt) do ( set JavaTemp=%%~x echo Regedit: JAVA_HOME path : !JavaTemp! ) if errorlevel 1 goto ERROR echo. set JAVA_HOME=%JavaTemp% set JAVA_HOME=%JAVA_HOME:\\=\% echo JAVA_HOME was found to be %JAVA_HOME% start /w regedit /e reg3.txt "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\JavaSoft\Java Runtime Environment" type reg3.txt | find "RuntimeLib" > reg4.txt if errorlevel 1 goto ERROR for /f "tokens=2 delims==" %%x in (reg4.txt) do ( set JavaTemp=%%~x echo Regedit: JRE_HOME path : !JavaTemp! ) if errorlevel 1 goto ERROR echo. set JRE_HOME=%JavaTemp% set JRE_HOME=%JRE_HOME:\\=\% echo JRE_HOME was found to be %JRE_HOME% goto END :ERROR echo A reg1.txt is: & type reg1.txt echo B reg2.txt is: & type reg2.txt echo :END echo END pause ),则所有其他答案都使用当前工作目录。这是错误的,因为它会阻止您从脚本所在目录以外的目录运行应用程序。

更好的解决方案:

sys._MEIPASS

答案 3 :(得分:8)

也许我错过了一个步骤或做错了什么但是上面的方法没有将PyInstaller的数据文件捆绑到一个exe文件中。让我分享一下我所做的步骤。

  1. 步骤:使用导入sys和os模块将上述方法之一写入py文件。我试过了他们两个。最后一个是:

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
    
  2. 步骤:将 pyi-makespec file.py 写入控制台,以创建file.spec文件。

  3. 步骤:使用Notepad ++打开file.spec,添加如下数据文件:

    a = Analysis(['C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.py'],
                 pathex=['C:\\Users\\TCK\\Desktop\\Projeler'],
                 binaries=[],
                 datas=[],
                 hiddenimports=[],
                 hookspath=[],
                 runtime_hooks=[],
                 excludes=[],
                 win_no_prefer_redirects=False,
                 win_private_assemblies=False,
                 cipher=block_cipher)
    #Add the file like the below example
    a.datas += [('Converter-GUI.ico', 'C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico', 'DATA')]
    pyz = PYZ(a.pure, a.zipped_data,
         cipher=block_cipher)
    exe = EXE(pyz,
              a.scripts,
              exclude_binaries=True,
              name='Converter-GUI',
              debug=False,
              strip=False,
              upx=True,
              #Turn the console option False if you don't want to see the console while executing the program.
              console=False,
              #Add an icon to the program.
              icon='C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico')
    
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=False,
                   upx=True,
                   name='Converter-GUI')
    
  4. 步骤:我按照上述步骤操作,然后保存spec文件。最后打开控制台并写了 pyinstaller file.spec (在我的例子中,file = Converter-GUI)。

  5. 结论:dist文件夹中仍有多个文件。

    注意:我使用的是Python 3.5。

    编辑:最后它适用于Jonathan Reinhart's method.

    1. 步骤:使用导入sys和os。

      将以下代码添加到python文件中
      def resource_path(relative_path):
      """ Get absolute path to resource, works for dev and for PyInstaller """
          base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
          return os.path.join(base_path, relative_path)
      
    2. 步骤:通过添加文件路径调用上述函数:

      image_path = resource_path("Converter-GUI.ico")
      
    3. 步骤:将调用函数的上述变量写入代码所需路径的位置。在我的情况下:

          self.window.iconbitmap(image_path)
      
    4. 步骤:在python文件的同一目录中打开控制台,编写如下代码:

          pyinstaller --onefile your_file.py
      
    5. 步骤:打开python文件的.spec文件并附加a.datas数组,并将该图标添加到exe类中,该类在上面的第3步编辑之前给出。
    6. 步骤:保存并退出路径文件。转到包含spec和py文件的文件夹。再次打开控制台窗口并键入以下命令:

          pyinstaller your_file.spec
      
    7. 在6.步骤之后,您可以使用一个文件。

答案 4 :(得分:6)

而是按照建议重写我的所有路径代码,而是更改了工作目录:

if getattr(sys, 'frozen', False):
    os.chdir(sys._MEIPASS)

只需在代码的开头添加这两行,就可以保留原样。

答案 5 :(得分:3)

使用MaxThis post的出色答案,即添加额外的数据文件(例如图像或声音)以及我自己的研究/测试,我发现了我认为添加这种数据的最简单方法文件。

如果您想观看现场示例,我的存储库是GitHub上的here

注意:这是通过pyinstaller使用--onefile-F命令进行编译的。

我的环境如下。


分两步解决问题

要解决该问题,我们需要专门告诉Pyinstaller,我们还有一些其他文件需要与应用程序“捆绑”。

我们还需要 使用“相对”路径 ,以便该应用程序以Python脚本或Frozen EXE的身份运行时可以正常运行。

话虽如此,我们需要一个允许我们拥有相对路径的功能。使用Max Posted的功能,我们可以轻松解决相对路径。

def img_resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

我们将使用上述功能,因此当应用程序以脚本或冻结EXE的形式运行时,应用程序图标就会显示出来。

icon_path = img_resource_path("app/img/app_icon.ico")
root.wm_iconbitmap(icon_path)

下一步是我们需要指导Pyinstaller在编译时在哪里找到多余的文件,以便在运行应用程序时在临时目录中创建它们。

我们可以通过documentation中所示的两种方法解决此问题,但是我个人更喜欢管理自己的.spec文件,因此我们将采用这种方式。

首先,您必须已经有一个.spec文件。就我而言,我可以通过运行带有额外参数的pyinstaller来创建所需的内容,您可以找到额外的参数here。因此,我的规格文件可能看起来与您的规格文件有些不同,但是在解释了重要的内容之后,我将所有这些文件发布以供参考。

added_files 本质上是一个包含Tuple的列表,在我的情况下,我只想添加一个SINGLE图像,但是您可以使用以下方式添加多个ico,png或jpg ('app/img/*.ico', 'app/img')您也可以像这样added_files = [ (), (), ()]创建另一个元组以具有多个导入

元组的第一部分定义您要添加的文件或类型的 以及在何处找到它们。认为这是CTRL + C

元组的第二部分告诉Pyinstaller,使其路径为“ app / img /”,并将文件放置在该目录中,与您运行.exe时创建的任何临时目录相对。将其视为CTRL + V

a = Analysis([main... 下,我设置了datas=added_files,最初它是datas=[],但我们要删除列表进口货是进口货,所以我们传入自定义进口货。

除非您需要EXE的特定图标,否则不需要执行此操作。在规范文件的底部,我告诉Pyinstaller使用选项icon='app\\img\\app_icon.ico'为exe设置我的应用程序图标。

added_files = [
    ('app/img/app_icon.ico','app/img/')
]
a = Analysis(['main.py'],
             pathex=['D:\\Github Repos\\Processes-Killer\\Process Killer'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='Process Killer',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True , uac_admin=True, icon='app\\img\\app_icon.ico')

编译为EXE

我很懒。我不喜欢输入太多东西。我创建了一个.bat文件,只需单击即可。您不必这样做,此代码将在没有它的情况下在命令提示符外壳中运行。

由于.spec文件包含我们所有的编译设置和args(aka选项),我们只需要将该.spec文件提供给Pyinstaller。

pyinstaller.exe "Process Killer.spec"

答案 6 :(得分:2)

轻微修改已接受的答案。

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)

    return os.path.join(os.path.abspath("."), relative_path)

答案 7 :(得分:1)

使用@Jonathon Reinhart的resource_path()

,将此代码段添加到查看包含在您的文件中的内容。
for root, dirs, files in os.walk(resource_path("")):
    print(root)
    for file in files:
        print( "  ",file)

答案 8 :(得分:1)

我已经很长时间(很长一段时间了)处理这个问题。我搜索了几乎所有来源,但事情并没有在我的脑海中浮现。

最后,我想我想出了要遵循的确切步骤,我想与大家分享。

请注意,我的答案使用的是其他人对此问题的回答的信息。

如何创建python项目的独立可执行文件。

假设,我们有一个project_folder,文件树如下:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv

首先,假设您已将sound/img/文件夹的路径定义为变量sound_dirimg_dir,如下所示:

img_dir = os.path.join(os.path.dirname(__file__), "img")
sound_dir = os.path.join(os.path.dirname(__file__), "sound")

您必须更改它们,如下所示:

img_dir = resource_path("img")
sound_dir = resource_path("sound")

resource_path()在脚本顶部的定义为:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

如果使用venv,则激活虚拟环境,

如果尚未安装pyinstaller,请通过pip3 install pyinstaller

运行:pyi-makespec --onefile main.py创建用于编译和构建过程的规范文件。

这会将文件层次结构更改为:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv
    main.spec

打开(带有编辑)main.spec

在其顶部插入:

added_files = [

("sound", "sound"),
("img", "img")

]

然后,将datas=[],的行更改为datas=added_files,

有关在main.spec上执行的操作的详细信息,请参见here.

运行pyinstaller --onefile main.spec

仅此而已,您可以从任何位置在main中运行project_folder/dist,而无需在其文件夹中放置其他任何东西。您只能分发该main文件。现在,它是真正的独立

答案 9 :(得分:1)

另一种解决方案是制作一个运行时挂钩,它将把您的数据(文件/文件夹)复制(或移动)到存储可执行文件的目录中。挂钩是一个简单的python文件,几乎可以在执行应用程序之前执行任何操作。为了进行设置,您应该使用pyinstaller的--runtime-hook=my_hook.py选项。因此,如果您的数据是图像文件夹,则应运行以下命令:

pyinstaller.py --onefile -F --add-data=images;images --runtime-hook=cp_images_hook.py main.py

cp_images_hook.py可能是这样的:

import sys
import os
import shutil

path = getattr(sys, '_MEIPASS', os.getcwd())

full_path = path+"\\images"
try:
    shutil.move(full_path, ".\\images")
except:
    print("Cannot create 'images' folder. Already exists.")

在每次执行之前,将 images 文件夹移至当前目录(从_MEIPASS文件夹中),因此可执行文件将始终可以对其进行访问。 这样就无需修改项目的代码。

第二个解决方案

您可以利用运行时挂钩机制更改当前目录,这对某些开发人员来说不是一个好习惯,但是可以正常工作。

挂钩代码可以在下面找到:

import sys
import os

path = getattr(sys, '_MEIPASS', os.getcwd())   
os.chdir(path)

答案 10 :(得分:0)

我发现现有的答案令人困惑,并花了很长时间才找出问题所在。这是我发现的所有内容的汇编。

当我运行我的应用时,我收到错误Failed to execute script foo(如果foo.py是主文件)。要解决此问题,请不要使用--noconsole运行PyInstaller(或修改main.spec以更改console=False => console=True)。这样,从命令行运行可执行文件,您将看到失败。

要检查的第一件事是它正确地打包了你的额外文件。如果要包含文件夹('x', 'x'),则应添加x之类的元组。

崩溃后,请勿点击“确定”。如果您使用的是Windows,则可以使用Search Everything。查找您的一个文件(例如sword.png)。您应该找到解压缩文件的临时路径(例如C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png)。您可以浏览此目录并确保它包含所有内容。如果您无法通过这种方式找到它,请查找main.exe.manifest(Windows)或python35.dll(如果您使用的是Python 3.5)。

如果安装程序包含所有内容,则下一个可能的问题是文件I / O:您的Python代码正在查看可执行文件的目录,而不是临时目录中的文件。

要解决这个问题,这个问题的任何答案都有效。就个人而言,我发现它们的混合都可以工作:首先在主入口点文件中有条件地更改目录,其他一切按原样运行:

if hasattr(sys, '_MEIPASS'): os.chdir(sys._MEIPASS)

答案 11 :(得分:0)

如果您仍在尝试将文件相对于可执行文件而不是放置在temp目录中,则需要自己复制文件。这就是我最终完成它的方式。

https://stackoverflow.com/a/59415662/999943

您在规范文件中添加了一个步骤,该步骤将文件系统复制到DISTPATH变量。

希望有帮助。

答案 12 :(得分:0)

我根据最大解决方案使用此

def getPath(filename):
    import os
    import sys
    from os import chdir
    from os.path import join
    from os.path import dirname
    from os import environ
    
    if hasattr(sys, '_MEIPASS'):
        # PyInstaller >= 1.6
        chdir(sys._MEIPASS)
        filename = join(sys._MEIPASS, filename)
    elif '_MEIPASS2' in environ:
        # PyInstaller < 1.6 (tested on 1.5 only)
        chdir(environ['_MEIPASS2'])
        filename = join(environ['_MEIPASS2'], filename)
    else:
        chdir(dirname(sys.argv[0]))
        filename = join(dirname(sys.argv[0]), filename)
        
    return filename

答案 13 :(得分:0)

对于那些仍在寻找最新答案的人,请按以下步骤进行:

documentation中有一个section on accessing added data files
这是它的简短和甜美。


您将要import pkgutil并找到将数据文件添加到的文件夹;即在元组中添加到规范文件中的第二个字符串:

data = [("path/to/mypackage/data_file.txt", "path/to/mypackage")]

知道数据文件的添加位置之后,就可以将其用作二进制数据读取,并根据需要对其进行解码。举个例子:

文件结构:

mypackage
      __init__.py  # This is a MUST in order for the package to be registered
      data_file.txt  # The data file you've added

data_file.txt

Hello world!

main.py

import pkgutil

file = pkgutil.get_data("mypackage", "data_file.txt")
contents = file.decode("utf-8")
print(contents)  # Hello world!

参考文献: