Pyinstaller onefile doesn't find data files

2020-07-27 07:03发布

问题:

I too am trying for the first time to build a simple --onefile exe which includes data files, but Pyinstaller doesn't seem to find them when building the .exe. A --onedir build seems to work fine.

I'm using the --debug switch at this point as well. I am able to run the onefile executable and can see that it appears to start working. The program finds the (sys._MEIPASS) temp directory ok (prints the needed directory name as directed) but reports a "no such file or directory" error when it looks for the first data file from the temp directory. I used archiveviewer.py on the .exe and DIDN'T find the needed data files there-- which seems to be the problem, but I can't figure out why. Data files for the build are in the directory the spec file describes. My complete spec file is

# -*- mode: python -*-

a = Analysis(['develop6.py'],
         pathex=['C:\\PYINST20'],
         hiddenimports=[],
         hookspath=None)

a.datas += [ ('conlist.txt', 'C:\\pyinst20\\conlist.txt', 'DATA'), ('imageblank.gif', 'C:\\pyinst20\\imageblank.gif', 'DATA')]

pyz = PYZ(a.pure)

exe = EXE(pyz,
      a.scripts,
      a.binaries,
      a.zipfiles,
      a.datas,
      name=os.path.join('dist', 'develop6.exe'),
      debug=True,
      strip=None,
      upx=True,
      console=True )

回答1:

(this question is old but a it is one of the only sources I found to solve the same problem, I will share my solution here in case it could help someone)

There is two main things to do to add data files to your script in --onefile mode.

1. Adapt the paths

In your script, adapt your paths to find the datafiles in the bundle. According to PyInstaller documentation here,the executable is launched from a temporary file, so your path must take care of this dynamic part :

For a file with the following relative path : ./your/file/is/here.ext

The code will be :

import sys
wd = sys._MEIPASS
file_path = os.path.join(wd,<your>,<file>,<is>,<here>)

Note : to make your code also work on other contexts, you can do the following :

import sys
import os
try:
   wd = sys._MEIPASS
except AttributeError:
   wd = os.getcwd()
file_path = os.path.join(wd,<your>,<file>,<is>,<here>)

2. Add the data files paths in the PyInstaller specs

According to PyInstaller documentation here, there is two ways to add data files to the bundle :

  1. Pass the option --add-files and the files as parameters when running pyinstaller <yourscript.py> from your script directory

  2. First generate a spec file by navigating to your script directory and running pyi-makespec <yourscript.py>, then add your files to the list of tuples data=[]. The tuples contains the actual path to the file and the path within your bundle. If you followed the first point, this should look like datas = [('/your/file/is/here.ext','/your/file/is/')]

Then run PyInstall <yourscript.spec> to build the bundle, based on your specs file.



回答2:

After going back and forward between confusing stack overflow threads and the Pyinstaller documentation I was able to finally bundle my app.

Step by step:

  1. In your app root directory and . venv/bin/activated:

    ➜  ~ pip install pyinstaller
    ➜  ~ pyi-makespec --windowed --onedir --i ./resources/img/icn.icns \
          --osx-bundle-identifier "com.myname.macOS.myappname" app.py
    
  2. Now, modify your app.spec just a tiny bit:

    added_files = [
     ('resources/img', 'resources/img'),
     ( 'README.md', '.' )
     ]
    

    initialize a dictionary added_files with the relative path to your resources and set datas = added_files. In my application I used images located at ./resources/img relative to my main.py file.

  3. And to finilize, this is perhaps the easiest to forget step and not-so obvious:

    ➜  ~ pyinstaller --onefile app.spec
    

Notice this last step is taken from here.



回答3:

I believe I found the problem, and it isn't related to the body of the spec file. Looks like my command line syntax was wrong when running pyinstaller with the spec file. When run properly:

pyinstaller.py [options] <my_specfile.spec>

Appears to work.



回答4:

We can follow the document to set different file paths when the script is running as a bundle or not. However, my work-around is to set up a stable top level directory where all non-script files live, and share this top level directory with all modules. Then within each module, I can create file path based on the top level directory.

For instance, in this layout, all the module code live in src folder. These modules require access to conf folder, database folder, and otherconfig.yaml.

├── conf
│   └── config.ini
├── database
├── entry_point.py
├── otherconfig.yaml
├── root_path.py
└── src
    ├── pkg1
    └── pkg2

root_path.py would look like this:

import os

path = os.getcwd()

In each module, I can create the file path for the config files like this:

import root_path

config_path = f"{root_path.path}/conf/config.ini"
otherconfig_path = f"{root_path.path}/otherconfig.yaml"

Then, after creating a one-file executable via pyinstaller -F entry_point.py (without adding any files), we just need to recreate the same layout, and the file paths in each module would still work.

├── bundled_executable
├── conf
│   └── config.ini
├── database
└── otherconfig.yaml

Of course, this work-around cannot hide the config files. But for my use case, I need to give users freedom to modify the config files, so not hiding the files actually work for me.