Bundling data files with PyInstaller (--onefile)

前端 未结 13 1585
清歌不尽
清歌不尽 2020-11-21 13:28

I\'m trying to build a one-file EXE with PyInstaller which is to include an image and an icon. I cannot for the life of me get it to work with --onefile.

<
相关标签:
13条回答
  • 2020-11-21 13:46

    Instead for rewriting all my path code as suggested, I changed the working directory:

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

    Just add those two lines at the beginning of your code, you can leave the rest as is.

    0 讨论(0)
  • 2020-11-21 13:48

    Newer versions of PyInstaller do not set the env variable anymore, so Shish's excellent answer will not work. Now the path gets set as 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)
    
    0 讨论(0)
  • 2020-11-21 13:48

    I have been dealing with this issue for a long(well, very long) time. I've searched almost every source but things were not getting in a pattern in my head.

    Finally, I think I have figured out exact steps to follow, I wanted to share.

    Note that, my answer uses informations on the answers of others on this question.

    How to create a standalone executable of a python project.

    Assume, we have a project_folder and the file tree is as follows:

    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
    

    First of all, let's say you have defined your paths to sound/ and img/ folders into variables sound_dir and img_dir as follows:

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

    You have to change them, as follows:

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

    Where, resource_path() is defined in the top of your script as:

    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)
    

    Activate virtual env if using a venv,

    Install pyinstaller if you didn't yet, by: pip3 install pyinstaller.

    Run: pyi-makespec --onefile main.py to create the spec file for the compile and build process.

    This will change file hierarchy to:

    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
    

    Open(with an edior) main.spec:

    At top of it, insert:

    added_files = [
    
    ("sound", "sound"),
    ("img", "img")
    
    ]
    

    Then, change the line of datas=[], to datas=added_files,

    For the details of the operations done on main.spec see here.

    Run pyinstaller --onefile main.spec

    And that is all, you can run main in project_folder/dist from anywhere, without having anything else in its folder. You can distribute only that main file. It is now, a true standalone.

    0 讨论(0)
  • 2020-11-21 13:57

    Using the excellent answer from Max and This post about adding extra data files like images or sound & my own research/testing, I've figured out what I believe is the easiest way to add such files.

    If you would like to see a live example, my repository is here on GitHub.

    Note: this is for compiling using the --onefile or -F command with pyinstaller.

    My environment is as follows.

    • Python 3.3.7
    • Tkinter 8.6 (check version)
    • Pyinstaller 3.6

    Solving the problem in 2 steps

    To solve the issue we need to specifically tell Pyinstaller that we have extra files that need to be "bundled" with the application.

    We also need to be using a 'relative' path, so the application can run properly when it's running as a Python Script or a Frozen EXE.

    With that being said we need a function that allows us to have relative paths. Using the function that Max Posted we can easily solve the relative pathing.

    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)
    

    We would use the above function like this so the application icon shows up when the app is running as either a Script OR Frozen EXE.

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

    The next step is that we need to instruct Pyinstaller on where to find the extra files when it's compiling so that when the application is run, they get created in the temp directory.

    We can solve this issue two ways as shown in the documentation, but I personally prefer managing my own .spec file so that's how we're going to do it.

    First, you must already have a .spec file. In my case, I was able to create what I needed by running pyinstaller with extra args, you can find extra args here. Because of this, my spec file may look a little different than yours but I'm posting all of it for reference after I explain the important bits.

    added_files is essentially a List containing Tuple's, in my case I'm only wanting to add a SINGLE image, but you can add multiple ico's, png's or jpg's using ('app/img/*.ico', 'app/img') You may also create another tuple like soadded_files = [ (), (), ()] to have multiple imports

    The first part of the tuple defines what file or what type of file's you would like to add as well as where to find them. Think of this as CTRL+C

    The second part of the tuple tells Pyinstaller, to make the path 'app/img/' and place the files in that directory RELATIVE to whatever temp directory gets created when you run the .exe. Think of this as CTRL+V

    Under a = Analysis([main..., I've set datas=added_files, originally it used to be datas=[] but we want out the list of imports to be, well, imported so we pass in our custom imports.

    You don't need to do this unless you want a specific icon for the EXE, at the bottom of the spec file I'm telling Pyinstaller to set my application icon for the exe with the option icon='app\\img\\app_icon.ico'.

    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')
    

    Compiling to EXE

    I'm very lazy; I don't like typing things more than I have to. I've created a .bat file that I can just click. You don't have to do this, this code will run in a command prompt shell just fine without it.

    Since the .spec file contains all of our compiling settings & args (aka options) we just have to give that .spec file to Pyinstaller.

    pyinstaller.exe "Process Killer.spec"
    
    0 讨论(0)
  • 2020-11-21 14:00

    Another solution is to make a runtime hook, which will copy(or move) your data (files/folders) to the directory at which the executable is stored. The hook is a simple python file that can almost do anything, just before the execution of your app. In order to set it, you should use the --runtime-hook=my_hook.py option of pyinstaller. So, in case your data is an images folder, you should run the command:

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

    The cp_images_hook.py could be something like this:

    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.")
    

    Before every execution the images folder is moved to the current directory (from the _MEIPASS folder), so the executable will always have access to it. In that way there is no need to modify your project's code.

    Second Solution

    You can take advantage of the runtime-hook mechanism and change the current directory, which is not a good practice according to some developers, but it works fine.

    The hook code can be found below:

    import sys
    import os
    
    path = getattr(sys, '_MEIPASS', os.getcwd())   
    os.chdir(path)
    
    0 讨论(0)
  • 2020-11-21 14:04

    All of the other answers use the current working directory in the case where the application is not PyInstalled (i.e. sys._MEIPASS is not set). That is wrong, as it prevents you from running your application from a directory other than the one where your script is.

    A better solution:

    import sys
    import os
    
    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)
    
    0 讨论(0)
提交回复
热议问题