1

I've looked up everywhere and got no definite response to a rather trivial question.

I have a Python project in PyCharm on Windows 7 that contains multiple .py files (which are connected via "from %package_name%.%script_name% import %class_name%") and a folder inside the project with two simple text files. I've installed PyInstaller 3.6 into project's venv and use it as an external tool, that points to a .spec file. So far, so good. The .spec file is as follows:

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\main.py'],
             pathex=['C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%'],
             binaries=[],
             datas=[('txt_files\\file1.txt', '.'), ('txt_files\\file2.txt', '.')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
a.datas += [
    ("C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\txt_files\\file1.txt","txt_files\\file1.txt","DATA"),
    ("C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\txt_files\\file2.txt","txt_files\\file2.txt","DATA"),
]
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='%project_name%',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='%project_name%')

The problem is that if I hard-code the absolute paths to the bundled .txt files in the scripts themselves, the app compiles and has no run-time errors. However, if I use relative paths inside the scripts, the app compiles, but gives a run-time error, that a .txt file (i.e. file1.txt) is not found INSIDE the /build (or /dist, I may be wrong here) directory (which is obviously not there).

Of course, hard-coding the absolute paths is a bad practice, especially, when talking not only about portability to another machine, but also making the app cross-platform. I know that the build process may depend on sys._MEIPASS, but I don't know exactly how to use it in my context.

In which script (main, .spec or other?) shall I put the part that gets the absolute path to a bundled file using sys._MEIPASS? And how should this code part look like on Python 3.7? I've seen different answers (i.e. this one) and already tried them, but none seemed to work in my case.

Anvbis
  • 43
  • 1
  • 8
  • Are you bundling into a single file executable (with `--onedir`) or a directory? – wstk Mar 30 '20 at 22:11
  • Yes, I want to generate a single bundled executable, but when I passed a `--onedir` option, I got the error `Security-Alert: try to store file outside of dist-directory. Aborting.`. Right now, to get the path to work inside the `venv` in code, I use `file_path = os.path.abspath(os.path.join(os.getcwd(), rel_path))`, where `rel_path` is `txt_files\\file1.txt`, and it works inside PyCharm, however, when running the app itself, it generates a `FileNotFoundError` in the `C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\dist\\%project_folder%\\txt_files\\file1.txt`. – Anvbis Mar 30 '20 at 23:26

2 Answers2

6

Using --onefile bundles all the datas together into the .exe file.

When you execute the file, these files are "unpacked" to a temporary file location. On Windows, this is usually C:\Users\<You>\AppData\Local\Temp\MEIxxx.

So, when you are developing your script, the data files (your text files in this example) will be located at

C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\txt_files\

but when the app is compiled, they will be extracted to the temporary directory mentioned above. So you need a way to tell the script whether you are developing, or it has been compiled. This is where you can use the 'frozen' flag (see the docs here)

An approach I have used before, is to create a utility function like this

def resolve_path(path):
    if getattr(sys, "frozen", False):
        # If the 'frozen' flag is set, we are in bundled-app mode!
        resolved_path = os.path.abspath(os.path.join(sys._MEIPASS, path))
    else:
        # Normal development mode. Use os.getcwd() or __file__ as appropriate in your case...
        resolved_path = os.path.abspath(os.path.join(os.getcwd(), path))

    return resolved_path

Then whenever you want to use a path in your script, for example accessing your text files you can do

with open(resolve_path("txt_files/file1.txt"), "r") as txt:
    ...

which should resolve the correct path whichever mode you are in.

A note on your .spec file

  1. You don't have to specify all the text files individually. You can of course, and you may have a good reason for doing so which is fine. But you could do

datas=[('txt_files', '.')]

which puts the contents of txt_files directory in the root of your bundle. Be careful with this however, because now the paths to your text files will be <dev directory>\txt_files\file1.txt but in the bundled app, they will be <MEIPASS directory>\file1.txt. You may want to keep the 'relative' part of the path the same by doing

datas=[('txt_files', 'txt_files')]

which will mirror the file structure between your development folder and your bundled app.

  1. Also consider if you build with the spec file, remove the COLLECT part in order to produce a onefile bundled executable.
wstk
  • 1,040
  • 8
  • 14
  • Ok, so that's what I have done - I installed the code in the script that reads the files, removed the `COLLECT` from the spec, set `exclude_binaries` to `False` (so that the bundled `.exe` file shall appear in the `dist` directory, like it should), and also added `a.datas` to the `EXE` part of the spec, but now when running the app in Windows console I get the error `Error loading Python DLL 'C:\Users'\%username%\AppData\Local\Temp\_MEIXXXXX\python37.dll LoadLibrary'`, and then some incomprehensible symbols, no matter how I change the `chcp` (probably `One or more arguments are invalid`). – Anvbis Apr 01 '20 at 00:39
  • How do I add Python into the `_MEIPASS` temp directory? – Anvbis Apr 01 '20 at 00:40
  • 1
    If you haven't you should add `a.binaries` also to the EXE step. As a slight aside - it is **much** easier to debug these issues when building in "one directory" mode. Often a good workflow is to get it working like this, and then switch to using `--onedir` once you are sure it's all OK. – wstk Apr 01 '20 at 08:30
  • All right, I added `a.binaries` to `EXE` and left the `--onedir` param as it is. Gonna test it now. – Anvbis Apr 01 '20 at 17:27
  • Finally, your solution with bundling mirrored `datas=[('txt_files', 'txt_files')]` worked. Thanks a lot! (don't forget to add closing ticks to the first 'txt_files' in the `datas`, though) – Anvbis Apr 01 '20 at 17:30
  • I would be happy if someone could look at problem here https://stackoverflow.com/questions/74172706/pyinstaller-exe-file-pandas-framework-appends-data-to-excel-file-in-temp-folder – xlmaster Oct 23 '22 at 23:55
0

I have the following FS

  • GitRepoForMySomeProject
    • MySomeProject
      • main.py
    • MyGitSubmodule
    • ExeSettings.spec
    • ExeVersion.py
    • ExeMySomeProject.bat ("pyinstaller ExeSettings.spec")

In main.py are using some submodule which placed into subfolder "MyGitSubmodule/ReportGenerator.py"

from MyGitSubmodule import ReportGenerator as _report_

Here is my ExeSettings.spec file, with relative path pathex=['../MySomeProject']

# -*- mode: python -*-

block_cipher = None


a = Analysis(['MySomeProject/main.py'],
             pathex=['../MySomeProject'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='MySomeProject',
          debug=False,
          strip=False,
          upx=True,
          console=False,
          icon='my.ico',
          version='ExeVersion.py')

if forget to add this path, we will have

ModuleNotFoundError: No module named "MyGitSubmodule"

Dmitry Ivanov
  • 508
  • 7
  • 9